/// <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];
        }
Example #3
0
        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
        }
Example #5
0
        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()); */
        }
Example #12
0
        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])
        }
Example #13
0
        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);
            }
        }
Example #14
0
 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);
 }
Example #15
0
        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
            }
        }
Example #18
0
        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]);
            }
        }
Example #19
0
        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]);
            }
        }
Example #20
0
        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.
        }
Example #22
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 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
            }
        }
Example #23
0
 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));
     }
 }
Example #24
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.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)))));
 }
Example #30
0
        /// <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'));
        }
Example #31
0
        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
            }
        }
Example #33
0
 /// <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);
 }
Example #34
0
        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"));
        }
Example #35
0
        /// <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
                }
        }
Example #36
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();
                }
            }
        }
    }
Example #39
0
 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
                }
        }
Example #41
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 = "";
             * } */
        }
    }
Example #43
0
        /// <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");
                }
            }
        }
Example #45
0
        /// <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();
                }
            }
        }
Example #46
0
 /// <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);
 }
Example #47
0
        /// <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'));
                }
            }
        }
Example #48
0
        // 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
        }
Example #49
0
 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.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
            }
        }
Example #51
0
        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));
                }
            }
        }
Example #53
0
        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;
                }
        }