/// <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 } }
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 }
/// <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> public static void Parse(Replay replay, byte[] buffer) { replay.TrackerEvents = new List <TrackerEvent>(); 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"); } var trackerEvent = new TrackerEvent(); trackerEvent.FramesSinceLastEvent = (int)TrackerEventStructure.read_vint(reader); 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 = replay.TrackerEvents.Sum(i => i.FramesSinceLastEvent); replay.ReplayLength = new TimeSpan(0, 0, (int)(replay.Frames / 16.0)); /* var trackerEventGroupBy = replay.TrackerEvents.GroupBy(i => i.TrackerEventType).OrderBy(i => i.Key); * Console.WriteLine(trackerEventGroupBy.Count()); */ }
private static void ParseHeader(Replay replay, BinaryReader reader) { reader.ReadBytes(3); // 'Magic' reader.ReadByte(); // Format BitConverter.ToInt32(reader.ReadBytes(4), 0); // Data Max Size BitConverter.ToInt32(reader.ReadBytes(4), 0); // Header Offset BitConverter.ToInt32(reader.ReadBytes(4), 0); // User Data Header Size var headerStructure = new TrackerEventStructure(reader); // [0] = Blob, "Heroes of the Storm replay 11" - Strange backward arrow before 11 as well. I don't think the '11' will change, as I believe it was also always '11' in Starcraft 2 replays. replay.ReplayVersion = string.Format("{0}.{1}.{2}.{3}", headerStructure.dictionary[1].dictionary[0].vInt.Value, headerStructure.dictionary[1].dictionary[1].vInt.Value, headerStructure.dictionary[1].dictionary[2].vInt.Value, headerStructure.dictionary[1].dictionary[3].vInt.Value); replay.ReplayBuild = (int)headerStructure.dictionary[1].dictionary[4].vInt.Value; 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 }
/// <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 } }
/// <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> 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; // Need to verify the player ID in the below code - particularly Custom Games where observers can take up spots in the client list replay.TimelineEvents.AddRange(replay.TrackerEvents.Where(i => i.TrackerEventType == TrackerEventType.CreepColor && i.Data.dictionary[1].blobText == "VehicleDragonUpgrade") .Select(i => new TimelineEvent { TimeSpan = i.TimeSpan, TimelineEventType = TimelineEventType.MapMechanicDragonShireDragon, PlayerID = (int)i.Data.dictionary[0].vInt.Value, Value = 1 })); /* var trackerEventGroupBy = replay.TrackerEvents.GroupBy(i => i.TrackerEventType).OrderBy(i => i.Key); * Console.WriteLine(trackerEventGroupBy.Count()); */ }
/// <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]; } }
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]) }
/// <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]; } }
/// <summary> Parses the replay.tracker.events file </summary> /// <param name="buffer"> The buffer containing the replay.tracker.events file. </param> public static List <TrackerEvent> Parse(byte[] buffer) { var trackerEvents = new List <TrackerEvent>(); var currentFrameCount = 0; using (var stream = new MemoryStream(buffer)) using (var reader = new BinaryReader(stream)) while (stream.Position < stream.Length) { 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 = TimeSpan.FromSeconds((int)(currentFrameCount / 16.0)) }; reader.ReadBytes(1); // Always 09 trackerEvent.TrackerEventType = (TrackerEventType)TrackerEventStructure.read_vint(reader); trackerEvent.Data = new TrackerEventStructure(reader); if (trackerEvent.TrackerEventType == TrackerEventType.StatGameEvent && trackerEvent.Data.dictionary[3].optionalData != null) { // m_fixedData is stored in fixed point 20.12 format foreach (var trackerEventArrayItem in trackerEvent.Data.dictionary[3].optionalData.array) { trackerEventArrayItem.dictionary[1].vInt = trackerEventArrayItem.dictionary[1].vInt.Value / 4096; } } trackerEvents.Add(trackerEvent); } return(trackerEvents); }
public TrackerEventStructure(BinaryReader reader) { DataType = reader.ReadByte(); switch (DataType) { case 0x00: // array array = new TrackerEventStructure[read_vint(reader)]; for (var i = 0; i < array.Length; i++) { array[i] = new TrackerEventStructure(reader); } break; case 0x01: // bitarray, weird alignment requirements - haven't seen it used yet so not spending time on it /* bits = self.read_vint() * data = self.read_bits(bits) */ throw new NotImplementedException(); case 0x02: // blob blob = reader.ReadBytes((int)read_vint(reader)); break; case 0x03: // choice choiceFlag = (int)read_vint(reader); choiceData = new TrackerEventStructure(reader); break; case 0x04: // optional if (reader.ReadByte() != 0) { optionalData = new TrackerEventStructure(reader); } break; case 0x05: // struct dictionary = new Dictionary <int, TrackerEventStructure>(); var dictionarySize = read_vint(reader); for (var i = 0; i < dictionarySize; i++) { dictionary[(int)read_vint(reader)] = new TrackerEventStructure(reader); } break; case 0x06: // u8 unsignedInt = reader.ReadByte(); break; case 0x07: // u32 unsignedInt = reader.ReadUInt32(); break; case 0x08: // u64 unsignedInt = reader.ReadUInt64(); break; case 0x09: // vint vInt = read_vint(reader); break; default: throw new NotImplementedException(); } }
public TrackerEventStructure(BinaryReader reader) { DataType = reader.ReadByte(); switch (DataType) { case 0x00: // array array = new TrackerEventStructure[read_vint(reader)]; for (var i = 0; i < array.Length; i++) array[i] = new TrackerEventStructure(reader); break; case 0x01: // bitarray, weird alignment requirements - haven't seen it used yet so not spending time on it /* bits = self.read_vint() data = self.read_bits(bits) */ throw new NotImplementedException(); case 0x02: // blob blob = reader.ReadBytes((int) read_vint(reader)); break; case 0x03: // choice choiceFlag = (int) read_vint(reader); choiceData = new TrackerEventStructure(reader); break; case 0x04: // optional if (reader.ReadByte() != 0) optionalData = new TrackerEventStructure(reader); break; case 0x05: // struct dictionary = new Dictionary<int, TrackerEventStructure>(); var dictionarySize = read_vint(reader); for (var i = 0; i < dictionarySize; i++) dictionary[(int) read_vint(reader)] = new TrackerEventStructure(reader); break; case 0x06: // u8 unsignedInt = reader.ReadByte(); break; case 0x07: // u32 unsignedInt = reader.ReadUInt32(); break; case 0x08: // u64 unsignedInt = reader.ReadUInt64(); break; case 0x09: // vint vInt = read_vint(reader); break; default: throw new NotImplementedException(); } }
/// <summary> Parses the replay.details file, applying it to a Replay object. </summary> /// <param name="replay"> The replay object to apply the parsed information to. </param> /// <param name="buffer"> The buffer containing the replay.details file. </param> public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) using (var reader = new BinaryReader(stream)) { var replayDetailsStructure = new TrackerEventStructure(reader); replay.Players = replayDetailsStructure.dictionary[0].optionalData.array.Select(i => new Player { Name = i.dictionary[0].blobText, BattleNetRegionId = (int)i.dictionary[1].dictionary[0].vInt.Value, BattleNetSubId = (int)i.dictionary[1].dictionary[2].vInt.Value, BattleNetId = (int)i.dictionary[1].dictionary[4].vInt.Value, // [2] = Race (SC2 Remnant, Always Empty String in Heroes of the Storm) Color = i.dictionary[3].dictionary.Keys.OrderBy(j => j).Select(j => (int)i.dictionary[3].dictionary[j].vInt.Value).ToArray(), // [4] = Player Type (2 = Human, 3 = Computer (Practice, Try Me, or Coop)) - This is more accurately gathered in replay.attributes.events Team = (int)i.dictionary[5].vInt.Value, Handicap = (int)i.dictionary[6].vInt.Value, // [7] = VInt, Default 0 IsWinner = i.dictionary[8].vInt.Value == 1, // [9] = Player Number (0 - 9) Character = i.dictionary[10].blobText }).ToArray(); if (replay.Players.Length != 10) { // Try Me Mode, or something strange return; } var playerIndexes = replay.TrackerEvents.Where(i => i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.PlayerSetupEvent && i.Data.dictionary[2].optionalData != null).Select(i => i.Data.dictionary[2].optionalData.vInt.Value).OrderBy(i => i).ToArray(); for (var i = 0; i < playerIndexes.Length; i++) { // The references between both of these classes are the same on purpose. // We want updates to one to propogate to the other. replay.ClientList[playerIndexes[i]] = replay.Players[i]; } replay.Map = replayDetailsStructure.dictionary[1].blobText; // [2] - This is typically an empty string, no need to decode. // [3] - Blob: "Minimap.tga" or "CustomMiniMap.tga" // [4] - Uint, Default 1 // [5] - Utc Timestamp replay.Timestamp = DateTime.FromFileTimeUtc(replayDetailsStructure.dictionary[5].vInt.Value); // [6] - Windows replays, this is Utc offset. Mac replays, this is actually the entire Local Timestamp // var potentialUtcOffset = new TimeSpan(replayDetailsStructure.dictionary[6].vInt.Value); // Console.WriteLine(potentialUtcOffset.ToString()); // [7] - Blob, Empty String // [8] - Blob, Empty String // [9] - Blob, Empty String // [10] - Optional, Array: 0 - Blob, "s2ma" // [11] - UInt, Default 0 // [12] - VInt, Default 4 // [13] - VInt, Default 1 or 7 // [14] - Optional, Null // [15] - VInt, Default 0 // [16] - Optional, UInt, Default 0 } }
/// <summary> Parses the replay.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.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 } }