public PlayerJoinEvent(BitReader bitReader, Replay replay, int playerIndex) { this.EventType = GameEventType.Inactive; // This should probably be a series of {shl; or} on .Read(1) // to make it version-independent if (replay.ReplayBuild < 22612) { this.JoinFlags = (int)bitReader.Read(4); } else { this.JoinFlags = (int)bitReader.Read(12); // unknown } // Initialize player if not exists (true for observers) Player player = replay.GetPlayerById(playerIndex); if (player == null) { var p = new Player { PlayerType = PlayerType.Spectator }; replay.ClientList[playerIndex] = player = p; } // Initialize wireframe player.Wireframe = new List<Unit>(); player.WireframeSubgroup = 0; // Initialize control groups player.Hotkeys = new List<Unit>[10]; }
public CameraEvent(BitReader bitReader, Replay replay) { TargetX = CFixedToDouble(bitReader.Read(16)); TargetY = CFixedToDouble(bitReader.Read(16)); HasDistance = bitReader.Read(1) != 0; if (HasDistance) { Distance = CFixedToDouble(bitReader.Read(16)); } HasPitch = bitReader.Read(1) != 0; if (HasPitch) { Pitch = RotationAmountToDegrees(bitReader.Read(16)); } HasYaw = bitReader.Read(1) != 0; if (HasYaw) { Yaw = RotationAmountToDegrees(bitReader.Read(16)); } HasHeightOffset = bitReader.Read(1) != 0; if (HasHeightOffset) { // Debug since we're unsure HeightOffset = CFixedToDouble(bitReader.Read(16)); } this.EventType = GameEventType.Other; }
public ReplayEditorViewModel(string replayPath) { this.CurrentFile = replayPath; try { this.replay = Replay.Parse(this.CurrentFile); } catch (Exception ex) { MessageBox.Show("The replay failed to load. Reason: " + ex.Message); return; } this.ReplayHeader = Path.GetFileName(this.CurrentFile); var messages = new ObservableCollection<PlayerChatMessage>(); foreach (var message in this.replay.ChatMessages) { int id = message.PlayerId - 1; if (id < this.replay.Players.Length) { var chat = new PlayerChatMessage { ChatMessage = message, Player = this.replay.Players[message.PlayerId - 1] }; messages.Add(chat); } } this.ChatMessageEditor = new ChatMessageEditViewModel { PlayerList = this.replay.Players }; this.ChatMessages = messages; }
/// <summary> Adds a single chat message to a replay. </summary> /// <param name="fileName"> The file name. </param> /// <param name="messages"> The messages to add. </param> public static void AddChatMessageToReplay(string fileName, IEnumerable<ChatMessage> messages) { var replay = new Replay(); // File in the version numbers for later use. MpqHeader.ParseHeader(replay, fileName); using (var archive = new CArchive(fileName)) { var files = archive.FindFiles("replay.*"); { const string CurFile = "replay.message.events"; var fileSize = (from f in files where f.FileName.Equals(CurFile) select f).Single().Size; var buffer = new byte[fileSize]; archive.ExportFile(CurFile, buffer); foreach (var message in messages) { buffer = GenerateChatMessage( buffer, message.Message, message.MessageTarget, message.PlayerId, (int)message.Timestamp.TotalSeconds); } archive.ImportFile("replay.message.events", buffer); } archive.Close(); } }
public SendResourcesEvent(BitReader bitReader, Replay replay) { this.EventType = GameEventType.Other; var playerId = (int)bitReader.Read(4); Target = replay.GetPlayerById(playerId); var someFlags = (int)bitReader.Read(3); if (someFlags-- > 0) // 4 { MineralsSent = ReadSignedAmount(bitReader.Read(32)); } if (someFlags-- > 0) // 3 { VespeneSent = ReadSignedAmount(bitReader.Read(32)); } if (someFlags-- > 0) // 2 { TerrazineSent = ReadSignedAmount(bitReader.Read(32)); } if (someFlags-- > 0) // 1 { CustomSent = ReadSignedAmount(bitReader.Read(32)); } }
/// <summary> Adds a single chat message to a replay. </summary> /// <param name="fileName"> The file name. </param> /// <param name="message"> The message. </param> /// <param name="target"> The target. </param> /// <param name="playerId"> The player id. </param> /// <param name="numSeconds"> The number of in-game seconds to insert the message at. </param> public static void AddChatMessageToReplay( string fileName, string message, ChatMessageTarget target, int playerId, int numSeconds) { var replay = new Replay(); // File in the version numbers for later use. MpqHeader.ParseHeader(replay, fileName); using (var archive = new CArchive(fileName)) { var files = archive.FindFiles("replay.*"); { const string CurFile = "replay.message.events"; var fileSize = (from f in files where f.FileName.Equals(CurFile) select f).Single().Size; var buffer = new byte[fileSize]; archive.ExportFile(CurFile, buffer); var arr = GenerateChatMessage(buffer, message, target, playerId, numSeconds); archive.ImportFile("replay.message.events", arr); } archive.Close(); } }
public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) { using (var reader = new BinaryReader(stream)) { var i = reader.ReadByte(); var playerList = new string[i]; for (int j = 0; j < i; j++) { var str = ReadString(reader); playerList[j] = str; reader.ReadBytes(5); } if (positionAfter(reader, new byte[] { 115, 50, 109, 97 })) { reader.ReadBytes(2); var gatewayStr = reader.ReadBytes(2); var gateway = Encoding.UTF8.GetString(gatewayStr); replay.Gateway = gateway; } else { replay.GameType = GameType.SinglePlayer; } } } }
/// <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, false)) { Parse(replay, stream); stream.Close(); } }
/// <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="stream"> The stream containing the replay.details file. </param> public static void Parse(Replay replay, Stream stream) { using (var reader = new BinaryReader(stream)) { reader.ReadBytes(6); var playerCount = reader.ReadByte() >> 1; // Parsing Player Info var players = new Player[playerCount]; for (int i = 0; i < playerCount; i++) { players[i] = PlayerDetails.Parse(reader); } replay.Players = players; var mapNameLength = KeyValueStruct.Parse(reader).Value; var mapBytes = reader.ReadBytes(mapNameLength); replay.Map = Encoding.UTF8.GetString(mapBytes); var stringLength = KeyValueStruct.Parse(reader).Value; // This is typically an empty string, no need to decode. var unknownString = reader.ReadBytes(stringLength); reader.ReadBytes(3); var mapPreviewNameLength = KeyValueStruct.Parse(reader).Value; var mapPreviewNameBytes = reader.ReadBytes(mapPreviewNameLength); // What have I learned: // While I can get the name of the map preview file, apparently MPQLib.dll will not // Support exporting the file since its not in the file list. I tried, and it threw an error... // Maybe my buffer wasn't big enough, but it was some temporary file error, so I don't think so. var mapPreviewName = Encoding.UTF8.GetString(mapPreviewNameBytes); reader.ReadBytes(3); var saveTime = KeyValueLongStruct.Parse(reader).Value; var saveTimeZone = KeyValueLongStruct.Parse(reader).Value; var time = DateTime.FromFileTime(saveTime); // Subtract the timezone to get the appropriate UTC time. time = time.Subtract(new TimeSpan(saveTimeZone)); // We create a new timestamp so we can properly set this as UTC time. replay.Timestamp = new DateTime(time.Ticks, DateTimeKind.Utc); reader.Close(); } }
public static void Parse(Replay replay, byte[] buffer) { int headerSize = 4; if (replay.ReplayBuild >= 17326) // 1.2.0 { headerSize = 5; } var numAttributes = BitConverter.ToInt32(buffer, headerSize); var attributes = new ReplayAttribute[numAttributes]; int initialOffset = 4 + headerSize; for (int i = 0; i < numAttributes; i++) { attributes[i] = ReplayAttribute.Parse(buffer, initialOffset + (i*13)); } var rae = new ReplayAttributeEvents { Attributes = attributes }; rae.ApplyAttributes(replay); }
public RequestResourcesEvent(BitReader bitReader, Replay replay) { this.EventType = GameEventType.Other; var someFlags = (int)bitReader.Read(3); if (someFlags-- > 0) // 4 { MineralsRequested = ReadSignedAmount(bitReader.Read(32)); } if (someFlags-- > 0) // 3 { VespeneRequested = ReadSignedAmount(bitReader.Read(32)); } if (someFlags-- > 0) // 2 { TerrazineRequested = ReadSignedAmount(bitReader.Read(32)); } if (someFlags-- > 0) // 1 { CustomRequested = ReadSignedAmount(bitReader.Read(32)); } }
/// <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)) { using (var reader = new BinaryReader(stream)) { var i = reader.ReadByte(); var playerList = new string[i]; for (int j = 0; j < i; j++) { var str = ReadString(reader); playerList[j] = str; reader.ReadBytes(5); } // Save the full list of clients. // This is no longer necessary since we get the client list elsewhere. //// replay.ClientList = playerList; if (PositionAfter(reader, new byte[] { 115, 50, 109, 97 })) { reader.ReadBytes(2); var gatewayStr = reader.ReadBytes(2); var gateway = Encoding.UTF8.GetString(gatewayStr); replay.Gateway = gateway; } else { replay.GameType = GameType.SinglePlayer; } } } }
/// <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 reader = new BinaryReader(new FileStream(filename, FileMode.Open))) { byte[] magic = reader.ReadBytes(3); byte format = reader.ReadByte(); byte[] buffer = reader.ReadBytes(4); var dataMaxSize = BitConverter.ToInt32(buffer, 0); buffer = reader.ReadBytes(4); int headerOffset = BitConverter.ToInt32(buffer, 0); buffer = reader.ReadBytes(4); int userDataHeaderSize = BitConverter.ToInt32(buffer, 0); int dataType = reader.ReadByte(); // Should be 0x05 = Array w/ Keys int numElements = ParseVlfNumber(reader); // The first value is the words: "Starcraft II Replay 11" int index = ParseVlfNumber(reader); int type = reader.ReadByte(); // Should be 0x02 = binary data int numValues = ParseVlfNumber(reader); byte[] starcraft2 = reader.ReadBytes(numValues); int index2 = ParseVlfNumber(reader); int type2 = reader.ReadByte(); // Should be 0x05 = Array w/ Keys int numElementsVersion = ParseVlfNumber(reader); var version = new int[numElementsVersion]; while (numElementsVersion > 0) { int i = ParseVlfNumber(reader); int t = reader.ReadByte(); // Type; if (t == 0x09) { // VLF version[i] = ParseVlfNumber(reader); } else if (t == 0x06) { // Byte version[i] = reader.ReadByte(); } else if (t == 0x07) { // 4 Bytes version[i] = BitConverter.ToInt32(reader.ReadBytes(4), 0); } numElementsVersion--; } // We now have the version. Just parse. replay.ReplayVersion = string.Format("{0}.{1}.{2}.{3}", version[0], version[1], version[2], version[3]); replay.ReplayBuild = version[4]; reader.Close(); } }
public AbilityEvent(BitReader bitReader, Replay replay, Player player, AbilityData abilityData, UnitData unitData) { uint flags; // 1.3.3 patch notes: // - Fixed an issue where the APM statistic could be artificially increased. // This adds the "failed" flag, which is triggered usually by holding down a // hotkey, leading to key repeat spamming the event throughout a single tick. if (replay.ReplayBuild < 18574) // < 1.3.3 { flags = bitReader.Read(17); } else if (replay.ReplayBuild < 22612) // < 1.5.0 { flags = bitReader.Read(18); } else { flags = bitReader.Read(20); } Queued = (flags & 2) != 0; RightClick = (flags & 8) != 0; WireframeClick = (flags & 0x20) != 0; ToggleAbility = (flags & 0x40) != 0; EnableAutoCast = (flags & 0x80) != 0; AbilityUsed = (flags & 0x100) != 0; WireframeUnload = (flags & 0x200) != 0; WireframeCancel = (flags & 0x400) != 0; MinimapClick = (flags & 0x10000) != 0; AbilityFailed = (flags & 0x20000) != 0; // flags & 0xf815 -> Debug for unknown flags // Never found any across all test data. DefaultAbility = (bitReader.Read(1) == 0); DefaultActor = true; if (!DefaultAbility) { AbilityType = abilityData.GetAbilityType( (int)bitReader.Read(16), (int)bitReader.Read(5)); DefaultActor = (bitReader.Read(1) == 0); if (!DefaultActor) { // I'm thinking this would be an array type... but I can't // find anything that causes this bit to be set. throw new InvalidOperationException("Unsupported: non-default actor"); } } if (DefaultActor) { // Deep copy the current wireframe as the actor list // ----- // If a user wants to deal with subgroups to get a more // concise actor list, the data is all here. We're not // going to bother, though, because there are several // exceptions to account for in determining event actors. Actors = new List<Unit>(player.Wireframe.Count); foreach (var unit in player.Wireframe) { Actors.Add(new Unit(unit)); } } var targetType = bitReader.Read(2); if (targetType == 1) // Location target { var targetX = bitReader.Read(20); var targetY = bitReader.Read(20); var targetZ = bitReader.Read(32); TargetLocation = Location.FromEventFormat(targetX, targetY, targetZ); } else if (targetType == 2) // Unit + Location target { TargetFlags = (int)bitReader.Read(8); WireframeIndex = (int)bitReader.Read(8); var unitId = (int)bitReader.Read(32); var unit = replay.GetUnitById(unitId); var unitTypeId = (int)bitReader.Read(16); if (unit == null) { var unitType = unitData.GetUnitType(unitTypeId); unit = new Unit(unitId, unitType); unit.typeId = unitTypeId; replay.GameUnits.Add(unitId, unit); } TargetUnit = unit; var targetHasPlayer = bitReader.Read(1) == 1; if (targetHasPlayer) { TargetPlayer = (int)bitReader.Read(4); } // 1.4.0 -- Don't really know what this was meant to fix if (replay.ReplayBuild >= 19595) { var targetHasTeam = bitReader.Read(1) == 1; if (targetHasTeam) { TargetTeam = (int)bitReader.Read(4); } } var targetX = bitReader.Read(20); var targetY = bitReader.Read(20); var targetZ = bitReader.Read(32); TargetLocation = Location.FromEventFormat(targetX, targetY, targetZ); } else if (targetType == 3) // Unit target { var id = bitReader.Read(32); // Again, if the user wants to determine exactly which // queue item is canceled in the case of a queue cancel // event (the most common case of this target specifier's // occurence), they can; however, it requires an additional // data structure that I don't want to bother with; however, // all the underlying data is available in the events list. TargetId = id; } var lastBit = bitReader.Read(1); // Should be 0; if not, misalignment is likely if (!AbilityFailed) { if (RightClick) { this.EventType = GameEventType.RightClick; } else { this.EventType = EventData.GetInstance().GetEventType(this.AbilityType); } } else { this.EventType = GameEventType.Inactive; } }
public SelectionEvent(BitReader bitReader, Replay replay, Player player, UnitData data) { int wireframeLength = 8; if (replay.ReplayBuild >= 22612) { wireframeLength = 9; // Maximum selection size has been increased to 500, up from 255. } // Parse select event and update player wireframe accordingly WireframeIndex = (int)bitReader.Read(4); player.WireframeSubgroup = SubgroupIndex = (int)bitReader.Read(wireframeLength); if (WireframeIndex == 10) { this.EventType = GameEventType.Selection; } else // This is a control group update, likely from a CAbilMorph { this.EventType = GameEventType.Inactive; } List<Unit> affectedWireframe; if (WireframeIndex == 10) { affectedWireframe = player.Wireframe; } else { affectedWireframe = player.Hotkeys[WireframeIndex]; } RemovedUnits = new List<Unit>(); var updateFlags = (int)bitReader.Read(2); ClearSelection = false; if (updateFlags == 1) { var numBits = (int)bitReader.Read(wireframeLength); var unitsRemoved = new bool[numBits]; var wireframeIndex = 0; while (numBits >= 8) { numBits -= 8; var flags = bitReader.Read(8); for (int i = 0; i < 8; i++) { unitsRemoved[wireframeIndex + i] = (flags & (1 << i)) != 0; } wireframeIndex += 8; } if (numBits != 0) { var flags = bitReader.Read(numBits); for (int i = 0; i < numBits; i++) { unitsRemoved[wireframeIndex + i] = (flags & (1 << i)) != 0; } wireframeIndex += numBits; } for (int i = 0; i < wireframeIndex; i++) { if (unitsRemoved[i]) { RemovedUnits.Add(affectedWireframe[i]); } } } else if (updateFlags == 2) { var indexArrayLength = (int)bitReader.Read(wireframeLength); if (indexArrayLength > 0) { for (int i = 0; i < indexArrayLength; i++) { RemovedUnits.Add(affectedWireframe[(int)bitReader.Read(wireframeLength)]); } } } else if (updateFlags == 3) { var indexArrayLength = (int)bitReader.Read(wireframeLength); if (indexArrayLength > 0) { AddedUnits = new List<Unit>(indexArrayLength); for (int i = 0; i < indexArrayLength; i++) { AddedUnits.Add(affectedWireframe[(int)bitReader.Read(wireframeLength)]); } } ClearSelection = true; } // Build removed unit types RemovedUnitTypes = new Dictionary<UnitType, int>(); foreach (var unit in RemovedUnits) { if (!RemovedUnitTypes.ContainsKey(unit.Type)) { RemovedUnitTypes.Add(unit.Type, 1); } else { RemovedUnitTypes[unit.Type]++; } } HandleUnitArrays(bitReader, replay, data); // Now, update the player wireframe. UpdateWireframe(player); // Check for Morph update if (AddedUnits.SequenceEqual(RemovedUnits)) { this.EventType = GameEventType.Inactive; } }
public SelectionEvent(BitReader bitReader, Replay replay, Player player, UnitData data) { int wireframeLength = 8; if (replay.ReplayBuild >= 22612) { wireframeLength = 9; // Maximum selection size has been increased to 500, up from 255. } // Parse select event and update player wireframe accordingly WireframeIndex = (int)bitReader.Read(4); player.WireframeSubgroup = SubgroupIndex = (int)bitReader.Read(wireframeLength); if (WireframeIndex == 10) { this.EventType = GameEventType.Selection; } else // This is a control group update, likely from a CAbilMorph { this.EventType = GameEventType.Inactive; } List <Unit> affectedWireframe; if (WireframeIndex == 10) { affectedWireframe = player.Wireframe; } else { affectedWireframe = player.Hotkeys[WireframeIndex]; } RemovedUnits = new List <Unit>(); var updateFlags = (int)bitReader.Read(2); ClearSelection = false; if (updateFlags == 1) { var numBits = (int)bitReader.Read(wireframeLength); var unitsRemoved = new bool[numBits]; var wireframeIndex = 0; while (numBits >= 8) { numBits -= 8; var flags = bitReader.Read(8); for (int i = 0; i < 8; i++) { unitsRemoved[wireframeIndex + i] = (flags & (1 << i)) != 0; } wireframeIndex += 8; } if (numBits != 0) { var flags = bitReader.Read(numBits); for (int i = 0; i < numBits; i++) { unitsRemoved[wireframeIndex + i] = (flags & (1 << i)) != 0; } wireframeIndex += numBits; } for (int i = 0; i < wireframeIndex; i++) { if (unitsRemoved[i]) { RemovedUnits.Add(affectedWireframe[i]); } } } else if (updateFlags == 2) { var indexArrayLength = (int)bitReader.Read(wireframeLength); if (indexArrayLength > 0) { for (int i = 0; i < indexArrayLength; i++) { RemovedUnits.Add(affectedWireframe[(int)bitReader.Read(wireframeLength)]); } } } else if (updateFlags == 3) { var indexArrayLength = (int)bitReader.Read(wireframeLength); if (indexArrayLength > 0) { AddedUnits = new List <Unit>(indexArrayLength); for (int i = 0; i < indexArrayLength; i++) { AddedUnits.Add(affectedWireframe[(int)bitReader.Read(wireframeLength)]); } } ClearSelection = true; } // Build removed unit types RemovedUnitTypes = new Dictionary <UnitType, int>(); foreach (var unit in RemovedUnits) { if (!RemovedUnitTypes.ContainsKey(unit.Type)) { RemovedUnitTypes.Add(unit.Type, 1); } else { RemovedUnitTypes[unit.Type]++; } } HandleUnitArrays(bitReader, replay, data); // Now, update the player wireframe. UpdateWireframe(player); // Check for Morph update if (AddedUnits.SequenceEqual(RemovedUnits)) { this.EventType = GameEventType.Inactive; } }
/// <summary> Parses a .SC2Replay file and returns relevant replay information. </summary> /// <param name="fileName"> Full path to a .SC2Replay file. </param> /// <returns> Returns the fully parsed Replay object. </returns> public static Replay Parse(string fileName) { if (File.Exists(fileName) == false) { throw new FileNotFoundException("The specified file does not exist.", fileName); } var replay = new Replay(); // File in the version numbers for later use. MpqHeader.ParseHeader(replay, fileName); CArchive archive; try { archive = new CArchive(fileName); } catch (IOException) { // Usually thrown if the archive name contains korean. Copy it to a local file and open. var tmpPath = Path.GetTempFileName(); File.Copy(fileName, tmpPath, true); archive = new CArchive(tmpPath); } try { var files = archive.FindFiles("replay.*"); { const string CurFile = "replay.initData"; var fileSize = (from f in files where f.FileName.Equals(CurFile) select f).Single().Size; var buffer = new byte[fileSize]; archive.ExportFile(CurFile, buffer); ReplayInitData.Parse(replay, buffer); } { // Local scope allows the byte[] to be GC sooner, and prevents misreferences const string CurFile = "replay.details"; var fileSize = (from f in files where f.FileName.Equals(CurFile) select f).Single().Size; var buffer = new byte[fileSize]; archive.ExportFile(CurFile, buffer); ReplayDetails.Parse(replay, buffer); } { const string CurFile = "replay.attributes.events"; var fileSize = (from f in files where f.FileName.Equals(CurFile) select f).Single().Size; var buffer = new byte[fileSize]; archive.ExportFile(CurFile, buffer); ReplayAttributeEvents.Parse(replay, buffer); } { const string CurFile = "replay.message.events"; var fileSize = (from f in files where f.FileName.Equals(CurFile) select f).Single().Size; var buffer = new byte[fileSize]; archive.ExportFile(CurFile, buffer); replay.ChatMessages = ReplayMessageEvents.Parse(buffer); } try { const string CurFile = "replay.game.events"; var fileSize = (from f in files where f.FileName.Equals(CurFile) select f).Single().Size; var buffer = new byte[fileSize]; archive.ExportFile(CurFile, buffer); replay.PlayerEvents = ReplayGameEvents.Parse(replay, buffer); } catch (Exception) { // In the current state, the parsing commonly fails. // Incase of failing, we should probably just ignore the results of the parse // And return. } } finally { archive.Dispose(); } replay.Timestamp = File.GetCreationTime(fileName); return replay; }
/// <summary> Parses a .SC2Replay file and returns relevant replay information. </summary> /// <param name="fileName"> Full path to a .SC2Replay file. </param> /// <param name="noEvents"> True if you don't want to parse events (uses about 5~10 MB on a pro replay, half on an amateur replay) </param> /// <returns> Returns the fully parsed Replay object. </returns> public static Replay Parse(string fileName, bool noEvents = false) { if (File.Exists(fileName) == false) { throw new FileNotFoundException("The specified file does not exist.", fileName); } var replay = new Replay(); // File in the version numbers for later use. MpqHeader.ParseHeader(replay, fileName); CArchive archive; try { archive = new CArchive(fileName); } catch (IOException) { // Usually thrown if the archive name contains korean. Copy it to a local file and open. var tmpPath = Path.GetTempFileName(); File.Copy(fileName, tmpPath, true); archive = new CArchive(tmpPath); } try { var files = archive.FindFiles("replay.*"); { const string CurFile = "replay.initData"; var fileSize = (from f in files where f.FileName.Equals(CurFile) select f).Single().Size; var buffer = new byte[fileSize]; archive.ExportFile(CurFile, buffer); ReplayInitData.Parse(replay, buffer); } { // Local scope allows the byte[] to be GC sooner, and prevents misreferences const string CurFile = "replay.details"; var fileSize = (from f in files where f.FileName.Equals(CurFile) select f).Single().Size; var buffer = new byte[fileSize]; archive.ExportFile(CurFile, buffer); ReplayDetails.Parse(replay, buffer); } { const string CurFile = "replay.attributes.events"; var fileSize = (from f in files where f.FileName.Equals(CurFile) select f).Single().Size; var buffer = new byte[fileSize]; archive.ExportFile(CurFile, buffer); ReplayAttributeEvents.Parse(replay, buffer); } { const string CurFile = "replay.message.events"; var fileSize = (from f in files where f.FileName.Equals(CurFile) select f).Single().Size; var buffer = new byte[fileSize]; archive.ExportFile(CurFile, buffer); try { replay.ChatMessages = ReplayMessageEvents.Parse(buffer); } catch // Chat may have been removed without maintaining the structure // Example: LiquidHayPro vs MFTarga.SC2Replay from TLPro pack #36 // You can see a date on the file in MPQ editor, and viewing the // replay in SC2 results in no chat at all. { replay.ChatMessages = new List<ChatMessage>(); } } try { if (!noEvents) { const string CurFile = "replay.game.events"; var fileSize = (from f in files where f.FileName.Equals(CurFile) select f).Single().Size; var buffer = new byte[fileSize]; archive.ExportFile(CurFile, buffer); replay.PlayerEvents = ReplayGameEvents.Parse(replay, buffer); } } catch (Exception) { // Likely to happen with any non-standard (i.e. format isn't melee nvn, locked alliances) replay. } } finally { archive.Dispose(); } replay.Timestamp = File.GetCreationTime(fileName); return replay; }
/// <summary> Erases the entire chat log of a file. </summary> /// <param name="fileName"> The file name. </param> public static void ClearChatLog(string fileName) { var replay = new Replay(); // File in the version numbers for later use. MpqHeader.ParseHeader(replay, fileName); using (var archive = new CArchive(fileName)) { var files = archive.FindFiles("replay.*"); { const string CurFile = "replay.message.events"; var fileSize = (from f in files where f.FileName.Equals(CurFile) select f).Single().Size; var buffer = new byte[fileSize]; archive.ExportFile(CurFile, buffer); var arr = ClearChatLog(buffer); archive.ImportFile("replay.message.events", arr); } archive.Close(); } }
public AbilityEvent(BitReader bitReader, Replay replay, Player player, AbilityData abilityData, UnitData unitData) { uint flags; // 1.3.3 patch notes: // - Fixed an issue where the APM statistic could be artificially increased. // This adds the "failed" flag, which is triggered usually by holding down a // hotkey, leading to key repeat spamming the event throughout a single tick. if (replay.ReplayBuild < 18574) // < 1.3.3 { flags = bitReader.Read(17); } else if (replay.ReplayBuild < 22612) // < 1.5.0 { flags = bitReader.Read(18); } else { flags = bitReader.Read(20); } Queued = (flags & 2) != 0; RightClick = (flags & 8) != 0; WireframeClick = (flags & 0x20) != 0; ToggleAbility = (flags & 0x40) != 0; EnableAutoCast = (flags & 0x80) != 0; AbilityUsed = (flags & 0x100) != 0; WireframeUnload = (flags & 0x200) != 0; WireframeCancel = (flags & 0x400) != 0; MinimapClick = (flags & 0x10000) != 0; AbilityFailed = (flags & 0x20000) != 0; // flags & 0xf815 -> Debug for unknown flags // Never found any across all test data. DefaultAbility = (bitReader.Read(1) == 0); DefaultActor = true; if (!DefaultAbility) { AbilityType = abilityData.GetAbilityType( (int)bitReader.Read(16), (int)bitReader.Read(5)); DefaultActor = (bitReader.Read(1) == 0); if (!DefaultActor) { // I'm thinking this would be an array type... but I can't // find anything that causes this bit to be set. throw new InvalidOperationException("Unsupported: non-default actor"); } } if (DefaultActor) { // Deep copy the current wireframe as the actor list // ----- // If a user wants to deal with subgroups to get a more // concise actor list, the data is all here. We're not // going to bother, though, because there are several // exceptions to account for in determining event actors. Actors = new List <Unit>(player.Wireframe.Count); foreach (var unit in player.Wireframe) { Actors.Add(new Unit(unit)); } } var targetType = bitReader.Read(2); if (targetType == 1) // Location target { var targetX = bitReader.Read(20); var targetY = bitReader.Read(20); var targetZ = bitReader.Read(32); TargetLocation = Location.FromEventFormat(targetX, targetY, targetZ); } else if (targetType == 2) // Unit + Location target { TargetFlags = (int)bitReader.Read(8); WireframeIndex = (int)bitReader.Read(8); var unitId = (int)bitReader.Read(32); var unit = replay.GetUnitById(unitId); var unitTypeId = (int)bitReader.Read(16); if (unit == null) { var unitType = unitData.GetUnitType(unitTypeId); unit = new Unit(unitId, unitType); unit.typeId = unitTypeId; replay.GameUnits.Add(unitId, unit); } TargetUnit = unit; var targetHasPlayer = bitReader.Read(1) == 1; if (targetHasPlayer) { TargetPlayer = (int)bitReader.Read(4); } // 1.4.0 -- Don't really know what this was meant to fix if (replay.ReplayBuild >= 19595) { var targetHasTeam = bitReader.Read(1) == 1; if (targetHasTeam) { TargetTeam = (int)bitReader.Read(4); } } var targetX = bitReader.Read(20); var targetY = bitReader.Read(20); var targetZ = bitReader.Read(32); TargetLocation = Location.FromEventFormat(targetX, targetY, targetZ); } else if (targetType == 3) // Unit target { var id = bitReader.Read(32); // Again, if the user wants to determine exactly which // queue item is canceled in the case of a queue cancel // event (the most common case of this target specifier's // occurence), they can; however, it requires an additional // data structure that I don't want to bother with; however, // all the underlying data is available in the events list. TargetId = id; } var lastBit = bitReader.Read(1); // Should be 0; if not, misalignment is likely if (!AbilityFailed) { if (RightClick) { this.EventType = GameEventType.RightClick; } else { this.EventType = EventData.GetInstance().GetEventType(this.AbilityType); } } else { this.EventType = GameEventType.Inactive; } }
/// <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... Encoding 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 this.Attributes) { switch (attribute.AttributeId) { case PlayerTypeAttribute: // 500 { string type = encoding.GetString(attribute.Value.Reverse().ToArray()); if (type.ToLower().Equals("comp")) { replay.ClientList[attribute.PlayerId].PlayerType = PlayerType.Computer; } else if (type.ToLower().Equals("humn")) { replay.ClientList[attribute.PlayerId].PlayerType = PlayerType.Human; } else { throw new Exception("Unexpected value"); } break; } case TeamSizeAttribute: { // This fixes issues with reversing the string before encoding. Without this, you get "\01v1" var teamSizeChar = encoding.GetString(attribute.Value, 0, 3).Reverse().ToArray(); replay.TeamSize = new string(teamSizeChar); break; } case DifficultyLevelAttribute: { string diffLevel = encoding.GetString(attribute.Value.Reverse().ToArray()); diffLevel = diffLevel.ToLower(); var player = replay.ClientList[attribute.PlayerId]; 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 GameSpeedAttribute: { string speed = encoding.GetString(attribute.Value.Reverse().ToArray()); speed = speed.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 PlayerRaceAttribute: { var race = encoding.GetString(attribute.Value.Reverse().ToArray()).ToLower(); var player = replay.ClientList[attribute.PlayerId]; switch (race) { case "prot": player.SelectedRace = Race.Protoss; break; case "zerg": player.SelectedRace = Race.Zerg; break; case "terr": player.SelectedRace = Race.Terran; break; case "rand": player.SelectedRace = Race.Random; break; } break; } case PlayerTeam1v1Attribute: { attributes1.Add(attribute); break; } case PlayerTeam2v2Attribute: { attributes2.Add(attribute); break; } case PlayerTeam3v3Attribute: { attributes3.Add(attribute); break; } case PlayerTeam4v4Attribute: { attributes4.Add(attribute); break; } case PlayerTeamFFAAttribute: { attributesffa.Add(attribute); break; } case GameTypeAttribute: { string gameTypeStr = encoding.GetString(attribute.Value.Reverse().ToArray()); gameTypeStr = gameTypeStr.ToLower().Trim('\0'); switch (gameTypeStr) { case "priv": replay.GameType = GameType.Private; break; case "amm": replay.GameType = GameType.Open; 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 var team = encoding.GetString(att.Value.Reverse().ToArray()).Trim('\0', 'T'); replay.ClientList[att.PlayerId].Team = int.Parse(team); } } // Skipping parsing the handicap, colors, and handicap since this is parsed elsewhere. }
public static List<IGameEvent> Parse(Replay replay, byte[] buffer) { // The GameEvent file changes significantly after 16561. // This is sometime around the first patch after release. Since // parsing replays this old should be extremely rare, I don't believe // its worth the effort to try to support both. If it is, it should be // done in another method. if (replay.ReplayBuild < 16561) { throw new NotSupportedException( "Replay builds under 16561 are not supported for parsing GameEvent log."); } var events = new List<IGameEvent>(); using (var stream = new MemoryStream(buffer)) { using (var reader = new BinaryReader(stream)) { var currentTime = 0; var numEvents = 0; while (reader.BaseStream.Position < reader.BaseStream.Length) { bool knownEvent = true; var timestamp = Timestamp.Parse(reader).Value; var nextByte = reader.ReadByte(); var eventType = nextByte >> 5; // 3 lowest bits var globalEventFlag = nextByte & 16; // 4th bit var playerId = nextByte & 15; // bits 5-8 Player player = null; if (playerId > 0) { player = replay.GetPlayerById(playerId); } var eventCode = reader.ReadByte(); currentTime += timestamp; var time = Timestamp.Create(currentTime); numEvents++; switch (eventType) { case 0x00: // initialization switch (eventCode) { case 0x0B: // Player enters game case 0x0C: // for build >= 17326 case 0x17: case 0x1B: case 0x2B: case 0x2C: break; case 0x05: // game starts break; default: knownEvent = false; break; } break; case 0x01: // Action switch (eventCode) { case 0x09: // player quits the game events.Add(new PlayerLeftEvent(player, time)); break; case 0x1B: case 0x2B: case 0x3B: case 0x4B: case 0x5B: case 0x6B: case 0x7B: case 0x8B: case 0x9B: case 0x0B: // player uses an ability int ability = -1; byte firstByte = reader.ReadByte(); byte temp = reader.ReadByte(); if (replay.ReplayBuild >= 18317) { byte lastTemp; ability = reader.Read() << 16 | reader.ReadByte() << 8 | (lastTemp = reader.ReadByte()); // 18574 should be the correct build? not sure if ((firstByte & 0x0c) == 0x0c && (firstByte & 1) == 0) { reader.ReadBytes(4); } else if (temp == 64 || temp == 66) { if (lastTemp > 14) { if ((lastTemp & 0x40) != 0) { reader.ReadBytes(2); reader.ReadBytes(4); reader.ReadBytes(2); } else { reader.ReadBytes(6); } } } else if (temp == 8 || temp == 10) { reader.ReadBytes(7); } else if (temp == 136 || temp == 138) { reader.ReadBytes(15); } /* { if ((temp & 0x80) == 0x80) { reader.ReadBytes(8); } reader.ReadBytes(10); ability = 0; } else { byte lastTemp; ability = reader.Read() << 16 | reader.ReadByte() << 8 | (lastTemp = reader.ReadByte()); if ((temp & 0x60) == 0x60) { reader.ReadBytes(4); } else { var flaga = ability & 0xF0; // some kind of flag if ((flaga & 0x20) == 0x20) { reader.ReadBytes(9); if ((firstByte & 8) == 8) { reader.ReadBytes(9); } } else if ((flaga & 0x10) == 0x10) { reader.ReadBytes(9); } else if ((flaga & 0x40) == 0x40) { reader.ReadBytes(18); } } } } */ if (ability != -1) { events.Add( new GameEventBase( replay.GetPlayerById(playerId), Timestamp.Create(currentTime))); } } if (ability == -1) { ability = (reader.ReadByte() << 16) | (reader.ReadByte() << 8) | (reader.ReadByte() & 0x3F); if (temp == 0x20 || temp == 0x22) { var nByte = ability & 0xFF; if (nByte > 0x07) { if (firstByte == 0x29 || firstByte == 0x19) { reader.ReadBytes(4); // Advance 4 bytes. break; } reader.ReadBytes(9); if ((nByte & 0x20) > 0) { reader.ReadBytes(9); } } } else if (temp == 0x48 || temp == 0x4A) { reader.ReadBytes(7); } else if (temp == 0x88 || temp == 0x8A) { reader.ReadBytes(15); } if ((temp & 0x20) != 0) { // TODO: Record player ability. // This is wrong, right? events.Add(new GameEventBase(replay.GetPlayerById(playerId), Timestamp.Create(currentTime))); } events.Add(new GameEventBase(replay.GetPlayerById(playerId), Timestamp.Create(currentTime))); } break; case 0x0C: // automatic update of hotkey? case 0x1C: case 0x2C: case 0x3C: // 01 01 01 01 11 01 03 02 02 38 00 01 02 3c 00 01 00 case 0x4C: // 01 02 02 01 0d 00 02 01 01 a8 00 00 01 case 0x5C: // 01 01 01 01 16 03 01 01 03 18 00 01 00 case 0x6C: // 01 04 08 01 03 00 02 01 01 34 c0 00 01 case 0x7C: // 01 05 10 01 01 10 02 01 01 1a a0 00 01 case 0x8C: case 0x9C: case 0xAC: // player changes selection if (replay.ReplayBuild >= 16561) { int bitmask = 0; byte nByte = 0; reader.ReadByte(); // skip flag byte var deselectFlags = reader.ReadByte(); if ((deselectFlags & 3) == 1) { nByte = reader.ReadByte(); var deselectionBits = (deselectFlags & 0xFC) | (nByte & 3); while (deselectionBits > 6) { nByte = reader.ReadByte(); deselectionBits -= 8; } deselectionBits += 2; deselectionBits = deselectionBits % 8; bitmask = (int)Math.Pow(2, deselectionBits) - 1; } else if ((deselectFlags & 3) == 2 || (deselectFlags & 3) == 3) { nByte = reader.ReadByte(); var deselectionBytes = (deselectFlags & 0xFC) | (nByte & 3); while (deselectionBytes > 0) { nByte = reader.ReadByte(); deselectionBytes--; } bitmask = 3; } else if ((deselectFlags & 3) == 0) { bitmask = 3; nByte = deselectFlags; } int numUnitTypeIDs = 0; var prevByte = nByte; nByte = reader.ReadByte(); if (bitmask > 0) { numUnitTypeIDs = (prevByte & (0xFF - bitmask)) | (nByte & bitmask); } else { numUnitTypeIDs = nByte; } for (int i = 0; i < numUnitTypeIDs; i++) { int unitTypeID = 0; int unitTypeCount = 0; for (int j = 0; j < 3; j++) { byte by = 0; prevByte = nByte; nByte = reader.ReadByte(); if (bitmask > 0) // Line 610 of sc2replay.php { by = (byte)((prevByte & (0xFF - bitmask)) | (nByte & bitmask)); } else { by = nByte; } unitTypeID = by << ((2 - j) * 8) | unitTypeID; } prevByte = nByte; nByte = reader.ReadByte(); if (bitmask > 0) { unitTypeCount = (prevByte & (0xFF - bitmask)) | (nByte & bitmask); } else { unitTypeCount = nByte; } // $uType[$i + 1]['count'] = $unitTypeCount; // $uType[$i + 1]['id'] = $unitTypeID; } var numUnits = 0; prevByte = nByte; nByte = reader.ReadByte(); if (bitmask > 0) { numUnits = (prevByte & (0xFF - bitmask)) | (nByte & bitmask); } else { numUnits = nByte; } for (int i = 0; i < numUnits; i++) { var unitID = 0; byte by = 0; for (int j = 0; j < 4; j++) { prevByte = nByte; nByte = reader.ReadByte(); if (bitmask > 0) { by = (byte)((prevByte & (0xFF - bitmask)) | (nextByte & bitmask)); } else { by = nByte; } if (j < 2) { unitID = (by << ((1 - j) * 8)) | unitID; } } // TODO: Record unitID // unitIDs[] = unitID; } var a = 0; //foreach($uType as $unitType){ //for($i = 1; $i <= $unitType['count']; $i++){ // $uid = $unitIDs[$a]; // //Bytes 3 + 4 contain flag info (perhaps same as in 1.00) // $this->addSelectedUnit($uid, $unitType['id'], $playerId, floor($time / 16)); // if ($this->debug) { // $this->debug(sprintf(" 0x%06X -> 0x%04X", $unitType['id'], $uid)); // } // $a++; //} if (eventCode == 0xAC) { events.Add(new GameEventBase(replay.GetPlayerById(playerId), Timestamp.Create(currentTime))); // $this->addPlayerAction($playerId, floor($time / 16)); } break; } // sc2replay.php: Line 666 throw new NotSupportedException("Event logic for builds < 16561 not implemented"); case 0x0D: // manually uses hotkey case 0x1D: case 0x2D: case 0x3D: case 0x4D: case 0x5D: case 0x6D: case 0x7D: case 0x8D: case 0x9D: // sc2replay.php: Line 802 HotkeyEvent hotkey = new HotkeyEvent(player, time); hotkey.Key = eventCode >> 4; var byte1 = reader.ReadByte(); int flag = byte1 & 0x03; hotkey.Action = flag; if (flag == 2) { hotkey.EventType = GameEventType.Selection; } byte byte2 = 0; if ((byte1 < 16) && ((byte1 & 0x8) == 8)) { byte b2 = (byte)(reader.ReadByte() & 0xF); reader.ReadBytes(b2); } else if (byte1 > 4) { int j; if (byte1 < 8) { j = reader.ReadByte(); if ((j & 0x7) > 4) { reader.ReadByte(); } if ((j & 0x8) != 0) { reader.ReadByte(); } } else { j = reader.ReadByte(); int shift = (byte1 >> 3) + ((j & 0xF) > 4 ? 1 : 0) + ((j & 0xF) > 12 ? 1 : 0); reader.ReadBytes(shift); if (replay.ReplayBuild >= 18574) { if (byte1 == 30 && j == 1) { reader.ReadBytes(14); } } } } events.Add(hotkey); break; case 0x1F: // send resources case 0x2F: case 0x3F: case 0x4F: case 0x5F: case 0x6F: case 0x7F: case 0x8F: reader.ReadByte(); // 0x84 var sender = playerId; var receiver = (eventCode & 0xF0) >> 4; // sent minerals var bytes = reader.ReadBytes(4); var mineralValue = (((bytes[0] << 20) | (bytes[1] << 12) | bytes[2] << 4 ) >> 1) + (bytes[3] & 0x0F); // sent gas bytes = reader.ReadBytes(4); var gasValue = (((bytes[0] << 20) | (bytes[1] << 12) | bytes[2] << 4 ) >> 1) + (bytes[3] & 0x0F); // last 8 bytes are unknown reader.ReadBytes(8); break; default: knownEvent = false; break; } break; case 0x02: // weird switch (eventCode) { case 0x06: reader.ReadBytes(8); break; case 0x07: reader.ReadBytes(4); break; case 0x49: // Unknown... break; default: knownEvent = false; break; } break; case 0x03: // replay switch (eventCode) { case 0x87: reader.ReadBytes(8); break; case 0x08: reader.ReadBytes(10); break; case 0x18: reader.ReadBytes(162); break; case 0x01: // camera movement case 0x11: case 0x21: case 0x31: case 0x41: case 0x51: case 0x61: case 0x71: case 0x81: case 0x91: case 0xA1: case 0xB1: case 0xC1: case 0xD1: case 0xE1: case 0xF1: reader.ReadBytes(3); var nByte = reader.ReadByte(); var aByte = nByte & 0x70; switch (aByte) { case 0x10: // zoom camera up or down case 0x20: case 0x30: // only 0x10 matters, but due to 0x70 mask in comparison, check for this too case 0x40: // rotate camera case 0x50: if (aByte == 0x10 || aByte == 0x30 || aByte == 0x50) { reader.ReadByte(); nByte = reader.ReadByte(); } if (aByte != 0x40) { if ((nByte & 0x20) > 0) { // zooming, if comparison is 0 max/min zoom reached reader.ReadByte(); nByte = reader.ReadByte(); } if ((nByte & 0x40) == 0) break; } reader.ReadBytes(2); //events.Add(new PlayerEvent(playerId, currentTime)); break; } break; default: knownEvent = false; break; } break; case 0x04: // inaction if ((eventCode & 0x0F) == 2) { reader.ReadBytes(2); break; } if ((eventCode & 0x0C) == 2) { break; } if ((eventCode & 0x0F) == 12) { break; } switch(eventCode) { case 0x16: reader.ReadBytes(24); break; case 0xC6: reader.ReadBytes(16); break; case 0x18: reader.ReadBytes(4); break; case 0x87: reader.ReadBytes(4); break; default: knownEvent = false; break; } break; case 0x05: // system switch (eventCode) { case 0x89: // automatic sync? reader.ReadBytes(4); break; default: knownEvent = false; break; } break; default: knownEvent = false; break; } if (knownEvent == false) { Debug.WriteLine("Unknown Event: " + eventCode + "," + currentTime + "," + eventType); throw new FormatException("An unknown event prevented the events file from being correctly parsed."); } } } } return events; }
public static List<IGameEvent> Parse(Replay replay, byte[] buffer) { // The GameEvent file changes significantly after 16561. // This is sometime around the first patch after release. Since // parsing replays this old should be extremely rare, I don't believe // its worth the effort to try to support both. If it is, it should be // done in another method. // // Still a bitstream, but stuff's moved around and event codes are different. if (replay.ReplayBuild < 16561) { throw new NotSupportedException( "Replay builds under 16561 are not supported for parsing GameEvent log."); } // Initialize Ability and Unit data. var effectiveBuild = BuildData.GetInstance().GetEffectiveBuild(replay.ReplayBuild); if (effectiveBuild == 0) { throw new NotSupportedException( String.Format("Replay build {0} is not supported by the current event database", replay.ReplayBuild)); } var abilityData = new AbilityData(effectiveBuild); var unitData = new UnitData(effectiveBuild); var events = new List<IGameEvent>(); // Keep a reference to know the game length. var ticksElapsed = 0; using (var stream = new MemoryStream(buffer)) { var bitReader = new BitReader(stream); var playersGone = new bool[0x10]; while (!bitReader.EndOfStream) { var intervalLength = 6 + (bitReader.Read(2) << 3); var interval = bitReader.Read(intervalLength); ticksElapsed += (int)interval; var playerIndex = (int)bitReader.Read(5); Player player; if (playerIndex < 0x10) { player = replay.GetPlayerById(playerIndex); } else { player = Player.Global; } var eventType = bitReader.Read(7); IGameEvent gameEvent; switch (eventType) { case 0x05: // Game start gameEvent = new GameStartEvent(); break; case 0x0b: case 0x0c: // Join game gameEvent = new PlayerJoinEvent(bitReader, replay, playerIndex); break; case 0x19: // Leave game gameEvent = new PlayerLeftEvent(player); playersGone[playerIndex] = true; DetectWinners(playersGone, replay); break; case 0x1b: // Ability gameEvent = new AbilityEvent(bitReader, replay, player, abilityData, unitData); break; case 0x1c: // Selection gameEvent = new SelectionEvent(bitReader, replay, player, unitData); break; case 0x1d: // Control groups gameEvent = new HotkeyEvent(bitReader, replay, player); break; case 0x1f: // Send resources gameEvent = new SendResourcesEvent(bitReader, replay); break; case 0x23: // ?? gameEvent = new GameEventBase(); gameEvent.EventType = GameEventType.Inactive; bitReader.Read(8); break; case 0x26: // ?? gameEvent = new GameEventBase(); gameEvent.EventType = GameEventType.Inactive; bitReader.Read(32); bitReader.Read(32); break; case 0x27: // Target critter - special gameEvent = new GameEventBase(); gameEvent.EventType = GameEventType.Selection; var unitId = bitReader.Read(32); break; case 0x31: // Camera gameEvent = new CameraEvent(bitReader, replay); break; case 0x37: // UI Event gameEvent = new GameEventBase(); gameEvent.EventType = GameEventType.Other; bitReader.Read(32); bitReader.Read(32); break; case 0x38: // weird sync event { gameEvent = new GameEventBase(); gameEvent.EventType = GameEventType.Other; for (var j = 0; j < 2; j++) { var length = bitReader.Read(8); for (var i = 0; i < length; i++) { bitReader.Read(32); } } break; } case 0x3c: // ??? gameEvent = new GameEventBase(); gameEvent.EventType = GameEventType.Inactive; bitReader.Read(16); break; case 0x46: // Request resources gameEvent = new RequestResourcesEvent(bitReader, replay); break; case 0x47: // ?? -- associated with send minerals gameEvent = new GameEventBase(); gameEvent.EventType = GameEventType.Inactive; bitReader.Read(32); break; case 0x48: // ?? -- sync event gameEvent = new GameEventBase(); gameEvent.EventType = GameEventType.Inactive; bitReader.Read(32); break; case 0x4C: // ?? -- seen with spectator bitReader.Read(4); gameEvent = new GameEventBase(); gameEvent.EventType = GameEventType.Inactive; break; case 0x59: // ?? -- sync flags maybe? bitReader.Read(32); gameEvent = new GameEventBase(); gameEvent.EventType = GameEventType.Inactive; break; default: // debug throw new InvalidOperationException(String.Format( "Unknown event type {0:x} at {1:x} in replay.game.events", eventType, bitReader.Cursor)); } gameEvent.Player = player; gameEvent.Time = Timestamp.Create(ticksElapsed); events.Add(gameEvent); bitReader.AlignToByte(); } } replay.GameLength = Timestamp.Create(ticksElapsed).TimeSpan; return events; }
/// <summary> Parses a .SC2Replay file and returns relevant replay information. </summary> /// <param name="fileName"> Full path to a .SC2Replay file. </param> /// <param name="noEvents"> True if you don't want to parse events (uses about 5~10 MB on a pro replay, half on an amateur replay) </param> /// <returns> Returns the fully parsed Replay object. </returns> public static Replay Parse(string fileName, bool noEvents = false) { if (File.Exists(fileName) == false) { throw new FileNotFoundException("The specified file does not exist.", fileName); } var replay = new Replay(); // File in the version numbers for later use. MpqHeader.ParseHeader(replay, fileName); CArchive archive; try { archive = new CArchive(fileName); } catch (IOException) { // Usually thrown if the archive name contains korean. Copy it to a local file and open. var tmpPath = Path.GetTempFileName(); File.Copy(fileName, tmpPath, true); archive = new CArchive(tmpPath); } try { var files = archive.FindFiles("replay.*"); { const string CurFile = "replay.initData"; var fileSize = (from f in files where f.FileName.Equals(CurFile) select f).Single().Size; var buffer = new byte[fileSize]; archive.ExportFile(CurFile, buffer); ReplayInitData.Parse(replay, buffer); } { // Local scope allows the byte[] to be GC sooner, and prevents misreferences const string CurFile = "replay.details"; var fileSize = (from f in files where f.FileName.Equals(CurFile) select f).Single().Size; var buffer = new byte[fileSize]; archive.ExportFile(CurFile, buffer); ReplayDetails.Parse(replay, buffer); } { const string CurFile = "replay.attributes.events"; var fileSize = (from f in files where f.FileName.Equals(CurFile) select f).Single().Size; var buffer = new byte[fileSize]; archive.ExportFile(CurFile, buffer); ReplayAttributeEvents.Parse(replay, buffer); } { const string CurFile = "replay.message.events"; var fileSize = (from f in files where f.FileName.Equals(CurFile) select f).Single().Size; var buffer = new byte[fileSize]; archive.ExportFile(CurFile, buffer); try { replay.ChatMessages = ReplayMessageEvents.Parse(buffer); } catch // Chat may have been removed without maintaining the structure // Example: LiquidHayPro vs MFTarga.SC2Replay from TLPro pack #36 // You can see a date on the file in MPQ editor, and viewing the // replay in SC2 results in no chat at all. { replay.ChatMessages = new List <ChatMessage>(); } } try { if (!noEvents) { const string CurFile = "replay.game.events"; var fileSize = (from f in files where f.FileName.Equals(CurFile) select f).Single().Size; var buffer = new byte[fileSize]; archive.ExportFile(CurFile, buffer); replay.PlayerEvents = ReplayGameEvents.Parse(replay, buffer); } } catch (Exception) { // Likely to happen with any non-standard (i.e. format isn't melee nvn, locked alliances) replay. } } finally { archive.Dispose(); } replay.Timestamp = File.GetCreationTime(fileName); return(replay); }
public HotkeyEvent(BitReader bitReader, Replay replay, Player player) { int wireframeLength = 8; if (replay.ReplayBuild >= 22612) { wireframeLength = 9; // Maximum selection size has been increased to 500, up from 255. } this.EventType = GameEventType.Selection; ControlGroup = (int)bitReader.Read(4); // throws ActionType = (HotkeyActionType)(int)bitReader.Read(2); var updateType = (int)bitReader.Read(2); // This is an internal update that is somewhat asynchronous to // the main wireframe. var unitsRemovedList = new List <Unit>(); if (updateType == 1) // Remove by flags { var numBits = (int)bitReader.Read(wireframeLength); var unitsRemoved = new bool[numBits]; var wireframeIndex = 0; while (numBits >= 8) { numBits -= 8; var flags = bitReader.Read(8); for (int i = 0; i < 8; i++) { unitsRemoved[wireframeIndex + i] = (flags & (1 << i)) != 0; } wireframeIndex += 8; } if (numBits != 0) { var flags = bitReader.Read(numBits); for (int i = 0; i < numBits; i++) { unitsRemoved[wireframeIndex + i] = (flags & (1 << i)) != 0; } wireframeIndex += numBits; } for (int i = 0; i < wireframeIndex; i++) { if (unitsRemoved[i]) { unitsRemovedList.Add(player.Hotkeys[ControlGroup][i]); } } } else if (updateType == 2) { var numIndices = (int)bitReader.Read(wireframeLength); for (int i = 0; i < numIndices; i++) { unitsRemovedList.Add(player.Hotkeys[ControlGroup][(int)bitReader.Read(wireframeLength)]); } } else if (updateType == 3) // Replace control group with portion of control group { // This happens fairly rarely, so I'll just invert the output unitsRemovedList = new List <Unit>(player.Hotkeys[ControlGroup]); var numIndices = (int)bitReader.Read(wireframeLength); for (int i = 0; i < numIndices; i++) { unitsRemovedList.Remove(player.Hotkeys[ControlGroup][(int)bitReader.Read(wireframeLength)]); } } if (ActionType == HotkeyActionType.AddToControlGroup) { var oldControlgroup = player.Hotkeys[ControlGroup]; List <Unit> newControlgroup; if (oldControlgroup != null) { newControlgroup = new List <Unit>(player.Wireframe.Count + oldControlgroup.Count); foreach (Unit unit in oldControlgroup) { newControlgroup.Add(unit); } } else { newControlgroup = new List <Unit>(player.Wireframe.Count); } foreach (Unit unit in player.Wireframe) { if (oldControlgroup == null || !oldControlgroup.Contains(unit)) { newControlgroup.Add(unit); } } newControlgroup.Sort((m, n) => m.Id - n.Id); player.Hotkeys[ControlGroup] = newControlgroup; } else if (ActionType == HotkeyActionType.SelectControlGroup) { player.Wireframe = new List <Unit>(player.Hotkeys[ControlGroup]); // Only see these two together because of the nature of it foreach (Unit unit in unitsRemovedList) { player.Wireframe.Remove(unit); } } else if (ActionType == HotkeyActionType.SetControlGroup) { player.Hotkeys[ControlGroup] = new List <Unit>(player.Wireframe); } // Copy ref list to property. Idk if this is a great idea, but it's likely // never more than 30 ish dwords per event? Can't be more than another meg // or three per replay. i.e. Can't be more than lolJava. ControlGroupUnits = new List <Unit>(player.Hotkeys[ControlGroup]); }
private static void DetectWinners(bool[] playersGone, Replay replay) { var teamsStillActive = new bool[0x10]; for (var i = 0; i < playersGone.Length; i++) { var player = replay.GetPlayerById(i); if (player != null && // player exists player.Team != 0 && // player is not neutral // -- Technically player team is 16 for spectators I think, but not defined here => 0 player.PlayerType != PlayerType.Spectator && // player is playing playersGone[i] == false) // player is still in-game { teamsStillActive[player.Team] = true; } } var winCandidate = 0; for (var i = 1; i < 0x10; i++) { if (teamsStillActive[i] && winCandidate == 0) { winCandidate = i; } else if (teamsStillActive[i]) // .Count(n=>n) > 0 { winCandidate = -1; } } if (winCandidate > 0) { foreach (var player in replay.ClientList) { if (player != null && player.Team == winCandidate) { player.IsWinner = true; } } } }
public HotkeyEvent(BitReader bitReader, Replay replay, Player player) { int wireframeLength = 8; if (replay.ReplayBuild >= 22612) { wireframeLength = 9; // Maximum selection size has been increased to 500, up from 255. } this.EventType = GameEventType.Selection; ControlGroup = (int)bitReader.Read(4); // throws ActionType = (HotkeyActionType)(int)bitReader.Read(2); var updateType = (int)bitReader.Read(2); // This is an internal update that is somewhat asynchronous to // the main wireframe. var unitsRemovedList = new List<Unit>(); if (updateType == 1) // Remove by flags { var numBits = (int)bitReader.Read(wireframeLength); var unitsRemoved = new bool[numBits]; var wireframeIndex = 0; while (numBits >= 8) { numBits -= 8; var flags = bitReader.Read(8); for (int i = 0; i < 8; i++) { unitsRemoved[wireframeIndex + i] = (flags & (1 << i)) != 0; } wireframeIndex += 8; } if (numBits != 0) { var flags = bitReader.Read(numBits); for (int i = 0; i < numBits; i++) { unitsRemoved[wireframeIndex + i] = (flags & (1 << i)) != 0; } wireframeIndex += numBits; } for (int i = 0; i < wireframeIndex; i++) { if (unitsRemoved[i]) { unitsRemovedList.Add(player.Hotkeys[ControlGroup][i]); } } } else if (updateType == 2) { var numIndices = (int)bitReader.Read(wireframeLength); for (int i = 0; i < numIndices; i++) { unitsRemovedList.Add(player.Hotkeys[ControlGroup][(int)bitReader.Read(wireframeLength)]); } } else if (updateType == 3) // Replace control group with portion of control group { // This happens fairly rarely, so I'll just invert the output unitsRemovedList = new List<Unit>(player.Hotkeys[ControlGroup]); var numIndices = (int)bitReader.Read(wireframeLength); for (int i = 0; i < numIndices; i++) { unitsRemovedList.Remove(player.Hotkeys[ControlGroup][(int)bitReader.Read(wireframeLength)]); } } if (ActionType == HotkeyActionType.AddToControlGroup) { var oldControlgroup = player.Hotkeys[ControlGroup]; List<Unit> newControlgroup; if (oldControlgroup != null) { newControlgroup = new List<Unit>(player.Wireframe.Count + oldControlgroup.Count); foreach (Unit unit in oldControlgroup) { newControlgroup.Add(unit); } } else { newControlgroup = new List<Unit>(player.Wireframe.Count); } foreach (Unit unit in player.Wireframe) { if (oldControlgroup == null || !oldControlgroup.Contains(unit)) { newControlgroup.Add(unit); } } newControlgroup.Sort((m, n) => m.Id - n.Id); player.Hotkeys[ControlGroup] = newControlgroup; } else if (ActionType == HotkeyActionType.SelectControlGroup) { player.Wireframe = new List<Unit>(player.Hotkeys[ControlGroup]); // Only see these two together because of the nature of it foreach (Unit unit in unitsRemovedList) { player.Wireframe.Remove(unit); } } else if (ActionType == HotkeyActionType.SetControlGroup) { player.Hotkeys[ControlGroup] = new List<Unit>(player.Wireframe); } // Copy ref list to property. Idk if this is a great idea, but it's likely // never more than 30 ish dwords per event? Can't be more than another meg // or three per replay. i.e. Can't be more than lolJava. ControlGroupUnits = new List<Unit>(player.Hotkeys[ControlGroup]); }
/// <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="stream"> The stream containing the replay.details file. </param> public static void Parse(Replay replay, Stream stream) { using (var reader = new BinaryReader(stream)) { reader.ReadBytes(6); var playerCount = reader.ReadByte() >> 1; var players = new Player[playerCount]; // Parsing Player Info for (int i = 0; i < playerCount; i++) { var parsedPlayer = PlayerDetails.Parse(reader); // The references between both of these classes are the same on purpose. // We want updates to one to propogate to the other. players[i] = parsedPlayer; replay.ClientList[i + 1] = parsedPlayer; } replay.Players = players; var mapNameLength = KeyValueStruct.Parse(reader).Value; var mapBytes = reader.ReadBytes(mapNameLength); replay.Map = Encoding.UTF8.GetString(mapBytes); var stringLength = KeyValueStruct.Parse(reader).Value; // This is typically an empty string, no need to decode. var unknownString = reader.ReadBytes(stringLength); reader.ReadBytes(3); var mapPreviewNameLength = KeyValueStruct.Parse(reader).Value; var mapPreviewNameBytes = reader.ReadBytes(mapPreviewNameLength); replay.MapPreviewName = Encoding.UTF8.GetString(mapPreviewNameBytes); reader.ReadBytes(3); var saveTime = KeyValueLongStruct.Parse(reader).Value; var saveTimeZone = KeyValueLongStruct.Parse(reader).Value; var time = DateTime.FromFileTime(saveTime); // Subtract the timezone to get the appropriate UTC time. time = time.Subtract(new TimeSpan(saveTimeZone)); // We create a new timestamp so we can properly set this as UTC time. replay.Timestamp = new DateTime(time.Ticks, DateTimeKind.Utc); // don't know what the next 14 bytes are for, so we skip them reader.ReadBytes(14); var resources = new List<ResourceInfo>(); reader.ReadBytes(2); // there are 2 bytes before each "s2ma" string var s2ma = Encoding.UTF8.GetString(reader.ReadBytes(4)); while(s2ma == "s2ma") { reader.ReadBytes(2); // 0x00, 0x00 resources.Add(new ResourceInfo { Gateway = Encoding.UTF8.GetString(reader.ReadBytes(2)), Hash = reader.ReadBytes(32), }); reader.ReadBytes(2); s2ma = Encoding.UTF8.GetString(reader.ReadBytes(4)); } var map = resources.Last(); replay.MapGateway = map.Gateway; replay.MapHash = map.Hash; reader.Close(); } }
/// <summary> /// Reads the 8 {16, 8, 8}, 8 {32} struct; the result is in AddedUnits / AddedUnitTypes. /// </summary> void HandleUnitArrays(BitReader bitReader, Replay replay, UnitData data) { int wireframeLength = 8; if (replay.ReplayBuild >= 22612) { wireframeLength = 9; // Maximum selection size has been increased to 500, up from 255. } var typesLength = (int)bitReader.Read(wireframeLength); AddedUnitTypes = new Dictionary<UnitType, int>(typesLength); // Guarantee order is maintained var subgroups = new List<KeyValuePair<UnitType, int>>(typesLength); for (int i = 0; i < typesLength; i++) { var unitTypeId = (int)bitReader.Read(16); var unitType = data.GetUnitType(unitTypeId); var unitSubtype = bitReader.Read(8); if (unitSubtype == 2) // hallucination -- cheers, Graylin { unitType = data.GetHallucination(unitType); } var unitCountType = (int)bitReader.Read(wireframeLength); if (unitType == UnitType.Unknown && AddedUnitTypes.ContainsKey(UnitType.Unknown)) { AddedUnitTypes[UnitType.Unknown] += unitCountType; } else { AddedUnitTypes.Add(unitType, unitCountType); } subgroups.Add(new KeyValuePair<UnitType, int>(unitType, unitCountType)); } var idsLength = (int)bitReader.Read(wireframeLength); AddedUnits = AddedUnits ?? new List<Unit>(idsLength); if (idsLength == 0) return; var subgroupsEnumerator = subgroups.GetEnumerator(); int currentSubgroupIndex; if (subgroupsEnumerator.MoveNext()) { currentSubgroupIndex = subgroupsEnumerator.Current.Value; } else return; for (int i = 0; i < idsLength; i++) { var unitId = (int)bitReader.Read(32); var unit = replay.GetUnitById(unitId); var unitType = subgroupsEnumerator.Current.Key; if (unit == null) { unit = new Unit(unitId, unitType); replay.GameUnits.Add(unitId, unit); } else { unit.UpdateType(unitType); } AddedUnits.Add(unit); if (--currentSubgroupIndex <= 0) { if (subgroupsEnumerator.MoveNext()) { currentSubgroupIndex = subgroupsEnumerator.Current.Value; } } } }
/// <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... Encoding 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 this.Attributes) { switch (attribute.AttributeId) { case PlayerTypeAttribute: // 500 { string type = encoding.GetString(attribute.Value.Reverse().ToArray()); if (type.ToLower().Equals("comp")) { replay.Players[attribute.PlayerId].PlayerType = PlayerType.Computer; } else if (type.ToLower().Equals("humn")) { replay.Players[attribute.PlayerId].PlayerType = PlayerType.Human; } else { throw new Exception("Unexpected value"); } break; } case TeamSizeAttribute: { // This fixes issues with reversing the string before encoding. Without this, you get "\01v1" var teamSizeChar = encoding.GetString(attribute.Value, 0, 3).Reverse().ToArray(); replay.TeamSize = new string(teamSizeChar); break; } case DifficultyLevelAttribute: { string diffLevel = encoding.GetString(attribute.Value.Reverse().ToArray()); diffLevel = diffLevel.ToLower(); var player = replay.Players[attribute.PlayerId]; 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 GameSpeedAttribute: { string speed = encoding.GetString(attribute.Value.Reverse().ToArray()); speed = speed.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 PlayerRaceAttribute: { var race = encoding.GetString(attribute.Value.Reverse().ToArray()).ToLower(); var player = replay.Players[attribute.PlayerId]; switch (race) { case "prot": player.SelectedRace = Race.Protoss; break; case "zerg": player.SelectedRace = Race.Zerg; break; case "terr": player.SelectedRace = Race.Terran; break; case "rand": player.SelectedRace = Race.Random; break; } break; } case PlayerTeam1v1Attribute: { attributes1.Add(attribute); break; } case PlayerTeam2v2Attribute: { attributes2.Add(attribute); break; } case PlayerTeam3v3Attribute: { attributes3.Add(attribute); break; } case PlayerTeam4v4Attribute: { attributes4.Add(attribute); break; } case PlayerTeamFFAAttribute: { attributesffa.Add(attribute); break; } case GameTypeAttribute: { string gameTypeStr = encoding.GetString(attribute.Value.Reverse().ToArray()); gameTypeStr = gameTypeStr.ToLower().Trim('\0'); switch (gameTypeStr) { case "priv": replay.GameType = GameType.Private; break; case "amm": replay.GameType = GameType.Open; 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 var team = encoding.GetString(att.Value.Reverse().ToArray()).Trim('\0', 'T'); replay.Players[att.PlayerId].Team = int.Parse(team); } } // Skipping parsing the handicap, colors, and handicap since this is parsed elsewhere. }
/// <summary> /// Reads the 8 {16, 8, 8}, 8 {32} struct; the result is in AddedUnits / AddedUnitTypes. /// </summary> void HandleUnitArrays(BitReader bitReader, Replay replay, UnitData data) { int wireframeLength = 8; if (replay.ReplayBuild >= 22612) { wireframeLength = 9; // Maximum selection size has been increased to 500, up from 255. } var typesLength = (int)bitReader.Read(wireframeLength); AddedUnitTypes = new Dictionary <UnitType, int>(typesLength); // Guarantee order is maintained var subgroups = new List <KeyValuePair <UnitType, int> >(typesLength); for (int i = 0; i < typesLength; i++) { var unitTypeId = (int)bitReader.Read(16); var unitType = data.GetUnitType(unitTypeId); var unitSubtype = bitReader.Read(8); if (unitSubtype == 2) // hallucination -- cheers, Graylin { unitType = data.GetHallucination(unitType); } var unitCountType = (int)bitReader.Read(wireframeLength); if (unitType == UnitType.Unknown && AddedUnitTypes.ContainsKey(UnitType.Unknown)) { AddedUnitTypes[UnitType.Unknown] += unitCountType; } else { AddedUnitTypes.Add(unitType, unitCountType); } subgroups.Add(new KeyValuePair <UnitType, int>(unitType, unitCountType)); } var idsLength = (int)bitReader.Read(wireframeLength); AddedUnits = AddedUnits ?? new List <Unit>(idsLength); if (idsLength == 0) { return; } var subgroupsEnumerator = subgroups.GetEnumerator(); int currentSubgroupIndex; if (subgroupsEnumerator.MoveNext()) { currentSubgroupIndex = subgroupsEnumerator.Current.Value; } else { return; } for (int i = 0; i < idsLength; i++) { var unitId = (int)bitReader.Read(32); var unit = replay.GetUnitById(unitId); var unitType = subgroupsEnumerator.Current.Key; if (unit == null) { unit = new Unit(unitId, unitType); replay.GameUnits.Add(unitId, unit); } else { unit.UpdateType(unitType); } AddedUnits.Add(unit); if (--currentSubgroupIndex <= 0) { if (subgroupsEnumerator.MoveNext()) { currentSubgroupIndex = subgroupsEnumerator.Current.Value; } } } }