/// <summary> Parses the replay.details file, applying it to a Replay object. </summary> /// <param name="replay"> The replay object to apply the parsed information to. </param> /// <param name="buffer"> The buffer containing the replay.details file. </param> public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) using (var reader = new BinaryReader(stream)) { var replayDetailsStructure = new TrackerEventStructure(reader); var playerId = -1; replay.Players = replayDetailsStructure.dictionary[0].optionalData.array.Select(i => new Player { PlayerId = ++playerId, Name = i.dictionary[0].blobText, BattleNetRegionId = (int)i.dictionary[1].dictionary[0].vInt.Value, BattleNetSubId = (int)i.dictionary[1].dictionary[2].vInt.Value, BattleNetId = (int)i.dictionary[1].dictionary[4].vInt.Value, // [2] = Race (SC2 Remnant, Always Empty String in Heroes of the Storm) Color = i.dictionary[3].dictionary.Keys.OrderBy(j => j).Select(j => (int)i.dictionary[3].dictionary[j].vInt.Value).ToArray(), // [4] = Player Type (2 = Human, 3 = Computer (Practice, Try Me, or Coop)) - This is more accurately gathered in replay.attributes.events Team = (int)i.dictionary[5].vInt.Value, Handicap = (int)i.dictionary[6].vInt.Value, // [7] = VInt, Default 0 IsWinner = i.dictionary[8].vInt.Value == 1, // [9] = Sometimes player index in ClientList array; usually 0-9, but can be higher if there are observers. I don't fully understand this, as this was incorrect in at least one Custom game, where this said ClientList[8] was null Character = i.dictionary[10].blobText }).ToArray(); if (replay.Players.Length != 10 || replay.Players.Count(i => i.IsWinner) != 5) // Try Me Mode, or something strange return; replay.Map = replayDetailsStructure.dictionary[1].blobText; // [2] - This is typically an empty string, no need to decode. // [3] - Blob: "Minimap.tga" or "CustomMiniMap.tga" // [4] - Uint, Default 1 // [5] - Utc Timestamp replay.Timestamp = DateTime.FromFileTimeUtc(replayDetailsStructure.dictionary[5].vInt.Value); // There was a bug during the below builds where timestamps were buggy for the Mac build of Heroes of the Storm // The replay, as well as viewing these replays in the game client, showed years such as 1970, 1999, etc // I couldn't find a way to get the correct timestamp, so I am just estimating based on when these builds were live if (replay.ReplayBuild == 34053 && replay.Timestamp < new DateTime(2015, 2, 8)) replay.Timestamp = new DateTime(2015, 2, 13); else if (replay.ReplayBuild == 34190 && replay.Timestamp < new DateTime(2015, 2, 15)) replay.Timestamp = new DateTime(2015, 2, 20); // [6] - Windows replays, this is Utc offset. Mac replays, this is actually the entire Local Timestamp // var potentialUtcOffset = new TimeSpan(replayDetailsStructure.dictionary[6].vInt.Value); // [7] - Blob, Empty String // [8] - Blob, Empty String // [9] - Blob, Empty String // [10] - Optional, Array: 0 - Blob, "s2ma" // [11] - UInt, Default 0 // [12] - VInt, Default 4 // [13] - VInt, Default 1 or 7 // [14] - Optional, Null // [15] - VInt, Default 0 // [16] - Optional, UInt, Default 0 } }
/// <summary> Parses the replay.tracker.events file, applying it to a Replay object. </summary> /// <param name="replay"> The replay object to apply the parsed information to. </param> /// <param name="buffer"> The buffer containing the replay.tracker.events file. </param> /// <param name="onlyParsePlayerSetupEvents"> If true, speeds up parsing by skipping Unit data, which is most of this file </param> public static void Parse(Replay replay, byte[] buffer, bool onlyParsePlayerSetupEvents = false) { replay.TrackerEvents = new List<TrackerEvent>(); var currentFrameCount = 0; using (var stream = new MemoryStream(buffer)) using (var reader = new BinaryReader(stream)) while (stream.Position < stream.Length) { var intro = reader.ReadBytes(3); // Always 03 ?? 09; Middle digit seems to have at least two possible values currentFrameCount += (int)TrackerEventStructure.read_vint(reader); var trackerEvent = new TrackerEvent { TimeSpan = new TimeSpan(0, 0, (int)(currentFrameCount / 16.0)) }; intro = reader.ReadBytes(1); // Always 09 trackerEvent.TrackerEventType = (TrackerEventType)TrackerEventStructure.read_vint(reader); trackerEvent.Data = new TrackerEventStructure(reader); replay.TrackerEvents.Add(trackerEvent); if (onlyParsePlayerSetupEvents && trackerEvent.TrackerEventType != TrackerEventType.PlayerSetupEvent) break; } // Populate the client list using player indexes var playerIndexes = replay.TrackerEvents.Where(i => i.TrackerEventType == TrackerEventType.PlayerSetupEvent && i.Data.dictionary[2].optionalData != null).Select(i => i.Data.dictionary[2].optionalData.vInt.Value).Distinct().OrderBy(i => i).ToArray(); for (var i = 0; i < playerIndexes.Length; i++) // The references between both of these classes are the same on purpose. // We want updates to one to propogate to the other. replay.ClientList[playerIndexes[i]] = replay.Players[i]; }
public Match ParseReplay(string path) { using (var archive = Nmpq.MpqArchive.Open(path)) { try { Replay replay = new Replay(); ReplayInitData.Parse(replay, archive.ReadFile(InitData)); ReplayTrackerEvents.Parse(replay, archive.ReadFile(TrackerEvents)); ReplayDetails.Parse(replay, archive.ReadFile(Details)); ReplayAttributeEvents.Parse(replay, archive.ReadFile(AttributeEvents)); var player = replay.Players.First(p => p.Name == accountName); Match match = new Match(); match.FileName = path.Split('\\').Last(); match.Map = replay.Map; match.Win = player.IsWinner; match.Character = new Hero(player.Character, replay.ReplayLength); match.TimeStamp = replay.Timestamp; replay = null; return match; } catch (Exception ex) { Logger.Log(string.Format("Error mapping {0}, exception details : {1}", path, ex.ToString())); return null; } } }
private static void ParseHeader(Replay replay, BinaryReader reader) { reader.ReadBytes(3); // 'Magic' reader.ReadByte(); // Format BitConverter.ToInt32(reader.ReadBytes(4), 0); // Data Max Size BitConverter.ToInt32(reader.ReadBytes(4), 0); // Header Offset BitConverter.ToInt32(reader.ReadBytes(4), 0); // User Data Header Size var headerStructure = new TrackerEventStructure(reader); // [0] = Blob, "Heroes of the Storm replay 11" - Strange backward arrow before 11 as well. I don't think the '11' will change, as I believe it was also always '11' in Starcraft 2 replays. replay.ReplayVersion = string.Format("{0}.{1}.{2}.{3}", headerStructure.dictionary[1].dictionary[0].vInt.Value, headerStructure.dictionary[1].dictionary[1].vInt.Value, headerStructure.dictionary[1].dictionary[2].vInt.Value, headerStructure.dictionary[1].dictionary[3].vInt.Value); replay.ReplayBuild = (int)headerStructure.dictionary[1].dictionary[4].vInt.Value; if (replay.ReplayBuild >= 39951) // 'm_dataBuildNum' may have always been incremented for these smaller 'hotfix' patches, but build 39951 is the first time I've noticed where a Hero's available talent selection has changed in one of these smaller patches // We probably want to use this as the most precise build number from now on replay.ReplayBuild = (int)headerStructure.dictionary[6].vInt.Value; // [2] = VInt, Default 2 - m_type replay.Frames = (int)headerStructure.dictionary[3].vInt.Value; // m_elapsedGameLoops // [4] = VInt, Default 0 - m_useScaledTime // [5] = Depending on replay build, either Blob with gibberish, or array of 16 bytes (basically a Blob), also with gibberish. Of ~770 pre-wipe replays, there were only 11 distinct blobs, so this is likely a map version hash or something - 'm_ngdpRootKey' // [6] = Replay Build (Usually the same as what is in [1], but can be incremented by itself for smaller 'hotfix' patches) - m_dataBuildNum // [7] = m_fixedFileHash }
public static Tuple<ReplayParseResult, Replay> ParseReplay(string fileName, bool ignoreErrors, bool deleteFile) { try { var replay = new Replay(); // File in the version numbers for later use. MpqHeader.ParseHeader(replay, fileName); if (!ignoreErrors && replay.ReplayBuild < 32455) return new Tuple<ReplayParseResult, Replay>(ReplayParseResult.PreAlphaWipe, null); using (var archive = new MpqArchive(fileName)) ParseReplayArchive(replay, archive, ignoreErrors); if (deleteFile) File.Delete(fileName); return ParseReplayResults(replay, ignoreErrors); } catch { return new Tuple<ReplayParseResult, Replay>(ReplayParseResult.Exception, null); } }
public static void Parse(Replay replay, byte[] buffer) { using (var memoryStream = new MemoryStream(buffer)) using (var binaryReader = new BinaryReader(memoryStream)) while (memoryStream.Position < memoryStream.Length) { var timeSpan = TimeSpan.FromSeconds((int)(binaryReader.ReadInt32() / 16.0)); // Always 1 or 2 or 3, but doesn't seem to be useful to us // Most of the time is 1 // 3 seems to usually be at the end of the game // This seems to be the 'data' for this event, but doesn't seem to be anything useful binaryReader.ReadByte(); Player player = null; var clientListIndex = binaryReader.ReadByte(); if (clientListIndex != 16 /* Global Event, or event with Observer */) player = replay.ClientList[clientListIndex]; // Team color? // Team 0 is always 255 / 255 / 255 // Team 1 is always 35 / 35 / 35 binaryReader.ReadBytes(3); // Always 255. May be part of the above three bytes binaryReader.ReadByte(); // Player name without BattleTag. For global events, this is an empty string. This can also contain an Observer's player name in a Custom game Encoding.UTF8.GetString(binaryReader.ReadBytes(binaryReader.ReadInt16())); } }
/// <summary> Parses the Replay.Messages.Events file. </summary> /// <param name="buffer"> Buffer containing the contents of the replay.messages.events file. </param> /// <returns> A list of chat messages parsed from the buffer. </returns> public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) { using (var reader = new BinaryReader(stream)) { int totalTime = 0; while (reader.BaseStream.Position < reader.BaseStream.Length) { // While not EOF var message = new ChatMessage(); var time = ParseTimestamp(reader); // sometimes we only have a header for the message if (reader.BaseStream.Position >= reader.BaseStream.Length) break; message.PlayerId = reader.ReadByte(); // I believe this 'PlayerId' is an index for this client list, which can include observers // var player = replay.ClientList[message.PlayerId]; totalTime += time; var opCode = reader.ReadByte(); if (opCode == 0x80) reader.ReadBytes(4); else if (opCode == 0x83) reader.ReadBytes(8); else if (opCode == 2 && message.PlayerId <= 10) { if (message.PlayerId == 80) continue; message.MessageTarget = (ChatMessageTarget)(opCode & 7); var length = reader.ReadByte(); if ((opCode & 8) == 8) length += 64; if ((opCode & 16) == 16) length += 128; message.Message = Encoding.UTF8.GetString(reader.ReadBytes(length)); } else { } if (message.Message != null) { message.Timestamp = new TimeSpan(0, 0, (int)Math.Round(totalTime / 16.0)); replay.ChatMessages.Add(message); } } } } }
/// <summary> Parses the Replay.Messages.Events file. </summary> /// <param name="buffer"> Buffer containing the contents of the replay.messages.events file. </param> /// <returns> A list of chat messages parsed from the buffer. </returns> public static void Parse(Replay replay, byte[] buffer) { var messages = new List<ChatMessage>(); using (var stream = new MemoryStream(buffer)) { using (var reader = new BinaryReader(stream)) { int totalTime = 0; while (reader.BaseStream.Position < reader.BaseStream.Length) { // While not EOF var message = new ChatMessage(); var time = ParseTimestamp(reader); message.PlayerId = reader.ReadByte(); totalTime += time; var opCode = reader.ReadByte(); if (opCode == 0x80) reader.ReadBytes(4); else if (opCode == 0x83) reader.ReadBytes(8); else if (opCode == 2 && message.PlayerId <= 10) { if (message.PlayerId == 80) continue; message.MessageTarget = (ChatMessageTarget)(opCode & 7); var length = reader.ReadByte(); if ((opCode & 8) == 8) length += 64; if ((opCode & 16) == 16) length += 128; message.Message = Encoding.UTF8.GetString(reader.ReadBytes(length)); } else { } if (message.Message != null) { message.Timestamp = new TimeSpan(0, 0, (int)Math.Round(totalTime / 16.0)); messages.Add(message); } } } } replay.ChatMessages = messages; }
/// <summary> Parses the replay.details file, applying it to a Replay object. </summary> /// <param name="replay"> The replay object to apply the parsed information to. </param> /// <param name="buffer"> The buffer containing the replay.details file. </param> public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) using (var reader = new BinaryReader(stream)) { var replayDetailsStructure = new TrackerEventStructure(reader); replay.Players = replayDetailsStructure.dictionary[0].optionalData.array.Select(i => new Player { Name = i.dictionary[0].blobText, BattleNetRegionId = (int)i.dictionary[1].dictionary[0].vInt.Value, BattleNetSubId = (int)i.dictionary[1].dictionary[2].vInt.Value, BattleNetId = (int)i.dictionary[1].dictionary[4].vInt.Value, // [2] = Race (SC2 Remnant, Always Empty String in Heroes of the Storm) Color = i.dictionary[3].dictionary.Keys.OrderBy(j => j).Select(j => (int)i.dictionary[3].dictionary[j].vInt.Value).ToArray(), // [4] = Player Type (2 = Human, 3 = Computer (Practice, Try Me, or Coop)) - This is more accurately gathered in replay.attributes.events Team = (int)i.dictionary[5].vInt.Value, Handicap = (int)i.dictionary[6].vInt.Value, // [7] = VInt, Default 0 IsWinner = i.dictionary[8].vInt.Value == 1, // [9] = Sometimes player index in ClientList array; usually 0-9, but can be higher if there are observers. I don't fully understand this, as this was incorrect in at least one Custom game, where this said ClientList[8] was null Character = i.dictionary[10].blobText }).ToArray(); if (replay.Players.Length != 10 || replay.Players.Count(i => i.IsWinner) != 5) // Try Me Mode, or something strange return; replay.Map = replayDetailsStructure.dictionary[1].blobText; // [2] - m_difficulty // [3] - m_thumbnail - "Minimap.tga", "CustomMiniMap.tga", etc // [4] - m_isBlizzardMap replay.Timestamp = DateTime.FromFileTimeUtc(replayDetailsStructure.dictionary[5].vInt.Value); // m_timeUTC // There was a bug during the below builds where timestamps were buggy for the Mac build of Heroes of the Storm // The replay, as well as viewing these replays in the game client, showed years such as 1970, 1999, etc // I couldn't find a way to get the correct timestamp, so I am just estimating based on when these builds were live if (replay.ReplayBuild == 34053 && replay.Timestamp < new DateTime(2015, 2, 8)) replay.Timestamp = new DateTime(2015, 2, 13); else if (replay.ReplayBuild == 34190 && replay.Timestamp < new DateTime(2015, 2, 15)) replay.Timestamp = new DateTime(2015, 2, 20); // [6] - m_timeLocalOffset - For Windows replays, this is Utc offset. For Mac replays, this is actually the entire Local Timestamp // [7] - m_description - Empty String // [8] - m_imageFilePath - Empty String // [9] - m_mapFileName - Empty String // [10] - m_cacheHandles - "s2ma" // [11] - m_miniSave - 0 // [12] - m_gameSpeed - 4 // [13] - m_defaultDifficulty - Usually 1 or 7 // [14] - m_modPaths - Null // [15] - m_campaignIndex - 0 // [16] - m_restartAsTransitionMap - 0 } }
static void Main(string[] args) { var heroesAccountsFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), @"Heroes of the Storm\Accounts"); var randomReplayFileName = Directory.GetFiles(heroesAccountsFolder, "*.StormReplay", SearchOption.AllDirectories).OrderBy(i => Guid.NewGuid()).First(); // Use temp directory for MpqLib directory permissions requirements var tmpPath = Path.GetTempFileName(); File.Copy(randomReplayFileName, tmpPath, true); try { // Create our Replay object: this object will be filled as you parse the different files in the .StormReplay archive var replay = new Replay(); MpqHeader.ParseHeader(replay, tmpPath); using (var archive = new CArchive(tmpPath)) { ReplayInitData.Parse(replay, GetMpqArchiveFileBytes(archive, ReplayInitData.FileName)); ReplayDetails.Parse(replay, GetMpqArchiveFileBytes(archive, ReplayDetails.FileName)); ReplayTrackerEvents.Parse(replay, GetMpqArchiveFileBytes(archive, ReplayTrackerEvents.FileName)); ReplayAttributeEvents.Parse(replay, GetMpqArchiveFileBytes(archive, ReplayAttributeEvents.FileName)); if (replay.ReplayBuild >= 32455) ReplayGameEvents.Parse(replay, GetMpqArchiveFileBytes(archive, ReplayGameEvents.FileName)); ReplayServerBattlelobby.Parse(replay, GetMpqArchiveFileBytes(archive, ReplayServerBattlelobby.FileName)); ReplayMessageEvents.Parse(replay, GetMpqArchiveFileBytes(archive, ReplayMessageEvents.FileName)); Unit.ParseUnitData(replay); } // Our Replay object now has all currently available information var playerDictionary = new Dictionary<int, Player>(); Console.WriteLine("Replay Build: " + replay.ReplayBuild); Console.WriteLine("Map: " + replay.Map); foreach (var player in replay.Players.OrderByDescending(i => i.IsWinner)) { playerDictionary[player.PlayerId] = player; Console.WriteLine("Player: " + player.Name + ", Win: " + player.IsWinner + ", Hero: " + player.Character + ", Lvl: " + player.CharacterLevel + (replay.ReplayBuild >= 32524 ? ", Talents: " + string.Join(",", player.Talents.OrderBy(i => i)) : "")); } foreach (var message in replay.ChatMessages) if (playerDictionary.ContainsKey(message.PlayerId)) Console.WriteLine(playerDictionary[message.PlayerId].Name + ": " + message.Message); Console.WriteLine("Press Any Key to Close"); Console.Read(); } finally { if (File.Exists(tmpPath)) File.Delete(tmpPath); } }
public static void Parse(Replay replay, byte[] buffer) { var headerSize = 5; var numAttributes = BitConverter.ToInt32(buffer, headerSize); var attributes = new ReplayAttribute[numAttributes]; var initialOffset = 4 + headerSize; for (int i = 0; i < numAttributes; i++) attributes[i] = ReplayAttribute.Parse(buffer, initialOffset + (i*13)); new ReplayAttributeEvents { Attributes = attributes.OrderBy(i => i.AttributeType).ToArray() }.ApplyAttributes(replay); /* var stringList = attributes.OrderBy(i => i.AttributeType); Console.WriteLine(stringList.Count()); */ }
private static void ParseHeader(Replay replay, BinaryReader reader) { reader.ReadBytes(3); // 'Magic' reader.ReadByte(); // Format BitConverter.ToInt32(reader.ReadBytes(4), 0); // Data Max Size BitConverter.ToInt32(reader.ReadBytes(4), 0); // Header Offset BitConverter.ToInt32(reader.ReadBytes(4), 0); // User Data Header Size var headerStructure = new TrackerEventStructure(reader); // [0] = Blob, "Heroes of the Storm replay 11" - Strange backward arrow before 11 as well. I don't think the '11' will change, as I believe it was also always '11' in Starcraft 2 replays. replay.ReplayVersion = string.Format("{0}.{1}.{2}.{3}", headerStructure.dictionary[1].dictionary[0].vInt.Value, headerStructure.dictionary[1].dictionary[1].vInt.Value, headerStructure.dictionary[1].dictionary[2].vInt.Value, headerStructure.dictionary[1].dictionary[3].vInt.Value); replay.ReplayBuild = (int)headerStructure.dictionary[1].dictionary[4].vInt.Value; // [2] = VInt, Default 2 // [3] = VInt, Frame Count (Very similar, though slightly different, than frame count from tracker event frame delta sum) // [4] = VInt, Default 0 // [5] = Depending on replay build, either Blob with gibberish, or array of 16 bytes (basically a Blob), also with gibberish. Of ~770 pre-wipe replays, there were only 11 distinct blobs, so this is likely a map version hash or something // [6] = Replay Build (Same as what is in [1]) }
public static Tuple<ReplayParseResult, Replay> ParseReplay(byte[] bytes, bool ignoreErrors = false) { try { var replay = new Replay(); // File in the version numbers for later use. MpqHeader.ParseHeader(replay, bytes); if (!ignoreErrors && replay.ReplayBuild < 32455) return new Tuple<ReplayParseResult, Replay>(ReplayParseResult.PreAlphaWipe, null); using (var memoryStream = new MemoryStream(bytes)) using (var archive = new MpqArchive(memoryStream)) ParseReplayArchive(replay, archive, ignoreErrors); return ParseReplayResults(replay, ignoreErrors); } catch { return new Tuple<ReplayParseResult, Replay>(ReplayParseResult.Exception, null); } }
private static Tuple<ReplayParseResult, Replay> ParseReplayResults(Replay replay, bool ignoreErrors) { if (ignoreErrors) return new Tuple<ReplayParseResult, Replay>(ReplayParseResult.UnexpectedResult, replay); else if (replay.Players.Length == 1) // Filter out 'Try Me' games, as they have unusual format that throws exceptions in other areas return new Tuple<ReplayParseResult, Replay>(ReplayParseResult.TryMeMode, null); else if (replay.Players.Length == 5) // Custom game with all computer players on the opposing team won't register them as players at all (Noticed at build 34053) return new Tuple<ReplayParseResult, Replay>(ReplayParseResult.ComputerPlayerFound, null); else if (replay.Players.All(i => !i.IsWinner) || replay.ReplayLength.TotalMinutes < 2) return new Tuple<ReplayParseResult, Replay>(ReplayParseResult.Incomplete, null); else if (replay.Timestamp < new DateTime(2014, 10, 6, 0, 0, 0, DateTimeKind.Utc)) return new Tuple<ReplayParseResult, Replay>(ReplayParseResult.PreAlphaWipe, null); else if (replay.Players.Any(i => i.PlayerType == PlayerType.Computer || i.Character == "Random Hero" || i.Name.Contains(' '))) return new Tuple<ReplayParseResult, Replay>(ReplayParseResult.ComputerPlayerFound, null); else if (replay.Players.Any(i => i.BattleNetRegionId >= 90 /* PTR/Test Region */)) return new Tuple<ReplayParseResult, Replay>(ReplayParseResult.PTRRegion, null); else if (replay.Players.Count(i => i.IsWinner) != 5 || replay.Players.Length != 10 || (replay.GameMode != GameMode.TeamLeague && replay.GameMode != GameMode.HeroLeague && replay.GameMode != GameMode.QuickMatch && replay.GameMode != GameMode.Custom)) return new Tuple<ReplayParseResult, Replay>(ReplayParseResult.UnexpectedResult, null); else return new Tuple<ReplayParseResult, Replay>(ReplayParseResult.Success, replay); }
static void Main(string[] args) { var path = args[0]; // Use temp directory for MpqLib directory permissions requirements var tmpPath = Path.GetTempFileName(); File.Copy(path, tmpPath, true); try { // Create our Replay object: this object will be filled as you parse the different files in the .StormReplay archive var replay = new Replay(); MpqHeader.ParseHeader(replay, tmpPath); using (var archive = new CArchive(tmpPath)) { ReplayInitData.Parse(replay, GetMpqArchiveFileBytes(archive, "replay.initData")); ReplayDetails.Parse(replay, GetMpqArchiveFileBytes(archive, "replay.details")); ReplayTrackerEvents.Parse(replay, GetMpqArchiveFileBytes(archive, "replay.tracker.events")); ReplayAttributeEvents.Parse(replay, GetMpqArchiveFileBytes(archive, "replay.attributes.events")); if (replay.ReplayBuild >= 32455) ReplayGameEvents.Parse(replay, GetMpqArchiveFileBytes(archive, "replay.game.events")); ReplayServerBattlelobby.Parse(replay, GetMpqArchiveFileBytes(archive, "replay.server.battlelobby")); Unit.ParseUnitData(replay); } // Our Replay object now has all currently available information Console.WriteLine(JsonConvert.SerializeObject(replay, Formatting.None, new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects, NullValueHandling = NullValueHandling.Ignore })); } finally { if (File.Exists(tmpPath)) File.Delete(tmpPath); } }
/// <summary> Parses the replay.tracker.events file, applying it to a Replay object. </summary> /// <param name="replay"> The replay object to apply the parsed information to. </param> /// <param name="buffer"> The buffer containing the replay.tracker.events file. </param> /// <param name="parseUnitData"> Determines whether or not to parse unit data </param> public static void Parse(Replay replay, byte[] buffer) { replay.TrackerEvents = new List<TrackerEvent>(); var currentFrameCount = 0; using (var stream = new MemoryStream(buffer)) using (var reader = new BinaryReader(stream)) while (stream.Position < stream.Length) { var intro = reader.ReadBytes(3); // Always 03 00 09 (Edit: Middle digit seems to have at least two possible values) if (intro[0] != 3 || /* intro[1] != 0 || */ intro[2] != 9) throw new Exception("Unexpected data in tracker event"); currentFrameCount += (int)TrackerEventStructure.read_vint(reader); var trackerEvent = new TrackerEvent { TimeSpan = new TimeSpan(0, 0, (int)(currentFrameCount / 16.0)) }; intro = reader.ReadBytes(1); // Always 09 if (intro[0] != 9) throw new Exception("Unexpected data in tracker event"); trackerEvent.TrackerEventType = (TrackerEventType)TrackerEventStructure.read_vint(reader); trackerEvent.Data = new TrackerEventStructure(reader); replay.TrackerEvents.Add(trackerEvent); } replay.Frames = currentFrameCount; replay.ReplayLength = replay.TrackerEvents.Last().TimeSpan; // Populate the client list using player indexes var playerIndexes = replay.TrackerEvents.Where(i => i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.PlayerSetupEvent && i.Data.dictionary[2].optionalData != null).Select(i => i.Data.dictionary[2].optionalData.vInt.Value).Distinct().OrderBy(i => i).ToArray(); for (var i = 0; i < playerIndexes.Length; i++) // The references between both of these classes are the same on purpose. // We want updates to one to propogate to the other. replay.ClientList[playerIndexes[i]] = replay.Players[i]; }
/// <summary> Parses the replay.server.battlelobby file in a replay file. </summary> /// <param name="replay"> The replay file to apply the parsed data to. </param> /// <param name="buffer"> The buffer containing the replay.server.battlelobby file. </param> public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) { var bitReader = new BitReader(stream); // 52124 and 52381 are non-tested ptr builds if (replay.ReplayBuild < 38793 || replay.ReplayBuild == 52124 || replay.ReplayBuild == 52381 || replay.GameMode == GameMode.Unknown) { GetBattleTags(replay, bitReader); return; } int s2mArrayLength = bitReader.ReadByte(); int stringLength = bitReader.ReadByte(); bitReader.ReadString(stringLength); for (var i = 1; i < s2mArrayLength; i++) { bitReader.Read(16); bitReader.ReadString(stringLength); } if (bitReader.ReadByte() != s2mArrayLength) { throw new Exception("s2ArrayLength not equal"); } for (var i = 0; i < s2mArrayLength; i++) { bitReader.ReadString(4); // s2m bitReader.ReadBytes(2); // 0x00 0x00 bitReader.ReadString(2); // Realm bitReader.ReadBytes(32); } if (replay.ReplayBuild < 55929) { // seems to be in all replays bitReader.ReadInt16(); bitReader.stream.Position = bitReader.stream.Position + 684; // seems to be in all replays bitReader.ReadInt16(); bitReader.stream.Position = bitReader.stream.Position + 1944; if (bitReader.ReadString(8) != "HumnComp") { throw new Exception("Not HumnComp"); } } bitReader.stream.Position = bitReader.stream.Position = bitReader.stream.Position + 19859; //// next section is language libraries? //// --------------------------------------- //for (int i = 0; ; i++) // no idea how to determine the count //{ // if (bitReader.ReadString(4).Substring(0, 2) != "s2") // s2mv; not sure if its going to be 'mv' all the time // { // bitReader.stream.Position = bitReader.stream.Position - 4; // break; // } // bitReader.ReadBytes(2); // 0x00 0x00 // bitReader.ReadString(2); // Realm // bitReader.ReadBytes(32); //} //bitReader.Read(32); //bitReader.Read(8); //bitReader.ReadByte(); //for (int i = 0; ; i++) // no idea how to determine the count //{ // if (bitReader.ReadString(4).Substring(0, 2) != "s2") // s2ml // { // bitReader.stream.Position = bitReader.stream.Position - 4; // break; // } // bitReader.ReadBytes(2); // 0x00 0x00 // bitReader.ReadString(2); // Realm // bitReader.ReadBytes(32); //} //for (int k = 0; k < 11; k++) //{ // // ruRU, zhCN, plPL, esMX, frFR, esES // // ptBR, itIT, enUs, deDe, koKR // bitReader.ReadString(4); // bitReader.ReadByte(); // for (int i = 0; ; i++) // { // if (bitReader.ReadString(4).Substring(0, 2) != "s2") // s2ml // { // bitReader.stream.Position = bitReader.stream.Position - 4; // break; // } // bitReader.ReadString(4); // s2ml // bitReader.ReadBytes(2); // 0x00 0x00 // bitReader.ReadString(2); // Realm // bitReader.ReadBytes(32); // } //} // new section, can't find a pattern // has blizzmaps#1, Hero, s2mv // -------------------- //bitReader.ReadBytes(8); // all 0x00 for (;;) { // we're just going to skip all the way down to the s2mh if (bitReader.ReadString(4) == "s2mh") { bitReader.stream.Position = bitReader.stream.Position - 4; break; } else { bitReader.stream.Position = bitReader.stream.Position - 3; } } for (var i = 0; i < s2mArrayLength; i++) { bitReader.ReadString(4); // s2mh bitReader.ReadBytes(2); // 0x00 0x00 bitReader.ReadString(2); // Realm bitReader.ReadBytes(32); } // Player collections - starting with HOTS 2.0 (live build 52860) // -------------------------------------------------------------- List <string> playerCollection = new List <string>(); int collectionSize = 0; if (replay.ReplayBuild >= 48027) { collectionSize = bitReader.ReadInt16(); } else { collectionSize = bitReader.ReadInt32(); } if (collectionSize > 5000) { throw new Exception("collectionSize is an unusually large number"); } for (int i = 0; i < collectionSize; i++) { if (replay.ReplayBuild >= 55929) { bitReader.ReadBytes(8); } else { playerCollection.Add(bitReader.ReadString(bitReader.ReadByte())); } } // use to determine if the collection item is usable by the player (owns/free to play/internet cafe) if (bitReader.ReadInt32() != collectionSize) { throw new Exception("skinArrayLength not equal"); } for (int i = 0; i < collectionSize; i++) { for (int j = 0; j < 16; j++) // 16 is total player slots { bitReader.ReadByte(); var num = bitReader.Read(8); if (replay.ReplayBuild < 55929) { if (replay.ClientListByUserID[j] != null) { if (num > 0) { replay.ClientListByUserID[j].PlayerCollectionDictionary.Add(playerCollection[i], true); } else if (num == 0) { replay.ClientListByUserID[j].PlayerCollectionDictionary.Add(playerCollection[i], false); } else { throw new NotImplementedException(); } } } } } // Player info // ------------------------ if (replay.ReplayBuild <= 43259 || replay.ReplayBuild == 47801) { // Builds that are not yet supported for detailed parsing // build 47801 is a ptr build that had new data in the battletag section, the data was changed in 47944 (patch for 47801) GetBattleTags(replay, bitReader); return; } bitReader.ReadInt32(); // m_randomSeed bitReader.ReadBytes(32); bitReader.ReadInt32(); // 0x19 if (replay.ReplayBuild <= 47479 || replay.ReplayBuild == 47903) { ExtendedBattleTagParsingOld(replay, bitReader); return; } for (int player = 0; player < replay.ClientListByUserID.Length; player++) { if (replay.ClientListByUserID[player] == null) { break; } string TId; if (player == 0) { var offset = bitReader.ReadByte(); bitReader.ReadString(2); // T: TId = bitReader.ReadString(12 + offset); } else { ReadByte0x00(bitReader); ReadByte0x00(bitReader); ReadByte0x00(bitReader); bitReader.Read(6); // get XXXXXXXX#YYY TId = Encoding.UTF8.GetString(ReadSpecialBlob(bitReader, 8)); } replay.ClientListByUserID[player].BattleNetTId = TId; // next 18 bytes bitReader.ReadBytes(4); // same for all players bitReader.ReadBytes(26); // repeat of the collection section above if (replay.ReplayBuild >= 51609) { int size = (int)bitReader.Read(12); // 3 bytes max 4095 if (size != collectionSize) { throw new Exception("size and collectionSize not equal"); } int bytesSize = collectionSize / 8; int bitsSize = (collectionSize % 8) + 2; // two additional unknown bits bitReader.ReadBytes(bytesSize); bitReader.Read(bitsSize); } else { if (replay.ReplayBuild >= 48027) { bitReader.ReadInt16(); } else { bitReader.ReadInt32(); } // each byte has a max value of 0x7F (127) bitReader.stream.Position = bitReader.stream.Position = bitReader.stream.Position + (collectionSize * 2); bitReader.Read(1); } if (bitReader.ReadBoolean()) { // use this to determine who is in a party // those in the same party will have the same exact 8 bytes of data // the party leader is the first one (in the order of the client list) replay.ClientListByUserID[player].PartyValue = bitReader.ReadInt32() + bitReader.ReadInt32(); } bitReader.Read(1); var battleTag = Encoding.UTF8.GetString(bitReader.ReadBlobPrecededWithLength(7)).Split('#'); // battleTag <name>#xxxxx if (battleTag.Length != 2 || battleTag[0] != replay.ClientListByUserID[player].Name) { throw new Exception("Couldn't find BattleTag"); } replay.ClientListByUserID[player].BattleTag = int.Parse(battleTag[1]); if (replay.ReplayBuild >= 52860 || (replay.ReplayVersionMajor == 2 && replay.ReplayBuild >= 51978)) { replay.ClientListByUserID[player].AccountLevel = bitReader.ReadInt32(); // player's account level, not available in custom games } bitReader.ReadBytes(27); // these similar bytes don't occur for last player } // some more data after this } }
public static void ParseUnitData(Replay replay) { // Get array of units from 'UnitBornEvent' replay.Units = replay.TrackerEvents.Where(i => i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitBornEvent).Select(i => new Unit { UnitID = Unit.GetUnitID((int)i.Data.dictionary[0].vInt.Value, (int)i.Data.dictionary[1].vInt.Value), Name = i.Data.dictionary[2].blobText, Group = Unit.UnitGroupDictionary.ContainsKey(i.Data.dictionary[2].blobText) ? Unit.UnitGroupDictionary[i.Data.dictionary[2].blobText] : Unit.UnitGroup.Unknown, TimeSpanBorn = i.TimeSpan, Team = i.Data.dictionary[3].vInt.Value == 11 || i.Data.dictionary[3].vInt.Value == 12 ? (int)i.Data.dictionary[3].vInt.Value - 11 : i.Data.dictionary[3].vInt.Value > 0 && i.Data.dictionary[3].vInt.Value <= 10 ? replay.Players[i.Data.dictionary[3].vInt.Value - 1].Team : (int?)null, PlayerControlledBy = i.Data.dictionary[3].vInt.Value > 0 && i.Data.dictionary[3].vInt.Value <= 10 ? replay.Players[i.Data.dictionary[3].vInt.Value - 1] : null, PointBorn = new Point { X = (int)i.Data.dictionary[5].vInt.Value, Y = (int)i.Data.dictionary[6].vInt.Value } }) .ToList(); // Add in information on unit deaths from 'UnitDiedEvent' var unitsDictionary = replay.Units.ToDictionary(i => i.UnitID, i => i); foreach (var unitDiedEvent in replay.TrackerEvents.Where(i => i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitDiedEvent).Select(i => new { UnitID = Unit.GetUnitID((int)i.Data.dictionary[0].vInt.Value, (int)i.Data.dictionary[1].vInt.Value), TimeSpanDied = i.TimeSpan, PlayerIDKilledBy = i.Data.dictionary[2].optionalData != null ? (int)i.Data.dictionary[2].optionalData.vInt.Value : (int?)null, PointDied = new Point { X = (int)i.Data.dictionary[3].vInt.Value, Y = (int)i.Data.dictionary[4].vInt.Value }, UnitKilledBy = i.Data.dictionary[5].optionalData != null ? unitsDictionary[Unit.GetUnitID((int)i.Data.dictionary[5].optionalData.vInt.Value, (int)i.Data.dictionary[6].optionalData.vInt.Value)] : null })) { var unitThatDied = unitsDictionary[unitDiedEvent.UnitID]; unitThatDied.TimeSpanDied = unitDiedEvent.TimeSpanDied; unitThatDied.PlayerKilledBy = unitDiedEvent.PlayerIDKilledBy.HasValue && unitDiedEvent.PlayerIDKilledBy.Value > 0 && unitDiedEvent.PlayerIDKilledBy.Value <= 10 ? replay.Players[unitDiedEvent.PlayerIDKilledBy.Value - 1] : null; unitThatDied.PointDied = unitDiedEvent.PointDied; unitThatDied.UnitKilledBy = unitDiedEvent.UnitKilledBy; // Sometimes 'PlayerIDKilledBy' will be outside of the range of players (1-10) // Minions that are killed by other minions or towers will have the 'team' that killed them in this field (11 or 12) // Some other units have interesting values I don't fully understand yet. For example, 'ItemCannonball' (the coins on Blackheart's Bay) will have 0 or 15 in this field. I'm guessing this is also which team acquires them, which may be useful // Other map objectives may also have this. I'll look into this more in the future. /* if (unitDiedEvent.PlayerIDKilledBy.HasValue && unitThatDied.PlayerKilledBy == null) Console.WriteLine(""); */ } // Add in information on unit ownership changes from 'UnitOwnerChangeEvent' (For example, players grabbing regen globes or a player grabbing a Garden Terror) foreach (var unitOwnerChangeEvent in replay.TrackerEvents.Where(i => i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitOwnerChangeEvent).Select(i => new { UnitID = Unit.GetUnitID((int)i.Data.dictionary[0].vInt.Value, (int)i.Data.dictionary[1].vInt.Value), TimeSpanOwnerChanged = i.TimeSpan, Team = i.Data.dictionary[2].vInt.Value == 11 || i.Data.dictionary[2].vInt.Value == 12 ? (int)i.Data.dictionary[2].vInt.Value - 11 : (int?)null, PlayerNewOwner = i.Data.dictionary[2].vInt.Value > 0 && i.Data.dictionary[2].vInt.Value <= 10 ? replay.Players[i.Data.dictionary[2].vInt.Value - 1] : null })) unitsDictionary[unitOwnerChangeEvent.UnitID].OwnerChangeEvents.Add(new OwnerChangeEvent { TimeSpanOwnerChanged = unitOwnerChangeEvent.TimeSpanOwnerChanged, Team = unitOwnerChangeEvent.Team ?? (unitOwnerChangeEvent.PlayerNewOwner != null ? unitOwnerChangeEvent.PlayerNewOwner.Team : (int?)null), PlayerNewOwner = unitOwnerChangeEvent.PlayerNewOwner }); // For simplicity, I set extra fields on units that are not initially controlled by a player, and only have one owner change event foreach (var unitWithOneOwnerChange in replay.Units.Where(i => i.OwnerChangeEvents.Count() == 1 && i.PlayerControlledBy == null)) { var singleOwnerChangeEvent = unitWithOneOwnerChange.OwnerChangeEvents.Single(); if (singleOwnerChangeEvent.PlayerNewOwner != null) { unitWithOneOwnerChange.PlayerControlledBy = singleOwnerChangeEvent.PlayerNewOwner; unitWithOneOwnerChange.TimeSpanAcquired = singleOwnerChangeEvent.TimeSpanOwnerChanged; unitWithOneOwnerChange.OwnerChangeEvents.Clear(); } } // Add in information from the 'UnitPositionEvent' // We need to go through the replay file in order because unit IDs are recycled var activeUnits = new Dictionary<int, Unit>(); foreach (var unitPositionEvent in replay.TrackerEvents.Where(i => i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitBornEvent || i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitPositionsEvent).OrderBy(i => i.TimeSpan)) if (unitPositionEvent.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitBornEvent) activeUnits[(int)unitPositionEvent.Data.dictionary[0].vInt.Value] = unitsDictionary[Unit.GetUnitID((int)unitPositionEvent.Data.dictionary[0].vInt.Value, (int)unitPositionEvent.Data.dictionary[1].vInt.Value)]; else { var currentUnitIndex = (int)unitPositionEvent.Data.dictionary[0].vInt.Value; for (var i = 0; i < unitPositionEvent.Data.dictionary[1].array.Length; i++) { currentUnitIndex += (int)unitPositionEvent.Data.dictionary[1].array[i++].vInt.Value; activeUnits[currentUnitIndex].Positions.Add(new Position { TimeSpan = unitPositionEvent.TimeSpan, Point = new Point { X = (int)unitPositionEvent.Data.dictionary[1].array[i++].vInt.Value, Y = (int)unitPositionEvent.Data.dictionary[1].array[i].vInt.Value } }); } } // Add an array of Hero units to each player // Currently I'm only getting single heroes (Lost Vikings not yet supported) var earlyGameTimeSpan = new TimeSpan(0, 0, 10); var heroUnitsDictionary = replay.Players.Where(i => replay.Units.Count(j => j.TimeSpanBorn < earlyGameTimeSpan && j.PlayerControlledBy == i && j.Name.StartsWith("Hero")) == 1).ToDictionary(i => i, i => replay.Units.Single(j => j.TimeSpanBorn < earlyGameTimeSpan && j.PlayerControlledBy == i && j.Name.StartsWith("Hero"))); foreach (var player in replay.Players) if (heroUnitsDictionary.ContainsKey(player)) player.HeroUnits = new[] { heroUnitsDictionary[player] }; // Add derived hero positions from associated unit born/acquired/died info // These are accurate positions: Picking up regen globes, spawning Locusts, etc // For Abathur locusts, we need to make sure they aren't spawning from a locust nest (Level 20 talent) var abathurLocustUnits = replay.Units.Where(i => i.Name == "AbathurLocustNormal" || i.Name == "AbathurLocustAssaultStrain" || i.Name == "AbathurLocustBombardStrain").ToList(); if (abathurLocustUnits.Any() && replay.Units.Any(i => i.Name == "AbathurLocustNest")) { var abathurLocustNests = replay.Units.Where(i => i.Name == "AbathurLocustNest"); foreach (var abathurLocustUnit in abathurLocustUnits.ToArray()) if (abathurLocustNests.Any(i => i.TimeSpanBorn <= abathurLocustUnit.TimeSpanBorn && (!i.TimeSpanDied.HasValue || i.TimeSpanDied >= abathurLocustUnit.TimeSpanBorn) && i.PointBorn.DistanceTo(abathurLocustUnit.PointBorn) <= 3)) abathurLocustUnits.Remove(abathurLocustUnit); } foreach (var unit in replay.Units.Where(i => Unit.UnitBornProvidesLocationForOwner.ContainsKey(i.Name) || i.Group == Unit.UnitGroup.HeroTalentSelection).Union(abathurLocustUnits).Where(i => heroUnitsDictionary.ContainsKey(i.PlayerControlledBy))) heroUnitsDictionary[unit.PlayerControlledBy].Positions.Add(new Position { TimeSpan = unit.TimeSpanBorn, Point = unit.PointBorn }); foreach (var unit in replay.Units.Where(i => Unit.UnitOwnerChangeProvidesLocationForOwner.ContainsKey(i.Name) && i.PlayerControlledBy != null).Where(i => heroUnitsDictionary.ContainsKey(i.PlayerControlledBy))) heroUnitsDictionary[unit.PlayerControlledBy].Positions.Add(new Position { TimeSpan = unit.TimeSpanAcquired.Value, Point = unit.PointBorn }); // Use 'CCmdUpdateTargetUnitEvent' to find an accurate location of units targeted // Excellent for finding frequent, accurate locations of heroes during team fights foreach (var updateTargetUnitEvent in replay.GameEvents.Where(i => i.eventType == GameEventType.CCmdUpdateTargetUnitEvent)) if (replay.Units.Any(i => i.UnitID == (int)updateTargetUnitEvent.data.array[2].unsignedInt.Value)) replay.Units.Single(i => i.UnitID == (int)updateTargetUnitEvent.data.array[2].unsignedInt.Value).Positions.Add(new Position { TimeSpan = updateTargetUnitEvent.TimeSpan, Point = Point.FromEventFormat( updateTargetUnitEvent.data.array[6].array[0].unsignedInt.Value, updateTargetUnitEvent.data.array[6].array[1].unsignedInt.Value) }); // Add in 'accurate' positions for each player's death, which sends them to their spawn point // Special Exceptions: // Uther: Level 20 respawn talent: Doesn't display the death animation when respawning, so probably doesn't count as a death in this situation. This is actually probably the best situation for us // Diablo: Fast respawn if he has enough souls. Not yet able to detect when this occurs // Murky: Respawns to his egg if his egg is alive when he dies // Lost Vikings: Individual Vikings spawn 25% faster per their trait, and 50% faster with a talent, but currently we aren't able to track their deaths individually foreach (var player in replay.Players.Where(i => i.HeroUnits.Length == 1 && i.Deaths.Length > 0)) { var fullTimerDeaths = new List<TimeSpan>(); if (player.HeroUnits[0].Name == "HeroMurky") { // Gather a list of the eggs Murky has placed throughout the game var murkyEggs = replay.Units.Where(i => i.PlayerControlledBy == player && i.Name == "MurkyRespawnEgg").OrderBy(i => i.TimeSpanBorn).ToArray(); var currentEggIndex = 0; foreach (var murkyDeath in player.Deaths) { // If Murky respawns at the egg, it will be 5 seconds after his death var murkyRespawnFromEggTimeSpan = murkyDeath.Add(TimeSpan.FromSeconds(5)); for (; currentEggIndex < murkyEggs.Length; currentEggIndex++) { if (murkyRespawnFromEggTimeSpan > murkyEggs[currentEggIndex].TimeSpanDied && currentEggIndex < murkyEggs.Length + 1) continue; // Check to see if there is an egg alive when Murky would respawn if (murkyRespawnFromEggTimeSpan >= murkyEggs[currentEggIndex].TimeSpanBorn && (!murkyEggs[currentEggIndex].TimeSpanDied.HasValue || murkyRespawnFromEggTimeSpan <= murkyEggs[currentEggIndex].TimeSpanDied.Value)) for (; murkyRespawnFromEggTimeSpan >= murkyDeath; murkyRespawnFromEggTimeSpan = murkyRespawnFromEggTimeSpan.Add(TimeSpan.FromSeconds(-1))) player.HeroUnits[0].Positions.Add(new Position { TimeSpan = murkyRespawnFromEggTimeSpan, Point = murkyEggs[currentEggIndex].PointBorn, IsEstimated = false }); else // Murky did not respawn at egg - give him the normal death timer fullTimerDeaths.Add(murkyDeath); break; } } } else fullTimerDeaths.AddRange(player.Deaths); // Normal death timer deaths // This is all deaths for most heroes, and Murky deaths if he didn't respawn from his egg if (fullTimerDeaths.Count != 0) { // Add a 'Position' at the player spawn when the death occurs player.HeroUnits[0].Positions.AddRange(fullTimerDeaths.Select(i => new Position { TimeSpan = i, Point = player.HeroUnits[0].PointBorn, IsEstimated = false })); // Add a 'Position' at the player spawn when the hero respawns if (player.HeroUnits[0].Name == "HeroDiablo") // Currently not able to tell if Diablo has a fast respawn - because of this we just always assume he does respawn quickly player.HeroUnits[0].Positions.AddRange(fullTimerDeaths.Select(i => new Position { TimeSpan = i.Add(TimeSpan.FromSeconds(5)), Point = player.HeroUnits[0].PointBorn, IsEstimated = false })); else { var currentTeamLevelMilestoneIndex = 1; foreach (var playerDeath in fullTimerDeaths) for (; currentTeamLevelMilestoneIndex < replay.TeamLevelMilestones[player.Team].Length; currentTeamLevelMilestoneIndex++) { Position spawnPosition = null; if (playerDeath < replay.TeamLevelMilestones[player.Team][currentTeamLevelMilestoneIndex]) spawnPosition = new Position { TimeSpan = playerDeath.Add(TimeSpan.FromSeconds(HeroDeathTimersByTeamLevelInSecondsForTalentLevels[currentTeamLevelMilestoneIndex - 1])), Point = player.HeroUnits[0].PointBorn, IsEstimated = false }; else if (currentTeamLevelMilestoneIndex == replay.TeamLevelMilestones[player.Team].Length - 1) spawnPosition = new Position { TimeSpan = playerDeath.Add(TimeSpan.FromSeconds(HeroDeathTimersByTeamLevelInSecondsForTalentLevels[currentTeamLevelMilestoneIndex])), Point = player.HeroUnits[0].PointBorn, IsEstimated = false }; if (spawnPosition != null) { var deathTimeSpan = playerDeath; while (deathTimeSpan < spawnPosition.TimeSpan) { // Add a 'Position' at the player spawn for every second the player is dead, to make sure we don't add 'estimated' positions during this time player.HeroUnits[0].Positions.Add(new Position { TimeSpan = deathTimeSpan, Point = player.HeroUnits[0].PointBorn, IsEstimated = false }); deathTimeSpan = deathTimeSpan.Add(TimeSpan.FromSeconds(1)); } player.HeroUnits[0].Positions.Add(spawnPosition); break; } } } } player.HeroUnits[0].Positions = player.HeroUnits[0].Positions.OrderBy(i => i.TimeSpan).ToList(); } // Estimate Hero positions from CCmdEvent and CCmdUpdateTargetPointEvent (Movement points) { // List of Hero units (Excluding heroes with multiple units like Lost Vikings - not sure how to handle those) // This is different from the above dictionary in that it excludes Abathur if he chooses the clone hero talent // It's okay to not estimate Abathur's position, as he rarely moves and we also get an accurate position each time he spawns a locust heroUnitsDictionary = replay.Players.Where(i => replay.Units.Count(j => j.PlayerControlledBy == i && j.Name.StartsWith("Hero")) == 1).ToDictionary(i => i, i => replay.Units.Single(j => j.PlayerControlledBy == i && j.Name.StartsWith("Hero"))); // This is a list of 'HeroUnit', 'TimeSpan', and 'EventPosition' for each CCmdEvent where ability data is null and a position is included var heroCCmdEventLists = replay.GameEvents.Where(i => i.eventType == GameEventType.CCmdEvent && i.data.array[1] == null && i.data.array[2] != null && i.data.array[2].array.Length == 3 && heroUnitsDictionary.ContainsKey(i.player)).Select(i => new { HeroUnit = heroUnitsDictionary[i.player], Position = new Position { TimeSpan = i.TimeSpan, Point = Point.FromEventFormat(i.data.array[2].array[0].unsignedInt.Value, i.data.array[2].array[1].unsignedInt.Value), IsEstimated = true } }) .GroupBy(i => i.HeroUnit) .Select(i => new { HeroUnit = i.Key, // Take the latest applicable CCmdEvent or CCmdUpdateTargetPointEvent if there are more than one in a second Positions = i.Select(j => j.Position).Union(replay.GameEvents.Where(j => j.player == i.Key.PlayerControlledBy && j.eventType == GameEventType.CCmdUpdateTargetPointEvent).Select(j => new Position { TimeSpan = j.TimeSpan, Point = Point.FromEventFormat(j.data.array[0].unsignedInt.Value, j.data.array[1].unsignedInt.Value), IsEstimated = true })).GroupBy(j => (int)j.TimeSpan.TotalSeconds).Select(j => j.OrderByDescending(k => k.TimeSpan).First()).OrderBy(j => j.TimeSpan).ToArray() }); const double PlayerSpeedUnitsPerSecond = 5.0; foreach (var heroCCmdEventList in heroCCmdEventLists) { // Estimate the hero unit travelling to each intended destination // Only save one position per second, and prefer accurate positions // Heroes can have a lot more positions, and probably won't be useful more frequently than this var heroTargetLocationArray = heroCCmdEventList.HeroUnit.Positions.Union(new[] { new Position { TimeSpan = heroCCmdEventList.HeroUnit.TimeSpanBorn, Point = heroCCmdEventList.HeroUnit.PointBorn } }).Union(heroCCmdEventList.Positions).GroupBy(i => (int)i.TimeSpan.TotalSeconds).Select(i => i.OrderBy(j => j.IsEstimated).First()).OrderBy(i => i.TimeSpan).ToArray(); var currentEstimatedPosition = heroTargetLocationArray[0]; for (var i = 0; i < heroTargetLocationArray.Length - 1; i++) if (!heroTargetLocationArray[i + 1].IsEstimated) currentEstimatedPosition = heroTargetLocationArray[i + 1]; else { var percentageOfDistanceTravelledToTargetLocation = (heroTargetLocationArray[i + 1].TimeSpan - currentEstimatedPosition.TimeSpan).TotalSeconds * PlayerSpeedUnitsPerSecond / currentEstimatedPosition.Point.DistanceTo(heroTargetLocationArray[i + 1].Point); currentEstimatedPosition = new Position { TimeSpan = heroTargetLocationArray[i + 1].TimeSpan, Point = percentageOfDistanceTravelledToTargetLocation >= 1 ? heroTargetLocationArray[i + 1].Point : new Point { X = (int)((heroTargetLocationArray[i + 1].Point.X - currentEstimatedPosition.Point.X) * percentageOfDistanceTravelledToTargetLocation + currentEstimatedPosition.Point.X), Y = (int)((heroTargetLocationArray[i + 1].Point.Y - currentEstimatedPosition.Point.Y) * percentageOfDistanceTravelledToTargetLocation + currentEstimatedPosition.Point.Y) }, IsEstimated = true }; heroCCmdEventList.HeroUnit.Positions.Add(currentEstimatedPosition); } heroCCmdEventList.HeroUnit.Positions = heroCCmdEventList.HeroUnit.Positions.OrderBy(i => i.TimeSpan).ToList(); } } foreach (var unit in replay.Units.Where(i => i.Positions.Any())) { // Save no more than one position event per second per unit unit.Positions = unit.Positions.GroupBy(i => (int)i.TimeSpan.TotalSeconds).Select(i => i.OrderBy(j => j.IsEstimated).First()).OrderBy(i => i.TimeSpan).ToList(); // If this is a Hero unit, adjust the 'PointDied' and 'TimeSpanDied' to the last position // Currently Hero units stop receiving tracker event updates after their first death if (unit.Group == Unit.UnitGroup.Hero) { var finalPosition = unit.Positions.Last(); unit.PointDied = finalPosition.Point; unit.TimeSpanDied = finalPosition.TimeSpan; } } // Add 'estimated' minion positions based on their fixed pathing // Without these positions, minions can appear to travel through walls straight across the map // These estimated positions are actually quite accurate, as minions always follow a path connecting each fort/keep in their lane var numberOfStructureTiers = replay.Units.Where(i => i.Name.StartsWith("TownTownHall")).Select(i => i.Name).Distinct().Count(); var uniqueTierName = replay.Units.First(i => i.Name.StartsWith("TownTownHall")).Name; var numberOfLanes = replay.Units.Count(i => i.Name == uniqueTierName && i.Team == 0); var minionWayPoints = replay.Units.Where(i => i.Name.StartsWith("TownTownHall")).Select(j => j.PointBorn).OrderBy(j => j.X).Skip(numberOfLanes).OrderByDescending(j => j.X).Skip(numberOfLanes).OrderBy(j => j.Y); for (var team = 0; team <= 1; team++) { // Gather all minion units for this team var minionUnits = replay.Units.Where(i => i.Team == team && i.Group == Unit.UnitGroup.Minions).ToArray(); // Each wave spawns together, but not necessarily from top to bottom // We will figure out what order the lanes are spawning in, and order by top to bottom later on var unitsPerLaneTemp = new List<Unit>[numberOfLanes]; for (var i = 0; i < unitsPerLaneTemp.Length; i++) unitsPerLaneTemp[i] = new List<Unit>(); var minionLaneOrderMinions = minionUnits.Where(i => i.Name == "WizardMinion").Take(numberOfLanes).ToArray(); var minionLaneOrder = new List<Tuple<int, int>>(); // *change* try { for (var i = 0; i < numberOfLanes; i++) minionLaneOrder.Add(new Tuple<int, int>(i, minionLaneOrderMinions[i].PointBorn.Y)); } catch (Exception ) { } minionLaneOrder = minionLaneOrder.OrderBy(i => i.Item2).ToList(); // Group minion units by lane var currentIndex = 0; var minionUnitsPerWave = 7; while (currentIndex < minionUnits.Length) for (var i = 0; i < unitsPerLaneTemp.Length; i++) for (var j = 0; j < minionUnitsPerWave; j++) { if (currentIndex == minionUnits.Length) break; unitsPerLaneTemp[i].Add(minionUnits[currentIndex++]); // CatapultMinions don't seem to spawn exactly with their minion wave, which is strange // For now I will leave them out of this, which means they may appear to travel through walls if (currentIndex < minionUnits.Length && minionUnits[currentIndex].Name == "CatapultMinion") currentIndex++; } // Order the lanes by top to bottom var unitsPerLane = unitsPerLaneTemp.ToArray(); // *change* try { for (var i = 0; i < unitsPerLane.Length; i++) unitsPerLane[i] = unitsPerLaneTemp[minionLaneOrder[i].Item1]; } catch (Exception ) { } for (var i = 0; i < numberOfLanes; i++) { // For each lane, take the forts in that lane, and see if the minions in that lane walked beyond this var currentLaneUnitsToAdjust = unitsPerLane[i].Where(j => j.Positions.Any() || j.TimeSpanDied.HasValue); var currentLaneWaypoints = minionWayPoints.Skip(numberOfStructureTiers * i).Take(numberOfStructureTiers); if (team == 0) currentLaneWaypoints = currentLaneWaypoints.OrderBy(j => j.X); else currentLaneWaypoints = currentLaneWaypoints.OrderByDescending(j => j.X); foreach (var laneUnit in currentLaneUnitsToAdjust) { var isLaneUnitModified = false; var beginningPosition = new Position { TimeSpan = laneUnit.TimeSpanBorn, Point = laneUnit.PointBorn }; var firstLaneUnitPosition = laneUnit.Positions.Any() ? laneUnit.Positions.First() : new Position { TimeSpan = laneUnit.TimeSpanDied.Value, Point = laneUnit.PointDied }; foreach (var laneWaypoint in currentLaneWaypoints) if ((team == 0 && firstLaneUnitPosition.Point.X > laneWaypoint.X) || team == 1 && firstLaneUnitPosition.Point.X < laneWaypoint.X) { var leg1Distance = beginningPosition.Point.DistanceTo(laneWaypoint); var newPosition = new Position { TimeSpan = beginningPosition.TimeSpan + TimeSpan.FromSeconds((long)((firstLaneUnitPosition.TimeSpan - beginningPosition.TimeSpan).TotalSeconds * (leg1Distance / (leg1Distance + laneWaypoint.DistanceTo(firstLaneUnitPosition.Point))))), Point = laneWaypoint }; laneUnit.Positions.Add(newPosition); beginningPosition = newPosition; isLaneUnitModified = true; } else break; if (isLaneUnitModified) laneUnit.Positions = laneUnit.Positions.OrderBy(j => j.TimeSpan).ToList(); } } } // Remove 'duplicate' positions that don't tell us anything foreach (var unit in replay.Units.Where(i => i.Positions.Count >= 3)) { var unitPositions = unit.Positions.ToArray(); for (var i = 1; i < unitPositions.Length - 1; i++) if (unitPositions[i].Point.X == unitPositions[i - 1].Point.X && unitPositions[i].Point.Y == unitPositions[i - 1].Point.Y && unitPositions[i].Point.X == unitPositions[i + 1].Point.X && unitPositions[i].Point.Y == unitPositions[i + 1].Point.Y) unit.Positions.Remove(unitPositions[i]); } }
public static void ParseUnitData(Replay replay) { { // We go through these events in chronological order, and keep a list of currently 'active' units, because UnitIDs are recycled var activeUnitsByIndex = new Dictionary<int, Unit>(); var activeUnitsByUnitID = new Dictionary<int, Unit>(); var activeHeroUnits = new Dictionary<Player, Unit>(); var isCheckingForAbathurLocusts = true; var updateTargetUnitEventArray = replay.GameEvents.Where(i => i.eventType == GameEventType.CCmdUpdateTargetUnitEvent).OrderBy(i => i.TimeSpan).ToArray(); var updateTargetUnitEventArrayIndex = 0; foreach (var unitTrackerEvent in replay.TrackerEvents.Where(i => i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitBornEvent || i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitRevivedEvent || i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitDiedEvent || i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitOwnerChangeEvent || i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitPositionsEvent)) { switch (unitTrackerEvent.TrackerEventType) { case ReplayTrackerEvents.TrackerEventType.UnitBornEvent: case ReplayTrackerEvents.TrackerEventType.UnitRevivedEvent: Unit newUnit; var newUnitIndex = (int)unitTrackerEvent.Data.dictionary[0].vInt.Value; if (unitTrackerEvent.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitBornEvent) newUnit = new Unit { UnitID = GetUnitID(newUnitIndex, (int)unitTrackerEvent.Data.dictionary[1].vInt.Value), Name = unitTrackerEvent.Data.dictionary[2].blobText, Group = UnitGroupDictionary.ContainsKey(unitTrackerEvent.Data.dictionary[2].blobText) ? UnitGroupDictionary[unitTrackerEvent.Data.dictionary[2].blobText] : UnitGroup.Unknown, TimeSpanBorn = unitTrackerEvent.TimeSpan, Team = unitTrackerEvent.Data.dictionary[3].vInt.Value == 11 || unitTrackerEvent.Data.dictionary[3].vInt.Value == 12 ? (int)unitTrackerEvent.Data.dictionary[3].vInt.Value - 11 : unitTrackerEvent.Data.dictionary[3].vInt.Value > 0 && unitTrackerEvent.Data.dictionary[3].vInt.Value <= 10 ? replay.Players[unitTrackerEvent.Data.dictionary[3].vInt.Value - 1].Team : (int?)null, PlayerControlledBy = unitTrackerEvent.Data.dictionary[3].vInt.Value > 0 && unitTrackerEvent.Data.dictionary[3].vInt.Value <= 10 ? replay.Players[unitTrackerEvent.Data.dictionary[3].vInt.Value - 1] : null, PointBorn = new Point { X = (int)unitTrackerEvent.Data.dictionary[5].vInt.Value, Y = (int)unitTrackerEvent.Data.dictionary[6].vInt.Value } }; else { var deadUnit = activeUnitsByIndex[newUnitIndex]; newUnit = new Unit { UnitID = deadUnit.UnitID, Name = deadUnit.Name, Group = deadUnit.Group, TimeSpanBorn = unitTrackerEvent.TimeSpan, Team = deadUnit.Team, PlayerControlledBy = deadUnit.PlayerControlledBy, PointBorn = new Point { X = (int)unitTrackerEvent.Data.dictionary[2].vInt.Value, Y = (int)unitTrackerEvent.Data.dictionary[3].vInt.Value } }; } replay.Units.Add(newUnit); activeUnitsByIndex[newUnitIndex] = newUnit; activeUnitsByUnitID[newUnit.UnitID] = newUnit; // Add Hero units to the controlling Player if (newUnit.PlayerControlledBy != null && newUnit.Name.StartsWith("Hero")) { newUnit.PlayerControlledBy.HeroUnits.Add(newUnit); activeHeroUnits[newUnit.PlayerControlledBy] = newUnit; } // Add derived hero positions from associated unit born/acquired/died info // These are accurate positions: Picking up regen globes, spawning Locusts, etc if (newUnit.PlayerControlledBy != null) { if (UnitBornProvidesLocationForOwner.ContainsKey(newUnit.Name) || newUnit.Group == UnitGroup.HeroTalentSelection) activeHeroUnits[newUnit.PlayerControlledBy].Positions.Add(new Position { TimeSpan = newUnit.TimeSpanBorn, Point = newUnit.PointBorn }); else if (isCheckingForAbathurLocusts) { // For Abathur locusts, we need to make sure they aren't spawning from a locust nest (Level 20 talent) if (newUnit.Name == "AbathurLocustNest") isCheckingForAbathurLocusts = false; else if (newUnit.Name == "AbathurLocustNormal" || newUnit.Name == "AbathurLocustAssaultStrain" || newUnit.Name == "AbathurLocustBombardStrain") activeHeroUnits[newUnit.PlayerControlledBy].Positions.Add(new Position { TimeSpan = newUnit.TimeSpanBorn, Point = newUnit.PointBorn }); } } break; case ReplayTrackerEvents.TrackerEventType.UnitDiedEvent: var unitThatDied = activeUnitsByIndex[(int)unitTrackerEvent.Data.dictionary[0].vInt.Value]; var playerIDKilledBy = unitTrackerEvent.Data.dictionary[2].optionalData != null ? (int)unitTrackerEvent.Data.dictionary[2].optionalData.vInt.Value : (int?)null; unitThatDied.TimeSpanDied = unitTrackerEvent.TimeSpan; unitThatDied.PlayerKilledBy = playerIDKilledBy.HasValue && playerIDKilledBy.Value > 0 && playerIDKilledBy.Value <= 10 ? replay.Players[playerIDKilledBy.Value - 1] : null; unitThatDied.PointDied = new Point { X = (int)unitTrackerEvent.Data.dictionary[3].vInt.Value, Y = (int)unitTrackerEvent.Data.dictionary[4].vInt.Value }; unitThatDied.UnitKilledBy = unitTrackerEvent.Data.dictionary[5].optionalData != null ? activeUnitsByIndex[(int)unitTrackerEvent.Data.dictionary[5].optionalData.vInt.Value] : null; // Sometimes 'PlayerIDKilledBy' will be outside of the range of players (1-10) // Minions that are killed by other minions or towers will have the 'team' that killed them in this field (11 or 12) // Some other units have interesting values I don't fully understand yet. For example, 'ItemCannonball' (the coins on Blackheart's Bay) will have 0 or 15 in this field. I'm guessing this is also which team acquires them, which may be useful // Other map objectives may also have this. I'll look into this more in the future. /* if (unitDiedEvent.PlayerIDKilledBy.HasValue && unitThatDied.PlayerKilledBy == null) Console.WriteLine(""); */ break; case ReplayTrackerEvents.TrackerEventType.UnitOwnerChangeEvent: var ownerChangeEvent = new OwnerChangeEvent { TimeSpanOwnerChanged = unitTrackerEvent.TimeSpan, Team = unitTrackerEvent.Data.dictionary[2].vInt.Value == 11 || unitTrackerEvent.Data.dictionary[2].vInt.Value == 12 ? (int)unitTrackerEvent.Data.dictionary[2].vInt.Value - 11 : (int?)null, PlayerNewOwner = unitTrackerEvent.Data.dictionary[2].vInt.Value > 0 && unitTrackerEvent.Data.dictionary[2].vInt.Value <= 10 ? replay.Players[unitTrackerEvent.Data.dictionary[2].vInt.Value - 1] : null }; if (!ownerChangeEvent.Team.HasValue && ownerChangeEvent.PlayerNewOwner != null) ownerChangeEvent.Team = ownerChangeEvent.PlayerNewOwner.Team; var unitOwnerChanged = activeUnitsByIndex[(int)unitTrackerEvent.Data.dictionary[0].vInt.Value]; unitOwnerChanged.OwnerChangeEvents.Add(ownerChangeEvent); if (unitOwnerChanged.PlayerControlledBy != null && UnitOwnerChangeProvidesLocationForOwner.ContainsKey(unitOwnerChanged.Name)) activeHeroUnits[unitOwnerChanged.PlayerControlledBy].Positions.Add(new Position { TimeSpan = ownerChangeEvent.TimeSpanOwnerChanged, Point = unitOwnerChanged.PointBorn }); break; case ReplayTrackerEvents.TrackerEventType.UnitPositionsEvent: var currentUnitIndex = (int)unitTrackerEvent.Data.dictionary[0].vInt.Value; for (var i = 0; i < unitTrackerEvent.Data.dictionary[1].array.Length; i++) { currentUnitIndex += (int)unitTrackerEvent.Data.dictionary[1].array[i++].vInt.Value; activeUnitsByIndex[currentUnitIndex].Positions.Add(new Position { TimeSpan = unitTrackerEvent.TimeSpan, Point = new Point { X = (int)unitTrackerEvent.Data.dictionary[1].array[i++].vInt.Value, Y = (int)unitTrackerEvent.Data.dictionary[1].array[i].vInt.Value } }); } break; } // Use 'CCmdUpdateTargetUnitEvent' to find an accurate location of units targeted // Excellent for finding frequent, accurate locations of heroes during team fights while (updateTargetUnitEventArrayIndex < updateTargetUnitEventArray.Length && unitTrackerEvent.TimeSpan > updateTargetUnitEventArray[updateTargetUnitEventArrayIndex].TimeSpan) if (activeUnitsByUnitID.ContainsKey((int)updateTargetUnitEventArray[updateTargetUnitEventArrayIndex++].data.array[2].unsignedInt.Value)) activeUnitsByUnitID[(int)updateTargetUnitEventArray[updateTargetUnitEventArrayIndex - 1].data.array[2].unsignedInt.Value].Positions.Add(new Position { TimeSpan = updateTargetUnitEventArray[updateTargetUnitEventArrayIndex - 1].TimeSpan, Point = Point.FromEventFormat( updateTargetUnitEventArray[updateTargetUnitEventArrayIndex - 1].data.array[6].array[0].unsignedInt.Value, updateTargetUnitEventArray[updateTargetUnitEventArrayIndex - 1].data.array[6].array[1].unsignedInt.Value) }); } } // For simplicity, I set extra fields on units that are not initially controlled by a player, and only have one owner change event foreach (var unitWithOneOwnerChange in replay.Units.Where(i => i.OwnerChangeEvents.Count == 1 && i.PlayerControlledBy == null)) { var singleOwnerChangeEvent = unitWithOneOwnerChange.OwnerChangeEvents.Single(); if (singleOwnerChangeEvent.PlayerNewOwner != null) { unitWithOneOwnerChange.PlayerControlledBy = singleOwnerChangeEvent.PlayerNewOwner; unitWithOneOwnerChange.TimeSpanAcquired = singleOwnerChangeEvent.TimeSpanOwnerChanged; unitWithOneOwnerChange.OwnerChangeEvents.Clear(); } } // Estimate Hero positions from CCmdEvent and CCmdUpdateTargetPointEvent (Movement points) { // Excluding heroes with multiple units like Lost Vikings, and Abathur with 'Ultimate Evolution' clones // It's okay to not estimate Abathur's position, as he rarely moves and we also get an accurate position each time he spawns a locust var playerToActiveHeroUnitIndexDictionary = replay.Players.Where(i => i.HeroUnits.Select(j => j.Name).Distinct().Count() == 1).ToDictionary(i => i, i => 0); // This is a list of 'Player', 'TimeSpan', and 'EventPosition' for each CCmdEvent where ability data is null and a position is included var playerCCmdEventLists = replay.GameEvents.Where(i => i.eventType == GameEventType.CCmdEvent && i.data.array[1] == null && i.data.array[2] != null && i.data.array[2].array.Length == 3 && playerToActiveHeroUnitIndexDictionary.ContainsKey(i.player)).Select(i => new { i.player, Position = new Position { TimeSpan = i.TimeSpan, Point = Point.FromEventFormat(i.data.array[2].array[0].unsignedInt.Value, i.data.array[2].array[1].unsignedInt.Value), IsEstimated = true } }) .GroupBy(i => i.player) .Select(i => new { Player = i.Key, Positions = i.Select(j => j.Position) // Union the CCmdUpdateTargetPointEvents for each Player .Union(replay.GameEvents.Where(j => j.player == i.Key && j.eventType == GameEventType.CCmdUpdateTargetPointEvent) .Select(j => new Position { TimeSpan = j.TimeSpan, Point = Point.FromEventFormat(j.data.array[0].unsignedInt.Value, j.data.array[1].unsignedInt.Value), IsEstimated = true })) // Take the single latest applicable CCmdEvent or CCmdUpdateTargetPointEvent if there are more than one in a second .GroupBy(j => (int)j.TimeSpan.TotalSeconds) .Select(j => j.OrderByDescending(k => k.TimeSpan).First()) .ToArray() }); // Find the applicable events for each Hero unit while they were alive var playerAndHeroCCmdEventLists = playerCCmdEventLists.Select(i => i.Player.HeroUnits.Select(j => new { HeroUnit = j, Positions = i.Positions.Where(k => k.TimeSpan > j.TimeSpanBorn && (!j.TimeSpanDied.HasValue || k.TimeSpan < j.TimeSpanDied.Value)).OrderBy(k => k.TimeSpan).ToArray() })); const double PlayerSpeedUnitsPerSecond = 5.0; foreach (var playerCCmdEventList in playerAndHeroCCmdEventLists) foreach (var heroCCmdEventList in playerCCmdEventList) { // Estimate the hero unit travelling to each intended destination // Only save one position per second, and prefer accurate positions // Heroes can have a lot more positions, and probably won't be useful more frequently than this var heroTargetLocationArray = heroCCmdEventList.HeroUnit.Positions.Union(new[] { new Position { TimeSpan = heroCCmdEventList.HeroUnit.TimeSpanBorn, Point = heroCCmdEventList.HeroUnit.PointBorn } }).Union(heroCCmdEventList.Positions).GroupBy(i => (int)i.TimeSpan.TotalSeconds).Select(i => i.OrderBy(j => j.IsEstimated).First()).OrderBy(i => i.TimeSpan).ToArray(); var currentEstimatedPosition = heroTargetLocationArray[0]; for (var i = 0; i < heroTargetLocationArray.Length - 1; i++) if (!heroTargetLocationArray[i + 1].IsEstimated) currentEstimatedPosition = heroTargetLocationArray[i + 1]; else { var percentageOfDistanceTravelledToTargetLocation = (heroTargetLocationArray[i + 1].TimeSpan - currentEstimatedPosition.TimeSpan).TotalSeconds * PlayerSpeedUnitsPerSecond / currentEstimatedPosition.Point.DistanceTo(heroTargetLocationArray[i + 1].Point); currentEstimatedPosition = new Position { TimeSpan = heroTargetLocationArray[i + 1].TimeSpan, Point = percentageOfDistanceTravelledToTargetLocation >= 1 ? heroTargetLocationArray[i + 1].Point : new Point { X = (int)((heroTargetLocationArray[i + 1].Point.X - currentEstimatedPosition.Point.X) * percentageOfDistanceTravelledToTargetLocation + currentEstimatedPosition.Point.X), Y = (int)((heroTargetLocationArray[i + 1].Point.Y - currentEstimatedPosition.Point.Y) * percentageOfDistanceTravelledToTargetLocation + currentEstimatedPosition.Point.Y) }, IsEstimated = true }; heroCCmdEventList.HeroUnit.Positions.Add(currentEstimatedPosition); } heroCCmdEventList.HeroUnit.Positions = heroCCmdEventList.HeroUnit.Positions.OrderBy(i => i.TimeSpan).ToList(); } } // Save no more than one position event per second per unit foreach (var unit in replay.Units.Where(i => i.Positions.Count > 0)) unit.Positions = unit.Positions.GroupBy(i => (int)i.TimeSpan.TotalSeconds).Select(i => i.OrderBy(j => j.IsEstimated).First()).OrderBy(i => i.TimeSpan).ToList(); // Add 'estimated' minion positions based on their fixed pathing // Without these positions, minions can appear to travel through walls straight across the map // These estimated positions are actually quite accurate, as minions always follow a path connecting each fort/keep in their lane var numberOfStructureTiers = replay.Units.Where(i => i.Name.StartsWith("TownTownHall")).Select(i => i.Name).Distinct().Count(); var uniqueTierName = replay.Units.First(i => i.Name.StartsWith("TownTownHall")).Name; var numberOfLanes = replay.Units.Count(i => i.Name == uniqueTierName && i.Team == 0); var minionWayPoints = replay.Units.Where(i => i.Name.StartsWith("TownTownHall")).Select(j => j.PointBorn).OrderBy(j => j.X).Skip(numberOfLanes).OrderByDescending(j => j.X).Skip(numberOfLanes).OrderBy(j => j.Y); for (var team = 0; team <= 1; team++) { // Gather all minion units for this team var minionUnits = replay.Units.Where(i => i.Team == team && i.Group == UnitGroup.Minions).ToArray(); // Each wave spawns together, but not necessarily from top to bottom // We will figure out what order the lanes are spawning in, and order by top to bottom later on var unitsPerLaneTemp = new List<Unit>[numberOfLanes]; for (var i = 0; i < unitsPerLaneTemp.Length; i++) unitsPerLaneTemp[i] = new List<Unit>(); var minionLaneOrderMinions = minionUnits.Where(i => i.Name == "WizardMinion").Take(numberOfLanes).ToArray(); var minionLaneOrder = new List<Tuple<int, int>>(); for (var i = 0; i < numberOfLanes; i++) minionLaneOrder.Add(new Tuple<int, int>(i, minionLaneOrderMinions[i].PointBorn.Y)); minionLaneOrder = minionLaneOrder.OrderBy(i => i.Item2).ToList(); // Group minion units by lane var currentIndex = 0; var minionUnitsPerWave = 7; while (currentIndex < minionUnits.Length) for (var i = 0; i < unitsPerLaneTemp.Length; i++) for (var j = 0; j < minionUnitsPerWave; j++) { if (currentIndex == minionUnits.Length) break; unitsPerLaneTemp[i].Add(minionUnits[currentIndex++]); // CatapultMinions don't seem to spawn exactly with their minion wave, which is strange // For now I will leave them out of this, which means they may appear to travel through walls if (currentIndex < minionUnits.Length && minionUnits[currentIndex].Name == "CatapultMinion") currentIndex++; } // Order the lanes by top to bottom var unitsPerLane = unitsPerLaneTemp.ToArray(); for (var i = 0; i < unitsPerLane.Length; i++) unitsPerLane[i] = unitsPerLaneTemp[minionLaneOrder[i].Item1]; for (var i = 0; i < numberOfLanes; i++) { // For each lane, take the forts in that lane, and see if the minions in that lane walked beyond this var currentLaneUnitsToAdjust = unitsPerLane[i].Where(j => j.Positions.Any() || j.TimeSpanDied.HasValue); var currentLaneWaypoints = minionWayPoints.Skip(numberOfStructureTiers * i).Take(numberOfStructureTiers); if (team == 0) currentLaneWaypoints = currentLaneWaypoints.OrderBy(j => j.X); else currentLaneWaypoints = currentLaneWaypoints.OrderByDescending(j => j.X); foreach (var laneUnit in currentLaneUnitsToAdjust) { var isLaneUnitModified = false; var beginningPosition = new Position { TimeSpan = laneUnit.TimeSpanBorn, Point = laneUnit.PointBorn }; var firstLaneUnitPosition = laneUnit.Positions.Any() ? laneUnit.Positions.First() : new Position { TimeSpan = laneUnit.TimeSpanDied.Value, Point = laneUnit.PointDied }; foreach (var laneWaypoint in currentLaneWaypoints) if ((team == 0 && firstLaneUnitPosition.Point.X > laneWaypoint.X) || team == 1 && firstLaneUnitPosition.Point.X < laneWaypoint.X) { var leg1Distance = beginningPosition.Point.DistanceTo(laneWaypoint); var newPosition = new Position { TimeSpan = beginningPosition.TimeSpan + TimeSpan.FromSeconds((long)((firstLaneUnitPosition.TimeSpan - beginningPosition.TimeSpan).TotalSeconds * (leg1Distance / (leg1Distance + laneWaypoint.DistanceTo(firstLaneUnitPosition.Point))))), Point = laneWaypoint }; laneUnit.Positions.Add(newPosition); beginningPosition = newPosition; isLaneUnitModified = true; } else break; if (isLaneUnitModified) laneUnit.Positions = laneUnit.Positions.OrderBy(j => j.TimeSpan).ToList(); } } } // Remove 'duplicate' positions that don't tell us anything foreach (var unit in replay.Units.Where(i => i.Positions.Count >= 3)) { var unitPositions = unit.Positions.ToArray(); for (var i = 1; i < unitPositions.Length - 1; i++) if (unitPositions[i].Point.X == unitPositions[i - 1].Point.X && unitPositions[i].Point.Y == unitPositions[i - 1].Point.Y && unitPositions[i].Point.X == unitPositions[i + 1].Point.X && unitPositions[i].Point.Y == unitPositions[i + 1].Point.Y) unit.Positions.Remove(unitPositions[i]); } }
private static void ParseReplayArchive(Replay replay, MpqArchive archive, bool ignoreErrors, bool fullParse = false) { archive.AddListfileFilenames(); // Replay Details ReplayDetails.Parse(replay, GetMpqFile(archive, ReplayDetails.FileName), ignoreErrors); if (!ignoreErrors) { if (replay.Players.Length != 10 || replay.Players.Count(i => i.IsWinner) != 5) { // Filter out 'Try Me' games, any games without 10 players, and incomplete games return; } else if (replay.Timestamp == DateTime.MinValue) { // Uncommon issue when parsing replay.details return; } else if (replay.Timestamp < new DateTime(2014, 10, 6, 0, 0, 0, DateTimeKind.Utc)) { // Technical Alpha replays return; } } // Replay Init Data ReplayInitData.Parse(replay, GetMpqFile(archive, ReplayInitData.FileName)); Console.Write(replay.GameMode + " "); if (replay.GameMode != GameMode.QuickMatch && replay.GameMode == GameMode.TeamLeague && replay.GameMode != GameMode.HeroLeague && replay.GameMode == GameMode.UnrankedDraft) { fullParse = false; } ReplayAttributeEvents.Parse(replay, GetMpqFile(archive, ReplayAttributeEvents.FileName)); replay.TrackerEvents = ReplayTrackerEvents.Parse(GetMpqFile(archive, ReplayTrackerEvents.FileName)); List <string> nextBans = new List <string>(); int pickCount = 0; if (replay.GameMode == GameMode.HeroLeague || replay.GameMode == GameMode.TeamLeague || replay.GameMode == GameMode.UnrankedDraft) { foreach (var trackerEvent in replay.TrackerEvents) { try { if (trackerEvent.TrackerEventType == ReplayTrackerEvents.TrackerEventType.HeroBannedEvent) { replay.OrderedBans.Add(trackerEvent.Data.dictionary[0].blobText); } if (trackerEvent.TrackerEventType == ReplayTrackerEvents.TrackerEventType.HeroPickedEvent) { pickCount++; replay.OrderedPicks.Add(trackerEvent.Data.dictionary[0].blobText); } } catch { } if (pickCount == 10) { break; } } } if (!fullParse) { return; } replay.GameEvents = new List <GameEvent>(); try { replay.GameEvents = ReplayGameEvents.Parse(GetMpqFile(archive, ReplayGameEvents.FileName), replay.ClientListByUserID, replay.ReplayBuild, replay.ReplayVersionMajor); replay.IsGameEventsParsedSuccessfully = true; } catch { replay.GameEvents = new List <GameEvent>(); } // Gather talent selections var talentGameEventsDictionary = replay.GameEvents .Where(i => i.eventType == GameEventType.CHeroTalentSelectedEvent) .GroupBy(i => i.player) .ToDictionary( i => i.Key, i => i.Select(j => new Talent { TalentID = (int)j.data.unsignedInt.Value, TimeSpanSelected = j.TimeSpan }).OrderBy(j => j.TimeSpanSelected).ToArray()); foreach (var player in talentGameEventsDictionary.Keys) { player.Talents = talentGameEventsDictionary[player]; } // Replay Server Battlelobby if (!ignoreErrors && archive.Any(i => i.Filename == ReplayServerBattlelobby.FileName)) { //ReplayServerBattlelobby.GetBattleTags(replay, GetMpqFile(archive, ReplayServerBattlelobby.FileName)); ReplayServerBattlelobby.Parse(replay, GetMpqFile(archive, ReplayServerBattlelobby.FileName)); } // Parse Unit Data using Tracker events Unit.ParseUnitData(replay); // Parse Statistics if (replay.ReplayBuild >= 40431) { try { Statistics.Parse(replay); replay.IsStatisticsParsedSuccessfully = true; } catch (Exception e) { replay.IsGameEventsParsedSuccessfully = false; } } // Replay Message Events // ReplayMessageEvents.Parse(replay, GetMpqFile(archive, ReplayMessageEvents.FileName)); // Replay Resumable Events // So far it doesn't look like this file has anything we would be interested in // ReplayResumableEvents.Parse(replay, GetMpqFile(archive, "replay.resumable.events")); }
/// <summary> /// Applies the set of attributes to a replay. /// </summary> /// <param name="replay">Replay to apply the attributes to.</param> public void ApplyAttributes(Replay replay) { // I'm not entirely sure this is the right encoding here. Might be unicode... var encoding = Encoding.UTF8; var attributes1 = new List <ReplayAttribute>(); var attributes2 = new List <ReplayAttribute>(); var attributes3 = new List <ReplayAttribute>(); var attributes4 = new List <ReplayAttribute>(); var attributesffa = new List <ReplayAttribute>(); foreach (var attribute in Attributes) { switch (attribute.AttributeType) { case ReplayAttributeEventType.PlayerTypeAttribute: // 500 { var type = encoding.GetString(attribute.Value.Reverse().ToArray()); if (type.ToLower().Equals("comp")) { replay.Players[attribute.PlayerId - 1].PlayerType = PlayerType.Computer; } else if (type.ToLower().Equals("humn")) { replay.Players[attribute.PlayerId - 1].PlayerType = PlayerType.Human; } else { throw new Exception("Unexpected value"); } break; } case ReplayAttributeEventType.TeamSizeAttribute: { // This fixes issues with reversing the string before encoding. Without this, you get "\01v1" replay.TeamSize = new string(encoding.GetString(attribute.Value, 0, 3).Reverse().ToArray()); break; } case ReplayAttributeEventType.DifficultyLevelAttribute: { var diffLevel = encoding.GetString(attribute.Value.Reverse().ToArray()).ToLower(); var player = replay.Players[attribute.PlayerId - 1]; switch (diffLevel) { case "vyey": player.Difficulty = Difficulty.VeryEasy; break; case "easy": player.Difficulty = Difficulty.Easy; break; case "medi": player.Difficulty = Difficulty.Medium; break; case "hard": player.Difficulty = Difficulty.Hard; break; case "vyhd": player.Difficulty = Difficulty.VeryHard; break; case "insa": player.Difficulty = Difficulty.Insane; break; } break; } case ReplayAttributeEventType.GameSpeedAttribute: { var speed = encoding.GetString(attribute.Value.Reverse().ToArray()).ToLower(); switch (speed) { case "slor": replay.GameSpeed = GameSpeed.Slower; break; case "slow": replay.GameSpeed = GameSpeed.Slow; break; case "norm": replay.GameSpeed = GameSpeed.Normal; break; case "fast": replay.GameSpeed = GameSpeed.Fast; break; case "fasr": replay.GameSpeed = GameSpeed.Faster; break; // Otherwise, Game Speed will remain "Unknown" } break; } case ReplayAttributeEventType.PlayerTeam1v1Attribute: { attributes1.Add(attribute); break; } case ReplayAttributeEventType.PlayerTeam2v2Attribute: { attributes2.Add(attribute); break; } case ReplayAttributeEventType.PlayerTeam3v3Attribute: { attributes3.Add(attribute); break; } case ReplayAttributeEventType.PlayerTeam4v4Attribute: { attributes4.Add(attribute); break; } case ReplayAttributeEventType.PlayerTeamFFAAttribute: { attributesffa.Add(attribute); break; } case ReplayAttributeEventType.GameTypeAttribute: { var gameTypeStr = encoding.GetString(attribute.Value.Reverse().ToArray()).ToLower().Trim('\0'); switch (gameTypeStr) { case "priv": replay.GameMode = GameMode.Custom; break; case "amm": if (replay.ReplayBuild < 33684) { replay.GameMode = GameMode.QuickMatch; } break; default: throw new Exception("Unexpected Game Type"); } break; } case ReplayAttributeEventType.Character: { replay.Players[attribute.PlayerId - 1].IsAutoSelect = encoding.GetString(attribute.Value.Reverse().ToArray()) == "Rand"; break; } case ReplayAttributeEventType.CharacterLevel: { var characterLevel = int.Parse(encoding.GetString(attribute.Value.Reverse().ToArray())); var player = replay.Players[attribute.PlayerId - 1]; player.CharacterLevel = characterLevel; break; } case ReplayAttributeEventType.HeroSelectionMode: { if (replay.GameMode != GameMode.Custom) { switch (encoding.GetString(attribute.Value.Reverse().ToArray()).ToLower().Trim('\0')) { case "stan": replay.GameMode = GameMode.QuickMatch; break; case "drft": replay.GameMode = GameMode.HeroLeague; break; } } } break; case (ReplayAttributeEventType)4011: // What is this? Draft order? break; case (ReplayAttributeEventType)4016: // What is this? Always '1' in Hero League // if (replay.GameMode == GameMode.HeroLeague && int.Parse(encoding.GetString(attribute.Value.Reverse().ToArray())) != 1) // Console.WriteLine("WAAT!?"); break; case (ReplayAttributeEventType)4017: // What is this? Always '5' in Hero League // if (replay.GameMode == GameMode.HeroLeague && int.Parse(encoding.GetString(attribute.Value.Reverse().ToArray())) != 5) // Console.WriteLine("WAAT!?"); break; } } List <ReplayAttribute> currentList = null; if (replay.TeamSize.Equals("1v1")) { currentList = attributes1; } else if (replay.TeamSize.Equals("2v2")) { currentList = attributes2; } else if (replay.TeamSize.Equals("3v3")) { currentList = attributes3; } else if (replay.TeamSize.Equals("4v4")) { currentList = attributes4; } else if (replay.TeamSize.Equals("FFA")) { currentList = attributesffa; } if (currentList != null) { foreach (var att in currentList) { // Reverse the values then parse, you don't notice the effects of this until theres 10+ teams o.o replay.Players[att.PlayerId - 1].Team = int.Parse(encoding.GetString(att.Value.Reverse().ToArray()).Trim('\0', 'T')); } } // Skipping parsing the handicap and colors since this is parsed elsewhere. }
/// <summary> Parses the replay.initdata file in a replay file. </summary> /// <param name="replay"> The replay file to apply the parsed data to. </param> /// <param name="buffer"> The buffer containing the replay.initdata file. </param> public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) { var reader = new BitReader(stream); var i = reader.ReadByte(); var playerList = new string[i]; for (int j = 0; j < i; j++) { var nameLength = reader.ReadByte(); var str = reader.ReadString(nameLength); playerList[j] = str; if (reader.ReadBoolean()) { var strLength = reader.ReadByte(); reader.AlignToByte(); // Clan Tag reader.ReadString(strLength); } if (reader.ReadBoolean()) { reader.ReadByte().ToString(); // Highest league } if (reader.ReadBoolean()) { reader.ReadInt32().ToString(); // Swarm level } reader.ReadInt32(); // Random seed (So far, always 0 in Heroes) if (reader.ReadBoolean()) { reader.ReadByte().ToString(); // Race Preference } if (reader.ReadBoolean()) { reader.ReadByte().ToString(); // Team Preference } reader.ReadBoolean(); //test map reader.ReadBoolean(); //test auto reader.ReadBoolean(); //examine reader.ReadBoolean(); //custom interface reader.Read(2); //observer reader.AlignToByte(); reader.ReadBytes(11); // Bunch of garbage \0 } // Marked as 'Random Value', so I will use as seed replay.RandomValue = (uint)reader.ReadInt32(); reader.ReadBlobPrecededWithLength(10); // Dflt reader.ReadBoolean(); // Lock Teams reader.ReadBoolean(); // Teams Together reader.ReadBoolean(); // Advanced Shared Control reader.ReadBoolean(); // Random Races reader.ReadBoolean(); // BattleNet reader.ReadBoolean(); // AMM reader.ReadBoolean(); // Competitive reader.ReadBoolean(); // No Victory Or Defeat reader.ReadBoolean(); // Unknown 0 reader.ReadBoolean(); // Unknown 1 reader.ReadBoolean(); // Unknown 2 reader.Read(2); // Fog reader.Read(2); // Observers reader.Read(2); // User Difficulty reader.ReadInt32(); reader.ReadInt32(); // 64 bit int: Client Debug Flags reader.Read(3); // Game Speed // Not sure what this 'Game Type' is reader.Read(3); var maxPlayers = reader.Read(5); if (maxPlayers != 10) // Max Players { replay.GameMode = GameMode.TryMe; } // About 1000 bytes from here is a list of characters, character skins, character mounts, artifact selections, and other data } }
private static Tuple <ReplayParseResult, Replay> ParseReplayResults(Replay replay, bool ignoreErrors, bool allowPTRRegion) { if (ignoreErrors) { return(new Tuple <ReplayParseResult, Replay>(ReplayParseResult.UnexpectedResult, replay)); } else if (replay.Players.Length == 1) { // Filter out 'Try Me' games, as they have unusual format that throws exceptions in other areas return(new Tuple <ReplayParseResult, Replay>(ReplayParseResult.TryMeMode, new Replay { ReplayBuild = replay.ReplayBuild })); } else if (replay.Players.Length <= 5) { // Custom game with all computer players on the opposing team won't register them as players at all (Noticed at build 34053) return(new Tuple <ReplayParseResult, Replay>(ReplayParseResult.ComputerPlayerFound, new Replay { ReplayBuild = replay.ReplayBuild })); } else if (replay.Players.All(i => !i.IsWinner) || replay.ReplayLength.TotalMinutes < 2) { return(new Tuple <ReplayParseResult, Replay>(ReplayParseResult.Incomplete, new Replay { ReplayBuild = replay.ReplayBuild })); } else if (replay.Timestamp == DateTime.MinValue) { return(new Tuple <ReplayParseResult, Replay>(ReplayParseResult.UnexpectedResult, new Replay { ReplayBuild = replay.ReplayBuild })); } else if (replay.Timestamp < new DateTime(2014, 10, 6, 0, 0, 0, DateTimeKind.Utc)) { return(new Tuple <ReplayParseResult, Replay>(ReplayParseResult.PreAlphaWipe, new Replay { ReplayBuild = replay.ReplayBuild })); } else if (replay.Players.Count(i => i.PlayerType == PlayerType.Computer || i.Character == "Random Hero" || i.Name.Contains(' ')) > (replay.GameMode == GameMode.Brawl ? 5 : 0)) { return(new Tuple <ReplayParseResult, Replay>(ReplayParseResult.ComputerPlayerFound, new Replay { ReplayBuild = replay.ReplayBuild })); } else if (!allowPTRRegion && replay.Players.Any(i => i.BattleNetRegionId >= 90 /* PTR/Test Region */)) { return(new Tuple <ReplayParseResult, Replay>(ReplayParseResult.PTRRegion, new Replay { ReplayBuild = replay.ReplayBuild })); } else if (replay.Players.Count(i => i.IsWinner) != 5 || replay.Players.Length != 10 || (replay.GameMode != GameMode.StormLeague && replay.GameMode != GameMode.TeamLeague && replay.GameMode != GameMode.HeroLeague && replay.GameMode != GameMode.UnrankedDraft && replay.GameMode != GameMode.QuickMatch && replay.GameMode != GameMode.Custom && replay.GameMode != GameMode.Brawl)) { return(new Tuple <ReplayParseResult, Replay>(ReplayParseResult.UnexpectedResult, new Replay { ReplayBuild = replay.ReplayBuild })); } else if (!replay.ReplayDetailParsedSuccessfully) { return(new Tuple <ReplayParseResult, Replay>(ReplayParseResult.SuccessReplayDetail, replay)); } else { return(new Tuple <ReplayParseResult, Replay>(ReplayParseResult.Success, replay)); } }
/// <summary> Parses the replay.server.battlelobby file in a replay file. </summary> /// <param name="replay"> The replay file to apply the parsed data to. </param> /// <param name="buffer"> The buffer containing the replay.server.battlelobby file. </param> public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) { var bitReader = new BitReader(stream); if (replay.ReplayBuild < 38793) { GetBattleTags(replay, bitReader); return; } int s2mArrayLength = bitReader.ReadByte(); int stringLength = bitReader.ReadByte(); bitReader.ReadString(stringLength); for (var i = 1; i < s2mArrayLength; i++) { bitReader.Read(16); bitReader.ReadString(stringLength); } if (bitReader.ReadByte() != s2mArrayLength) { throw new Exception("s2ArrayLength not equal"); } for (var i = 0; i < s2mArrayLength; i++) { bitReader.ReadString(4); // s2m bitReader.ReadBytes(2); // 0x00 0x00 bitReader.ReadString(2); // Realm bitReader.ReadBytes(32); } // seems to be in all replays bitReader.ReadInt16(); bitReader.stream.Position = bitReader.stream.Position + 684; // seems to be in all replays bitReader.ReadInt16(); bitReader.stream.Position = bitReader.stream.Position + 1944; if (bitReader.ReadString(8) != "HumnComp") { throw new Exception("Not HumnComp"); } // seems to be in all replays bitReader.stream.Position = bitReader.stream.Position = bitReader.stream.Position + 19859; // next section is language libraries? // --------------------------------------- bitReader.Read(8); bitReader.Read(8); for (int i = 0; ; i++) // no idea how to determine the count { if (bitReader.ReadString(4).Substring(0, 2) != "s2") // s2mv; not sure if its going to be 'mv' all the time { bitReader.stream.Position = bitReader.stream.Position - 4; break; } bitReader.ReadBytes(2); // 0x00 0x00 bitReader.ReadString(2); // Realm bitReader.ReadBytes(32); } bitReader.Read(32); bitReader.Read(8); bitReader.ReadByte(); for (int i = 0; ; i++) // no idea how to determine the count { if (bitReader.ReadString(4).Substring(0, 2) != "s2") // s2ml { bitReader.stream.Position = bitReader.stream.Position - 4; break; } bitReader.ReadBytes(2); // 0x00 0x00 bitReader.ReadString(2); // Realm bitReader.ReadBytes(32); } for (int k = 0; k < 11; k++) { // ruRU, zhCN, plPL, esMX, frFR, esES // ptBR, itIT, enUs, deDe, koKR bitReader.ReadString(4); bitReader.ReadByte(); for (int i = 0; ; i++) { if (bitReader.ReadString(4).Substring(0, 2) != "s2") // s2ml { bitReader.stream.Position = bitReader.stream.Position - 4; break; } bitReader.ReadString(4); // s2ml bitReader.ReadBytes(2); // 0x00 0x00 bitReader.ReadString(2); // Realm bitReader.ReadBytes(32); } } // new section, can't find a pattern // has blizzmaps#1, Hero, s2mv // -------------------- bitReader.ReadBytes(8); // all 0x00 for (;;) { // we're just going to skip all the way down to the s2mh if (bitReader.ReadString(4) == "s2mh") { bitReader.stream.Position = bitReader.stream.Position - 4; break; } else { bitReader.stream.Position = bitReader.stream.Position - 3; } } for (var i = 0; i < s2mArrayLength; i++) { bitReader.ReadString(4); // s2mh bitReader.ReadBytes(2); // 0x00 0x00 bitReader.ReadString(2); // Realm bitReader.ReadBytes(32); } // All the Heroes, skins, mounts, effects, some other weird stuff (Cocoon, ArtifactSlot2, TestMountRideSurf, etc...) // -------------------------------------------------------------- List <string> skins = new List <string>(); int skinArrayLength = 0; if (replay.ReplayBuild >= 48027) { skinArrayLength = bitReader.ReadInt16(); } else { skinArrayLength = bitReader.ReadInt32(); } if (skinArrayLength > 1000) { throw new Exception("skinArrayLength is an unusually large number"); } for (int i = 0; i < skinArrayLength; i++) { skins.Add(bitReader.ReadString(bitReader.ReadByte())); } // use to determine if the heroes, skins, mounts are usable by the player (owns/free to play/internet cafe) if (bitReader.ReadInt32() != skinArrayLength) { throw new Exception("skinArrayLength not equal"); } for (int i = 0; i < skinArrayLength; i++) { for (int j = 0; j < 16; j++) // 16 is total player slots { bitReader.ReadByte(); // new values beginning on ptr 47801 // 0xC3 = free to play? // more values: 0xC1, 0x02, 0x83 var num = bitReader.Read(8); if (replay.ClientListByUserID[j] != null) { if (num > 0) { // usable } else if (num == 0) { // not usable } else { throw new NotImplementedException(); } } } } // Player info // ------------------------ if (replay.ReplayBuild <= 43259 || replay.ReplayBuild == 47801) { // Builds that are not yet supported for detailed parsing // build 47801 is a ptr build that had new data in the battletag section, the data was changed in 47944 (patch for 47801) GetBattleTags(replay, bitReader); return; } bitReader.ReadInt32(); bitReader.ReadBytes(33); ReadByte0x00(bitReader); ReadByte0x00(bitReader); bitReader.ReadByte(); // 0x19 if (replay.ReplayBuild <= 47479 || replay.ReplayBuild == 47903) { ExtendedBattleTagParsingOld(replay, bitReader); return; } for (int player = 0; player < replay.ClientListByUserID.Length; player++) { if (replay.ClientListByUserID[player] == null) { break; } string TId; if (player == 0) { var offset = bitReader.ReadByte(); bitReader.ReadString(2); // T: TId = bitReader.ReadString(12 + offset); } else { ReadByte0x00(bitReader); ReadByte0x00(bitReader); ReadByte0x00(bitReader); bitReader.Read(6); // get XXXXXXXX#YYY TId = Encoding.UTF8.GetString(ReadSpecialBlob(bitReader, 8)); } // next 30 bytes bitReader.ReadBytes(4); // same for all players bitReader.ReadBytes(14); bitReader.ReadBytes(12); // same for all players // these were important in ptr build 47801, not sure what it's used for now // each byte has a max value of 0x7F (127) if (replay.ReplayBuild >= 48027) { bitReader.ReadInt16(); } else { bitReader.ReadInt32(); } // this data is a repeat of the usable skins section above //bitReader.stream.Position = bitReader.stream.Position = bitReader.stream.Position + (skinArrayLength * 2); for (int i = 0; i < skinArrayLength; i++) { // each byte has a max value of 0x7F (127) int value = 0; int x = (int)bitReader.Read(8); if (x > 0) { value += x + 127; } value += (int)bitReader.Read(8); } bitReader.Read(1); if (bitReader.ReadBoolean()) { // use this to determine who is in a party // those in the same party will have the same exact 8 bytes of data // the party leader is the first one (in the order of the client list) bitReader.ReadBytes(8); } bitReader.Read(1); var battleTag = Encoding.UTF8.GetString(bitReader.ReadBlobPrecededWithLength(7)).Split('#'); // battleTag <name>#xxxxx if (battleTag.Length != 2 || battleTag[0] != replay.ClientListByUserID[player].Name) { throw new Exception("Couldn't find BattleTag"); } replay.ClientListByUserID[player].BattleTag = int.Parse(battleTag[1]); // these similar bytes don't occur for last player bitReader.ReadBytes(27); } // some more data after this } }
/// <summary> Parses the replay.initdata file in a replay file. </summary> /// <param name="replay"> The replay file to apply the parsed data to. </param> /// <param name="buffer"> The buffer containing the replay.initdata file. </param> public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) { var reader = new BitReader(stream); var playerListLength = reader.Read(5); for (var i = 0; i < playerListLength; i++) { var playerName = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(8)); // Player name // Populate the name for each client in the client list by UserID if (playerName != "") { replay.ClientListByUserID[i] = new Player { Name = playerName } } ; if (reader.ReadBoolean()) { reader.ReadBlobPrecededWithLength(8); // clanTag } if (reader.ReadBoolean()) { reader.ReadBlobPrecededWithLength(40); // Clan Logo } if (reader.ReadBoolean()) { reader.Read(8); // highestLeague } if (reader.ReadBoolean()) { reader.ReadInt32(); // combinedRaceLevels } reader.ReadInt32(); // Random seed (So far, always 0 in Heroes) if (reader.ReadBoolean()) { reader.Read(8); // Race Preference } if (reader.ReadBoolean()) { reader.Read(8); // Team Preference } reader.ReadBoolean(); //test map reader.ReadBoolean(); //test auto reader.ReadBoolean(); //examine reader.ReadBoolean(); //custom interface reader.ReadInt32(); // m_testType reader.Read(2); //observer Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(9)); // m_hero - Currently Empty String Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(9)); // m_skin - Currently Empty String Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(9)); // m_mount - Currently Empty String if (replay.ReplayVersionMajor >= 2) { Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(9)); // m_banner - Currently Empty String Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(9)); // m_spray - Currently Empty String } Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); // m_toonHandle - Currently Empty String } // Marked as 'Random Value', so I will use as seed replay.RandomValue = (uint)reader.ReadInt32(); reader.ReadBlobPrecededWithLength(10); // m_gameCacheName - "Dflt" reader.ReadBoolean(); // Lock Teams reader.ReadBoolean(); // Teams Together reader.ReadBoolean(); // Advanced Shared Control reader.ReadBoolean(); // Random Races reader.ReadBoolean(); // BattleNet reader.ReadBoolean(); // AMM reader.ReadBoolean(); // Competitive reader.ReadBoolean(); // m_practice reader.ReadBoolean(); // m_cooperative reader.ReadBoolean(); // m_noVictoryOrDefeat reader.ReadBoolean(); // m_heroDuplicatesAllowed reader.Read(2); // Fog reader.Read(2); // Observers reader.Read(2); // User Difficulty reader.ReadInt32(); reader.ReadInt32(); // 64 bit int: Client Debug Flags // m_ammId if (replay.ReplayBuild >= 43905 && reader.ReadBoolean()) { switch (reader.ReadInt32()) { case 50021: // Versus AI (Cooperative) case 50041: // Practice break; case 50001: replay.GameMode = GameMode.QuickMatch; break; case 50031: replay.GameMode = GameMode.Brawl; break; case 50051: replay.GameMode = GameMode.UnrankedDraft; break; case 50061: replay.GameMode = GameMode.HeroLeague; break; case 50071: replay.GameMode = GameMode.TeamLeague; break; default: replay.GameMode = GameMode.Unknown; break; } } reader.Read(3); // Game Speed // Not sure what this 'Game Type' is reader.Read(3); var maxUsers = reader.Read(5); if (maxUsers != 10 && replay.GameMode != GameMode.Brawl) // Max Players { replay.GameMode = GameMode.TryMe; } reader.Read(5); // Max Observers reader.Read(5); // Max Players reader.Read(4); // + 1 = Max Teams reader.Read(6); // Max Colors reader.Read(8); // + 1 = Max Races // Max Controls if (replay.ReplayBuild < 59279) { reader.Read(8); } else { reader.Read(4); } replay.MapSize = new Point { X = (int)reader.Read(8), Y = (int)reader.Read(8) }; if (replay.MapSize.Y == 1) { replay.MapSize.Y = replay.MapSize.X; } else if (replay.MapSize.X == 0) { replay.MapSize.X = replay.MapSize.Y; } // I haven't tested the following code on replays before build 39595 (End of 2015) if (replay.ReplayBuild < 39595) { return; } reader.Read(32); // m_mapFileSyncChecksum reader.ReadBlobPrecededWithLength(11); // m_mapFileName reader.ReadBlobPrecededWithLength(8); // m_mapAuthorName reader.Read(32); // m_modFileSyncChecksum // m_slotDescriptions var slotDescriptionLength = reader.Read(5); for (var i = 0; i < slotDescriptionLength; i++) { reader.ReadBitArray(reader.Read(6)); // m_allowedColors reader.ReadBitArray(reader.Read(8)); // m_allowedRaces reader.ReadBitArray(reader.Read(6)); // m_allowedDifficulty // m_allowedControls if (replay.ReplayBuild < 59279) { reader.ReadBitArray(reader.Read(8)); } else { reader.ReadBitArray(reader.Read(4)); } reader.ReadBitArray(reader.Read(2)); // m_allowedObserveTypes reader.ReadBitArray(reader.Read(7)); // m_allowedAIBuilds } reader.Read(6); // m_defaultDifficulty reader.Read(7); // m_defaultAIBuild // m_cacheHandles var cacheHandlesLength = reader.Read(6); for (var i = 0; i < cacheHandlesLength; i++) { reader.ReadBytes(40); } reader.ReadBoolean(); // m_hasExtensionMod reader.ReadBoolean(); // m_isBlizzardMap reader.ReadBoolean(); // m_isPremadeFFA reader.ReadBoolean(); // m_isCoopMode #region m_lobbyState reader.Read(3); // m_phase reader.Read(5); // m_maxUsers reader.Read(5); // m_maxObservers // m_slots var slotsLength = reader.Read(5); for (var i = 0; i < slotsLength; i++) { int?userID = null; reader.Read(8); // m_control if (reader.ReadBoolean()) { userID = (int)reader.Read(4); // m_userId } reader.Read(4); // m_teamId if (reader.ReadBoolean()) { reader.Read(5); // m_colorPref } if (reader.ReadBoolean()) { reader.Read(8); // m_racePref } reader.Read(6); // m_difficulty reader.Read(7); // m_aiBuild reader.Read(7); // m_handicap // m_observe var observerStatus = reader.Read(2); reader.Read(32); // m_logoIndex reader.ReadBlobPrecededWithLength(9); // m_hero var skinAndSkinTint = Encoding.ASCII.GetString(reader.ReadBlobPrecededWithLength(9)); // m_skin if (skinAndSkinTint == "") { skinAndSkinTint = null; } var mountAndMountTint = Encoding.ASCII.GetString(reader.ReadBlobPrecededWithLength(9)); // m_mount if (mountAndMountTint == "") { mountAndMountTint = null; } // m_artifacts var artifactsLength = reader.Read(4); for (var j = 0; j < artifactsLength; j++) { reader.ReadBlobPrecededWithLength(9); } int?workingSetSlotID = null; if (reader.ReadBoolean()) { workingSetSlotID = (int)reader.Read(8); // m_workingSetSlotId } if (userID.HasValue && workingSetSlotID.HasValue) { if (replay.ClientListByWorkingSetSlotID[workingSetSlotID.Value] != null) { replay.ClientListByUserID[userID.Value] = replay.ClientListByWorkingSetSlotID[workingSetSlotID.Value]; } if (observerStatus == 2) { replay.ClientListByUserID[userID.Value].PlayerType = PlayerType.Spectator; } replay.ClientListByUserID[userID.Value].SkinAndSkinTint = skinAndSkinTint; replay.ClientListByUserID[userID.Value].MountAndMountTint = mountAndMountTint; } // m_rewards var rewardsLength = reader.Read(17); for (var j = 0; j < rewardsLength; j++) { reader.Read(32); } reader.ReadBlobPrecededWithLength(7); // m_toonHandle // m_licenses if (replay.ReplayBuild < 49582 || replay.ReplayBuild == 49838) { var licensesLength = reader.Read(9); for (var j = 0; j < licensesLength; j++) { reader.Read(32); } } if (reader.ReadBoolean()) { reader.Read(4); // m_tandemLeaderUserId } if (replay.ReplayBuild <= 41504) { reader.ReadBlobPrecededWithLength(9); // m_commander - Empty string reader.Read(32); // m_commanderLevel - So far, always 0 } if (reader.ReadBoolean() && userID.HasValue) // m_hasSilencePenalty { replay.ClientListByUserID[userID.Value].IsSilenced = true; } if (replay.ReplayBuild >= 61718 && reader.ReadBoolean() && userID.HasValue) // m_hasVoiceSilencePenalty { replay.ClientListByUserID[userID.Value].IsVoiceSilence = true; } if (replay.ReplayVersionMajor >= 2) { Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(9)); // m_banner Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(9)); // m_spray Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(9)); // m_announcerPack Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(9)); // m_voiceLine // m_heroMasteryTiers if (replay.ReplayBuild >= 52561) { var heroMasteryTiersLength = reader.Read(10); for (var j = 0; j < heroMasteryTiersLength; j++) { reader.Read(32); // m_hero reader.Read(8); // m_tier } } } } if (reader.Read(32) != replay.RandomValue) // m_randomSeed { throw new Exception("Replay Random Seed Values in Replay Init Data did not match"); } if (reader.ReadBoolean()) { reader.Read(4); // m_hostUserId } reader.ReadBoolean(); // m_isSinglePlayer reader.Read(8); // m_pickedMapTag - So far, always 0 reader.Read(32); // m_gameDuration - So far, always 0 reader.Read(6); // m_defaultDifficulty reader.Read(7); // m_defaultAIBuild #endregion } } }
/// <summary> Parses the replay.initdata file in a replay file. </summary> /// <param name="replay"> The replay file to apply the parsed data to. </param> /// <param name="buffer"> The buffer containing the replay.initdata file. </param> public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) { var reader = new BitReader(stream); var i = reader.ReadByte(); var playerList = new string[i]; for (int j = 0; j < i; j++) { playerList[j] = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(8)); if (reader.ReadBoolean()) { var clanTag = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(8)); // Console.WriteLine(clanTag); } if (reader.ReadBoolean()) { // Clan Logo reader.ReadBlobPrecededWithLength(40); } if (reader.ReadBoolean()) { var highestLeague = reader.Read(8); // Console.WriteLine(highestLeague); } if (reader.ReadBoolean()) { var combinedRaceLevels = reader.ReadInt32(); // Console.WriteLine(combinedRaceLevels); } reader.ReadInt32(); // Random seed (So far, always 0 in Heroes) if (reader.ReadBoolean()) { reader.Read(8); // Race Preference } if (reader.ReadBoolean()) { reader.Read(8); // Team Preference } reader.ReadBoolean(); //test map reader.ReadBoolean(); //test auto reader.ReadBoolean(); //examine reader.ReadBoolean(); //custom interface var unknown1 = reader.ReadInt32(); reader.Read(2); //observer var unknown2 = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); var unknown3 = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); var unknown4 = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); var unknown5 = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); var unknown6 = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); var unknown7 = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); // Console.WriteLine(unknown1 + unknown2 + unknown3 + unknown4 + unknown5 + unknown6 + unknown7); } // Marked as 'Random Value', so I will use as seed replay.RandomValue = (uint)reader.ReadInt32(); reader.ReadBlobPrecededWithLength(10); // Dflt reader.ReadBoolean(); // Lock Teams reader.ReadBoolean(); // Teams Together reader.ReadBoolean(); // Advanced Shared Control reader.ReadBoolean(); // Random Races reader.ReadBoolean(); // BattleNet reader.ReadBoolean(); // AMM reader.ReadBoolean(); // Competitive reader.ReadBoolean(); // No Victory Or Defeat reader.ReadBoolean(); // Unknown 0 reader.ReadBoolean(); // Unknown 1 reader.ReadBoolean(); // Unknown 2 reader.Read(2); // Fog reader.Read(2); // Observers reader.Read(2); // User Difficulty reader.ReadInt32(); reader.ReadInt32(); // 64 bit int: Client Debug Flags reader.Read(3); // Game Speed // Not sure what this 'Game Type' is reader.Read(3); var maxUsers = reader.Read(5); if (maxUsers != 10) // Max Players { replay.GameMode = GameMode.TryMe; } reader.Read(5); // Max Observers reader.Read(5); // Max Players reader.Read(4); // + 1 = Max Teams reader.Read(6); // Max Colors reader.Read(8); // + 1 = Max Races reader.Read(8); // Max Controls replay.MapSize = new Point { X = (int)reader.Read(8), Y = (int)reader.Read(8) }; if (replay.MapSize.Y == 1) { replay.MapSize.Y = replay.MapSize.X; } else if (replay.MapSize.X == 0) { replay.MapSize.X = replay.MapSize.Y; } // About 1000 bytes from here is a list of characters, character skins, character mounts, artifact selections, and other data } }
/// <summary> Parses the Replay.Messages.Events file. </summary> /// <param name="buffer"> Buffer containing the contents of the replay.messages.events file. </param> /// <returns> A list of chat messages parsed from the buffer. </returns> public static void Parse(Replay replay, byte[] buffer) { var messages = new List <ChatMessage>(); using (var stream = new MemoryStream(buffer)) { using (var reader = new BinaryReader(stream)) { int totalTime = 0; while (reader.BaseStream.Position < reader.BaseStream.Length) { // While not EOF var message = new ChatMessage(); var time = ParseTimestamp(reader); // sometimes we only have a header for the message if (reader.BaseStream.Position >= reader.BaseStream.Length) { break; } message.PlayerId = reader.ReadByte(); // I believe this 'PlayerId' is an index for this client list, which can include observers // var player = replay.ClientList[message.PlayerId]; totalTime += time; var opCode = reader.ReadByte(); if (opCode == 0x80) { reader.ReadBytes(4); } else if (opCode == 0x83) { reader.ReadBytes(8); } else if (opCode == 2 && message.PlayerId <= 10) { if (message.PlayerId == 80) { continue; } message.MessageTarget = (ChatMessageTarget)(opCode & 7); var length = reader.ReadByte(); if ((opCode & 8) == 8) { length += 64; } if ((opCode & 16) == 16) { length += 128; } message.Message = Encoding.UTF8.GetString(reader.ReadBytes(length)); } else { } if (message.Message != null) { message.Timestamp = new TimeSpan(0, 0, (int)Math.Round(totalTime / 16.0)); messages.Add(message); } } } } replay.ChatMessages = messages; }
internal static void DetailedParse(BitReader bitReader, Replay replay, int s2mArrayLength) { bitReader.AlignToByte(); for (; ;) { // we're just going to skip all the way down to the s2mh if (bitReader.ReadString(4) == "s2mh") { bitReader.stream.Position = bitReader.stream.Position - 4; break; } else { bitReader.stream.Position = bitReader.stream.Position - 3; } } for (var i = 0; i < s2mArrayLength; i++) { bitReader.ReadString(4); // s2mh bitReader.ReadBytes(2); // 0x00 0x00 bitReader.ReadString(2); // Realm bitReader.ReadBytes(32); } // Player collections - starting with HOTS 2.0 (live build 52860) // strings gone starting with build (ptr) 55929 // -------------------------------------------------------------- List <string> playerCollection = new List <string>(); int collectionSize = 0; if (replay.ReplayBuild >= 48027) { collectionSize = bitReader.ReadInt16(); } else { collectionSize = bitReader.ReadInt32(); } if (collectionSize > 8000) { throw new DetailedParsedException("collectionSize is an unusually large number"); } for (int i = 0; i < collectionSize; i++) { if (replay.ReplayBuild >= 55929) { bitReader.ReadBytes(8); // most likey an identifier for the item; first six bytes are 0x00 } else { playerCollection.Add(bitReader.ReadString(bitReader.ReadByte())); } } // use to determine if the collection item is usable by the player (owns/free to play/internet cafe) if (bitReader.ReadInt32() != collectionSize) { throw new DetailedParsedException("skinArrayLength not equal"); } for (int i = 0; i < collectionSize; i++) { for (int j = 0; j < 16; j++) // 16 is total player slots { bitReader.ReadByte(); var num = bitReader.Read(8); if (replay.ReplayBuild < 55929) { if (replay.ClientListByUserID[j] != null) { if (num > 0) { replay.ClientListByUserID[j].PlayerCollectionDictionary.Add(playerCollection[i], true); } else if (num == 0) { replay.ClientListByUserID[j].PlayerCollectionDictionary.Add(playerCollection[i], false); } else { throw new NotImplementedException(); } } } } } // Player info // ------------------------ if (replay.ReplayBuild <= 43259 || replay.ReplayBuild == 47801) { // Builds that are not yet supported for detailed parsing // build 47801 is a ptr build that had new data in the battletag section, the data was changed in 47944 (patch for 47801) GetBattleTags(replay, bitReader); return; } // m_randomSeed, set it if it hasn't been set if (replay.RandomValue == 0) { replay.RandomValue = (uint)bitReader.ReadInt32(); } else { bitReader.ReadInt32(); } bitReader.ReadBytes(32); bitReader.ReadInt32(); // 0x19 if (replay.ReplayBuild <= 47479 || replay.ReplayBuild == 47903) { ExtendedBattleTagParsingOld(replay, bitReader); return; } for (int player = 0; player < replay.ClientListByUserID.Length; player++) { if (replay.ClientListByUserID[player] == null) { break; } if (player == 0) { var offset = bitReader.ReadByte(); bitReader.ReadString(2); // T: replay.ClientListByUserID[player].BattleNetTId = bitReader.ReadString(12 + offset); // TId } else { ReadByte0x00(bitReader); ReadByte0x00(bitReader); ReadByte0x00(bitReader); bitReader.Read(6); // get XXXXXXXX#YYY replay.ClientListByUserID[player].BattleNetTId = Encoding.UTF8.GetString(ReadSpecialBlob(bitReader, 8)); // TId } // next 30 bytes bitReader.ReadBytes(4); // same for all players bitReader.ReadBytes(25); bitReader.Read(7); bool noCollection = bitReader.ReadBoolean(); // repeat of the collection section above if (replay.ReplayBuild >= 51609 && !noCollection) { int size = (int)bitReader.Read(12); // 3 bytes if (size == collectionSize) { int bytesSize = collectionSize / 8; int bitsSize = collectionSize % 8; bitReader.ReadBytes(bytesSize); bitReader.Read(bitsSize); bitReader.ReadBoolean(); } // else if not equal, then data isn't available, most likely an observer } else if (!noCollection) { if (replay.ReplayBuild >= 48027) { bitReader.ReadInt16(); } else { bitReader.ReadInt32(); } // each byte has a max value of 0x7F (127) bitReader.stream.Position = bitReader.stream.Position + (collectionSize * 2); } bitReader.ReadBoolean(); // m_hasSilencePenalty if (replay.ReplayBuild >= 61718) { bitReader.ReadBoolean(); bitReader.ReadBoolean(); // m_hasVoiceSilencePenalty } if (replay.ReplayBuild >= 66977) { bitReader.ReadBoolean(); // m_isBlizzardStaff } if (bitReader.ReadBoolean()) // is player in party { replay.ClientListByUserID[player].PartyValue = bitReader.ReadInt32() + bitReader.ReadInt32(); // players in same party will have the same exact 8 bytes of data } bitReader.ReadBoolean(); var battleTag = Encoding.UTF8.GetString(bitReader.ReadBlobPrecededWithLength(7)).Split('#'); // battleTag <name>#xxxxx if (battleTag.Length != 2 || battleTag[0] != replay.ClientListByUserID[player].Name) { throw new DetailedParsedException("Couldn't find BattleTag"); } replay.ClientListByUserID[player].BattleTag = int.Parse(battleTag[1]); if (replay.ReplayBuild >= 52860 || (replay.ReplayVersionMajor == 2 && replay.ReplayBuild >= 51978)) { replay.ClientListByUserID[player].AccountLevel = bitReader.ReadInt32(); // player's account level, not available in custom games } bitReader.ReadBytes(27); // these similar bytes don't occur for last player } // some more data after this // there is also a CSTM string down here, if it exists, the game is a custom game }
public static string Base64EncodeStandaloneBattlelobby(Replay replay) { return(Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Join(",", replay.Players.Select(i => i.BattleNetRegionId + "#" + i.Name + "#" + i.BattleTag + "#" + i.Team))))); }
/// <summary> /// Applies the set of attributes to a replay. /// </summary> /// <param name="replay">Replay to apply the attributes to.</param> public void ApplyAttributes(Replay replay) { // I'm not entirely sure this is the right encoding here. Might be unicode... var encoding = Encoding.UTF8; var attributes1 = new List<ReplayAttribute>(); var attributes2 = new List<ReplayAttribute>(); var attributes3 = new List<ReplayAttribute>(); var attributes4 = new List<ReplayAttribute>(); var attributesffa = new List<ReplayAttribute>(); foreach (var attribute in Attributes) switch (attribute.AttributeType) { case ReplayAttributeEventType.PlayerTypeAttribute: // 500 { var type = encoding.GetString(attribute.Value.Reverse().ToArray()); if (type.ToLower().Equals("comp")) replay.Players[attribute.PlayerId - 1].PlayerType = PlayerType.Computer; else if (type.ToLower().Equals("humn")) replay.Players[attribute.PlayerId - 1].PlayerType = PlayerType.Human; else throw new Exception("Unexpected value"); break; } case ReplayAttributeEventType.TeamSizeAttribute: { // This fixes issues with reversing the string before encoding. Without this, you get "\01v1" replay.TeamSize = new string(encoding.GetString(attribute.Value, 0, 3).Reverse().ToArray()); break; } case ReplayAttributeEventType.DifficultyLevelAttribute: { var diffLevel = encoding.GetString(attribute.Value.Reverse().ToArray()).ToLower(); var player = replay.Players[attribute.PlayerId - 1]; switch (diffLevel) { case "vyey": player.Difficulty = Difficulty.VeryEasy; break; case "easy": player.Difficulty = Difficulty.Easy; break; case "medi": player.Difficulty = Difficulty.Medium; break; case "hdvh": // *change* player.Difficulty = Difficulty.Hard; break; case "vyhd": player.Difficulty = Difficulty.VeryHard; break; default: break; } break; } case ReplayAttributeEventType.GameSpeedAttribute: { var speed = encoding.GetString(attribute.Value.Reverse().ToArray()).ToLower(); switch (speed) { case "slor": replay.GameSpeed = GameSpeed.Slower; break; case "slow": replay.GameSpeed = GameSpeed.Slow; break; case "norm": replay.GameSpeed = GameSpeed.Normal; break; case "fast": replay.GameSpeed = GameSpeed.Fast; break; case "fasr": replay.GameSpeed = GameSpeed.Faster; break; // Otherwise, Game Speed will remain "Unknown" } break; } case ReplayAttributeEventType.PlayerTeam1v1Attribute: { attributes1.Add(attribute); break; } case ReplayAttributeEventType.PlayerTeam2v2Attribute: { attributes2.Add(attribute); break; } case ReplayAttributeEventType.PlayerTeam3v3Attribute: { attributes3.Add(attribute); break; } case ReplayAttributeEventType.PlayerTeam4v4Attribute: { attributes4.Add(attribute); break; } case ReplayAttributeEventType.PlayerTeamFFAAttribute: { attributesffa.Add(attribute); break; } case ReplayAttributeEventType.GameTypeAttribute: { switch (encoding.GetString(attribute.Value.Reverse().ToArray()).ToLower().Trim('\0')) { case "priv": replay.GameMode = GameMode.Custom; break; case "amm": if (replay.ReplayBuild < 33684) replay.GameMode = GameMode.QuickMatch; break; default: throw new Exception("Unexpected Game Type"); } break; } case ReplayAttributeEventType.Character: { replay.Players[attribute.PlayerId - 1].IsAutoSelect = encoding.GetString(attribute.Value.Reverse().ToArray()) == "Rand"; break; } case ReplayAttributeEventType.CharacterLevel: { var characterLevel = int.Parse(encoding.GetString(attribute.Value.Reverse().ToArray())); var player = replay.Players[attribute.PlayerId - 1]; player.CharacterLevel = characterLevel; break; } case ReplayAttributeEventType.HeroSelectionMode: { if (replay.GameMode != GameMode.Custom) switch (encoding.GetString(attribute.Value.Reverse().ToArray()).ToLower().Trim('\0')) { case "stan": replay.GameMode = GameMode.QuickMatch; break; case "drft": replay.GameMode = GameMode.HeroLeague; break; } } break; case ReplayAttributeEventType.HeroDraftMode: if (replay.GameMode == GameMode.HeroLeague && encoding.GetString(attribute.Value.Reverse().ToArray()).ToLower().Trim('\0') == "fcfs") replay.GameMode = GameMode.TeamLeague; break; case (ReplayAttributeEventType)4011: // What is this? Draft order? break; case (ReplayAttributeEventType)4016: // What is this? Always '1' in Hero League // if (replay.GameMode == GameMode.HeroLeague && int.Parse(encoding.GetString(attribute.Value.Reverse().ToArray())) != 1) // Console.WriteLine("WAAT!?"); break; case (ReplayAttributeEventType)4017: // What is this? Always '5' in Hero League // if (replay.GameMode == GameMode.HeroLeague && int.Parse(encoding.GetString(attribute.Value.Reverse().ToArray())) != 5) // Console.WriteLine("WAAT!?"); break; } List<ReplayAttribute> currentList = null; if (replay.TeamSize.Equals("1v1")) currentList = attributes1; else if (replay.TeamSize.Equals("2v2")) currentList = attributes2; else if (replay.TeamSize.Equals("3v3")) currentList = attributes3; else if (replay.TeamSize.Equals("4v4")) currentList = attributes4; else if (replay.TeamSize.Equals("FFA")) currentList = attributesffa; if (currentList != null) foreach (var att in currentList) // Reverse the values then parse, you don't notice the effects of this until theres 10+ teams o.o replay.Players[att.PlayerId - 1].Team = int.Parse(encoding.GetString(att.Value.Reverse().ToArray()).Trim('\0', 'T')); }
public static void Parse(Replay replay) { // I believe these 'PlayerID' are just indexes to the ClientList, but we should use the info given in this file just to be safe var playerIDDictionary = new Dictionary <int, Player>(); for (var i = 0; i < replay.TeamLevels.Length; i++) { replay.TeamLevels[i] = new Dictionary <int, TimeSpan>(); replay.TeamPeriodicXPBreakdown[i] = new List <PeriodicXPBreakdown>(); } var playerIDTalentIndexDictionary = new Dictionary <int, int>(); foreach (var trackerEvent in replay.TrackerEvents.Where(i => i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UpgradeEvent || i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.StatGameEvent || i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.ScoreResultEvent)) { switch (trackerEvent.TrackerEventType) { case ReplayTrackerEvents.TrackerEventType.UpgradeEvent: switch (trackerEvent.Data.dictionary[1].blobText) { case "CreepColor": // Not sure what this is - it's been in the replay file since Alpha, so it may just be a SC2 remnant break; case "IsPlayer11": case "IsPlayer12": // Also not sure what this is break; case "GatesAreOpen": case "MinionsAreSpawning": case "GallTalentNetherCallsUpgrade": case "TracerJumperButtonSwap": // Not really interested in these break; case "VehicleDragonUpgrade": break; case "NovaSnipeMasterDamageUpgrade": playerIDDictionary[(int)trackerEvent.Data.dictionary[0].vInt.Value].UpgradeEvents.Add(new UpgradeEvent { TimeSpan = trackerEvent.TimeSpan, UpgradeEventType = UpgradeEventType.NovaSnipeMasterDamageUpgrade, Value = (int)trackerEvent.Data.dictionary[2].vInt.Value }); break; case "GallTalentDarkDescentUpgrade": playerIDDictionary[(int)trackerEvent.Data.dictionary[0].vInt.Value].UpgradeEvents.Add(new UpgradeEvent { TimeSpan = trackerEvent.TimeSpan, UpgradeEventType = UpgradeEventType.GallTalentDarkDescentUpgrade, Value = (int)trackerEvent.Data.dictionary[2].vInt.Value }); break; default: // New Upgrade Event - let's log it until we can identify and properly track it playerIDDictionary[(int)trackerEvent.Data.dictionary[0].vInt.Value].MiscellaneousUpgradeEventDictionary[trackerEvent.Data.dictionary[1].blobText] = true; break; } break; case ReplayTrackerEvents.TrackerEventType.StatGameEvent: switch (trackerEvent.Data.dictionary[0].blobText) { case "GameStart": // {StatGameEvent: {"GameStart", , , [{{"MapSizeX"}, 248}, {{"MapSizeY"}, 208}]}} if (trackerEvent.Data.dictionary[3].optionalData.array[0].dictionary[0].dictionary[0].blobText == "MapSizeX" && trackerEvent.Data.dictionary[3].optionalData.array[1].dictionary[0].dictionary[0].blobText == "MapSizeY") { replay.MapSize = new Point { X = (int)trackerEvent.Data.dictionary[3].optionalData.array[0].dictionary[1].vInt.Value, Y = (int)trackerEvent.Data.dictionary[3].optionalData.array[1].dictionary[1].vInt.Value } } ; break; case "PlayerInit": // {StatGameEvent: {"PlayerInit", [{{"Controller"}, "User"}, {{"ToonHandle"}, "1-Hero-1-XXXXX"}], [{{"PlayerID"}, 1}, {{"Team"}, 1}], }} if (trackerEvent.Data.dictionary[1].optionalData.array[1].dictionary[0].dictionary[0].blobText == "ToonHandle" && trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[0].dictionary[0].blobText == "PlayerID") { playerIDDictionary[(int)trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[1].vInt.Value] = replay.Players.Single(i => i.BattleNetId == int.Parse(trackerEvent.Data.dictionary[1].optionalData.array[1].dictionary[1].blobText.Split('-').Last())); } break; case "LevelUp": // {StatGameEvent: {"LevelUp", , [{{"PlayerID"}, 6}, {{"Level"}, 1}], }} if (trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[0].dictionary[0].blobText == "PlayerID" && trackerEvent.Data.dictionary[2].optionalData.array[1].dictionary[0].dictionary[0].blobText == "Level") { var team = playerIDDictionary[(int)trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[1].vInt.Value].Team; var level = (int)trackerEvent.Data.dictionary[2].optionalData.array[1].dictionary[1].vInt.Value; if (!replay.TeamLevels[team].ContainsKey(level)) { replay.TeamLevels[team][level] = trackerEvent.TimeSpan; } } break; case "TalentChosen": // {StatGameEvent: {"TalentChosen", [{{"PurchaseName"}, "NovaCombatStyleAdvancedCloaking"}], [{{"PlayerID"}, 6}], }} if (trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[0].dictionary[0].blobText == "PlayerID" && trackerEvent.Data.dictionary[1].optionalData != null && trackerEvent.Data.dictionary[1].optionalData.array[0].dictionary[0].dictionary[0].blobText == "PurchaseName") { var playerID = (int)trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[1].vInt.Value; if (!playerIDTalentIndexDictionary.ContainsKey(playerID)) { playerIDTalentIndexDictionary[playerID] = 0; } if (playerIDDictionary[playerID].Talents.Length > playerIDTalentIndexDictionary[playerID]) { playerIDDictionary[playerID].Talents[playerIDTalentIndexDictionary[playerID]++].TalentName = trackerEvent.Data.dictionary[1].optionalData.array[0].dictionary[1].blobText; } else { // A talent was selected while a player was disconnected // This makes it more difficult to match a 'TalentName' with a 'TalentID' // Since this is rare, I'll just clear all 'TalentName' for that player foreach (var talent in playerIDDictionary[playerID].Talents) { talent.TalentName = null; } } } break; case "PeriodicXPBreakdown": // {StatGameEvent: {"PeriodicXPBreakdown", , [{{"Team"}, 1}, {{"TeamLevel"}, 9}], [{{"GameTime"}, 420}, {{"PreviousGameTime"}, 360}, {{"MinionXP"}, 10877}, {{"CreepXP"}, 0}, {{"StructureXP"}, 1200}, {{"HeroXP"}, 3202}, {{"TrickleXP"}, 7700}]}} if (trackerEvent.Data.dictionary[2].optionalData.array[1].dictionary[0].dictionary[0].blobText == "TeamLevel" && trackerEvent.Data.dictionary[3].optionalData.array[0].dictionary[0].dictionary[0].blobText == "GameTime" && trackerEvent.Data.dictionary[3].optionalData.array[1].dictionary[0].dictionary[0].blobText == "PreviousGameTime" && trackerEvent.Data.dictionary[3].optionalData.array[2].dictionary[0].dictionary[0].blobText == "MinionXP" && trackerEvent.Data.dictionary[3].optionalData.array[3].dictionary[0].dictionary[0].blobText == "CreepXP" && trackerEvent.Data.dictionary[3].optionalData.array[4].dictionary[0].dictionary[0].blobText == "StructureXP" && trackerEvent.Data.dictionary[3].optionalData.array[5].dictionary[0].dictionary[0].blobText == "HeroXP" && trackerEvent.Data.dictionary[3].optionalData.array[6].dictionary[0].dictionary[0].blobText == "TrickleXP") { replay.TeamPeriodicXPBreakdown[(int)trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[1].vInt.Value - 1].Add(new PeriodicXPBreakdown { TeamLevel = (int)trackerEvent.Data.dictionary[2].optionalData.array[1].dictionary[1].vInt.Value, TimeSpan = trackerEvent.TimeSpan, MinionXP = (int)trackerEvent.Data.dictionary[3].optionalData.array[2].dictionary[1].vInt.Value, CreepXP = (int)trackerEvent.Data.dictionary[3].optionalData.array[3].dictionary[1].vInt.Value, StructureXP = (int)trackerEvent.Data.dictionary[3].optionalData.array[4].dictionary[1].vInt.Value, HeroXP = (int)trackerEvent.Data.dictionary[3].optionalData.array[5].dictionary[1].vInt.Value, TrickleXP = (int)trackerEvent.Data.dictionary[3].optionalData.array[6].dictionary[1].vInt.Value }); } break; case "EndOfGameXPBreakdown": // {StatGameEvent: {"EndOfGameXPBreakdown", , [{{"PlayerID"}, 4}], [{{"MinionXP"}, 31222}, {{"CreepXP"}, 1476}, {{"StructureXP"}, 10550}, {{"HeroXP"}, 22676}, {{"TrickleXP"}, 27280}]}} if (trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[0].dictionary[0].blobText == "PlayerID" && trackerEvent.Data.dictionary[3].optionalData.array[0].dictionary[0].dictionary[0].blobText == "MinionXP" && trackerEvent.Data.dictionary[3].optionalData.array[1].dictionary[0].dictionary[0].blobText == "CreepXP" && trackerEvent.Data.dictionary[3].optionalData.array[2].dictionary[0].dictionary[0].blobText == "StructureXP" && trackerEvent.Data.dictionary[3].optionalData.array[3].dictionary[0].dictionary[0].blobText == "HeroXP" && trackerEvent.Data.dictionary[3].optionalData.array[4].dictionary[0].dictionary[0].blobText == "TrickleXP" && (!replay.TeamPeriodicXPBreakdown[playerIDDictionary[(int)trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[1].vInt.Value].Team].Any() || replay.TeamPeriodicXPBreakdown[playerIDDictionary[(int)trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[1].vInt.Value].Team].Last().TimeSpan != trackerEvent.TimeSpan)) { replay.TeamPeriodicXPBreakdown[playerIDDictionary[(int)trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[1].vInt.Value].Team].Add(new PeriodicXPBreakdown { TeamLevel = replay.TeamLevels[playerIDDictionary[(int)trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[1].vInt.Value].Team].Keys.Max(), TimeSpan = trackerEvent.TimeSpan, MinionXP = (int)trackerEvent.Data.dictionary[3].optionalData.array[0].dictionary[1].vInt.Value, CreepXP = (int)trackerEvent.Data.dictionary[3].optionalData.array[1].dictionary[1].vInt.Value, StructureXP = (int)trackerEvent.Data.dictionary[3].optionalData.array[2].dictionary[1].vInt.Value, HeroXP = (int)trackerEvent.Data.dictionary[3].optionalData.array[3].dictionary[1].vInt.Value, TrickleXP = (int)trackerEvent.Data.dictionary[3].optionalData.array[4].dictionary[1].vInt.Value }); } break; case "TownStructureInit": break; // {StatGameEvent: {"TownStructureInit", , [{{"TownID"}, 5}, {{"Team"}, 1}, {{"Lane"}, 3}], [{{"PositionX"}, 59}, {{"PositionY"}, 93}]}} case "JungleCampInit": break; // {StatGameEvent: {"JungleCampInit", , [{{"CampID"}, 1}], [{{"PositionX"}, 101}, {{"PositionY"}, 74}]}} case "PlayerSpawned": break; // {StatGameEvent: {"PlayerSpawned", [{{"Hero"}, "HeroLeoric"}], [{{"PlayerID"}, 1}], }} case "GatesOpen": break; // {StatGameEvent: {"GatesOpen", , , }} case "PlayerDeath": break; // {StatGameEvent: {"PlayerDeath", , [{{"PlayerID"}, 8}, {{"KillingPlayer"}, 1}, {{"KillingPlayer"}, 2}, {{"KillingPlayer"}, 3}, {{"KillingPlayer"}, 4}, {{"KillingPlayer"}, 5}], [{{"PositionX"}, 130}, {{"PositionY"}, 80}]}} case "RegenGlobePickedUp": break; // {StatGameEvent: {"RegenGlobePickedUp", , [{{"PlayerID"}, 1}], }} case "JungleCampCapture": // {StatGameEvent: {"JungleCampCapture", [{{"CampType"}, "Boss Camp"}], [{{"CampID"}, 1}], [{{"TeamID"}, 1}]}} if (trackerEvent.Data.dictionary[1].optionalData.array[0].dictionary[1].blobText == "Boss Camp") { var teamID = trackerEvent.Data.dictionary[3].optionalData.array[0].dictionary[1].vInt.Value; // TODO: LOG THIS SOMEWHERE } break; case "TownStructureDeath": break; // {StatGameEvent: {"TownStructureDeath", , [{{"TownID"}, 8}, {{"KillingPlayer"}, 1}, {{"KillingPlayer"}, 2}, {{"KillingPlayer"}, 3}, {{"KillingPlayer"}, 4}, {{"KillingPlayer"}, 5}], }} case "EndOfGameTimeSpentDead": break; // {StatGameEvent: {"EndOfGameTimeSpentDead", , [{{"PlayerID"}, 2}], [{{"Time"}, 162}]}} // Map Objectives case "Altar Captured": break; // {StatGameEvent: {"Altar Captured", , [{{"Firing Team"}, 2}, {{"Towns Owned"}, 3}], }} case "Town Captured": break; // {StatGameEvent: {"Town Captured", , [{{"New Owner"}, 12}], }} case "Six Town Event Start": break; // {StatGameEvent: {"Six Town Event Start", , [{{"Owning Team"}, 1}], [{{"Start Time"}, 742}]}} case "Six Town Event End": break; // {StatGameEvent: {"Six Town Event End", , [{{"Owning Team"}, 1}], [{{"End Time"}, 747}]}} case "SkyTempleActivated": break; // {StatGameEvent: {"SkyTempleActivated", , [{{"Event"}, 1}, {{"TempleID"}, 1}], }} case "SkyTempleCaptured": break; // {StatGameEvent: {"SkyTempleCaptured", , [{{"Event"}, 1}, {{"TempleID"}, 2}, {{"TeamID"}, 2}], }} case "SkyTempleShotsFired": break; // {StatGameEvent: {"SkyTempleShotsFired", , [{{"Event"}, 1}, {{"TempleID"}, 2}, {{"TeamID"}, 2}], [{{"SkyTempleShotsDamage"}, 450}]}} case "Immortal Defeated": break; // {StatGameEvent: {"Immortal Defeated", , [{{"Event"}, 1}, {{"Winning Team"}, 1}, {{"Immortal Fight Duration"}, 62}], [{{"Immortal Power Percent"}, 14}]}} case "Boss Duel Started": break; // {StatGameEvent: {"Boss Duel Started", , [{{"Boss Duel Number"}, 1}], }} case "SoulEatersSpawned": break; // {StatGameEvent: {"SoulEatersSpawned", , [{{"Event"}, 1}, {{"TeamScore"}, 50}, {{"OpponentScore"}, 5}], [{{"TeamID"}, 2}]}} case "TributeCollected": break; // {StatGameEvent: {"TributeCollected", , [{{"Event"}, 1}], [{{"TeamID"}, 2}]}} case "RavenCurseActivated": break; // {StatGameEvent: {"RavenCurseActivated", , [{{"Event"}, 1}, {{"TeamScore"}, 3}, {{"OpponentScore"}, 2}], [{{"TeamID"}, 2}]}} case "GhostShipCaptured": break; // {StatGameEvent: {"GhostShipCaptured", , [{{"Event"}, 1}, {{"TeamScore"}, 10}, {{"OpponentScore"}, 6}], [{{"TeamID"}, 2}]}} case "GardenTerrorActivated": break; // {StatGameEvent: {"GardenTerrorActivated", , , [{{"Event"}, 1}, {{"TeamID"}, 2}]}} case "Infernal Shrine Captured": break; // {StatGameEvent: {"Infernal Shrine Captured", , [{{"Event"}, 1}, {{"Winning Team"}, 2}, {{"Winning Score"}, 40}, {{"Losing Score"}, 33}], }} case "Punisher Killed": break; // {StatGameEvent: {"Punisher Killed", [{{"Punisher Type"}, "BombardShrine"}], [{{"Event"}, 1}, {{"Owning Team of Punisher"}, 2}, {{"Duration"}, 20}], [{{"Siege Damage Done"}, 726}, {{"Hero Damage Done"}, 0}]}} case "DragonKnightActivated": break; // {StatGameEvent: {"DragonKnightActivated", , [{{"Event"}, 1}], [{{"TeamID"}, 2}]}} default: break; } break; case ReplayTrackerEvents.TrackerEventType.ScoreResultEvent: var scoreResultEventDictionary = trackerEvent.Data.dictionary[0].array.ToDictionary(i => i.dictionary[0].blobText, i => i.dictionary[1].array.Select(j => j.array.Length == 1 ? (int)j.array[0].dictionary[0].vInt.Value : (int?)null).ToArray()); foreach (var scoreResultEventKey in scoreResultEventDictionary.Keys) { var scoreResultEventValueArray = scoreResultEventDictionary[scoreResultEventKey]; switch (scoreResultEventKey) { case "Takedowns": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.Takedowns = scoreResultEventValueArray[i].Value; } } break; case "SoloKill": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.SoloKills = scoreResultEventValueArray[i].Value; } } break; case "Assists": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.Assists = scoreResultEventValueArray[i].Value; } } break; case "Deaths": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.Deaths = scoreResultEventValueArray[i].Value; } } break; case "HeroDamage": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.HeroDamage = scoreResultEventValueArray[i].Value; } } break; case "SiegeDamage": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.SiegeDamage = scoreResultEventValueArray[i].Value; } } break; case "StructureDamage": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.StructureDamage = scoreResultEventValueArray[i].Value; } } break; case "MinionDamage": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.MinionDamage = scoreResultEventValueArray[i].Value; } } break; case "CreepDamage": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.CreepDamage = scoreResultEventValueArray[i].Value; } } break; case "SummonDamage": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.SummonDamage = scoreResultEventValueArray[i].Value; } } break; case "TimeCCdEnemyHeroes": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue && scoreResultEventValueArray[i].Value > 0) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.TimeCCdEnemyHeroes = TimeSpan.FromSeconds(scoreResultEventValueArray[i].Value); } } break; case "Healing": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue && scoreResultEventValueArray[i].Value > 0) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.Healing = scoreResultEventValueArray[i].Value; } } break; case "SelfHealing": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.SelfHealing = scoreResultEventValueArray[i].Value; } } break; case "DamageTaken": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue && scoreResultEventValueArray[i].Value > 0) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.DamageTaken = scoreResultEventValueArray[i].Value; } } break; case "ExperienceContribution": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.ExperienceContribution = scoreResultEventValueArray[i].Value; } } break; case "TownKills": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.TownKills = scoreResultEventValueArray[i].Value; } } break; case "TimeSpentDead": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.TimeSpentDead = TimeSpan.FromSeconds(scoreResultEventValueArray[i].Value); } } break; case "MercCampCaptures": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.MercCampCaptures = scoreResultEventValueArray[i].Value; } } break; case "WatchTowerCaptures": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.WatchTowerCaptures = scoreResultEventValueArray[i].Value; } } break; case "MetaExperience": for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue) { replay.ClientListByWorkingSetSlotID[i].ScoreResult.MetaExperience = scoreResultEventValueArray[i].Value; } } break; default: for (var i = 0; i < scoreResultEventValueArray.Length; i++) { if (scoreResultEventValueArray[i].HasValue) { replay.ClientListByWorkingSetSlotID[i].MiscellaneousScoreResultEventDictionary[scoreResultEventKey] = scoreResultEventValueArray[i].Value; } } break; } } break; } } }
/// <summary> Parses the replay.initdata file in a replay file. </summary> /// <param name="replay"> The replay file to apply the parsed data to. </param> /// <param name="buffer"> The buffer containing the replay.initdata file. </param> public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) { var reader = new BitReader(stream); var playerListLength = reader.Read(5); for (var i = 0; i < playerListLength; i++) { var playerName = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(8)); // Player name // Populate the client list if (playerName != "") { if (i < replay.Players.Length && replay.Players[i].Name == playerName) // 99.9% of matches have 10 players and 10 clients replay.ClientList[i] = replay.Players[i]; else // Some Custom games with Observers may have the client list in a different order than player list // Hopefully in these rare cases, nobody will be sharing the same name :) replay.ClientList[i] = replay.Players.SingleOrDefault(j => j.Name == playerName); if (replay.ClientList[i] == null) replay.ClientList[i] = new Player { Name = playerName }; } if (reader.ReadBoolean()) reader.ReadBlobPrecededWithLength(8); // clanTag if (reader.ReadBoolean()) reader.ReadBlobPrecededWithLength(40); // Clan Logo if (reader.ReadBoolean()) reader.Read(8); // highestLeague if (reader.ReadBoolean()) reader.ReadInt32(); // combinedRaceLevels reader.ReadInt32(); // Random seed (So far, always 0 in Heroes) if (reader.ReadBoolean()) reader.Read(8); // Race Preference if (reader.ReadBoolean()) reader.Read(8); // Team Preference reader.ReadBoolean(); //test map reader.ReadBoolean(); //test auto reader.ReadBoolean(); //examine reader.ReadBoolean(); //custom interface reader.ReadInt32(); // m_testType reader.Read(2); //observer Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(9)); // m_hero - Currently Empty String Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(9)); // m_skin - Currently Empty String Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(9)); // m_mount - Currently Empty String Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); // m_toonHandle - Currently Empty String } // Marked as 'Random Value', so I will use as seed replay.RandomValue = (uint)reader.ReadInt32(); reader.ReadBlobPrecededWithLength(10); // m_gameCacheName - "Dflt" reader.ReadBoolean(); // Lock Teams reader.ReadBoolean(); // Teams Together reader.ReadBoolean(); // Advanced Shared Control reader.ReadBoolean(); // Random Races reader.ReadBoolean(); // BattleNet reader.ReadBoolean(); // AMM reader.ReadBoolean(); // Competitive reader.ReadBoolean(); // m_practice reader.ReadBoolean(); // m_cooperative reader.ReadBoolean(); // m_noVictoryOrDefeat reader.ReadBoolean(); // m_heroDuplicatesAllowed reader.Read(2); // Fog reader.Read(2); // Observers reader.Read(2); // User Difficulty reader.ReadInt32(); reader.ReadInt32(); // 64 bit int: Client Debug Flags reader.Read(3); // Game Speed // Not sure what this 'Game Type' is reader.Read(3); var maxUsers = reader.Read(5); if (maxUsers != 10) // Max Players replay.GameMode = GameMode.TryMe; reader.Read(5); // Max Observers reader.Read(5); // Max Players reader.Read(4); // + 1 = Max Teams reader.Read(6); // Max Colors reader.Read(8); // + 1 = Max Races reader.Read(8); // Max Controls replay.MapSize = new Point { X = (int)reader.Read(8), Y = (int)reader.Read(8) }; if (replay.MapSize.Y == 1) replay.MapSize.Y = replay.MapSize.X; else if (replay.MapSize.X == 0) replay.MapSize.X = replay.MapSize.Y; // I haven't tested the following code on replays before build 39595 (End of 2015) if (replay.ReplayBuild < 39595) return; reader.Read(32); // m_mapFileSyncChecksum reader.ReadBlobPrecededWithLength(11); // m_mapFileName reader.ReadBlobPrecededWithLength(8); // m_mapAuthorName reader.Read(32); // m_modFileSyncChecksum // m_slotDescriptions var slotDescriptionLength = reader.Read(5); for (var i = 0; i < slotDescriptionLength; i++) { reader.ReadBitArray(reader.Read(6)); // m_allowedColors reader.ReadBitArray(reader.Read(8)); // m_allowedRaces reader.ReadBitArray(reader.Read(6)); // m_allowedDifficulty reader.ReadBitArray(reader.Read(8)); // m_allowedControls reader.ReadBitArray(reader.Read(2)); // m_allowedObserveTypes reader.ReadBitArray(reader.Read(7)); // m_allowedAIBuilds } reader.Read(6); // m_defaultDifficulty reader.Read(7); // m_defaultAIBuild // m_cacheHandles var cacheHandlesLength = reader.Read(6); for (var i = 0; i < cacheHandlesLength; i++) reader.ReadBytes(40); reader.ReadBoolean(); // m_hasExtensionMod reader.ReadBoolean(); // m_isBlizzardMap reader.ReadBoolean(); // m_isPremadeFFA reader.ReadBoolean(); // m_isCoopMode #region m_lobbyState reader.Read(3); // m_phase reader.Read(5); // m_maxUsers reader.Read(5); // m_maxObservers // m_slots var slotsLength = reader.Read(5); for (var i = 0; i < slotsLength; i++) { int? clientListIndex = null; reader.Read(8); // m_control if (reader.ReadBoolean()) clientListIndex = (int)reader.Read(4); // m_userId reader.Read(4); // m_teamId if (reader.ReadBoolean()) reader.Read(5); // m_colorPref if (reader.ReadBoolean()) reader.Read(8); // m_racePref reader.Read(6); // m_difficulty reader.Read(7); // m_aiBuild reader.Read(7); // m_handicap // m_observe var observerStatus = reader.Read(2); if (observerStatus == 2) replay.ClientList[clientListIndex.Value].PlayerType = PlayerType.Spectator; reader.Read(32); // m_logoIndex reader.ReadBlobPrecededWithLength(9); // m_hero var skinAndSkinTint = Encoding.ASCII.GetString(reader.ReadBlobPrecededWithLength(9)); // m_skin if (skinAndSkinTint == "") skinAndSkinTint = null; if (clientListIndex.HasValue && replay.ClientList[clientListIndex.Value] != null) replay.ClientList[clientListIndex.Value].SkinAndSkinTint = skinAndSkinTint; var mountAndMountTint = Encoding.ASCII.GetString(reader.ReadBlobPrecededWithLength(9)); // m_mount if (mountAndMountTint == "") mountAndMountTint = null; if (clientListIndex.HasValue && replay.ClientList[clientListIndex.Value] != null) replay.ClientList[clientListIndex.Value].MountAndMountTint = mountAndMountTint; // m_artifacts var artifactsLength = reader.Read(4); for (var j = 0; j < artifactsLength; j++) reader.ReadBlobPrecededWithLength(9); if (reader.ReadBoolean()) reader.Read(8); // m_workingSetSlotId // m_rewards var rewardsLength = reader.Read(17); for (var j = 0; j < rewardsLength; j++) reader.Read(32); reader.ReadBlobPrecededWithLength(7); // m_toonHandle // m_licenses var licensesLength = reader.Read(9); for (var j = 0; j < licensesLength; j++) reader.Read(32); if (reader.ReadBoolean()) reader.Read(4); // m_tandemLeaderUserId reader.ReadBlobPrecededWithLength(9); // m_commander - Empty string reader.Read(32); // m_commanderLevel - So far, always 0 if (reader.ReadBoolean() && clientListIndex.HasValue && replay.ClientList[clientListIndex.Value] != null) // m_hasSilencePenalty replay.ClientList[clientListIndex.Value].IsSilenced = true; } if (reader.Read(32) != replay.RandomValue) // m_randomSeed throw new Exception("Replay Random Seed Values in Replay Init Data did not match"); if (reader.ReadBoolean()) reader.Read(4); // m_hostUserId reader.ReadBoolean(); // m_isSinglePlayer reader.Read(8); // m_pickedMapTag - So far, always 0 reader.Read(32); // m_gameDuration - So far, always 0 reader.Read(6); // m_defaultDifficulty reader.Read(7); // m_defaultAIBuild #endregion } }
/// <summary> /// Parses the MPQ header on a file to determine version and build numbers. /// </summary> /// <param name="replay">Replay object to store </param> /// <param name="filename">Filename of the file to open.</param> public static void ParseHeader(Replay replay, string filename) { using (var fileStream = new FileStream(filename, FileMode.Open)) using (var reader = new BinaryReader(fileStream)) ParseHeader(replay, reader); }
private static void ParseReplayArchive(Replay replay, MpqArchive archive, bool ignoreErrors, bool detailedBattleLobbyParsing = false) { archive.AddListfileFilenames(); // Replay Details ReplayDetails.Parse(replay, GetMpqFile(archive, ReplayDetails.FileName), ignoreErrors); if (!ignoreErrors) { if (replay.Players.Length != 10 || replay.Players.Count(i => i.IsWinner) != 5) { // Filter out 'Try Me' games, any games without 10 players, and incomplete games return; } else if (replay.Timestamp == DateTime.MinValue) { // Uncommon issue when parsing replay.details return; } else if (replay.Timestamp < new DateTime(2014, 10, 6, 0, 0, 0, DateTimeKind.Utc)) { // Technical Alpha replays return; } } // Replay Init Data ReplayInitData.Parse(replay, GetMpqFile(archive, ReplayInitData.FileName)); ReplayAttributeEvents.Parse(replay, GetMpqFile(archive, ReplayAttributeEvents.FileName)); replay.TrackerEvents = ReplayTrackerEvents.Parse(GetMpqFile(archive, ReplayTrackerEvents.FileName)); try { replay.GameEvents = ReplayGameEvents.Parse(GetMpqFile(archive, ReplayGameEvents.FileName), replay.ClientListByUserID, replay.ReplayBuild, replay.ReplayVersionMajor); replay.IsGameEventsParsedSuccessfully = true; } catch { throw new Exception("GameEvents failed to parse"); } { // Gather talent selections var talentGameEventsDictionary = replay.GameEvents .Where(i => i.eventType == GameEventType.CHeroTalentSelectedEvent) .GroupBy(i => i.player) .ToDictionary( i => i.Key, i => i.Select(j => new Talent { TalentID = (int)j.data.unsignedInt.Value, TimeSpanSelected = j.TimeSpan }).OrderBy(j => j.TimeSpanSelected).ToArray()); foreach (var player in talentGameEventsDictionary.Keys) { player.Talents = talentGameEventsDictionary[player]; } } // Replay Server Battlelobby if (!ignoreErrors && archive.Any(i => i.Filename == ReplayServerBattlelobby.FileName)) { if (detailedBattleLobbyParsing) { ReplayServerBattlelobby.Parse(replay, GetMpqFile(archive, ReplayServerBattlelobby.FileName)); } else { ReplayServerBattlelobby.GetBattleTags(replay, GetMpqFile(archive, ReplayServerBattlelobby.FileName)); } } // Parse Unit Data using Tracker events Unit.ParseUnitData(replay); // Parse Statistics if (replay.ReplayBuild >= 40431) { try { Statistics.Parse(replay); replay.IsStatisticsParsedSuccessfully = true; } catch { replay.IsGameEventsParsedSuccessfully = false; } } // Replay Message Events ReplayMessageEvents.Parse(replay, GetMpqFile(archive, ReplayMessageEvents.FileName)); // Replay Resumable Events // So far it doesn't look like this file has anything we would be interested in // ReplayResumableEvents.Parse(replay, GetMpqFile(archive, "replay.resumable.events")); }
/// <summary> Parses the replay.details file, applying it to a Replay object. </summary> /// <param name="replay"> The replay object to apply the parsed information to. </param> /// <param name="buffer"> The buffer containing the replay.details file. </param> public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) using (var reader = new BinaryReader(stream)) { var replayDetailsStructure = new TrackerEventStructure(reader); replay.Players = replayDetailsStructure.dictionary[0].optionalData.array.Select(i => new Player { Name = i.dictionary[0].blobText, BattleNetRegionId = (int)i.dictionary[1].dictionary[0].vInt.Value, BattleNetSubId = (int)i.dictionary[1].dictionary[2].vInt.Value, BattleNetId = (int)i.dictionary[1].dictionary[4].vInt.Value, // [2] = Race (SC2 Remnant, Always Empty String in Heroes of the Storm) Color = i.dictionary[3].dictionary.Keys.OrderBy(j => j).Select(j => (int)i.dictionary[3].dictionary[j].vInt.Value).ToArray(), // [4] = Player Type (2 = Human, 3 = Computer (Practice, Try Me, or Coop)) - This is more accurately gathered in replay.attributes.events Team = (int)i.dictionary[5].vInt.Value, Handicap = (int)i.dictionary[6].vInt.Value, // [7] = VInt, Default 0 IsWinner = i.dictionary[8].vInt.Value == 1, // [9] = Sometimes player index in ClientList array; usually 0-9, but can be higher if there are observers. I don't fully understand this, as this was incorrect in at least one Custom game, where this said ClientList[8] was null Character = i.dictionary[10].blobText }).ToArray(); if (replay.Players.Length != 10 || replay.Players.Count(i => i.IsWinner) != 5) { // Try Me Mode, or something strange return; } replay.Map = replayDetailsStructure.dictionary[1].blobText; // [2] - This is typically an empty string, no need to decode. // [3] - Blob: "Minimap.tga" or "CustomMiniMap.tga" // [4] - Uint, Default 1 // [5] - Utc Timestamp replay.Timestamp = DateTime.FromFileTimeUtc(replayDetailsStructure.dictionary[5].vInt.Value); // There was a bug during the below builds where timestamps were buggy for the Mac build of Heroes of the Storm // The replay, as well as viewing these replays in the game client, showed years such as 1970, 1999, etc // I couldn't find a way to get the correct timestamp, so I am just estimating based on when these builds were live if (replay.ReplayBuild == 34053 && replay.Timestamp < new DateTime(2015, 2, 8)) { replay.Timestamp = new DateTime(2015, 2, 13); } else if (replay.ReplayBuild == 34190 && replay.Timestamp < new DateTime(2015, 2, 15)) { replay.Timestamp = new DateTime(2015, 2, 20); } // [6] - Windows replays, this is Utc offset. Mac replays, this is actually the entire Local Timestamp // var potentialUtcOffset = new TimeSpan(replayDetailsStructure.dictionary[6].vInt.Value); // [7] - Blob, Empty String // [8] - Blob, Empty String // [9] - Blob, Empty String // [10] - Optional, Array: 0 - Blob, "s2ma" // [11] - UInt, Default 0 // [12] - VInt, Default 4 // [13] - VInt, Default 1 or 7 // [14] - Optional, Null // [15] - VInt, Default 0 // [16] - Optional, UInt, Default 0 } }
private static void ParseReplayArchive(Replay replay, MpqArchive archive, bool ignoreErrors) { archive.AddListfileFilenames(); // Replay Details ReplayDetails.Parse(replay, GetMpqFile(archive, ReplayDetails.FileName)); if (!ignoreErrors && (replay.Players.Length != 10 || replay.Players.Count(i => i.IsWinner) != 5)) // Filter out 'Try Me' games, any games without 10 players, and incomplete games return; else if (!ignoreErrors && replay.Timestamp < new DateTime(2014, 10, 6, 0, 0, 0, DateTimeKind.Utc)) // Technical Alpha replays return; // Replay Init Data ReplayInitData.Parse(replay, GetMpqFile(archive, ReplayInitData.FileName)); ReplayAttributeEvents.Parse(replay, GetMpqFile(archive, ReplayAttributeEvents.FileName)); replay.TrackerEvents = ReplayTrackerEvents.Parse(GetMpqFile(archive, ReplayTrackerEvents.FileName)); try { replay.GameEvents = ReplayGameEvents.Parse(GetMpqFile(archive, ReplayGameEvents.FileName), replay.ClientList, replay.ReplayBuild); replay.IsGameEventsParsedSuccessfully = true; } catch { replay.GameEvents = new List<GameEvent>(); } { // Gather talent selections var talentGameEventsDictionary = replay.GameEvents .Where(i => i.eventType == GameEventType.CHeroTalentSelectedEvent) .GroupBy(i => i.player) .ToDictionary( i => i.Key, i => i.Select(j => new Talent { TalentID = (int)j.data.unsignedInt.Value, TimeSpanSelected = j.TimeSpan }).OrderBy(j => j.TimeSpanSelected).ToArray()); foreach (var player in talentGameEventsDictionary.Keys) player.Talents = talentGameEventsDictionary[player]; } // Replay Server Battlelobby if (!ignoreErrors) ReplayServerBattlelobby.Parse(replay, GetMpqFile(archive, ReplayServerBattlelobby.FileName)); // Parse Unit Data using Tracker events Unit.ParseUnitData(replay); // Parse Statistics if (replay.ReplayBuild >= 40431) try { Statistics.Parse(replay); replay.IsStatisticsParsedSuccessfully = true; } catch { } // Replay Message Events // ReplayMessageEvents.Parse(replay, GetMpqFile(archive, ReplayMessageEvents.FileName)); // Replay Resumable Events // So far it doesn't look like this file has anything we would be interested in // ReplayResumableEvents.Parse(replay, GetMpqFile(archive, "replay.resumable.events")); }
public static void Parse(Replay replay, byte[] buffer) { var gameEvents = new List<GameEvent>(); var ticksElapsed = 0; using (var stream = new MemoryStream(buffer)) { var bitReader = new Heroes.ReplayParser.Streams.BitReader(stream); while (!bitReader.EndOfStream) { var gameEvent = new GameEvent(); ticksElapsed += (int)bitReader.Read(6 + (bitReader.Read(2) << 3)); gameEvent.ticksElapsed = ticksElapsed; var playerIndex = (int)bitReader.Read(5); if (playerIndex == 16) gameEvent.isGlobal = true; else gameEvent.player = replay.ClientList[playerIndex]; gameEvent.eventType = (GameEventType)bitReader.Read(7); switch (gameEvent.eventType) { case GameEventType.CStartGameEvent: break; case GameEventType.CUserFinishedLoadingSyncEvent: break; case GameEventType.CUserOptionsEvent: gameEvent.data = new TrackerEventStructure { array = new[] { // Names for user options may or may not be accurate // Referenced from https://raw.githubusercontent.com/Blizzard/s2protocol/master/protocol38215.py (Void Beta) new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_gameFullyDownloaded new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_developmentCheatsEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_testCheatsEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_multiplayerCheatsEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_syncChecksummingEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_isMapToMapTransition new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_startingRally new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_debugPauseEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_useGalaxyAsserts new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_platformMac // m_cameraFollow? new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_baseBuildNum new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_buildNum new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_versionFlags new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) } /* m_hotkeyProfile, Referenced as 9 bit length */ } }; break; case GameEventType.CBankFileEvent: gameEvent.data = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) }; break; case GameEventType.CBankSectionEvent: gameEvent.data = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(6) }; break; case GameEventType.CBankKeyEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(6) }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) } } }; break; case GameEventType.CBankSignatureEvent: gameEvent.data = new TrackerEventStructure { DataType = 2, array = new TrackerEventStructure[bitReader.Read(5)] }; for (var i = 0; i < gameEvent.data.array.Length; i++) gameEvent.data.array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(8) }; gameEvent.data.blob = bitReader.ReadBlobPrecededWithLength(7); break; case GameEventType.CCameraSaveEvent: bitReader.Read(3); // m_which bitReader.Read(16); // x bitReader.Read(16); // y break; case GameEventType.CCommandManagerResetEvent: bitReader.Read(32); // m_sequence break; case GameEventType.CCmdEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[5] }; // m_cmdFlags if (replay.ReplayBuild < 33684) gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[22] }; else if (replay.ReplayBuild < 37117) gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[23] }; else if (replay.ReplayBuild < 38236) gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[24] }; else gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[25] }; for (var i = 0; i < gameEvent.data.array[0].array.Length; i++) gameEvent.data.array[0].array[i] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(1) }; // m_abil if (bitReader.ReadBoolean()) { gameEvent.data.array[1] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_abilLink new TrackerEventStructure { unsignedInt = bitReader.Read(5) }, // m_abilCmdIndex new TrackerEventStructure() } }; if (bitReader.ReadBoolean()) // m_abilCmdData gameEvent.data.array[1].array[2].unsignedInt = bitReader.Read(8); } // m_data switch (bitReader.Read(2)) { case 0: // None break; case 1: // TargetPoint gameEvent.data.array[2] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } }; break; case 2: // TargetUnit gameEvent.data.array[2] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_targetUnitFlags new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, // m_timer new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_tag new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_snapshotUnitLink new TrackerEventStructure(), new TrackerEventStructure(), new TrackerEventStructure(), } }; if (bitReader.ReadBoolean()) // m_snapshotControlPlayerId gameEvent.data.array[2].array[4].unsignedInt = bitReader.Read(4); if (bitReader.ReadBoolean()) // m_snapshotUpkeepPlayerId gameEvent.data.array[2].array[5].unsignedInt = bitReader.Read(4); // m_snapshotPoint gameEvent.data.array[2].array[6].array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } }; break; case 3: // Data gameEvent.data.array[2] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; } if (replay.ReplayBuild >= 33684) bitReader.Read(32); // m_sequence if (bitReader.ReadBoolean()) gameEvent.data.array[3] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // m_otherUnit if (bitReader.ReadBoolean()) gameEvent.data.array[4] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // m_unitGroup break; case GameEventType.CSelectionDeltaEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(4) }, // m_controlGroupId // m_delta new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(9) }, // m_subgroupIndex new TrackerEventStructure(), new TrackerEventStructure(), new TrackerEventStructure(), new TrackerEventStructure() } } } }; // m_removeMask switch (bitReader.Read(2)) { case 0: // None break; case 1: // Mask bitReader.Read(bitReader.Read(9)); break; case 2: // OneIndices case 3: // ZeroIndices gameEvent.data.array[1].array[1] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] }; for (var i = 0; i < gameEvent.data.array[1].array[1].array.Length; i++) gameEvent.data.array[1].array[1].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(9) }; break; } // m_addSubgroups gameEvent.data.array[1].array[2] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] }; for (var i = 0; i < gameEvent.data.array[1].array[2].array.Length; i++) gameEvent.data.array[1].array[2].array[i] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_unitLink new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, // m_subgroupPriority new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, // m_intraSubgroupPriority new TrackerEventStructure { unsignedInt = bitReader.Read(9) } } }; // m_count // m_addUnitTags gameEvent.data.array[1].array[3] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] }; for (var i = 0; i < gameEvent.data.array[1].array[3].array.Length; i++) gameEvent.data.array[1].array[3].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; case GameEventType.CControlGroupUpdateEvent: bitReader.Read(4); // m_controlGroupIndex // m_controlGroupUpdate if (replay.ReplayBuild < 36359) // Not sure exactly when this change happened - roughly around here. This primarily affected 'The Lost Vikings' hero bitReader.Read(2); else bitReader.Read(3); // m_mask switch (bitReader.Read(2)) { case 0: // None break; case 1: // Mask bitReader.Read(bitReader.Read(9)); break; case 2: // OneIndices case 3: // ZeroIndices gameEvent.data.array[1].array[1] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] }; for (var i = 0; i < gameEvent.data.array[1].array[1].array.Length; i++) gameEvent.data.array[1].array[1].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(9) }; break; } break; case GameEventType.CResourceTradeEvent: bitReader.Read(4); // m_recipientId bitReader.Read(32); // m_resources, should be offset -2147483648 bitReader.Read(32); // m_resources, should be offset -2147483648 bitReader.Read(32); // m_resources, should be offset -2147483648 break; case GameEventType.CTriggerChatMessageEvent: gameEvent.data = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(10) }; break; case GameEventType.CTriggerPingEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } }; break; case GameEventType.CUnitClickEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // m_unitTag break; case GameEventType.CTriggerSkippedEvent: break; case GameEventType.CTriggerSoundLengthQueryEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } }; break; case GameEventType.CTriggerSoundOffsetEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; case GameEventType.CTriggerTransmissionOffsetEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } }; break; case GameEventType.CTriggerTransmissionCompleteEvent: gameEvent.data = new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }; break; case GameEventType.CCameraUpdateEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[6] }; if (bitReader.ReadBoolean()) // m_target, x/y gameEvent.data.array[0] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, new TrackerEventStructure { unsignedInt = bitReader.Read(16) } } }; if (bitReader.ReadBoolean()) // m_distance gameEvent.data.array[1] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) }; if (bitReader.ReadBoolean()) // m_pitch gameEvent.data.array[2] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) }; if (bitReader.ReadBoolean()) // m_yaw gameEvent.data.array[3] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) }; if (bitReader.ReadBoolean()) // m_reason gameEvent.data.array[4] = new TrackerEventStructure { vInt = bitReader.Read(8) - 128 }; // m_follow gameEvent.data.array[5] = new TrackerEventStructure { unsignedInt = bitReader.Read(1) }; break; case GameEventType.CTriggerPlanetMissionLaunchedEvent: bitReader.Read(32); // m_difficultyLevel, offset -2147483648 break; case GameEventType.CTriggerDialogControlEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) /* Actually signed - not handled correctly */ }, new TrackerEventStructure { vInt = bitReader.Read(32) /* Actually signed - not handled correctly */ }, new TrackerEventStructure() } }; switch (bitReader.Read(3)) { case 0: // None break; case 1: // Checked gameEvent.data.array[2].unsignedInt = bitReader.Read(1); break; case 2: // ValueChanged gameEvent.data.array[2].unsignedInt = bitReader.Read(32); break; case 3: // SelectionChanged gameEvent.data.array[2].vInt = bitReader.Read(32); /* Actually signed - not handled correctly */ break; case 4: // TextChanged gameEvent.data.array[2].DataType = 2; gameEvent.data.array[2].blob = bitReader.ReadBlobPrecededWithLength(11); break; case 5: // MouseButton gameEvent.data.array[2].unsignedInt = bitReader.Read(32); break; } break; case GameEventType.CTriggerSoundLengthSyncEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[2] }; gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(7)] }; for (var i = 0; i < gameEvent.data.array[0].array.Length; i++) gameEvent.data.array[0].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; gameEvent.data.array[1] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(7)] }; for (var i = 0; i < gameEvent.data.array[1].array.Length; i++) gameEvent.data.array[1].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; case GameEventType.CTriggerConversationSkippedEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(1) }; break; case GameEventType.CTriggerMouseClickedEvent: bitReader.Read(32); // m_button bitReader.ReadBoolean(); // m_down bitReader.Read(11); // m_posUI X bitReader.Read(11); // m_posUI Y bitReader.Read(20); // m_posWorld X bitReader.Read(20); // m_posWorld Y bitReader.Read(32); // m_posWorld Z (Offset -2147483648) bitReader.Read(8); // m_flags (-128) break; case GameEventType.CTriggerMouseMovedEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(11) }, new TrackerEventStructure { unsignedInt = bitReader.Read(11) }, new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } }, new TrackerEventStructure { vInt = bitReader.Read(8) - 128 } } }; break; case GameEventType.CTriggerHotkeyPressedEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // May be missing an offset value break; case GameEventType.CTriggerTargetModeUpdateEvent: bitReader.Read(16); // m_abilLink bitReader.Read(5); // m_abilCmdIndex bitReader.Read(8); // m_state (-128) break; case GameEventType.CTriggerSoundtrackDoneEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; case GameEventType.CTriggerKeyPressedEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(8) - 128 }, new TrackerEventStructure { vInt = bitReader.Read(8) - 128 } } }; break; case GameEventType.CTriggerCutsceneBookmarkFiredEvent: // m_cutsceneId, m_bookmarkName gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) } } }; break; case GameEventType.CTriggerCutsceneEndSceneFiredEvent: // m_cutsceneId gameEvent.data = new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }; break; case GameEventType.CGameUserLeaveEvent: break; case GameEventType.CGameUserJoinEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[5] }; gameEvent.data.array[0] = new TrackerEventStructure { unsignedInt = bitReader.Read(2) }; gameEvent.data.array[1] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(8) }; if (bitReader.ReadBoolean()) gameEvent.data.array[2] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) }; if (bitReader.ReadBoolean()) gameEvent.data.array[3] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(8) }; if (bitReader.ReadBoolean()) gameEvent.data.array[4] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBytes(40) }; break; case GameEventType.CCommandManagerStateEvent: gameEvent.data = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(2) }; // m_state if (replay.ReplayBuild >= 33684) if (bitReader.ReadBoolean()) // m_sequence gameEvent.data.array = new[] { new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(8) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(8) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(16) } }; break; case GameEventType.CCmdUpdateTargetPointEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(32) - 2147483648 } } }; break; case GameEventType.CCmdUpdateTargetUnitEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[7] }; gameEvent.data.array[0] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(16) }; // m_targetUnitFlags gameEvent.data.array[1] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(8) }; // m_timer gameEvent.data.array[2] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(32) }; // m_tag gameEvent.data.array[3] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(16) }; // m_snapshotUnitLink if (bitReader.ReadBoolean()) gameEvent.data.array[4] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(4) }; // m_snapshotControlPlayerId if (bitReader.ReadBoolean()) gameEvent.data.array[5] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(4) }; // m_snapshotUpkeepPlayerId gameEvent.data.array[6] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(32) - 2147483648 } } }; // m_snapshotPoint (x, y, z) break; case GameEventType.CHeroTalentSelectedEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // m_index break; case GameEventType.CHeroTalentTreeSelectionPanelToggled: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(1) }; // m_shown break; default: throw new NotImplementedException(); } bitReader.AlignToByte(); gameEvents.Add(gameEvent); } } replay.GameEvents = gameEvents; // Gather talent selections var talentGameEvents = replay.GameEvents.Where(i => i.eventType == GameEventType.CHeroTalentSelectedEvent); if (talentGameEvents.Any(i => i.player == null)) throw new Exception("Invalid Player for CHeroTalentSelected Game Event"); foreach (var player in replay.Players) player.Talents = talentGameEvents.Where(i => i.player == player).Select(j => new Tuple<int, TimeSpan>((int)j.data.unsignedInt.Value, j.TimeSpan)).OrderBy(j => j.Item1).ToArray(); // Gather Team Level Milestones (From talent choices: 1 / 4 / 7 / 10 / 13 / 16 / 20) for (var currentTeam = 0; currentTeam < replay.TeamLevelMilestones.Length; currentTeam++) { var maxTalentChoices = replay.Players.Where(i => i.Team == currentTeam).Select(i => i.Talents.Length).Max(); replay.TeamLevelMilestones[currentTeam] = new TimeSpan[maxTalentChoices]; var appropriatePlayers = replay.Players.Where(j => j.Team == currentTeam && j.Talents.Length == maxTalentChoices); for (var i = 0; i < replay.TeamLevelMilestones[currentTeam].Length; i++) replay.TeamLevelMilestones[currentTeam][i] = appropriatePlayers.Select(j => j.Talents[i].Item2).Min(); } // Uncomment this to write out all replay.game.events to individual text files in the 'C:\HOTSLogs\' folder /* var eventGroups = replay.GameEvents.GroupBy(i => i.eventType).Select(i => new { EventType = i.Key, EventCount = i.Count(), Events = i.OrderBy(j => j.TimeSpan) }); string eventGroupData = ""; foreach (var eventGroup in eventGroups) { foreach (var eventData in eventGroup.Events) eventGroupData += eventData.TimeSpan + ": " + eventData.player + ": " + eventData + "\r\n"; File.WriteAllText(@"C:\HOTSLogs\" + (int)eventGroup.EventType + " " + eventGroup.EventType + @".txt", eventGroupData); eventGroupData = ""; } */ }
public static void Parse(Replay replay, byte[] buffer) { var gameEvents = new List <GameEvent>(); var ticksElapsed = 0; using (var stream = new MemoryStream(buffer)) { var bitReader = new Heroes.ReplayParser.Streams.BitReader(stream); while (!bitReader.EndOfStream) { var gameEvent = new GameEvent(); ticksElapsed += (int)bitReader.Read(6 + (bitReader.Read(2) << 3)); gameEvent.ticksElapsed = ticksElapsed; gameEvent.playerIndex = (int)bitReader.Read(5); if (gameEvent.playerIndex == 16) { gameEvent.isGlobal = true; } gameEvent.eventType = (GameEventType)bitReader.Read(7); switch (gameEvent.eventType) { case GameEventType.CUserFinishedLoadingSyncEvent: break; case GameEventType.CUserOptionsEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // Base Build Number new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { blob = bitReader.ReadBlobPrecededWithLength(7) } } }; break; case GameEventType.CBankFileEvent: gameEvent.data = new TrackerEventStructure { blob = bitReader.ReadBlobPrecededWithLength(7) }; break; case GameEventType.CBankSectionEvent: gameEvent.data = new TrackerEventStructure { blob = bitReader.ReadBlobPrecededWithLength(6) }; break; case GameEventType.CBankKeyEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { blob = bitReader.ReadBlobPrecededWithLength(6) }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { blob = bitReader.ReadBlobPrecededWithLength(7) } } }; break; case GameEventType.CBankSignatureEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(5)] }; for (var i = 0; i < gameEvent.data.array.Length; i++) { gameEvent.data.array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(8) } } ; gameEvent.data.blob = bitReader.ReadBlobPrecededWithLength(7); break; case GameEventType.CCameraSaveEvent: bitReader.Read(3); bitReader.Read(16); bitReader.Read(16); break; case GameEventType.CCommandManagerResetEvent: bitReader.Read(32); break; case GameEventType.CCmdEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[5] }; if (replay.ReplayBuild < 33684) { gameEvent.data.array[0] = new TrackerEventStructure { unsignedInt = bitReader.Read(22) } } ; else { gameEvent.data.array[0] = new TrackerEventStructure { unsignedInt = bitReader.Read(23) } }; if (bitReader.ReadBoolean()) { gameEvent.data.array[1] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, new TrackerEventStructure { unsignedInt = bitReader.Read(5) }, new TrackerEventStructure() } }; if (bitReader.ReadBoolean()) { gameEvent.data.array[1].array[2].unsignedInt = bitReader.Read(8); } } switch (bitReader.Read(2)) { case 0: // None break; case 1: // TargetPoint gameEvent.data.array[2] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } }; break; case 2: // TargetUnit gameEvent.data.array[2] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, new TrackerEventStructure(), new TrackerEventStructure(), new TrackerEventStructure(), } }; if (bitReader.ReadBoolean()) { gameEvent.data.array[2].array[4].unsignedInt = bitReader.Read(4); } if (bitReader.ReadBoolean()) { gameEvent.data.array[2].array[5].unsignedInt = bitReader.Read(4); } gameEvent.data.array[2].array[6].array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } }; break; case 3: // Data gameEvent.data.array[2] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; } if (replay.ReplayBuild >= 33684) { bitReader.Read(32); } if (bitReader.ReadBoolean()) { gameEvent.data.array[3] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } ; if (bitReader.ReadBoolean()) { gameEvent.data.array[4] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } ; break; case GameEventType.CSelectionDeltaEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(4) }, new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(9) }, new TrackerEventStructure(), new TrackerEventStructure(), new TrackerEventStructure(), new TrackerEventStructure() } } } }; switch (bitReader.Read(2)) { case 0: // None break; case 1: // Mask bitReader.Read(bitReader.Read(9)); break; case 2: // OneIndices case 3: // ZeroIndices gameEvent.data.array[1].array[1] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] }; for (var i = 0; i < gameEvent.data.array[1].array[1].array.Length; i++) { gameEvent.data.array[1].array[1].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(9) } } ; break; } gameEvent.data.array[1].array[2] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] }; for (var i = 0; i < gameEvent.data.array[1].array[2].array.Length; i++) { gameEvent.data.array[1].array[2].array[i] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, new TrackerEventStructure { unsignedInt = bitReader.Read(9) } } } } ; gameEvent.data.array[1].array[3] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] }; for (var i = 0; i < gameEvent.data.array[1].array[3].array.Length; i++) { gameEvent.data.array[1].array[3].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } ; break; case GameEventType.CResourceTradeEvent: bitReader.Read(4); bitReader.Read(32); bitReader.Read(32); bitReader.Read(32); break; case GameEventType.CTriggerChatMessageEvent: gameEvent.data = new TrackerEventStructure { blob = bitReader.ReadBlobPrecededWithLength(10) }; break; case GameEventType.CTriggerPingEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } }; break; case GameEventType.CUnitClickEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; case GameEventType.CTriggerSkippedEvent: break; case GameEventType.CTriggerSoundLengthQueryEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } }; break; case GameEventType.CTriggerSoundOffsetEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; case GameEventType.CTriggerTransmissionOffsetEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } }; break; case GameEventType.CTriggerTransmissionCompleteEvent: gameEvent.data = new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }; break; case GameEventType.CCameraUpdateEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[6] }; if (bitReader.ReadBoolean()) { gameEvent.data.array[0] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, new TrackerEventStructure { unsignedInt = bitReader.Read(16) } } } } ; if (bitReader.ReadBoolean()) { gameEvent.data.array[1] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) } } ; if (bitReader.ReadBoolean()) { gameEvent.data.array[2] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) } } ; if (bitReader.ReadBoolean()) { gameEvent.data.array[3] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) } } ; if (bitReader.ReadBoolean()) { gameEvent.data.array[4] = new TrackerEventStructure { vInt = bitReader.Read(8) - 128 } } ; gameEvent.data.array[5] = new TrackerEventStructure { unsignedInt = bitReader.Read(1) }; break; case GameEventType.CTriggerPlanetMissionLaunchedEvent: bitReader.Read(32); break; case GameEventType.CTriggerDialogControlEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) /* Actually signed - not handled correctly */ }, new TrackerEventStructure { vInt = bitReader.Read(32) /* Actually signed - not handled correctly */ }, new TrackerEventStructure() } }; switch (bitReader.Read(3)) { case 0: // None break; case 1: // Checked gameEvent.data.array[2].unsignedInt = bitReader.Read(1); break; case 2: // ValueChanged gameEvent.data.array[2].unsignedInt = bitReader.Read(32); break; case 3: // SelectionChanged gameEvent.data.array[2].vInt = bitReader.Read(32); /* Actually signed - not handled correctly */ break; case 4: // TextChanged gameEvent.data.array[2].blob = bitReader.ReadBlobPrecededWithLength(11); break; case 5: // MouseButton gameEvent.data.array[2].unsignedInt = bitReader.Read(32); break; } break; case GameEventType.CTriggerSoundLengthSyncEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[2] }; gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(7)] }; for (var i = 0; i < gameEvent.data.array[0].array.Length; i++) { gameEvent.data.array[0].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } ; gameEvent.data.array[1] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(7)] }; for (var i = 0; i < gameEvent.data.array[1].array.Length; i++) { gameEvent.data.array[1].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } ; break; case GameEventType.CTriggerMouseClickedEvent: bitReader.Read(32); bitReader.ReadBoolean(); bitReader.Read(11); bitReader.Read(11); bitReader.Read(20); bitReader.Read(20); bitReader.Read(32); bitReader.Read(8); break; case GameEventType.CTriggerMouseMovedEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(11) }, new TrackerEventStructure { unsignedInt = bitReader.Read(11) }, new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } }, new TrackerEventStructure { vInt = bitReader.Read(8) - 128 } } }; break; case GameEventType.CTriggerHotkeyPressedEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // May be missing an offset value break; case GameEventType.CTriggerTargetModeUpdateEvent: bitReader.Read(16); bitReader.Read(5); bitReader.Read(8); break; case GameEventType.CTriggerSoundtrackDoneEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; case GameEventType.CTriggerKeyPressedEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(8) - 128 }, new TrackerEventStructure { vInt = bitReader.Read(8) - 128 } } }; break; case GameEventType.CTriggerCutsceneBookmarkFiredEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { blob = bitReader.ReadBlobPrecededWithLength(7) } } }; break; case GameEventType.CTriggerCutsceneEndSceneFiredEvent: gameEvent.data = new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }; break; case GameEventType.CGameUserLeaveEvent: break; case GameEventType.CGameUserJoinEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[5] }; gameEvent.data.array[0] = new TrackerEventStructure { unsignedInt = bitReader.Read(2) }; gameEvent.data.array[1] = new TrackerEventStructure { blob = bitReader.ReadBlobPrecededWithLength(8) }; if (bitReader.ReadBoolean()) { gameEvent.data.array[2] = new TrackerEventStructure { blob = bitReader.ReadBlobPrecededWithLength(7) } } ; if (bitReader.ReadBoolean()) { gameEvent.data.array[3] = new TrackerEventStructure { blob = bitReader.ReadBlobPrecededWithLength(8) } } ; if (bitReader.ReadBoolean()) { gameEvent.data.array[4] = new TrackerEventStructure { blob = bitReader.ReadBytes(40) } } ; break; case GameEventType.CCommandManagerStateEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(2) }; if (replay.ReplayBuild >= 33684) { if (bitReader.ReadBoolean()) { bitReader.Read(32); } } break; case GameEventType.CCmdUpdateTargetPointEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } }; break; case GameEventType.CCmdUpdateTargetUnitEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[7] }; gameEvent.data.array[0] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) }; gameEvent.data.array[1] = new TrackerEventStructure { unsignedInt = bitReader.Read(8) }; gameEvent.data.array[2] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; gameEvent.data.array[3] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) }; if (bitReader.ReadBoolean()) { gameEvent.data.array[4] = new TrackerEventStructure { unsignedInt = bitReader.Read(4) } } ; if (bitReader.ReadBoolean()) { gameEvent.data.array[5] = new TrackerEventStructure { unsignedInt = bitReader.Read(4) } } ; gameEvent.data.array[6] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } }; break; case GameEventType.CHeroTalentSelectedEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; case GameEventType.CHeroTalentTreeSelectionPanelToggled: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(1) }; break; default: throw new NotImplementedException(); } bitReader.AlignToByte(); gameEvents.Add(gameEvent); } } replay.GameEvents = gameEvents; var talentGameEvents = replay.GameEvents.Where(i => i.eventType == GameEventType.CHeroTalentSelectedEvent); for (var i = 0; i < replay.ClientList.Length; i++) { if (replay.ClientList[i] != null) { replay.ClientList[i].Talents = talentGameEvents.Where(j => j.playerIndex == i).Select(j => (int)j.data.unsignedInt.Value).OrderBy(j => j).ToArray(); } } } }
public static void ParseHeader(Replay replay, byte[] bytes) { using (var memoryStream = new MemoryStream(bytes)) using (var reader = new BinaryReader(memoryStream)) ParseHeader(replay, reader); }
/// <summary> Parses the replay.details file, applying it to a Replay object. </summary> /// <param name="replay"> The replay object to apply the parsed information to. </param> /// <param name="buffer"> The buffer containing the replay.details file. </param> public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) using (var reader = new BinaryReader(stream)) { var replayDetailsStructure = new TrackerEventStructure(reader); replay.Players = replayDetailsStructure.dictionary[0].optionalData.array.Select(i => new Player { Name = i.dictionary[0].blobText, BattleNetRegionId = (int)i.dictionary[1].dictionary[0].vInt.Value, BattleNetSubId = (int)i.dictionary[1].dictionary[2].vInt.Value, BattleNetId = (int)i.dictionary[1].dictionary[4].vInt.Value, // [2] = Race (SC2 Remnant, Always Empty String in Heroes of the Storm) Color = i.dictionary[3].dictionary.Keys.OrderBy(j => j).Select(j => (int)i.dictionary[3].dictionary[j].vInt.Value).ToArray(), // [4] = Player Type (2 = Human, 3 = Computer (Practice, Try Me, or Coop)) - This is more accurately gathered in replay.attributes.events Team = (int)i.dictionary[5].vInt.Value, Handicap = (int)i.dictionary[6].vInt.Value, // [7] = VInt, Default 0 IsWinner = i.dictionary[8].vInt.Value == 1, // [9] = Player Number (0 - 9) Character = i.dictionary[10].blobText }).ToArray(); if (replay.Players.Length != 10) { // Try Me Mode, or something strange return; } var playerIndexes = replay.TrackerEvents.Where(i => i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.PlayerSetupEvent && i.Data.dictionary[2].optionalData != null).Select(i => i.Data.dictionary[2].optionalData.vInt.Value).OrderBy(i => i).ToArray(); for (var i = 0; i < playerIndexes.Length; i++) { // The references between both of these classes are the same on purpose. // We want updates to one to propogate to the other. replay.ClientList[playerIndexes[i]] = replay.Players[i]; } replay.Map = replayDetailsStructure.dictionary[1].blobText; // [2] - This is typically an empty string, no need to decode. // [3] - Blob: "Minimap.tga" or "CustomMiniMap.tga" // [4] - Uint, Default 1 // [5] - Utc Timestamp replay.Timestamp = DateTime.FromFileTimeUtc(replayDetailsStructure.dictionary[5].vInt.Value); // [6] - Windows replays, this is Utc offset. Mac replays, this is actually the entire Local Timestamp // var potentialUtcOffset = new TimeSpan(replayDetailsStructure.dictionary[6].vInt.Value); // Console.WriteLine(potentialUtcOffset.ToString()); // [7] - Blob, Empty String // [8] - Blob, Empty String // [9] - Blob, Empty String // [10] - Optional, Array: 0 - Blob, "s2ma" // [11] - UInt, Default 0 // [12] - VInt, Default 4 // [13] - VInt, Default 1 or 7 // [14] - Optional, Null // [15] - VInt, Default 0 // [16] - Optional, UInt, Default 0 } }
/// <summary> Parses the replay.initdata file in a replay file. </summary> /// <param name="replay"> The replay file to apply the parsed data to. </param> /// <param name="buffer"> The buffer containing the replay.initdata file. </param> public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) { var reader = new BitReader(stream); var playerListLength = reader.Read(5); for (var i = 0; i < playerListLength; i++) { var playerName = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(8)); // Player name // Populate the client list if (playerName != "") { if (i < replay.Players.Length && replay.Players[i].Name == playerName) { // 99.9% of matches have 10 players and 10 clients replay.ClientList[i] = replay.Players[i]; } else { // Some Custom games with Observers may have the client list in a different order than player list // Hopefully in these rare cases, nobody will be sharing the same name :) replay.ClientList[i] = replay.Players.SingleOrDefault(j => j.Name == playerName); } if (replay.ClientList[i] == null) { replay.ClientList[i] = new Player { Name = playerName } } ; } if (reader.ReadBoolean()) { reader.ReadBlobPrecededWithLength(8); // clanTag } if (reader.ReadBoolean()) { reader.ReadBlobPrecededWithLength(40); // Clan Logo } if (reader.ReadBoolean()) { reader.Read(8); // highestLeague } if (reader.ReadBoolean()) { reader.ReadInt32(); // combinedRaceLevels } reader.ReadInt32(); // Random seed (So far, always 0 in Heroes) if (reader.ReadBoolean()) { reader.Read(8); // Race Preference } if (reader.ReadBoolean()) { reader.Read(8); // Team Preference } reader.ReadBoolean(); //test map reader.ReadBoolean(); //test auto reader.ReadBoolean(); //examine reader.ReadBoolean(); //custom interface reader.ReadInt32(); // m_testType reader.Read(2); //observer Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(9)); // m_hero - Currently Empty String Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(9)); // m_skin - Currently Empty String Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(9)); // m_mount - Currently Empty String Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); // m_toonHandle - Currently Empty String } // Marked as 'Random Value', so I will use as seed replay.RandomValue = (uint)reader.ReadInt32(); reader.ReadBlobPrecededWithLength(10); // m_gameCacheName - "Dflt" reader.ReadBoolean(); // Lock Teams reader.ReadBoolean(); // Teams Together reader.ReadBoolean(); // Advanced Shared Control reader.ReadBoolean(); // Random Races reader.ReadBoolean(); // BattleNet reader.ReadBoolean(); // AMM reader.ReadBoolean(); // Competitive reader.ReadBoolean(); // m_practice reader.ReadBoolean(); // m_cooperative reader.ReadBoolean(); // m_noVictoryOrDefeat reader.ReadBoolean(); // m_heroDuplicatesAllowed reader.Read(2); // Fog reader.Read(2); // Observers reader.Read(2); // User Difficulty reader.ReadInt32(); reader.ReadInt32(); // 64 bit int: Client Debug Flags reader.Read(3); // Game Speed // Not sure what this 'Game Type' is reader.Read(3); var maxUsers = reader.Read(5); if (maxUsers != 10) // Max Players { replay.GameMode = GameMode.TryMe; } reader.Read(5); // Max Observers reader.Read(5); // Max Players reader.Read(4); // + 1 = Max Teams reader.Read(6); // Max Colors reader.Read(8); // + 1 = Max Races reader.Read(8); // Max Controls replay.MapSize = new Point { X = (int)reader.Read(8), Y = (int)reader.Read(8) }; if (replay.MapSize.Y == 1) { replay.MapSize.Y = replay.MapSize.X; } else if (replay.MapSize.X == 0) { replay.MapSize.X = replay.MapSize.Y; } // I haven't tested the following code on replays before build 39595 (End of 2015) if (replay.ReplayBuild < 39595) { return; } reader.Read(32); // m_mapFileSyncChecksum reader.ReadBlobPrecededWithLength(11); // m_mapFileName reader.ReadBlobPrecededWithLength(8); // m_mapAuthorName reader.Read(32); // m_modFileSyncChecksum // m_slotDescriptions var slotDescriptionLength = reader.Read(5); for (var i = 0; i < slotDescriptionLength; i++) { reader.ReadBitArray(reader.Read(6)); // m_allowedColors reader.ReadBitArray(reader.Read(8)); // m_allowedRaces reader.ReadBitArray(reader.Read(6)); // m_allowedDifficulty reader.ReadBitArray(reader.Read(8)); // m_allowedControls reader.ReadBitArray(reader.Read(2)); // m_allowedObserveTypes reader.ReadBitArray(reader.Read(7)); // m_allowedAIBuilds } reader.Read(6); // m_defaultDifficulty reader.Read(7); // m_defaultAIBuild // m_cacheHandles var cacheHandlesLength = reader.Read(6); for (var i = 0; i < cacheHandlesLength; i++) { reader.ReadBytes(40); } reader.ReadBoolean(); // m_hasExtensionMod reader.ReadBoolean(); // m_isBlizzardMap reader.ReadBoolean(); // m_isPremadeFFA reader.ReadBoolean(); // m_isCoopMode #region m_lobbyState reader.Read(3); // m_phase reader.Read(5); // m_maxUsers reader.Read(5); // m_maxObservers // m_slots var slotsLength = reader.Read(5); for (var i = 0; i < slotsLength; i++) { int?clientListIndex = null; reader.Read(8); // m_control if (reader.ReadBoolean()) { clientListIndex = (int)reader.Read(4); // m_userId } reader.Read(4); // m_teamId if (reader.ReadBoolean()) { reader.Read(5); // m_colorPref } if (reader.ReadBoolean()) { reader.Read(8); // m_racePref } reader.Read(6); // m_difficulty reader.Read(7); // m_aiBuild reader.Read(7); // m_handicap // m_observe var observerStatus = reader.Read(2); if (observerStatus == 2 && clientListIndex.HasValue) { replay.ClientList[clientListIndex.Value].PlayerType = PlayerType.Spectator; } reader.Read(32); // m_logoIndex reader.ReadBlobPrecededWithLength(9); // m_hero var skinAndSkinTint = Encoding.ASCII.GetString(reader.ReadBlobPrecededWithLength(9)); // m_skin if (skinAndSkinTint == "") { skinAndSkinTint = null; } if (clientListIndex.HasValue && replay.ClientList[clientListIndex.Value] != null) { replay.ClientList[clientListIndex.Value].SkinAndSkinTint = skinAndSkinTint; } var mountAndMountTint = Encoding.ASCII.GetString(reader.ReadBlobPrecededWithLength(9)); // m_mount if (mountAndMountTint == "") { mountAndMountTint = null; } if (clientListIndex.HasValue && replay.ClientList[clientListIndex.Value] != null) { replay.ClientList[clientListIndex.Value].MountAndMountTint = mountAndMountTint; } // m_artifacts var artifactsLength = reader.Read(4); for (var j = 0; j < artifactsLength; j++) { reader.ReadBlobPrecededWithLength(9); } if (reader.ReadBoolean()) { reader.Read(8); // m_workingSetSlotId } // m_rewards var rewardsLength = reader.Read(17); for (var j = 0; j < rewardsLength; j++) { reader.Read(32); } reader.ReadBlobPrecededWithLength(7); // m_toonHandle // m_licenses var licensesLength = reader.Read(9); for (var j = 0; j < licensesLength; j++) { reader.Read(32); } if (reader.ReadBoolean()) { reader.Read(4); // m_tandemLeaderUserId } reader.ReadBlobPrecededWithLength(9); // m_commander - Empty string reader.Read(32); // m_commanderLevel - So far, always 0 if (reader.ReadBoolean() && clientListIndex.HasValue && replay.ClientList[clientListIndex.Value] != null) // m_hasSilencePenalty { replay.ClientList[clientListIndex.Value].IsSilenced = true; } } if (reader.Read(32) != replay.RandomValue) // m_randomSeed { throw new Exception("Replay Random Seed Values in Replay Init Data did not match"); } if (reader.ReadBoolean()) { reader.Read(4); // m_hostUserId } reader.ReadBoolean(); // m_isSinglePlayer reader.Read(8); // m_pickedMapTag - So far, always 0 reader.Read(32); // m_gameDuration - So far, always 0 reader.Read(6); // m_defaultDifficulty reader.Read(7); // m_defaultAIBuild #endregion } } }
public static void Parse(Replay replay, byte[] buffer) { var gameEvents = new List <GameEvent>(); var ticksElapsed = 0; using (var stream = new MemoryStream(buffer)) { var bitReader = new Heroes.ReplayParser.Streams.BitReader(stream); while (!bitReader.EndOfStream) { var gameEvent = new GameEvent(); ticksElapsed += (int)bitReader.Read(6 + (bitReader.Read(2) << 3)); gameEvent.ticksElapsed = ticksElapsed; var playerIndex = (int)bitReader.Read(5); if (playerIndex == 16) { gameEvent.isGlobal = true; } else { gameEvent.player = replay.ClientList[playerIndex]; } gameEvent.eventType = (GameEventType)bitReader.Read(7); switch (gameEvent.eventType) { case GameEventType.CStartGameEvent: break; case GameEventType.CUserFinishedLoadingSyncEvent: break; case GameEventType.CUserOptionsEvent: gameEvent.data = new TrackerEventStructure { array = new[] { // Names for user options may or may not be accurate // Referenced from https://raw.githubusercontent.com/Blizzard/s2protocol/master/protocol34784.py (Void Beta) new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_gameFullyDownloaded new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_developmentCheatsEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_testCheatsEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_multiplayerCheatsEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_syncChecksummingEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_isMapToMapTransition new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_startingRally new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_debugPauseEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_useGalaxyAsserts new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_platformMac // m_cameraFollow? new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_baseBuildNum new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_buildNum new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_versionFlags new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) } /* m_hotkeyProfile, Referenced as 9 bit length */ } }; break; case GameEventType.CBankFileEvent: gameEvent.data = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) }; break; case GameEventType.CBankSectionEvent: gameEvent.data = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(6) }; break; case GameEventType.CBankKeyEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(6) }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) } } }; break; case GameEventType.CBankSignatureEvent: gameEvent.data = new TrackerEventStructure { DataType = 2, array = new TrackerEventStructure[bitReader.Read(5)] }; for (var i = 0; i < gameEvent.data.array.Length; i++) { gameEvent.data.array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(8) } } ; gameEvent.data.blob = bitReader.ReadBlobPrecededWithLength(7); break; case GameEventType.CCameraSaveEvent: bitReader.Read(3); // m_which bitReader.Read(16); // x bitReader.Read(16); // y break; case GameEventType.CCommandManagerResetEvent: bitReader.Read(32); // m_sequence break; case GameEventType.CCmdEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[5] }; // m_cmdFlags if (replay.ReplayBuild < 33684) { gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[22] } } ; else if (replay.ReplayBuild < 37117) { gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[23] } } ; else { gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[24] } }; for (var i = 0; i < gameEvent.data.array[0].array.Length; i++) { gameEvent.data.array[0].array[i] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(1) } } ; if (bitReader.ReadBoolean()) { gameEvent.data.array[1] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_abilLink new TrackerEventStructure { unsignedInt = bitReader.Read(5) }, // m_abilCmdIndex new TrackerEventStructure() } }; if (bitReader.ReadBoolean()) { // m_abilCmdData, potentially 10 bits gameEvent.data.array[1].array[2].unsignedInt = bitReader.Read(8); } } switch (bitReader.Read(2)) { case 0: // None break; case 1: // TargetPoint gameEvent.data.array[2] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } }; break; case 2: // TargetUnit gameEvent.data.array[2] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, new TrackerEventStructure(), new TrackerEventStructure(), new TrackerEventStructure(), } }; if (bitReader.ReadBoolean()) { gameEvent.data.array[2].array[4].unsignedInt = bitReader.Read(4); } if (bitReader.ReadBoolean()) { gameEvent.data.array[2].array[5].unsignedInt = bitReader.Read(4); } gameEvent.data.array[2].array[6].array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } }; break; case 3: // Data gameEvent.data.array[2] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; } if (replay.ReplayBuild >= 33684) { bitReader.Read(32); // m_sequence } if (bitReader.ReadBoolean()) { gameEvent.data.array[3] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } ; // m_otherUnit if (bitReader.ReadBoolean()) { gameEvent.data.array[4] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } ; // m_unitGroup break; case GameEventType.CSelectionDeltaEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(4) }, // m_controlGroupId new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(9) }, // m_subgroupIndex new TrackerEventStructure(), new TrackerEventStructure(), new TrackerEventStructure(), new TrackerEventStructure() } } } }; // m_removeMask switch (bitReader.Read(2)) { case 0: // None break; case 1: // Mask bitReader.Read(bitReader.Read(9)); break; case 2: // OneIndices case 3: // ZeroIndices gameEvent.data.array[1].array[1] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] }; for (var i = 0; i < gameEvent.data.array[1].array[1].array.Length; i++) { gameEvent.data.array[1].array[1].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(9) } } ; break; } // m_addSubgroups gameEvent.data.array[1].array[2] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] }; for (var i = 0; i < gameEvent.data.array[1].array[2].array.Length; i++) { gameEvent.data.array[1].array[2].array[i] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, new TrackerEventStructure { unsignedInt = bitReader.Read(9) } } } } ; // m_addUnitTags gameEvent.data.array[1].array[3] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] }; for (var i = 0; i < gameEvent.data.array[1].array[3].array.Length; i++) { gameEvent.data.array[1].array[3].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } ; break; case GameEventType.CControlGroupUpdateEvent: bitReader.Read(4); // m_controlGroupIndex bitReader.Read(2); // m_controlGroupUpdate switch (bitReader.Read(2)) // m_mask { case 0: // None break; case 1: // Mask bitReader.Read(9); break; case 2: // One Indices for (var i = 0; i < bitReader.Read(9); i++) { bitReader.Read(9); } break; case 3: // Zero Indices for (var i = 0; i < bitReader.Read(9); i++) { bitReader.Read(9); } break; } break; case GameEventType.CResourceTradeEvent: bitReader.Read(4); // m_recipientId bitReader.Read(32); // m_resources, should be offset -2147483648 bitReader.Read(32); // m_resources, should be offset -2147483648 bitReader.Read(32); // m_resources, should be offset -2147483648 break; case GameEventType.CTriggerChatMessageEvent: gameEvent.data = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(10) }; break; case GameEventType.CTriggerPingEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } }; break; case GameEventType.CUnitClickEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // m_unitTag break; case GameEventType.CTriggerSkippedEvent: break; case GameEventType.CTriggerSoundLengthQueryEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } }; break; case GameEventType.CTriggerSoundOffsetEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; case GameEventType.CTriggerTransmissionOffsetEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } }; break; case GameEventType.CTriggerTransmissionCompleteEvent: gameEvent.data = new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }; break; case GameEventType.CCameraUpdateEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[6] }; if (bitReader.ReadBoolean()) { // m_target, x/y gameEvent.data.array[0] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, new TrackerEventStructure { unsignedInt = bitReader.Read(16) } } } } ; if (bitReader.ReadBoolean()) { // m_distance gameEvent.data.array[1] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) } } ; if (bitReader.ReadBoolean()) { // m_pitch gameEvent.data.array[2] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) } } ; if (bitReader.ReadBoolean()) { // m_yaw gameEvent.data.array[3] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) } } ; if (bitReader.ReadBoolean()) { // m_reason gameEvent.data.array[4] = new TrackerEventStructure { vInt = bitReader.Read(8) - 128 } } ; // m_follow gameEvent.data.array[5] = new TrackerEventStructure { unsignedInt = bitReader.Read(1) }; break; case GameEventType.CTriggerPlanetMissionLaunchedEvent: bitReader.Read(32); // m_difficultyLevel, offset -2147483648 break; case GameEventType.CTriggerDialogControlEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) /* Actually signed - not handled correctly */ }, new TrackerEventStructure { vInt = bitReader.Read(32) /* Actually signed - not handled correctly */ }, new TrackerEventStructure() } }; switch (bitReader.Read(3)) { case 0: // None break; case 1: // Checked gameEvent.data.array[2].unsignedInt = bitReader.Read(1); break; case 2: // ValueChanged gameEvent.data.array[2].unsignedInt = bitReader.Read(32); break; case 3: // SelectionChanged gameEvent.data.array[2].vInt = bitReader.Read(32); /* Actually signed - not handled correctly */ break; case 4: // TextChanged gameEvent.data.array[2].DataType = 2; gameEvent.data.array[2].blob = bitReader.ReadBlobPrecededWithLength(11); break; case 5: // MouseButton gameEvent.data.array[2].unsignedInt = bitReader.Read(32); break; } break; case GameEventType.CTriggerSoundLengthSyncEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[2] }; gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(7)] }; for (var i = 0; i < gameEvent.data.array[0].array.Length; i++) { gameEvent.data.array[0].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } ; gameEvent.data.array[1] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(7)] }; for (var i = 0; i < gameEvent.data.array[1].array.Length; i++) { gameEvent.data.array[1].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } ; break; case GameEventType.CTriggerConversationSkippedEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(1) }; break; case GameEventType.CTriggerMouseClickedEvent: bitReader.Read(32); // m_button bitReader.ReadBoolean(); // m_down bitReader.Read(11); // m_posUI X bitReader.Read(11); // m_posUI Y bitReader.Read(20); // m_posWorld X bitReader.Read(20); // m_posWorld Y bitReader.Read(32); // m_posWorld Z (Offset -2147483648) bitReader.Read(8); // m_flags (-128) break; case GameEventType.CTriggerMouseMovedEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(11) }, new TrackerEventStructure { unsignedInt = bitReader.Read(11) }, new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } }, new TrackerEventStructure { vInt = bitReader.Read(8) - 128 } } }; break; case GameEventType.CTriggerHotkeyPressedEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // May be missing an offset value break; case GameEventType.CTriggerTargetModeUpdateEvent: bitReader.Read(16); // m_abilLink bitReader.Read(5); // m_abilCmdIndex bitReader.Read(8); // m_state (-128) break; case GameEventType.CTriggerSoundtrackDoneEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; case GameEventType.CTriggerKeyPressedEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(8) - 128 }, new TrackerEventStructure { vInt = bitReader.Read(8) - 128 } } }; break; case GameEventType.CTriggerCutsceneBookmarkFiredEvent: // m_cutsceneId, m_bookmarkName gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) } } }; break; case GameEventType.CTriggerCutsceneEndSceneFiredEvent: // m_cutsceneId gameEvent.data = new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }; break; case GameEventType.CGameUserLeaveEvent: break; case GameEventType.CGameUserJoinEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[5] }; gameEvent.data.array[0] = new TrackerEventStructure { unsignedInt = bitReader.Read(2) }; gameEvent.data.array[1] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(8) }; if (bitReader.ReadBoolean()) { gameEvent.data.array[2] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) } } ; if (bitReader.ReadBoolean()) { gameEvent.data.array[3] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(8) } } ; if (bitReader.ReadBoolean()) { gameEvent.data.array[4] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBytes(40) } } ; break; case GameEventType.CCommandManagerStateEvent: gameEvent.data = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(2) }; // m_state if (replay.ReplayBuild >= 33684) { if (bitReader.ReadBoolean()) { // m_sequence gameEvent.data.array = new[] { new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(8) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(8) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(16) } } } } ; break; case GameEventType.CCmdUpdateTargetPointEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(32) - 2147483648 } } }; break; case GameEventType.CCmdUpdateTargetUnitEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[7] }; gameEvent.data.array[0] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(16) }; // m_targetUnitFlags gameEvent.data.array[1] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(8) }; // m_timer gameEvent.data.array[2] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(32) }; // m_tag gameEvent.data.array[3] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(16) }; // m_snapshotUnitLink if (bitReader.ReadBoolean()) { gameEvent.data.array[4] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(4) } } ; // m_snapshotControlPlayerId if (bitReader.ReadBoolean()) { gameEvent.data.array[5] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(4) } } ; // m_snapshotUpkeepPlayerId gameEvent.data.array[6] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(32) - 2147483648 } } }; // m_snapshotPoint (x, y, z) break; case GameEventType.CHeroTalentSelectedEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // m_index break; case GameEventType.CHeroTalentTreeSelectionPanelToggled: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(1) }; // m_shown break; default: throw new NotImplementedException(); } bitReader.AlignToByte(); gameEvents.Add(gameEvent); } } replay.GameEvents = gameEvents; // Gather talent selections var talentGameEvents = replay.GameEvents.Where(i => i.eventType == GameEventType.CHeroTalentSelectedEvent); if (talentGameEvents.Any(i => i.player == null)) { throw new Exception("Invalid Player for CHeroTalentSelected Game Event"); } foreach (var player in replay.Players) { player.Talents = talentGameEvents.Where(i => i.player == player).Select(j => new Tuple <int, TimeSpan>((int)j.data.unsignedInt.Value, j.TimeSpan)).OrderBy(j => j.Item1).ToArray(); } // Gather Team Level Milestones (From talent choices: 1 / 4 / 7 / 10 / 13 / 16 / 20) for (var currentTeam = 0; currentTeam < replay.TeamLevelMilestones.Length; currentTeam++) { var maxTalentChoices = replay.Players.Where(i => i.Team == currentTeam).Select(i => i.Talents.Length).Max(); replay.TeamLevelMilestones[currentTeam] = new TimeSpan[maxTalentChoices]; var appropriatePlayers = replay.Players.Where(j => j.Team == currentTeam && j.Talents.Length == maxTalentChoices); for (var i = 0; i < replay.TeamLevelMilestones[currentTeam].Length; i++) { replay.TeamLevelMilestones[currentTeam][i] = appropriatePlayers.Select(j => j.Talents[i].Item2).Min(); } } // Gather death events var deathAnimationOffset = TimeSpan.FromSeconds(-2); foreach (var playerDeathEvents in replay.GameEvents.Where(i => i.eventType == GameEventType.CTriggerCutsceneBookmarkFiredEvent && i.data.array != null && i.data.array.Length == 2 && i.data.array[1].blobText == "Loop Start").GroupBy(i => i.player)) { playerDeathEvents.Key.Deaths = playerDeathEvents.Select(i => i.TimeSpan.Add(deathAnimationOffset)).OrderBy(i => i).ToArray(); } // Uncomment this to write out all replay.game.events to individual text files in the 'C:\HOTSLogs\' folder /* var eventGroups = replay.GameEvents.GroupBy(i => i.eventType).Select(i => new { EventType = i.Key, EventCount = i.Count(), Events = i.OrderBy(j => j.TimeSpan) }); * string eventGroupData = ""; * foreach (var eventGroup in eventGroups) * { * foreach (var eventData in eventGroup.Events) * eventGroupData += eventData.TimeSpan + ": " + eventData.player + ": " + eventData + "\r\n"; * File.WriteAllText(@"C:\HOTSLogs\" + (int)eventGroup.EventType + " " + eventGroup.EventType + @".txt", eventGroupData); * eventGroupData = ""; * } */ } }
/// <summary> Parses the replay.details file, applying it to a Replay object. </summary> /// <param name="replay"> The replay object to apply the parsed information to. </param> /// <param name="buffer"> The buffer containing the replay.details file. </param> public static void Parse(Replay replay, byte[] buffer, bool ignoreErrors = false) { using (var stream = new MemoryStream(buffer)) using (var reader = new BinaryReader(stream)) { var replayDetailsStructure = new TrackerEventStructure(reader); replay.Players = replayDetailsStructure.dictionary[0].optionalData.array.Select(i => new Player { Name = i.dictionary[0].blobText, BattleNetRegionId = (int)i.dictionary[1].dictionary[0].vInt.Value, BattleNetSubId = (int)i.dictionary[1].dictionary[2].vInt.Value, BattleNetId = (int)i.dictionary[1].dictionary[4].vInt.Value, // [2] = Race (SC2 Remnant, Always Empty String in Heroes of the Storm) Color = i.dictionary[3].dictionary.Keys.OrderBy(j => j).Select(j => (int)i.dictionary[3].dictionary[j].vInt.Value).ToArray(), // [4] = Player Type (2 = Human, 3 = Computer (Practice, Try Me, or Cooperative)) - This is more accurately gathered in replay.attributes.events Team = (int)i.dictionary[5].vInt.Value, Handicap = (int)i.dictionary[6].vInt.Value, // [7] = VInt, Default 0 - 'm_observe' IsWinner = i.dictionary[8].vInt.Value == 1, // [9] = 'm_workingSetSlotId' Character = i.dictionary[10].blobText }).ToArray(); if (!ignoreErrors && (replay.Players.Length != 10 || replay.Players.Count(i => i.IsWinner) != 5)) { // Try Me Mode, or something strange return; } for (var i = 0; i < replay.Players.Length; i++) { replay.ClientListByWorkingSetSlotID[replayDetailsStructure.dictionary[0].optionalData.array[i].dictionary[9].optionalData.vInt.Value] = replay.Players[i]; } replay.Map = replayDetailsStructure.dictionary[1].blobText; // [2] - m_difficulty // [3] - m_thumbnail - "Minimap.tga", "CustomMiniMap.tga", etc // [4] - m_isBlizzardMap replay.Timestamp = DateTime.FromFileTimeUtc(replayDetailsStructure.dictionary[5].vInt.Value); // m_timeUTC // There was a bug during the below builds where timestamps were buggy for the Mac build of Heroes of the Storm // The replay, as well as viewing these replays in the game client, showed years such as 1970, 1999, etc // I couldn't find a way to get the correct timestamp, so I am just estimating based on when these builds were live if (replay.ReplayBuild == 34053 && replay.Timestamp < new DateTime(2015, 2, 8)) { replay.Timestamp = new DateTime(2015, 2, 13); } else if (replay.ReplayBuild == 34190 && replay.Timestamp < new DateTime(2015, 2, 15)) { replay.Timestamp = new DateTime(2015, 2, 20); } // [6] - m_timeLocalOffset - For Windows replays, this is Utc offset. For Mac replays, this is actually the entire Local Timestamp // [7] - m_description - Empty String // [8] - m_imageFilePath - Empty String // [9] - m_mapFileName - Empty String // [10] - m_cacheHandles - "s2ma" // [11] - m_miniSave - 0 // [12] - m_gameSpeed - 4 // [13] - m_defaultDifficulty - Usually 1 or 7 // [14] - m_modPaths - Null // [15] - m_campaignIndex - 0 // [16] - m_restartAsTransitionMap - 0 } }
/// <summary> Parses the replay.server.battlelobby file in a replay file. </summary> /// <param name="replay"> The replay file to apply the parsed data to. </param> /// <param name="buffer"> The buffer containing the replay.initdata file. </param> public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) { var reader = new BitReader(stream); int arrayLength = reader.ReadByte(); var stringLength = reader.ReadByte(); for (var i = 0; i < arrayLength; i++) { reader.ReadString(stringLength); reader.ReadBytes(2); // Unknown } // This is not always here; we can't blindly wait for 's2mh' /* while (!reader.EndOfStream) * if (reader.ReadString(1) == "s" && reader.ReadString(1) == "2" && reader.ReadString(1) == "m" && reader.ReadString(1) == "h") * { * reader.stream.Position -= 4; * break; * } * * if (reader.EndOfStream) * return; * * for (var j = 0; j < arrayLength; j++) * { * reader.ReadString(4); // s2mh * reader.ReadBytes(2); // 0x00 0x00 * reader.ReadBytes(2); // 'Realm' * reader.ReadBytes(32); // 'DepHash' * } * reader.ReadBytes(2); // 0x00 0x00 * * // Different Skins / Artifacts / Characters - I think this is what users mouse over in the UI before the game * arrayLength = reader.ReadInt16(); * for (var j = 0; j < arrayLength; j++) * reader.ReadString(reader.ReadByte()); * * reader.ReadBytes(2); // 0x00 0x00 * reader.ReadInt16(); * * do * arrayLength = reader.ReadByte(); * while (!reader.EndOfStream && (arrayLength == 0 || arrayLength == 1)); * * if (reader.EndOfStream) * return; */ // Now get the BattleTag for each player var battleTagDigits = new List <char>(); foreach (var player in replay.Players.Where(i => i != null)) { // Find each player's name, and then their associated BattleTag battleTagDigits.Clear(); var playerNameBytes = Encoding.UTF8.GetBytes(player.Name); while (!reader.EndOfStream) { var isFound = true; for (var i = 0; i < playerNameBytes.Length + 1; i++) { if ((i == playerNameBytes.Length && reader.ReadByte() != 35 /* '#' Character */) || (i < playerNameBytes.Length && reader.ReadByte() != playerNameBytes[i])) { isFound = false; break; } } if (isFound) { break; } } // Get the numbers from the BattleTag while (!reader.EndOfStream) { var currentCharacter = (char)reader.ReadByte(); if (char.IsDigit(currentCharacter)) { battleTagDigits.Add(currentCharacter); } else { break; } } if (reader.EndOfStream) { break; } player.BattleTag = int.Parse(string.Join("", battleTagDigits)); } if (replay.Players.Any(i => i != null && i.BattleTag == 0)) { throw new Exception("Couldn't retrieve BattleTag"); } } }
/// <summary> Parses the Replay.Messages.Events file. </summary> /// <param name="buffer"> Buffer containing the contents of the replay.messages.events file. </param> /// <returns> A list of messages parsed from the buffer. </returns> public static void Parse(Replay replay, byte[] buffer) { if (buffer.Length <= 1) { // Chat has been removed from this replay return; } var ticksElapsed = 0; using (var stream = new MemoryStream(buffer)) { var bitReader = new Streams.BitReader(stream); while (!bitReader.EndOfStream) { var message = new Message(); ticksElapsed += (int)bitReader.Read(6 + (bitReader.Read(2) << 3)); message.Timestamp = new TimeSpan(0, 0, (int)Math.Round(ticksElapsed / 16.0)); var playerIndex = (int)bitReader.Read(5); if (playerIndex != 16) { message.MessageSender = replay.ClientListByUserID[playerIndex]; } message.MessageEventType = (MessageEventType)bitReader.Read(4); switch (message.MessageEventType) { case MessageEventType.SChatMessage: { ChatMessage chatMessage = new ChatMessage(); chatMessage.MessageTarget = (MessageTarget)bitReader.Read(3); // m_recipient (the target) chatMessage.Message = Encoding.UTF8.GetString(bitReader.ReadBlobPrecededWithLength(11)); // m_string message.ChatMessage = chatMessage; replay.Messages.Add(message); break; } case MessageEventType.SPingMessage: { PingMessage pingMessage = new PingMessage(); pingMessage.MessageTarget = (MessageTarget)bitReader.Read(3); // m_recipient (the target) pingMessage.XCoordinate = bitReader.ReadInt32() - (-2147483648); // m_point x pingMessage.YCoordinate = bitReader.ReadInt32() - (-2147483648); // m_point y message.PingMessage = pingMessage; replay.Messages.Add(message); break; } case MessageEventType.SLoadingProgressMessage: { // can be used to keep track of how fast/slow players are loading // also includes players who are reloading the game var progress = bitReader.ReadInt32() - (-2147483648); // m_progress break; } case MessageEventType.SServerPingMessage: { break; } case MessageEventType.SReconnectNotifyMessage: { bitReader.Read(2); // m_status; is either a 1 or a 2 break; } case MessageEventType.SPlayerAnnounceMessage: { PlayerAnnounceMessage announceMessage = new PlayerAnnounceMessage(); announceMessage.AnnouncementType = (AnnouncementType)bitReader.Read(2); switch (announceMessage.AnnouncementType) { case AnnouncementType.None: { break; } case AnnouncementType.Ability: { AbilityAnnouncment ability = new AbilityAnnouncment(); ability.AbilityLink = bitReader.ReadInt16(); // m_abilLink ability.AbilityIndex = (int)bitReader.Read(5); // m_abilCmdIndex ability.ButtonLink = bitReader.ReadInt16(); // m_buttonLink announceMessage.AbilityAnnouncement = ability; break; } case AnnouncementType.Behavior: // no idea what triggers this { bitReader.ReadInt16(); // m_behaviorLink bitReader.ReadInt16(); // m_buttonLink break; } case AnnouncementType.Vitals: { VitalAnnouncment vital = new VitalAnnouncment(); vital.VitalType = (VitalType)(bitReader.ReadInt16() - (-32768)); announceMessage.VitalAnnouncement = vital; break; } default: throw new NotImplementedException(); } if (replay.ReplayBuild > 45635) { // m_announceLink bitReader.ReadInt16(); } bitReader.ReadInt32(); // m_otherUnitTag bitReader.ReadInt32(); // m_unitTag message.PlayerAnnounceMessage = announceMessage; replay.Messages.Add(message); break; } default: throw new NotImplementedException(); } bitReader.AlignToByte(); } } }
/// <summary> /// Applies the set of attributes to a replay. /// </summary> /// <param name="replay">Replay to apply the attributes to.</param> private static void ApplyAttributes(Replay replay, ReplayAttribute[] Attributes) { // I'm not entirely sure this is the right encoding here. Might be unicode... var encoding = Encoding.UTF8; var attributes1 = new List <ReplayAttribute>(); var attributes2 = new List <ReplayAttribute>(); var attributes3 = new List <ReplayAttribute>(); var attributes4 = new List <ReplayAttribute>(); var attributesffa = new List <ReplayAttribute>(); foreach (var attribute in Attributes) { switch (attribute.AttributeType) { case ReplayAttributeEventType.PlayerTypeAttribute: { var type = encoding.GetString(attribute.Value.Reverse().ToArray()).ToLower(); if (type == "comp") { replay.Players[attribute.PlayerId - 1].PlayerType = PlayerType.Computer; } else if (type == "humn") { replay.Players[attribute.PlayerId - 1].PlayerType = PlayerType.Human; } else { throw new Exception("Unexpected value for PlayerType"); } break; } case ReplayAttributeEventType.TeamSizeAttribute: { // This fixes issues with reversing the string before encoding. Without this, you get "\01v1" replay.TeamSize = new string(encoding.GetString(attribute.Value, 0, 3).Reverse().ToArray()); break; } case ReplayAttributeEventType.DifficultyLevelAttribute: { var diffLevel = encoding.GetString(attribute.Value.Reverse().ToArray()); var player = replay.Players[attribute.PlayerId - 1]; switch (diffLevel) { case "VyEy": player.Difficulty = Difficulty.Beginner; break; case "Easy": player.Difficulty = Difficulty.Recruit; break; case "Medi": player.Difficulty = Difficulty.Adept; break; case "HdVH": player.Difficulty = Difficulty.Veteran; break; case "VyHd": player.Difficulty = Difficulty.Elite; break; } break; } case ReplayAttributeEventType.GameSpeedAttribute: { var speed = encoding.GetString(attribute.Value.Reverse().ToArray()).ToLower(); switch (speed) { case "slor": replay.GameSpeed = GameSpeed.Slower; break; case "slow": replay.GameSpeed = GameSpeed.Slow; break; case "norm": replay.GameSpeed = GameSpeed.Normal; break; case "fast": replay.GameSpeed = GameSpeed.Fast; break; case "fasr": replay.GameSpeed = GameSpeed.Faster; break; // Otherwise, Game Speed will remain "Unknown" } break; } case ReplayAttributeEventType.PlayerTeam1v1Attribute: { attributes1.Add(attribute); break; } case ReplayAttributeEventType.PlayerTeam2v2Attribute: { attributes2.Add(attribute); break; } case ReplayAttributeEventType.PlayerTeam3v3Attribute: { attributes3.Add(attribute); break; } case ReplayAttributeEventType.PlayerTeam4v4Attribute: { attributes4.Add(attribute); break; } case ReplayAttributeEventType.PlayerTeamFFAAttribute: { attributesffa.Add(attribute); break; } case ReplayAttributeEventType.GameTypeAttribute: { switch (encoding.GetString(attribute.Value.Reverse().ToArray()).ToLower().Trim('\0')) { case "priv": replay.GameMode = GameMode.Custom; break; case "amm": if (replay.ReplayBuild < 33684) { replay.GameMode = GameMode.QuickMatch; } break; default: throw new Exception("Unexpected Game Type"); } break; } case ReplayAttributeEventType.Hero: { replay.Players[attribute.PlayerId - 1].IsAutoSelect = encoding.GetString(attribute.Value.Reverse().ToArray()) == "Rand"; break; } case ReplayAttributeEventType.SkinAndSkinTint: if (encoding.GetString(attribute.Value.Reverse().ToArray()) == "Rand") { replay.Players[attribute.PlayerId - 1].IsAutoSelect = true; } break; case ReplayAttributeEventType.CharacterLevel: { var characterLevel = int.Parse(encoding.GetString(attribute.Value.Reverse().ToArray())); var player = replay.Players[attribute.PlayerId - 1]; player.CharacterLevel = characterLevel; if (player.IsAutoSelect && player.CharacterLevel > 1) { player.IsAutoSelect = false; } break; } case ReplayAttributeEventType.LobbyMode: { if (replay.GameMode != GameMode.Custom) { switch (encoding.GetString(attribute.Value.Reverse().ToArray()).ToLower().Trim('\0')) { case "stan": replay.GameMode = GameMode.QuickMatch; break; case "drft": replay.GameMode = GameMode.HeroLeague; break; } } } break; case ReplayAttributeEventType.ReadyMode: if (replay.GameMode == GameMode.HeroLeague && encoding.GetString(attribute.Value.Reverse().ToArray()).ToLower().Trim('\0') == "fcfs") { replay.GameMode = GameMode.TeamLeague; } break; case (ReplayAttributeEventType)4011: // What is this? Draft order? break; case (ReplayAttributeEventType)4016: // What is this? Always '1' in Hero League // if (replay.GameMode == GameMode.HeroLeague && int.Parse(encoding.GetString(attribute.Value.Reverse().ToArray())) != 1) // Console.WriteLine("WAAT!?"); break; case (ReplayAttributeEventType)4017: // What is this? Always '5' in Hero League // if (replay.GameMode == GameMode.HeroLeague && int.Parse(encoding.GetString(attribute.Value.Reverse().ToArray())) != 5) // Console.WriteLine("WAAT!?"); break; case ReplayAttributeEventType.DraftBanMode: // Options: No Ban (""), One Ban ("1ban"), Two Ban ("2ban"), Mid Ban ("Mban", Default) break; case ReplayAttributeEventType.DraftTeam1BanChooserSlot: case ReplayAttributeEventType.DraftTeam2BanChooserSlot: // For Ranked Play, this is always "Hmmr" -> Highest MMR break; case ReplayAttributeEventType.DraftTeam1Ban1LockedIn: case ReplayAttributeEventType.DraftTeam1Ban2LockedIn: case ReplayAttributeEventType.DraftTeam2Ban1LockedIn: case ReplayAttributeEventType.DraftTeam2Ban2LockedIn: // So far I've only seen an empty string here break; case ReplayAttributeEventType.DraftTeam1Ban1: case ReplayAttributeEventType.DraftTeam1Ban2: case ReplayAttributeEventType.DraftTeam2Ban1: case ReplayAttributeEventType.DraftTeam2Ban2: var draftTeamBanValue = encoding.GetString(attribute.Value.Reverse().ToArray()).Trim('\0'); if (draftTeamBanValue != "") { switch (attribute.AttributeType) { case ReplayAttributeEventType.DraftTeam1Ban1: replay.TeamHeroBans[0][0] = draftTeamBanValue; break; case ReplayAttributeEventType.DraftTeam1Ban2: replay.TeamHeroBans[0][1] = draftTeamBanValue; break; case ReplayAttributeEventType.DraftTeam2Ban1: replay.TeamHeroBans[1][0] = draftTeamBanValue; break; case ReplayAttributeEventType.DraftTeam2Ban2: replay.TeamHeroBans[1][1] = draftTeamBanValue; break; } } break; } } List <ReplayAttribute> currentList = null; if (replay.TeamSize.Equals("1v1")) { currentList = attributes1; } else if (replay.TeamSize.Equals("2v2")) { currentList = attributes2; } else if (replay.TeamSize.Equals("3v3")) { currentList = attributes3; } else if (replay.TeamSize.Equals("4v4")) { currentList = attributes4; } else if (replay.TeamSize.Equals("FFA")) { currentList = attributesffa; } if (currentList != null) { foreach (var att in currentList) { // Reverse the values then parse, you don't notice the effects of this until theres 10+ teams o.o replay.Players[att.PlayerId - 1].Team = int.Parse(encoding.GetString(att.Value.Reverse().ToArray()).Trim('\0', 'T')); } } }
// used for builds <= 47479 and 47903 private static void ExtendedBattleTagParsingOld(Replay replay, BitReader bitReader) { bool changed47479 = false; if (replay.ReplayBuild == 47479 && DetectBattleTagChangeBuild47479(replay, bitReader)) { changed47479 = true; } for (int i = 0; i < replay.ClientListByUserID.Length; i++) { if (replay.ClientListByUserID[i] == null) { break; } string TId; string TId_2; // this first one is weird, nothing to indicate the length of the string if (i == 0) { var offset = bitReader.ReadByte(); bitReader.ReadString(2); // T: TId = bitReader.ReadString(12 + offset); if (replay.ReplayBuild <= 47479 && !changed47479) { bitReader.ReadBytes(6); ReadByte0x00(bitReader); ReadByte0x00(bitReader); ReadByte0x00(bitReader); bitReader.Read(6); // get T: again TId_2 = Encoding.UTF8.GetString(ReadSpecialBlob(bitReader, 8)); if (TId != TId_2) { throw new Exception("TID dup not equal"); } } } else { ReadByte0x00(bitReader); ReadByte0x00(bitReader); ReadByte0x00(bitReader); bitReader.Read(6); // get XXXXXXXX#YYY TId = Encoding.UTF8.GetString(ReadSpecialBlob(bitReader, 8)); if (replay.ReplayBuild <= 47479 && !changed47479) { bitReader.ReadBytes(6); ReadByte0x00(bitReader); ReadByte0x00(bitReader); ReadByte0x00(bitReader); bitReader.Read(6); // get T: again TId_2 = Encoding.UTF8.GetString(ReadSpecialBlob(bitReader, 8)); if (TId != TId_2) { throw new Exception("TID dup not equal"); } } } // next 31 bytes bitReader.ReadBytes(4); // same for all players bitReader.ReadByte(); bitReader.ReadBytes(8); // same for all players bitReader.ReadBytes(4); bitReader.ReadBytes(14); // same for all players if (replay.ReplayBuild >= 47903 || changed47479) { bitReader.ReadBytes(40); } else if (replay.ReplayBuild >= 47219 || replay.ReplayBuild == 47024) { bitReader.ReadBytes(39); } else if (replay.ReplayBuild >= 45889) { bitReader.ReadBytes(38); } else if (replay.ReplayBuild >= 45228) { bitReader.ReadBytes(37); } else if (replay.ReplayBuild >= 44468) { bitReader.ReadBytes(36); } else { bitReader.ReadBytes(35); } if (replay.ReplayBuild >= 47903 || changed47479) { bitReader.Read(1); } else if (replay.ReplayBuild >= 47219 || replay.ReplayBuild == 47024) { bitReader.Read(6); } else if (replay.ReplayBuild >= 46690 || replay.ReplayBuild == 46416) { bitReader.Read(5); } else if (replay.ReplayBuild >= 45889) { bitReader.Read(2); } else if (replay.ReplayBuild >= 45228) { bitReader.Read(3); } else { bitReader.Read(5); } if (bitReader.ReadBoolean()) { // use this to determine who is in a party // those in the same party will have the same exact 8 bytes of data // the party leader is the first one (in the order of the client list) bitReader.ReadBytes(8); } bitReader.Read(1); var battleTag = Encoding.UTF8.GetString(bitReader.ReadBlobPrecededWithLength(7)).Split('#'); // battleTag <name>#xxxxx if (battleTag.Length != 2 || battleTag[0] != replay.ClientListByUserID[i].Name) { throw new Exception("Couldn't find BattleTag"); } replay.ClientListByUserID[i].BattleTag = int.Parse(battleTag[1]); // these similar bytes don't occur for last player bitReader.ReadBytes(27); } // some more bytes after (at least 700) // theres some HeroICONs and other repetitive stuff }
/// <summary> Parses the replay.initdata file in a replay file. </summary> /// <param name="replay"> The replay file to apply the parsed data to. </param> /// <param name="buffer"> The buffer containing the replay.initdata file. </param> public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) { var reader = new BitReader(stream); var i = reader.ReadByte(); var playerList = new string[i]; for (int j = 0; j < i; j++) { playerList[j] = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(8)); if (reader.ReadBoolean()) { var clanTag = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(8)); // Console.WriteLine(clanTag); } if (reader.ReadBoolean()) // Clan Logo reader.ReadBlobPrecededWithLength(40); if (reader.ReadBoolean()) { var highestLeague = reader.Read(8); // Console.WriteLine(highestLeague); } if (reader.ReadBoolean()) { var combinedRaceLevels = reader.ReadInt32(); // Console.WriteLine(combinedRaceLevels); } reader.ReadInt32(); // Random seed (So far, always 0 in Heroes) if (reader.ReadBoolean()) reader.Read(8); // Race Preference if (reader.ReadBoolean()) reader.Read(8); // Team Preference reader.ReadBoolean(); //test map reader.ReadBoolean(); //test auto reader.ReadBoolean(); //examine reader.ReadBoolean(); //custom interface var unknown1 = reader.ReadInt32(); reader.Read(2); //observer var unknown2 = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); var unknown3 = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); var unknown4 = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); var unknown5 = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); var unknown6 = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); var unknown7 = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); // Console.WriteLine(unknown1 + unknown2 + unknown3 + unknown4 + unknown5 + unknown6 + unknown7); } // Marked as 'Random Value', so I will use as seed replay.RandomValue = (uint)reader.ReadInt32(); reader.ReadBlobPrecededWithLength(10); // Dflt reader.ReadBoolean(); // Lock Teams reader.ReadBoolean(); // Teams Together reader.ReadBoolean(); // Advanced Shared Control reader.ReadBoolean(); // Random Races reader.ReadBoolean(); // BattleNet reader.ReadBoolean(); // AMM reader.ReadBoolean(); // Competitive reader.ReadBoolean(); // No Victory Or Defeat reader.ReadBoolean(); // Unknown 0 reader.ReadBoolean(); // Unknown 1 reader.ReadBoolean(); // Unknown 2 reader.Read(2); // Fog reader.Read(2); // Observers reader.Read(2); // User Difficulty reader.ReadInt32(); reader.ReadInt32(); // 64 bit int: Client Debug Flags reader.Read(3); // Game Speed // Not sure what this 'Game Type' is reader.Read(3); var maxUsers = reader.Read(5); if (maxUsers != 10) // Max Players replay.GameMode = GameMode.TryMe; reader.Read(5); // Max Observers reader.Read(5); // Max Players reader.Read(4); // + 1 = Max Teams reader.Read(6); // Max Colors reader.Read(8); // + 1 = Max Races reader.Read(8); // Max Controls replay.MapSize = new Point { X = (int)reader.Read(8), Y = (int)reader.Read(8) }; if (replay.MapSize.Y == 1) replay.MapSize.Y = replay.MapSize.X; else if (replay.MapSize.X == 0) replay.MapSize.X = replay.MapSize.Y; // About 1000 bytes from here is a list of characters, character skins, character mounts, artifact selections, and other data } }
private static void GetBattleTags(Replay replay, BitReader reader) { // Search for the BattleTag for each player var battleTagDigits = new List <char>(); for (var playerNum = 0; playerNum < replay.Players.Length; playerNum++) { var player = replay.Players[playerNum]; if (player == null) { continue; } // Find each player's name, and then their associated BattleTag battleTagDigits.Clear(); var playerNameBytes = Encoding.UTF8.GetBytes(player.Name); while (!reader.EndOfStream) { var isFound = true; for (var i = 0; i < playerNameBytes.Length + 1; i++) { if ((i == playerNameBytes.Length && reader.ReadByte() != 35 /* '#' Character */) || (i < playerNameBytes.Length && reader.ReadByte() != playerNameBytes[i])) { isFound = false; break; } } if (isFound) { break; } } // Get the digits from the BattleTag while (!reader.EndOfStream) { var currentCharacter = (char)reader.ReadByte(); if (playerNum == 9 && (currentCharacter == 'z' || currentCharacter == 'Ø')) { // If player is in slot 9, there's a chance that an extra digit could be appended to the BattleTag battleTagDigits.RemoveAt(battleTagDigits.Count - 1); break; } else if (char.IsDigit(currentCharacter)) { battleTagDigits.Add(currentCharacter); } else { break; } } if (reader.EndOfStream) { break; } player.BattleTag = int.Parse(string.Join("", battleTagDigits)); } }
/// <summary> Parses the replay.server.battlelobby file in a replay file. </summary> /// <param name="replay"> The replay file to apply the parsed data to. </param> /// <param name="buffer"> The buffer containing the replay.initdata file. </param> public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) { var reader = new BitReader(stream); int arrayLength = reader.ReadByte(); var stringLength = reader.ReadByte(); for (var i = 0; i < arrayLength; i++) { reader.ReadString(stringLength); reader.ReadBytes(2); // Unknown } // This is not always here; we can't blindly wait for 's2mh' /* while (!reader.EndOfStream) if (reader.ReadString(1) == "s" && reader.ReadString(1) == "2" && reader.ReadString(1) == "m" && reader.ReadString(1) == "h") { reader.stream.Position -= 4; break; } if (reader.EndOfStream) return; for (var j = 0; j < arrayLength; j++) { reader.ReadString(4); // s2mh reader.ReadBytes(2); // 0x00 0x00 reader.ReadBytes(2); // 'Realm' reader.ReadBytes(32); // 'DepHash' } reader.ReadBytes(2); // 0x00 0x00 // Different Skins / Artifacts / Characters - I think this is what users mouse over in the UI before the game arrayLength = reader.ReadInt16(); for (var j = 0; j < arrayLength; j++) reader.ReadString(reader.ReadByte()); reader.ReadBytes(2); // 0x00 0x00 reader.ReadInt16(); do arrayLength = reader.ReadByte(); while (!reader.EndOfStream && (arrayLength == 0 || arrayLength == 1)); if (reader.EndOfStream) return; */ // Now get the BattleTag for each player var battleTagDigits = new List<char>(); foreach (var player in replay.Players.Where(i => i != null)) { // Find each player's name, and then their associated BattleTag battleTagDigits.Clear(); var playerNameBytes = Encoding.UTF8.GetBytes(player.Name); while (!reader.EndOfStream) { var isFound = true; for (var i = 0; i < playerNameBytes.Length + 1; i++) if ((i == playerNameBytes.Length && reader.ReadByte() != 35 /* '#' Character */) || (i < playerNameBytes.Length && reader.ReadByte() != playerNameBytes[i])) { isFound = false; break; } if (isFound) break; } // Get the numbers from the BattleTag while (!reader.EndOfStream) { var currentCharacter = (char)reader.ReadByte(); if (char.IsDigit(currentCharacter)) battleTagDigits.Add(currentCharacter); else break; } if (reader.EndOfStream) break; player.BattleTag = int.Parse(string.Join("", battleTagDigits)); } } }
public static void Parse(Replay replay) { // I believe these 'PlayerID' are just indexes to the ClientList, but we should use the info given in this file just to be safe var playerIDDictionary = new Dictionary<int, Player>(); for (var i = 0; i < replay.TeamLevels.Length; i++) { replay.TeamLevels[i] = new Dictionary<int, TimeSpan>(); replay.TeamPeriodicXPBreakdown[i] = new List<PeriodicXPBreakdown>(); } var playerIDTalentIndexDictionary = new Dictionary<int, int>(); foreach (var trackerEvent in replay.TrackerEvents.Where(i => i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UpgradeEvent || i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.StatGameEvent || i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.ScoreResultEvent)) switch (trackerEvent.TrackerEventType) { case ReplayTrackerEvents.TrackerEventType.UpgradeEvent: // Contains interesting data such as tracking some 'Gathering Power' type talents: {UpgradeEvent: {6, "NovaSnipeMasterDamageUpgrade", 1}} // We should save these kind of statistics somewhere break; case ReplayTrackerEvents.TrackerEventType.StatGameEvent: switch (trackerEvent.Data.dictionary[0].blobText) { case "GameStart": // {StatGameEvent: {"GameStart", , , [{{"MapSizeX"}, 248}, {{"MapSizeY"}, 208}]}} if (trackerEvent.Data.dictionary[3].optionalData.array[0].dictionary[0].dictionary[0].blobText == "MapSizeX" && trackerEvent.Data.dictionary[3].optionalData.array[1].dictionary[0].dictionary[0].blobText == "MapSizeY") replay.MapSize = new Point { X = (int)trackerEvent.Data.dictionary[3].optionalData.array[0].dictionary[1].vInt.Value, Y = (int)trackerEvent.Data.dictionary[3].optionalData.array[1].dictionary[1].vInt.Value }; break; case "PlayerInit": // {StatGameEvent: {"PlayerInit", [{{"Controller"}, "User"}, {{"ToonHandle"}, "1-Hero-1-XXXXX"}], [{{"PlayerID"}, 1}, {{"Team"}, 1}], }} if (trackerEvent.Data.dictionary[1].optionalData.array[1].dictionary[0].dictionary[0].blobText == "ToonHandle" && trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[0].dictionary[0].blobText == "PlayerID") playerIDDictionary[(int)trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[1].vInt.Value] = replay.Players.Single(i => i.BattleNetId == int.Parse(trackerEvent.Data.dictionary[1].optionalData.array[1].dictionary[1].blobText.Split('-').Last())); break; case "LevelUp": // {StatGameEvent: {"LevelUp", , [{{"PlayerID"}, 6}, {{"Level"}, 1}], }} if (trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[0].dictionary[0].blobText == "PlayerID" && trackerEvent.Data.dictionary[2].optionalData.array[1].dictionary[0].dictionary[0].blobText == "Level") { var team = playerIDDictionary[(int)trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[1].vInt.Value].Team; var level = (int)trackerEvent.Data.dictionary[2].optionalData.array[1].dictionary[1].vInt.Value; if (!replay.TeamLevels[team].ContainsKey(level)) replay.TeamLevels[team][level] = trackerEvent.TimeSpan; } break; case "TalentChosen": // {StatGameEvent: {"TalentChosen", [{{"PurchaseName"}, "NovaCombatStyleAdvancedCloaking"}], [{{"PlayerID"}, 6}], }} if (trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[0].dictionary[0].blobText == "PlayerID" && trackerEvent.Data.dictionary[1].optionalData != null && trackerEvent.Data.dictionary[1].optionalData.array[0].dictionary[0].dictionary[0].blobText == "PurchaseName") { var playerID = (int)trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[1].vInt.Value; if (!playerIDTalentIndexDictionary.ContainsKey(playerID)) playerIDTalentIndexDictionary[playerID] = 0; if (playerIDDictionary[playerID].Talents.Length > playerIDTalentIndexDictionary[playerID]) playerIDDictionary[playerID].Talents[playerIDTalentIndexDictionary[playerID]++].TalentName = trackerEvent.Data.dictionary[1].optionalData.array[0].dictionary[1].blobText; else // A talent was selected while a player was disconnected // This makes it more difficult to match a 'TalentName' with a 'TalentID' // Since this is rare, I'll just clear all 'TalentName' for that player foreach (var talent in playerIDDictionary[playerID].Talents) talent.TalentName = null; } break; case "PeriodicXPBreakdown": // {StatGameEvent: {"PeriodicXPBreakdown", , [{{"Team"}, 1}, {{"TeamLevel"}, 9}], [{{"GameTime"}, 420}, {{"PreviousGameTime"}, 360}, {{"MinionXP"}, 10877}, {{"CreepXP"}, 0}, {{"StructureXP"}, 1200}, {{"HeroXP"}, 3202}, {{"TrickleXP"}, 7700}]}} if (trackerEvent.Data.dictionary[2].optionalData.array[1].dictionary[0].dictionary[0].blobText == "TeamLevel" && trackerEvent.Data.dictionary[3].optionalData.array[0].dictionary[0].dictionary[0].blobText == "GameTime" && trackerEvent.Data.dictionary[3].optionalData.array[1].dictionary[0].dictionary[0].blobText == "PreviousGameTime" && trackerEvent.Data.dictionary[3].optionalData.array[2].dictionary[0].dictionary[0].blobText == "MinionXP" && trackerEvent.Data.dictionary[3].optionalData.array[3].dictionary[0].dictionary[0].blobText == "CreepXP" && trackerEvent.Data.dictionary[3].optionalData.array[4].dictionary[0].dictionary[0].blobText == "StructureXP" && trackerEvent.Data.dictionary[3].optionalData.array[5].dictionary[0].dictionary[0].blobText == "HeroXP" && trackerEvent.Data.dictionary[3].optionalData.array[6].dictionary[0].dictionary[0].blobText == "TrickleXP") replay.TeamPeriodicXPBreakdown[(int)trackerEvent.Data.dictionary[2].optionalData.array[0].dictionary[1].vInt.Value - 1].Add(new PeriodicXPBreakdown { TeamLevel = (int)trackerEvent.Data.dictionary[2].optionalData.array[1].dictionary[1].vInt.Value, TimeSpan = trackerEvent.TimeSpan, MinionXP = (int)trackerEvent.Data.dictionary[3].optionalData.array[2].dictionary[1].vInt.Value, CreepXP = (int)trackerEvent.Data.dictionary[3].optionalData.array[3].dictionary[1].vInt.Value, StructureXP = (int)trackerEvent.Data.dictionary[3].optionalData.array[4].dictionary[1].vInt.Value, HeroXP = (int)trackerEvent.Data.dictionary[3].optionalData.array[5].dictionary[1].vInt.Value, TrickleXP = (int)trackerEvent.Data.dictionary[3].optionalData.array[6].dictionary[1].vInt.Value }); break; case "TownStructureInit": break; // {StatGameEvent: {"TownStructureInit", , [{{"TownID"}, 5}, {{"Team"}, 1}, {{"Lane"}, 3}], [{{"PositionX"}, 59}, {{"PositionY"}, 93}]}} case "JungleCampInit": break; // {StatGameEvent: {"JungleCampInit", , [{{"CampID"}, 1}], [{{"PositionX"}, 101}, {{"PositionY"}, 74}]}} case "PlayerSpawned": break; // {StatGameEvent: {"PlayerSpawned", [{{"Hero"}, "HeroLeoric"}], [{{"PlayerID"}, 1}], }} case "GatesOpen": break; // {StatGameEvent: {"GatesOpen", , , }} case "PlayerDeath": break; // {StatGameEvent: {"PlayerDeath", , [{{"PlayerID"}, 8}, {{"KillingPlayer"}, 1}, {{"KillingPlayer"}, 2}, {{"KillingPlayer"}, 3}, {{"KillingPlayer"}, 4}, {{"KillingPlayer"}, 5}], [{{"PositionX"}, 130}, {{"PositionY"}, 80}]}} case "RegenGlobePickedUp": break; // {StatGameEvent: {"RegenGlobePickedUp", , [{{"PlayerID"}, 1}], }} case "JungleCampCapture": break; // {StatGameEvent: {"JungleCampCapture", [{{"CampType"}, "Siege Camp"}], [{{"CampID"}, 1}], [{{"TeamID"}, 1}]}} case "TownStructureDeath": break; // {StatGameEvent: {"TownStructureDeath", , [{{"TownID"}, 8}, {{"KillingPlayer"}, 1}, {{"KillingPlayer"}, 2}, {{"KillingPlayer"}, 3}, {{"KillingPlayer"}, 4}, {{"KillingPlayer"}, 5}], }} case "EndOfGameXPBreakdown": break; // {StatGameEvent: {"EndOfGameXPBreakdown", , [{{"PlayerID"}, 4}], [{{"MinionXP"}, 31222}, {{"CreepXP"}, 1476}, {{"StructureXP"}, 10550}, {{"HeroXP"}, 22676}, {{"TrickleXP"}, 27280}]}} case "EndOfGameTimeSpentDead": break; // {StatGameEvent: {"EndOfGameTimeSpentDead", , [{{"PlayerID"}, 2}], [{{"Time"}, 162}]}} // Map Objectives case "Altar Captured": break; // {StatGameEvent: {"Altar Captured", , [{{"Firing Team"}, 2}, {{"Towns Owned"}, 3}], }} case "Town Captured": break; // {StatGameEvent: {"Town Captured", , [{{"New Owner"}, 12}], }} case "SkyTempleActivated": break; // {StatGameEvent: {"SkyTempleActivated", , [{{"Event"}, 1}, {{"TempleID"}, 1}], }} case "SkyTempleCaptured": break; // {StatGameEvent: {"SkyTempleCaptured", , [{{"Event"}, 1}, {{"TempleID"}, 2}, {{"TeamID"}, 2}], }} case "SkyTempleShotsFired": break; // {StatGameEvent: {"SkyTempleShotsFired", , [{{"Event"}, 1}, {{"TempleID"}, 2}, {{"TeamID"}, 2}], [{{"SkyTempleShotsDamage"}, 450}]}} case "Immortal Defeated": break; // {StatGameEvent: {"Immortal Defeated", , [{{"Event"}, 1}, {{"Winning Team"}, 1}, {{"Immortal Fight Duration"}, 62}], [{{"Immortal Power Percent"}, 14}]}} case "Boss Duel Started": break; // {StatGameEvent: {"Boss Duel Started", , [{{"Boss Duel Number"}, 1}], }} case "SoulEatersSpawned": break; // {StatGameEvent: {"SoulEatersSpawned", , [{{"Event"}, 1}, {{"TeamScore"}, 50}, {{"OpponentScore"}, 5}], [{{"TeamID"}, 2}]}} case "TributeCollected": break; // {StatGameEvent: {"TributeCollected", , [{{"Event"}, 1}], [{{"TeamID"}, 2}]}} case "RavenCurseActivated": break; // {StatGameEvent: {"RavenCurseActivated", , [{{"Event"}, 1}, {{"TeamScore"}, 3}, {{"OpponentScore"}, 2}], [{{"TeamID"}, 2}]}} case "GhostShipCaptured": break; // {StatGameEvent: {"GhostShipCaptured", , [{{"Event"}, 1}, {{"TeamScore"}, 10}, {{"OpponentScore"}, 6}], [{{"TeamID"}, 2}]}} case "GardenTerrorActivated": break; // {StatGameEvent: {"GardenTerrorActivated", , , [{{"Event"}, 1}, {{"TeamID"}, 2}]}} case "Infernal Shrine Captured": break; // {StatGameEvent: {"Infernal Shrine Captured", , [{{"Event"}, 1}, {{"Winning Team"}, 2}, {{"Winning Score"}, 40}, {{"Losing Score"}, 33}], }} case "Punisher Killed": break; // {StatGameEvent: {"Punisher Killed", [{{"Punisher Type"}, "BombardShrine"}], [{{"Event"}, 1}, {{"Owning Team of Punisher"}, 2}, {{"Duration"}, 20}], [{{"Siege Damage Done"}, 726}, {{"Hero Damage Done"}, 0}]}} default: break; } break; case ReplayTrackerEvents.TrackerEventType.ScoreResultEvent: var scoreResultEventDictionary = trackerEvent.Data.dictionary[0].array.ToDictionary(i => i.dictionary[0].blobText, i => i.dictionary[1].array.Select(j => j.array.Length == 1 ? (int) j.array[0].dictionary[0].vInt.Value : (int?)null).ToArray()); foreach (var scoreResultEventKey in scoreResultEventDictionary.Keys) { var scoreResultEventValueArray = scoreResultEventDictionary[scoreResultEventKey]; switch (scoreResultEventKey) { case "Takedowns": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue) replay.ClientList[i].ScoreResult.Takedowns = scoreResultEventValueArray[i].Value; break; case "SoloKill": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue) replay.ClientList[i].ScoreResult.SoloKills = scoreResultEventValueArray[i].Value; break; case "Assists": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue) replay.ClientList[i].ScoreResult.Assists = scoreResultEventValueArray[i].Value; break; case "Deaths": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue) replay.ClientList[i].ScoreResult.Deaths = scoreResultEventValueArray[i].Value; break; case "HeroDamage": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue) replay.ClientList[i].ScoreResult.HeroDamage = scoreResultEventValueArray[i].Value; break; case "SiegeDamage": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue) replay.ClientList[i].ScoreResult.SiegeDamage = scoreResultEventValueArray[i].Value; break; case "StructureDamage": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue) replay.ClientList[i].ScoreResult.StructureDamage = scoreResultEventValueArray[i].Value; break; case "MinionDamage": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue) replay.ClientList[i].ScoreResult.MinionDamage = scoreResultEventValueArray[i].Value; break; case "CreepDamage": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue) replay.ClientList[i].ScoreResult.CreepDamage = scoreResultEventValueArray[i].Value; break; case "SummonDamage": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue) replay.ClientList[i].ScoreResult.SummonDamage = scoreResultEventValueArray[i].Value; break; case "TimeCCdEnemyHeroes": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue && scoreResultEventValueArray[i].Value > 0) replay.ClientList[i].ScoreResult.TimeCCdEnemyHeroes = TimeSpan.FromSeconds(scoreResultEventValueArray[i].Value); break; case "Healing": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue && scoreResultEventValueArray[i].Value > 0) replay.ClientList[i].ScoreResult.Healing = scoreResultEventValueArray[i].Value; break; case "SelfHealing": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue) replay.ClientList[i].ScoreResult.SelfHealing = scoreResultEventValueArray[i].Value; break; case "DamageTaken": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue && scoreResultEventValueArray[i].Value > 0) replay.ClientList[i].ScoreResult.DamageTaken = scoreResultEventValueArray[i].Value; break; case "ExperienceContribution": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue) replay.ClientList[i].ScoreResult.ExperienceContribution = scoreResultEventValueArray[i].Value; break; case "TownKills": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue) replay.ClientList[i].ScoreResult.TownKills = scoreResultEventValueArray[i].Value; break; case "TimeSpentDead": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue) replay.ClientList[i].ScoreResult.TimeSpentDead = TimeSpan.FromSeconds(scoreResultEventValueArray[i].Value); break; case "MercCampCaptures": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue) replay.ClientList[i].ScoreResult.MercCampCaptures = scoreResultEventValueArray[i].Value; break; case "WatchTowerCaptures": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue) replay.ClientList[i].ScoreResult.WatchTowerCaptures = scoreResultEventValueArray[i].Value; break; case "MetaExperience": for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue) replay.ClientList[i].ScoreResult.MetaExperience = scoreResultEventValueArray[i].Value; break; default: for (var i = 0; i < scoreResultEventValueArray.Length; i++) if (scoreResultEventValueArray[i].HasValue) replay.ClientList[i].MiscellaneousScoreResultEventDictionary[scoreResultEventKey] = scoreResultEventValueArray[i].Value; break; } } break; } }