/// <summary> Parses the replay.initdata file in a replay file. </summary> /// <param name="replay"> The replay file to apply the parsed data to. </param> /// <param name="buffer"> The buffer containing the replay.initdata file. </param> public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) { var reader = new BitReader(stream); var i = reader.ReadByte(); var playerList = new string[i]; for (int j = 0; j < i; j++) { playerList[j] = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(8)); if (reader.ReadBoolean()) { var clanTag = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(8)); // Console.WriteLine(clanTag); } if (reader.ReadBoolean()) // Clan Logo reader.ReadBlobPrecededWithLength(40); if (reader.ReadBoolean()) { var highestLeague = reader.Read(8); // Console.WriteLine(highestLeague); } if (reader.ReadBoolean()) { var combinedRaceLevels = reader.ReadInt32(); // Console.WriteLine(combinedRaceLevels); } reader.ReadInt32(); // Random seed (So far, always 0 in Heroes) if (reader.ReadBoolean()) reader.Read(8); // Race Preference if (reader.ReadBoolean()) reader.Read(8); // Team Preference reader.ReadBoolean(); //test map reader.ReadBoolean(); //test auto reader.ReadBoolean(); //examine reader.ReadBoolean(); //custom interface var unknown1 = reader.ReadInt32(); reader.Read(2); //observer var unknown2 = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); var unknown3 = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); var unknown4 = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); var unknown5 = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); var unknown6 = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); var unknown7 = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); // Console.WriteLine(unknown1 + unknown2 + unknown3 + unknown4 + unknown5 + unknown6 + unknown7); } // Marked as 'Random Value', so I will use as seed replay.RandomValue = (uint)reader.ReadInt32(); reader.ReadBlobPrecededWithLength(10); // Dflt reader.ReadBoolean(); // Lock Teams reader.ReadBoolean(); // Teams Together reader.ReadBoolean(); // Advanced Shared Control reader.ReadBoolean(); // Random Races reader.ReadBoolean(); // BattleNet reader.ReadBoolean(); // AMM reader.ReadBoolean(); // Competitive reader.ReadBoolean(); // No Victory Or Defeat reader.ReadBoolean(); // Unknown 0 reader.ReadBoolean(); // Unknown 1 reader.ReadBoolean(); // Unknown 2 reader.Read(2); // Fog reader.Read(2); // Observers reader.Read(2); // User Difficulty reader.ReadInt32(); reader.ReadInt32(); // 64 bit int: Client Debug Flags reader.Read(3); // Game Speed // Not sure what this 'Game Type' is reader.Read(3); var maxUsers = reader.Read(5); if (maxUsers != 10) // Max Players replay.GameMode = GameMode.TryMe; reader.Read(5); // Max Observers reader.Read(5); // Max Players reader.Read(4); // + 1 = Max Teams reader.Read(6); // Max Colors reader.Read(8); // + 1 = Max Races reader.Read(8); // Max Controls replay.MapSize = new Point { X = (int)reader.Read(8), Y = (int)reader.Read(8) }; if (replay.MapSize.Y == 1) replay.MapSize.Y = replay.MapSize.X; else if (replay.MapSize.X == 0) replay.MapSize.X = replay.MapSize.Y; // About 1000 bytes from here is a list of characters, character skins, character mounts, artifact selections, and other data } }
public static List<GameEvent> Parse(byte[] buffer, Player[] clientList, int replayBuild) { // Referenced from https://raw.githubusercontent.com/Blizzard/heroprotocol/master/protocol39445.py var gameEvents = new List<GameEvent>(); var ticksElapsed = 0; using (var stream = new MemoryStream(buffer)) { var bitReader = new Streams.BitReader(stream); while (!bitReader.EndOfStream) { var gameEvent = new GameEvent(); ticksElapsed += (int)bitReader.Read(6 + (bitReader.Read(2) << 3)); gameEvent.ticksElapsed = ticksElapsed; var playerIndex = (int)bitReader.Read(5); if (playerIndex == 16) gameEvent.isGlobal = true; else gameEvent.player = clientList[playerIndex]; gameEvent.eventType = (GameEventType)bitReader.Read(7); switch (gameEvent.eventType) { case GameEventType.CStartGameEvent: break; case GameEventType.CUserFinishedLoadingSyncEvent: break; case GameEventType.CUserOptionsEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_gameFullyDownloaded new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_developmentCheatsEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_testCheatsEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_multiplayerCheatsEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_syncChecksummingEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_isMapToMapTransition new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_debugPauseEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_useGalaxyAsserts new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_platformMac new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_cameraFollow new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_baseBuildNum new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_buildNum new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_versionFlags new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(9) } /* m_hotkeyProfile */ } }; break; case GameEventType.CBankFileEvent: gameEvent.data = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) }; break; case GameEventType.CBankSectionEvent: gameEvent.data = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(6) }; break; case GameEventType.CBankKeyEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(6) }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) } } }; break; case GameEventType.CBankSignatureEvent: gameEvent.data = new TrackerEventStructure { DataType = 2, array = new TrackerEventStructure[bitReader.Read(5)] }; for (var i = 0; i < gameEvent.data.array.Length; i++) gameEvent.data.array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(8) }; gameEvent.data.blob = bitReader.ReadBlobPrecededWithLength(7); break; case GameEventType.CCameraSaveEvent: bitReader.Read(3); // m_which bitReader.Read(16); // x bitReader.Read(16); // y break; case GameEventType.CCommandManagerResetEvent: bitReader.Read(32); // m_sequence break; case GameEventType.CGameCheatEvent: // m_target gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[4] }; switch (bitReader.Read(2)) { case 0: // None break; case 1: // TargetPoint gameEvent.data.array[0] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } }; break; case 2: // TargetUnit gameEvent.data.array[0] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_targetUnitFlags new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, // m_timer new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_tag new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_snapshotUnitLink new TrackerEventStructure(), new TrackerEventStructure(), new TrackerEventStructure(), } }; if (bitReader.ReadBoolean()) // m_snapshotControlPlayerId gameEvent.data.array[0].array[4].unsignedInt = bitReader.Read(4); if (bitReader.ReadBoolean()) // m_snapshotUpkeepPlayerId gameEvent.data.array[0].array[5].unsignedInt = bitReader.Read(4); // m_snapshotPoint gameEvent.data.array[0].array[6].array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } }; break; } bitReader.Read(32); // m_time Encoding.ASCII.GetString(bitReader.ReadBlobPrecededWithLength(10)); // m_verb Encoding.ASCII.GetString(bitReader.ReadBlobPrecededWithLength(10)); // m_arguments break; case GameEventType.CCmdEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[5] }; // m_cmdFlags if (replayBuild < 33684) gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[22] }; else if (replayBuild < 37117) gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[23] }; else if (replayBuild < 38236) gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[24] }; else gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[25] }; for (var i = 0; i < gameEvent.data.array[0].array.Length; i++) gameEvent.data.array[0].array[i] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(1) }; // m_abil if (bitReader.ReadBoolean()) { gameEvent.data.array[1] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_abilLink new TrackerEventStructure { unsignedInt = bitReader.Read(5) }, // m_abilCmdIndex new TrackerEventStructure() } }; if (bitReader.ReadBoolean()) // m_abilCmdData gameEvent.data.array[1].array[2].unsignedInt = bitReader.Read(8); } // m_data switch (bitReader.Read(2)) { case 0: // None break; case 1: // TargetPoint gameEvent.data.array[2] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } }; break; case 2: // TargetUnit gameEvent.data.array[2] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_targetUnitFlags new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, // m_timer new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_tag new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_snapshotUnitLink new TrackerEventStructure(), new TrackerEventStructure(), new TrackerEventStructure(), } }; if (bitReader.ReadBoolean()) // m_snapshotControlPlayerId gameEvent.data.array[2].array[4].unsignedInt = bitReader.Read(4); if (bitReader.ReadBoolean()) // m_snapshotUpkeepPlayerId gameEvent.data.array[2].array[5].unsignedInt = bitReader.Read(4); // m_snapshotPoint gameEvent.data.array[2].array[6].array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } }; break; case 3: // Data gameEvent.data.array[2] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; } if (replayBuild >= 33684) bitReader.Read(32); // m_sequence if (bitReader.ReadBoolean()) gameEvent.data.array[3] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // m_otherUnit if (bitReader.ReadBoolean()) gameEvent.data.array[4] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // m_unitGroup break; case GameEventType.CSelectionDeltaEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(4) }, // m_controlGroupId // m_delta new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(9) }, // m_subgroupIndex new TrackerEventStructure(), new TrackerEventStructure(), new TrackerEventStructure(), new TrackerEventStructure() } } } }; // m_removeMask switch (bitReader.Read(2)) { case 0: // None break; case 1: // Mask bitReader.Read(bitReader.Read(9)); break; case 2: // OneIndices case 3: // ZeroIndices gameEvent.data.array[1].array[1] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] }; for (var i = 0; i < gameEvent.data.array[1].array[1].array.Length; i++) gameEvent.data.array[1].array[1].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(9) }; break; } // m_addSubgroups gameEvent.data.array[1].array[2] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] }; for (var i = 0; i < gameEvent.data.array[1].array[2].array.Length; i++) gameEvent.data.array[1].array[2].array[i] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_unitLink new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, // m_subgroupPriority new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, // m_intraSubgroupPriority new TrackerEventStructure { unsignedInt = bitReader.Read(9) } } }; // m_count // m_addUnitTags gameEvent.data.array[1].array[3] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] }; for (var i = 0; i < gameEvent.data.array[1].array[3].array.Length; i++) gameEvent.data.array[1].array[3].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; case GameEventType.CControlGroupUpdateEvent: bitReader.Read(4); // m_controlGroupIndex // m_controlGroupUpdate if (replayBuild < 36359) // Not sure exactly when this change happened - roughly around here. This primarily affected 'The Lost Vikings' hero bitReader.Read(2); else bitReader.Read(3); // m_mask switch (bitReader.Read(2)) { case 0: // None break; case 1: // Mask bitReader.Read(bitReader.Read(9)); break; case 2: // OneIndices case 3: // ZeroIndices gameEvent.data.array[1].array[1] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] }; for (var i = 0; i < gameEvent.data.array[1].array[1].array.Length; i++) gameEvent.data.array[1].array[1].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(9) }; break; } break; case GameEventType.CSelectionSyncCheckEvent: bitReader.Read(4); // m_controlGroupId // m_selectionSyncData bitReader.Read(9); // m_count bitReader.Read(9); // m_subgroupCount bitReader.Read(9); // m_activeSubgroupIndex bitReader.Read(32); // m_unitTagsChecksum bitReader.Read(32); // m_subgroupIndicesChecksum bitReader.Read(32); // m_subgroupsChecksum break; case GameEventType.CResourceTradeEvent: bitReader.Read(4); // m_recipientId bitReader.Read(32); // m_resources, should be offset -2147483648 bitReader.Read(32); // m_resources, should be offset -2147483648 bitReader.Read(32); // m_resources, should be offset -2147483648 break; case GameEventType.CTriggerChatMessageEvent: gameEvent.data = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(10) }; break; case GameEventType.CTriggerPingEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } }; break; case GameEventType.CUnitClickEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // m_unitTag break; case GameEventType.CTriggerSkippedEvent: break; case GameEventType.CTriggerSoundLengthQueryEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } }; break; case GameEventType.CTriggerSoundOffsetEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; case GameEventType.CTriggerTransmissionOffsetEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } }; break; case GameEventType.CTriggerTransmissionCompleteEvent: gameEvent.data = new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }; break; case GameEventType.CCameraUpdateEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[6] }; if (bitReader.ReadBoolean()) // m_target, x/y gameEvent.data.array[0] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, new TrackerEventStructure { unsignedInt = bitReader.Read(16) } } }; if (bitReader.ReadBoolean()) // m_distance gameEvent.data.array[1] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) }; if (bitReader.ReadBoolean()) // m_pitch gameEvent.data.array[2] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) }; if (bitReader.ReadBoolean()) // m_yaw gameEvent.data.array[3] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) }; if (bitReader.ReadBoolean()) // m_reason gameEvent.data.array[4] = new TrackerEventStructure { vInt = bitReader.Read(8) - 128 }; // m_follow gameEvent.data.array[5] = new TrackerEventStructure { unsignedInt = bitReader.Read(1) }; break; case GameEventType.CTriggerPlanetMissionLaunchedEvent: bitReader.Read(32); // m_difficultyLevel, offset -2147483648 break; case GameEventType.CTriggerDialogControlEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) /* Actually signed - not handled correctly */ }, new TrackerEventStructure { vInt = bitReader.Read(32) /* Actually signed - not handled correctly */ }, new TrackerEventStructure() } }; switch (bitReader.Read(3)) { case 0: // None break; case 1: // Checked gameEvent.data.array[2].unsignedInt = bitReader.Read(1); break; case 2: // ValueChanged gameEvent.data.array[2].unsignedInt = bitReader.Read(32); break; case 3: // SelectionChanged gameEvent.data.array[2].vInt = bitReader.Read(32); /* Actually signed - not handled correctly */ break; case 4: // TextChanged gameEvent.data.array[2].DataType = 2; gameEvent.data.array[2].blob = bitReader.ReadBlobPrecededWithLength(11); break; case 5: // MouseButton gameEvent.data.array[2].unsignedInt = bitReader.Read(32); break; } break; case GameEventType.CTriggerSoundLengthSyncEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[2] }; gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(7)] }; for (var i = 0; i < gameEvent.data.array[0].array.Length; i++) gameEvent.data.array[0].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; gameEvent.data.array[1] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(7)] }; for (var i = 0; i < gameEvent.data.array[1].array.Length; i++) gameEvent.data.array[1].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; case GameEventType.CTriggerConversationSkippedEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(1) }; break; case GameEventType.CTriggerMouseClickedEvent: bitReader.Read(32); // m_button bitReader.ReadBoolean(); // m_down bitReader.Read(11); // m_posUI X bitReader.Read(11); // m_posUI Y bitReader.Read(20); // m_posWorld X bitReader.Read(20); // m_posWorld Y bitReader.Read(32); // m_posWorld Z (Offset -2147483648) bitReader.Read(8); // m_flags (-128) break; case GameEventType.CTriggerMouseMovedEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(11) }, new TrackerEventStructure { unsignedInt = bitReader.Read(11) }, new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } }, new TrackerEventStructure { vInt = bitReader.Read(8) - 128 } } }; break; case GameEventType.CTriggerHotkeyPressedEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // May be missing an offset value break; case GameEventType.CTriggerTargetModeUpdateEvent: bitReader.Read(16); // m_abilLink bitReader.Read(5); // m_abilCmdIndex bitReader.Read(8); // m_state (-128) break; case GameEventType.CTriggerSoundtrackDoneEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; case GameEventType.CTriggerKeyPressedEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(8) - 128 }, new TrackerEventStructure { vInt = bitReader.Read(8) - 128 } } }; break; case GameEventType.CTriggerCutsceneBookmarkFiredEvent: // m_cutsceneId, m_bookmarkName gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) } } }; break; case GameEventType.CTriggerCutsceneEndSceneFiredEvent: // m_cutsceneId gameEvent.data = new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }; break; case GameEventType.CGameUserLeaveEvent: break; case GameEventType.CGameUserJoinEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[5] }; gameEvent.data.array[0] = new TrackerEventStructure { unsignedInt = bitReader.Read(2) }; gameEvent.data.array[1] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(8) }; if (bitReader.ReadBoolean()) gameEvent.data.array[2] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) }; if (bitReader.ReadBoolean()) gameEvent.data.array[3] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(8) }; if (bitReader.ReadBoolean()) gameEvent.data.array[4] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBytes(40) }; break; case GameEventType.CCommandManagerStateEvent: gameEvent.data = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(2) }; // m_state if (replayBuild >= 33684) if (bitReader.ReadBoolean()) // m_sequence gameEvent.data.array = new[] { new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(8) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(8) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(16) } }; break; case GameEventType.CCmdUpdateTargetPointEvent: if (replayBuild >= 40336 && bitReader.ReadBoolean()) bitReader.Read(32); gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(32) - 2147483648 } } }; break; case GameEventType.CCmdUpdateTargetUnitEvent: if (replayBuild >= 40336 && bitReader.ReadBoolean()) bitReader.Read(32); gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[7] }; gameEvent.data.array[0] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(16) }; // m_targetUnitFlags gameEvent.data.array[1] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(8) }; // m_timer gameEvent.data.array[2] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(32) }; // m_tag gameEvent.data.array[3] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(16) }; // m_snapshotUnitLink if (bitReader.ReadBoolean()) gameEvent.data.array[4] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(4) }; // m_snapshotControlPlayerId if (bitReader.ReadBoolean()) gameEvent.data.array[5] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(4) }; // m_snapshotUpkeepPlayerId gameEvent.data.array[6] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(32) - 2147483648 } } }; // m_snapshotPoint (x, y, z) break; case GameEventType.CHeroTalentSelectedEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // m_index break; case GameEventType.CHeroTalentTreeSelectionPanelToggled: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(1) }; // m_shown break; default: throw new NotImplementedException(); } bitReader.AlignToByte(); gameEvents.Add(gameEvent); } } return gameEvents; // Uncomment this to write out all replay.game.events to individual text files in the 'C:\HOTSLogs\' folder /* var eventGroups = replay.GameEvents.GroupBy(i => i.eventType).Select(i => new { EventType = i.Key, EventCount = i.Count(), Events = i.OrderBy(j => j.TimeSpan) }); string eventGroupData = ""; foreach (var eventGroup in eventGroups) { foreach (var eventData in eventGroup.Events) eventGroupData += eventData.TimeSpan + ": " + eventData.player + ": " + eventData + "\r\n"; File.WriteAllText(@"C:\HOTSLogs\" + (int)eventGroup.EventType + " " + eventGroup.EventType + @".txt", eventGroupData); eventGroupData = ""; } */ }
public static void Parse(Replay replay, byte[] buffer) { // Referenced from https://raw.githubusercontent.com/Blizzard/heroprotocol/master/protocol39445.py var gameEvents = new List <GameEvent>(); var ticksElapsed = 0; using (var stream = new MemoryStream(buffer)) { var bitReader = new Streams.BitReader(stream); while (!bitReader.EndOfStream) { var gameEvent = new GameEvent(); ticksElapsed += (int)bitReader.Read(6 + (bitReader.Read(2) << 3)); gameEvent.ticksElapsed = ticksElapsed; var playerIndex = (int)bitReader.Read(5); if (playerIndex == 16) { gameEvent.isGlobal = true; } else { gameEvent.player = replay.ClientList[playerIndex]; } gameEvent.eventType = (GameEventType)bitReader.Read(7); switch (gameEvent.eventType) { case GameEventType.CStartGameEvent: break; case GameEventType.CUserFinishedLoadingSyncEvent: break; case GameEventType.CUserOptionsEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_gameFullyDownloaded new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_developmentCheatsEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_testCheatsEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_multiplayerCheatsEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_syncChecksummingEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_isMapToMapTransition new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_debugPauseEnabled new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_useGalaxyAsserts new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_platformMac new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, // m_cameraFollow new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_baseBuildNum new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_buildNum new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_versionFlags new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(9) } /* m_hotkeyProfile */ } }; break; case GameEventType.CBankFileEvent: gameEvent.data = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) }; break; case GameEventType.CBankSectionEvent: gameEvent.data = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(6) }; break; case GameEventType.CBankKeyEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(6) }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) } } }; break; case GameEventType.CBankSignatureEvent: gameEvent.data = new TrackerEventStructure { DataType = 2, array = new TrackerEventStructure[bitReader.Read(5)] }; for (var i = 0; i < gameEvent.data.array.Length; i++) { gameEvent.data.array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(8) } } ; gameEvent.data.blob = bitReader.ReadBlobPrecededWithLength(7); break; case GameEventType.CCameraSaveEvent: bitReader.Read(3); // m_which bitReader.Read(16); // x bitReader.Read(16); // y break; case GameEventType.CCommandManagerResetEvent: bitReader.Read(32); // m_sequence break; case GameEventType.CCmdEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[5] }; // m_cmdFlags if (replay.ReplayBuild < 33684) { gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[22] } } ; else if (replay.ReplayBuild < 37117) { gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[23] } } ; else if (replay.ReplayBuild < 38236) { gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[24] } } ; else { gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[25] } }; for (var i = 0; i < gameEvent.data.array[0].array.Length; i++) { gameEvent.data.array[0].array[i] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(1) } } ; // m_abil if (bitReader.ReadBoolean()) { gameEvent.data.array[1] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_abilLink new TrackerEventStructure { unsignedInt = bitReader.Read(5) }, // m_abilCmdIndex new TrackerEventStructure() } }; if (bitReader.ReadBoolean()) { // m_abilCmdData gameEvent.data.array[1].array[2].unsignedInt = bitReader.Read(8); } } // m_data switch (bitReader.Read(2)) { case 0: // None break; case 1: // TargetPoint gameEvent.data.array[2] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } }; break; case 2: // TargetUnit gameEvent.data.array[2] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_targetUnitFlags new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, // m_timer new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, // m_tag new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_snapshotUnitLink new TrackerEventStructure(), new TrackerEventStructure(), new TrackerEventStructure(), } }; if (bitReader.ReadBoolean()) { // m_snapshotControlPlayerId gameEvent.data.array[2].array[4].unsignedInt = bitReader.Read(4); } if (bitReader.ReadBoolean()) { // m_snapshotUpkeepPlayerId gameEvent.data.array[2].array[5].unsignedInt = bitReader.Read(4); } // m_snapshotPoint gameEvent.data.array[2].array[6].array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } }; break; case 3: // Data gameEvent.data.array[2] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; } if (replay.ReplayBuild >= 33684) { bitReader.Read(32); // m_sequence } if (bitReader.ReadBoolean()) { gameEvent.data.array[3] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } ; // m_otherUnit if (bitReader.ReadBoolean()) { gameEvent.data.array[4] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } ; // m_unitGroup break; case GameEventType.CSelectionDeltaEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(4) }, // m_controlGroupId // m_delta new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(9) }, // m_subgroupIndex new TrackerEventStructure(), new TrackerEventStructure(), new TrackerEventStructure(), new TrackerEventStructure() } } } }; // m_removeMask switch (bitReader.Read(2)) { case 0: // None break; case 1: // Mask bitReader.Read(bitReader.Read(9)); break; case 2: // OneIndices case 3: // ZeroIndices gameEvent.data.array[1].array[1] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] }; for (var i = 0; i < gameEvent.data.array[1].array[1].array.Length; i++) { gameEvent.data.array[1].array[1].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(9) } } ; break; } // m_addSubgroups gameEvent.data.array[1].array[2] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] }; for (var i = 0; i < gameEvent.data.array[1].array[2].array.Length; i++) { gameEvent.data.array[1].array[2].array[i] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, // m_unitLink new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, // m_subgroupPriority new TrackerEventStructure { unsignedInt = bitReader.Read(8) }, // m_intraSubgroupPriority new TrackerEventStructure { unsignedInt = bitReader.Read(9) } } } } ; // m_count // m_addUnitTags gameEvent.data.array[1].array[3] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] }; for (var i = 0; i < gameEvent.data.array[1].array[3].array.Length; i++) { gameEvent.data.array[1].array[3].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } ; break; case GameEventType.CControlGroupUpdateEvent: bitReader.Read(4); // m_controlGroupIndex // m_controlGroupUpdate if (replay.ReplayBuild < 36359) // Not sure exactly when this change happened - roughly around here. This primarily affected 'The Lost Vikings' hero { bitReader.Read(2); } else { bitReader.Read(3); } // m_mask switch (bitReader.Read(2)) { case 0: // None break; case 1: // Mask bitReader.Read(bitReader.Read(9)); break; case 2: // OneIndices case 3: // ZeroIndices gameEvent.data.array[1].array[1] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(9)] }; for (var i = 0; i < gameEvent.data.array[1].array[1].array.Length; i++) { gameEvent.data.array[1].array[1].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(9) } } ; break; } break; case GameEventType.CResourceTradeEvent: bitReader.Read(4); // m_recipientId bitReader.Read(32); // m_resources, should be offset -2147483648 bitReader.Read(32); // m_resources, should be offset -2147483648 bitReader.Read(32); // m_resources, should be offset -2147483648 break; case GameEventType.CTriggerChatMessageEvent: gameEvent.data = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(10) }; break; case GameEventType.CTriggerPingEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { unsignedInt = bitReader.Read(1) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } }; break; case GameEventType.CUnitClickEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // m_unitTag break; case GameEventType.CTriggerSkippedEvent: break; case GameEventType.CTriggerSoundLengthQueryEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(32) }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } }; break; case GameEventType.CTriggerSoundOffsetEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; case GameEventType.CTriggerTransmissionOffsetEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } }; break; case GameEventType.CTriggerTransmissionCompleteEvent: gameEvent.data = new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }; break; case GameEventType.CCameraUpdateEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[6] }; if (bitReader.ReadBoolean()) { // m_target, x/y gameEvent.data.array[0] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(16) }, new TrackerEventStructure { unsignedInt = bitReader.Read(16) } } } } ; if (bitReader.ReadBoolean()) { // m_distance gameEvent.data.array[1] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) } } ; if (bitReader.ReadBoolean()) { // m_pitch gameEvent.data.array[2] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) } } ; if (bitReader.ReadBoolean()) { // m_yaw gameEvent.data.array[3] = new TrackerEventStructure { unsignedInt = bitReader.Read(16) } } ; if (bitReader.ReadBoolean()) { // m_reason gameEvent.data.array[4] = new TrackerEventStructure { vInt = bitReader.Read(8) - 128 } } ; // m_follow gameEvent.data.array[5] = new TrackerEventStructure { unsignedInt = bitReader.Read(1) }; break; case GameEventType.CTriggerPlanetMissionLaunchedEvent: bitReader.Read(32); // m_difficultyLevel, offset -2147483648 break; case GameEventType.CTriggerDialogControlEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) /* Actually signed - not handled correctly */ }, new TrackerEventStructure { vInt = bitReader.Read(32) /* Actually signed - not handled correctly */ }, new TrackerEventStructure() } }; switch (bitReader.Read(3)) { case 0: // None break; case 1: // Checked gameEvent.data.array[2].unsignedInt = bitReader.Read(1); break; case 2: // ValueChanged gameEvent.data.array[2].unsignedInt = bitReader.Read(32); break; case 3: // SelectionChanged gameEvent.data.array[2].vInt = bitReader.Read(32); /* Actually signed - not handled correctly */ break; case 4: // TextChanged gameEvent.data.array[2].DataType = 2; gameEvent.data.array[2].blob = bitReader.ReadBlobPrecededWithLength(11); break; case 5: // MouseButton gameEvent.data.array[2].unsignedInt = bitReader.Read(32); break; } break; case GameEventType.CTriggerSoundLengthSyncEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[2] }; gameEvent.data.array[0] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(7)] }; for (var i = 0; i < gameEvent.data.array[0].array.Length; i++) { gameEvent.data.array[0].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } ; gameEvent.data.array[1] = new TrackerEventStructure { array = new TrackerEventStructure[bitReader.Read(7)] }; for (var i = 0; i < gameEvent.data.array[1].array.Length; i++) { gameEvent.data.array[1].array[i] = new TrackerEventStructure { unsignedInt = bitReader.Read(32) } } ; break; case GameEventType.CTriggerConversationSkippedEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(1) }; break; case GameEventType.CTriggerMouseClickedEvent: bitReader.Read(32); // m_button bitReader.ReadBoolean(); // m_down bitReader.Read(11); // m_posUI X bitReader.Read(11); // m_posUI Y bitReader.Read(20); // m_posWorld X bitReader.Read(20); // m_posWorld Y bitReader.Read(32); // m_posWorld Z (Offset -2147483648) bitReader.Read(8); // m_flags (-128) break; case GameEventType.CTriggerMouseMovedEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(11) }, new TrackerEventStructure { unsignedInt = bitReader.Read(11) }, new TrackerEventStructure { array = new[] { new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 } } }, new TrackerEventStructure { vInt = bitReader.Read(8) - 128 } } }; break; case GameEventType.CTriggerHotkeyPressedEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // May be missing an offset value break; case GameEventType.CTriggerTargetModeUpdateEvent: bitReader.Read(16); // m_abilLink bitReader.Read(5); // m_abilCmdIndex bitReader.Read(8); // m_state (-128) break; case GameEventType.CTriggerSoundtrackDoneEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; break; case GameEventType.CTriggerKeyPressedEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(8) - 128 }, new TrackerEventStructure { vInt = bitReader.Read(8) - 128 } } }; break; case GameEventType.CTriggerCutsceneBookmarkFiredEvent: // m_cutsceneId, m_bookmarkName gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }, new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) } } }; break; case GameEventType.CTriggerCutsceneEndSceneFiredEvent: // m_cutsceneId gameEvent.data = new TrackerEventStructure { vInt = bitReader.Read(32) - 2147483648 }; break; case GameEventType.CGameUserLeaveEvent: break; case GameEventType.CGameUserJoinEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[5] }; gameEvent.data.array[0] = new TrackerEventStructure { unsignedInt = bitReader.Read(2) }; gameEvent.data.array[1] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(8) }; if (bitReader.ReadBoolean()) { gameEvent.data.array[2] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(7) } } ; if (bitReader.ReadBoolean()) { gameEvent.data.array[3] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBlobPrecededWithLength(8) } } ; if (bitReader.ReadBoolean()) { gameEvent.data.array[4] = new TrackerEventStructure { DataType = 2, blob = bitReader.ReadBytes(40) } } ; break; case GameEventType.CCommandManagerStateEvent: gameEvent.data = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(2) }; // m_state if (replay.ReplayBuild >= 33684) { if (bitReader.ReadBoolean()) { // m_sequence gameEvent.data.array = new[] { new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(8) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(8) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(16) } } } } ; break; case GameEventType.CCmdUpdateTargetPointEvent: gameEvent.data = new TrackerEventStructure { array = new[] { new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(32) - 2147483648 } } }; break; case GameEventType.CCmdUpdateTargetUnitEvent: gameEvent.data = new TrackerEventStructure { array = new TrackerEventStructure[7] }; gameEvent.data.array[0] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(16) }; // m_targetUnitFlags gameEvent.data.array[1] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(8) }; // m_timer gameEvent.data.array[2] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(32) }; // m_tag gameEvent.data.array[3] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(16) }; // m_snapshotUnitLink if (bitReader.ReadBoolean()) { gameEvent.data.array[4] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(4) } } ; // m_snapshotControlPlayerId if (bitReader.ReadBoolean()) { gameEvent.data.array[5] = new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(4) } } ; // m_snapshotUpkeepPlayerId gameEvent.data.array[6] = new TrackerEventStructure { array = new[] { new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 7, unsignedInt = bitReader.Read(20) }, new TrackerEventStructure { DataType = 9, vInt = bitReader.Read(32) - 2147483648 } } }; // m_snapshotPoint (x, y, z) break; case GameEventType.CHeroTalentSelectedEvent: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(32) }; // m_index break; case GameEventType.CHeroTalentTreeSelectionPanelToggled: gameEvent.data = new TrackerEventStructure { unsignedInt = bitReader.Read(1) }; // m_shown break; default: throw new NotImplementedException(); } bitReader.AlignToByte(); gameEvents.Add(gameEvent); } } replay.GameEvents = gameEvents; // Gather talent selections var talentGameEventsDictionary = replay.GameEvents .Where(i => i.eventType == GameEventType.CHeroTalentSelectedEvent) .GroupBy(i => i.player) .ToDictionary( i => i.Key, i => i.Select(j => new Tuple <int, TimeSpan>((int)j.data.unsignedInt.Value, j.TimeSpan)).OrderBy(j => j.Item2).ToArray()); foreach (var player in talentGameEventsDictionary.Keys) { player.Talents = talentGameEventsDictionary[player]; } // Gather Team Level Milestones (From talent choices: 1 / 4 / 7 / 10 / 13 / 16 / 20) for (var currentTeam = 0; currentTeam < replay.TeamLevelMilestones.Length; currentTeam++) { var maxTalentChoices = replay.Players.Where(i => i.Team == currentTeam).Select(i => i.Talents.Length).Max(); replay.TeamLevelMilestones[currentTeam] = new TimeSpan[maxTalentChoices]; var appropriatePlayers = replay.Players.Where(j => j.Team == currentTeam && j.Talents.Length == maxTalentChoices); for (var i = 0; i < replay.TeamLevelMilestones[currentTeam].Length; i++) { replay.TeamLevelMilestones[currentTeam][i] = appropriatePlayers.Select(j => j.Talents[i].Item2).Min(); } } // Uncomment this to write out all replay.game.events to individual text files in the 'C:\HOTSLogs\' folder /* var eventGroups = replay.GameEvents.GroupBy(i => i.eventType).Select(i => new { EventType = i.Key, EventCount = i.Count(), Events = i.OrderBy(j => j.TimeSpan) }); * string eventGroupData = ""; * foreach (var eventGroup in eventGroups) * { * foreach (var eventData in eventGroup.Events) * eventGroupData += eventData.TimeSpan + ": " + eventData.player + ": " + eventData + "\r\n"; * File.WriteAllText(@"C:\HOTSLogs\" + (int)eventGroup.EventType + " " + eventGroup.EventType + @".txt", eventGroupData); * eventGroupData = ""; * } */ } }
/// <summary> Parses the replay.initdata file in a replay file. </summary> /// <param name="replay"> The replay file to apply the parsed data to. </param> /// <param name="buffer"> The buffer containing the replay.initdata file. </param> public static void Parse(Replay replay, byte[] buffer) { using (var stream = new MemoryStream(buffer)) { var reader = new BitReader(stream); var playerListLength = reader.Read(5); for (var i = 0; i < playerListLength; i++) { var playerName = Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(8)); // Player name // Populate the client list if (playerName != "") { if (i < replay.Players.Length && replay.Players[i].Name == playerName) // 99.9% of matches have 10 players and 10 clients replay.ClientList[i] = replay.Players[i]; else // Some Custom games with Observers may have the client list in a different order than player list // Hopefully in these rare cases, nobody will be sharing the same name :) replay.ClientList[i] = replay.Players.SingleOrDefault(j => j.Name == playerName); if (replay.ClientList[i] == null) replay.ClientList[i] = new Player { Name = playerName }; } if (reader.ReadBoolean()) reader.ReadBlobPrecededWithLength(8); // clanTag if (reader.ReadBoolean()) reader.ReadBlobPrecededWithLength(40); // Clan Logo if (reader.ReadBoolean()) reader.Read(8); // highestLeague if (reader.ReadBoolean()) reader.ReadInt32(); // combinedRaceLevels reader.ReadInt32(); // Random seed (So far, always 0 in Heroes) if (reader.ReadBoolean()) reader.Read(8); // Race Preference if (reader.ReadBoolean()) reader.Read(8); // Team Preference reader.ReadBoolean(); //test map reader.ReadBoolean(); //test auto reader.ReadBoolean(); //examine reader.ReadBoolean(); //custom interface reader.ReadInt32(); // m_testType reader.Read(2); //observer Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(9)); // m_hero - Currently Empty String Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(9)); // m_skin - Currently Empty String Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(9)); // m_mount - Currently Empty String Encoding.UTF8.GetString(reader.ReadBlobPrecededWithLength(7)); // m_toonHandle - Currently Empty String } // Marked as 'Random Value', so I will use as seed replay.RandomValue = (uint)reader.ReadInt32(); reader.ReadBlobPrecededWithLength(10); // m_gameCacheName - "Dflt" reader.ReadBoolean(); // Lock Teams reader.ReadBoolean(); // Teams Together reader.ReadBoolean(); // Advanced Shared Control reader.ReadBoolean(); // Random Races reader.ReadBoolean(); // BattleNet reader.ReadBoolean(); // AMM reader.ReadBoolean(); // Competitive reader.ReadBoolean(); // m_practice reader.ReadBoolean(); // m_cooperative reader.ReadBoolean(); // m_noVictoryOrDefeat reader.ReadBoolean(); // m_heroDuplicatesAllowed reader.Read(2); // Fog reader.Read(2); // Observers reader.Read(2); // User Difficulty reader.ReadInt32(); reader.ReadInt32(); // 64 bit int: Client Debug Flags reader.Read(3); // Game Speed // Not sure what this 'Game Type' is reader.Read(3); var maxUsers = reader.Read(5); if (maxUsers != 10) // Max Players replay.GameMode = GameMode.TryMe; reader.Read(5); // Max Observers reader.Read(5); // Max Players reader.Read(4); // + 1 = Max Teams reader.Read(6); // Max Colors reader.Read(8); // + 1 = Max Races reader.Read(8); // Max Controls replay.MapSize = new Point { X = (int)reader.Read(8), Y = (int)reader.Read(8) }; if (replay.MapSize.Y == 1) replay.MapSize.Y = replay.MapSize.X; else if (replay.MapSize.X == 0) replay.MapSize.X = replay.MapSize.Y; // I haven't tested the following code on replays before build 39595 (End of 2015) if (replay.ReplayBuild < 39595) return; reader.Read(32); // m_mapFileSyncChecksum reader.ReadBlobPrecededWithLength(11); // m_mapFileName reader.ReadBlobPrecededWithLength(8); // m_mapAuthorName reader.Read(32); // m_modFileSyncChecksum // m_slotDescriptions var slotDescriptionLength = reader.Read(5); for (var i = 0; i < slotDescriptionLength; i++) { reader.ReadBitArray(reader.Read(6)); // m_allowedColors reader.ReadBitArray(reader.Read(8)); // m_allowedRaces reader.ReadBitArray(reader.Read(6)); // m_allowedDifficulty reader.ReadBitArray(reader.Read(8)); // m_allowedControls reader.ReadBitArray(reader.Read(2)); // m_allowedObserveTypes reader.ReadBitArray(reader.Read(7)); // m_allowedAIBuilds } reader.Read(6); // m_defaultDifficulty reader.Read(7); // m_defaultAIBuild // m_cacheHandles var cacheHandlesLength = reader.Read(6); for (var i = 0; i < cacheHandlesLength; i++) reader.ReadBytes(40); reader.ReadBoolean(); // m_hasExtensionMod reader.ReadBoolean(); // m_isBlizzardMap reader.ReadBoolean(); // m_isPremadeFFA reader.ReadBoolean(); // m_isCoopMode #region m_lobbyState reader.Read(3); // m_phase reader.Read(5); // m_maxUsers reader.Read(5); // m_maxObservers // m_slots var slotsLength = reader.Read(5); for (var i = 0; i < slotsLength; i++) { int? clientListIndex = null; reader.Read(8); // m_control if (reader.ReadBoolean()) clientListIndex = (int)reader.Read(4); // m_userId reader.Read(4); // m_teamId if (reader.ReadBoolean()) reader.Read(5); // m_colorPref if (reader.ReadBoolean()) reader.Read(8); // m_racePref reader.Read(6); // m_difficulty reader.Read(7); // m_aiBuild reader.Read(7); // m_handicap // m_observe var observerStatus = reader.Read(2); if (observerStatus == 2) replay.ClientList[clientListIndex.Value].PlayerType = PlayerType.Spectator; reader.Read(32); // m_logoIndex reader.ReadBlobPrecededWithLength(9); // m_hero var skinAndSkinTint = Encoding.ASCII.GetString(reader.ReadBlobPrecededWithLength(9)); // m_skin if (skinAndSkinTint == "") skinAndSkinTint = null; if (clientListIndex.HasValue && replay.ClientList[clientListIndex.Value] != null) replay.ClientList[clientListIndex.Value].SkinAndSkinTint = skinAndSkinTint; var mountAndMountTint = Encoding.ASCII.GetString(reader.ReadBlobPrecededWithLength(9)); // m_mount if (mountAndMountTint == "") mountAndMountTint = null; if (clientListIndex.HasValue && replay.ClientList[clientListIndex.Value] != null) replay.ClientList[clientListIndex.Value].MountAndMountTint = mountAndMountTint; // m_artifacts var artifactsLength = reader.Read(4); for (var j = 0; j < artifactsLength; j++) reader.ReadBlobPrecededWithLength(9); if (reader.ReadBoolean()) reader.Read(8); // m_workingSetSlotId // m_rewards var rewardsLength = reader.Read(17); for (var j = 0; j < rewardsLength; j++) reader.Read(32); reader.ReadBlobPrecededWithLength(7); // m_toonHandle // m_licenses var licensesLength = reader.Read(9); for (var j = 0; j < licensesLength; j++) reader.Read(32); if (reader.ReadBoolean()) reader.Read(4); // m_tandemLeaderUserId reader.ReadBlobPrecededWithLength(9); // m_commander - Empty string reader.Read(32); // m_commanderLevel - So far, always 0 if (reader.ReadBoolean() && clientListIndex.HasValue && replay.ClientList[clientListIndex.Value] != null) // m_hasSilencePenalty replay.ClientList[clientListIndex.Value].IsSilenced = true; } if (reader.Read(32) != replay.RandomValue) // m_randomSeed throw new Exception("Replay Random Seed Values in Replay Init Data did not match"); if (reader.ReadBoolean()) reader.Read(4); // m_hostUserId reader.ReadBoolean(); // m_isSinglePlayer reader.Read(8); // m_pickedMapTag - So far, always 0 reader.Read(32); // m_gameDuration - So far, always 0 reader.Read(6); // m_defaultDifficulty reader.Read(7); // m_defaultAIBuild #endregion } }
/// <summary> Parses the Replay.Messages.Events file. </summary> /// <param name="buffer"> Buffer containing the contents of the replay.messages.events file. </param> /// <returns> A list of messages parsed from the buffer. </returns> public static void Parse(Replay replay, byte[] buffer) { if (buffer.Length <= 1) { // Chat has been removed from this replay return; } var ticksElapsed = 0; using (var stream = new MemoryStream(buffer)) { var bitReader = new Streams.BitReader(stream); while (!bitReader.EndOfStream) { var message = new Message(); ticksElapsed += (int)bitReader.Read(6 + (bitReader.Read(2) << 3)); message.Timestamp = new TimeSpan(0, 0, (int)Math.Round(ticksElapsed / 16.0)); var playerIndex = (int)bitReader.Read(5); if (playerIndex != 16) { message.MessageSender = replay.ClientListByUserID[playerIndex]; } message.MessageEventType = (MessageEventType)bitReader.Read(4); switch (message.MessageEventType) { case MessageEventType.SChatMessage: { ChatMessage chatMessage = new ChatMessage(); chatMessage.MessageTarget = (MessageTarget)bitReader.Read(3); // m_recipient (the target) chatMessage.Message = Encoding.UTF8.GetString(bitReader.ReadBlobPrecededWithLength(11)); // m_string message.ChatMessage = chatMessage; replay.Messages.Add(message); break; } case MessageEventType.SPingMessage: { PingMessage pingMessage = new PingMessage(); pingMessage.MessageTarget = (MessageTarget)bitReader.Read(3); // m_recipient (the target) pingMessage.XCoordinate = bitReader.ReadInt32() - (-2147483648); // m_point x pingMessage.YCoordinate = bitReader.ReadInt32() - (-2147483648); // m_point y message.PingMessage = pingMessage; replay.Messages.Add(message); break; } case MessageEventType.SLoadingProgressMessage: { // can be used to keep track of how fast/slow players are loading // also includes players who are reloading the game var progress = bitReader.ReadInt32() - (-2147483648); // m_progress break; } case MessageEventType.SServerPingMessage: { break; } case MessageEventType.SReconnectNotifyMessage: { bitReader.Read(2); // m_status; is either a 1 or a 2 break; } case MessageEventType.SPlayerAnnounceMessage: { PlayerAnnounceMessage announceMessage = new PlayerAnnounceMessage(); announceMessage.AnnouncementType = (AnnouncementType)bitReader.Read(2); switch (announceMessage.AnnouncementType) { case AnnouncementType.None: { break; } case AnnouncementType.Ability: { AbilityAnnouncment ability = new AbilityAnnouncment(); ability.AbilityLink = bitReader.ReadInt16(); // m_abilLink ability.AbilityIndex = (int)bitReader.Read(5); // m_abilCmdIndex ability.ButtonLink = bitReader.ReadInt16(); // m_buttonLink announceMessage.AbilityAnnouncement = ability; break; } case AnnouncementType.Behavior: // no idea what triggers this { bitReader.ReadInt16(); // m_behaviorLink bitReader.ReadInt16(); // m_buttonLink break; } case AnnouncementType.Vitals: { VitalAnnouncment vital = new VitalAnnouncment(); vital.VitalType = (VitalType)(bitReader.ReadInt16() - (-32768)); announceMessage.VitalAnnouncement = vital; break; } default: throw new NotImplementedException(); } if (replay.ReplayBuild > 45635) { // m_announceLink bitReader.ReadInt16(); } bitReader.ReadInt32(); // m_otherUnitTag bitReader.ReadInt32(); // m_unitTag message.PlayerAnnounceMessage = announceMessage; replay.Messages.Add(message); break; } default: throw new NotImplementedException(); } bitReader.AlignToByte(); } } }