public Player() { Color = new int[0]; SkinAndSkinTint = null; MountAndMountTint = null; Talents = new Tuple<int, TimeSpan>[0]; HeroUnits = new Unit[0]; Deaths = new Tuple<TimeSpan, TimeSpan?>[0]; }
public static void ParseUnitData(Replay replay) { { // We go through these events in chronological order, and keep a list of currently 'active' units, because UnitIDs are recycled var activeUnitsByIndex = new Dictionary<int, Unit>(); var activeUnitsByUnitID = new Dictionary<int, Unit>(); var activeHeroUnits = new Dictionary<Player, Unit>(); var isCheckingForAbathurLocusts = true; var updateTargetUnitEventArray = replay.GameEvents.Where(i => i.eventType == GameEventType.CCmdUpdateTargetUnitEvent).OrderBy(i => i.TimeSpan).ToArray(); var updateTargetUnitEventArrayIndex = 0; foreach (var unitTrackerEvent in replay.TrackerEvents.Where(i => i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitBornEvent || i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitRevivedEvent || i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitDiedEvent || i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitOwnerChangeEvent || i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitPositionsEvent)) { switch (unitTrackerEvent.TrackerEventType) { case ReplayTrackerEvents.TrackerEventType.UnitBornEvent: case ReplayTrackerEvents.TrackerEventType.UnitRevivedEvent: Unit newUnit; var newUnitIndex = (int)unitTrackerEvent.Data.dictionary[0].vInt.Value; if (unitTrackerEvent.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitBornEvent) newUnit = new Unit { UnitID = GetUnitID(newUnitIndex, (int)unitTrackerEvent.Data.dictionary[1].vInt.Value), Name = unitTrackerEvent.Data.dictionary[2].blobText, Group = UnitGroupDictionary.ContainsKey(unitTrackerEvent.Data.dictionary[2].blobText) ? UnitGroupDictionary[unitTrackerEvent.Data.dictionary[2].blobText] : UnitGroup.Unknown, TimeSpanBorn = unitTrackerEvent.TimeSpan, Team = unitTrackerEvent.Data.dictionary[3].vInt.Value == 11 || unitTrackerEvent.Data.dictionary[3].vInt.Value == 12 ? (int)unitTrackerEvent.Data.dictionary[3].vInt.Value - 11 : unitTrackerEvent.Data.dictionary[3].vInt.Value > 0 && unitTrackerEvent.Data.dictionary[3].vInt.Value <= 10 ? replay.Players[unitTrackerEvent.Data.dictionary[3].vInt.Value - 1].Team : (int?)null, PlayerControlledBy = unitTrackerEvent.Data.dictionary[3].vInt.Value > 0 && unitTrackerEvent.Data.dictionary[3].vInt.Value <= 10 ? replay.Players[unitTrackerEvent.Data.dictionary[3].vInt.Value - 1] : null, PointBorn = new Point { X = (int)unitTrackerEvent.Data.dictionary[5].vInt.Value, Y = (int)unitTrackerEvent.Data.dictionary[6].vInt.Value } }; else { var deadUnit = activeUnitsByIndex[newUnitIndex]; newUnit = new Unit { UnitID = deadUnit.UnitID, Name = deadUnit.Name, Group = deadUnit.Group, TimeSpanBorn = unitTrackerEvent.TimeSpan, Team = deadUnit.Team, PlayerControlledBy = deadUnit.PlayerControlledBy, PointBorn = new Point { X = (int)unitTrackerEvent.Data.dictionary[2].vInt.Value, Y = (int)unitTrackerEvent.Data.dictionary[3].vInt.Value } }; } replay.Units.Add(newUnit); activeUnitsByIndex[newUnitIndex] = newUnit; activeUnitsByUnitID[newUnit.UnitID] = newUnit; // Add Hero units to the controlling Player if (newUnit.PlayerControlledBy != null && newUnit.Name.StartsWith("Hero")) { newUnit.PlayerControlledBy.HeroUnits.Add(newUnit); activeHeroUnits[newUnit.PlayerControlledBy] = newUnit; } // Add derived hero positions from associated unit born/acquired/died info // These are accurate positions: Picking up regen globes, spawning Locusts, etc if (newUnit.PlayerControlledBy != null) { if (UnitBornProvidesLocationForOwner.ContainsKey(newUnit.Name) || newUnit.Group == UnitGroup.HeroTalentSelection) activeHeroUnits[newUnit.PlayerControlledBy].Positions.Add(new Position { TimeSpan = newUnit.TimeSpanBorn, Point = newUnit.PointBorn }); else if (isCheckingForAbathurLocusts) { // For Abathur locusts, we need to make sure they aren't spawning from a locust nest (Level 20 talent) if (newUnit.Name == "AbathurLocustNest") isCheckingForAbathurLocusts = false; else if (newUnit.Name == "AbathurLocustNormal" || newUnit.Name == "AbathurLocustAssaultStrain" || newUnit.Name == "AbathurLocustBombardStrain") activeHeroUnits[newUnit.PlayerControlledBy].Positions.Add(new Position { TimeSpan = newUnit.TimeSpanBorn, Point = newUnit.PointBorn }); } } break; case ReplayTrackerEvents.TrackerEventType.UnitDiedEvent: var unitThatDied = activeUnitsByIndex[(int)unitTrackerEvent.Data.dictionary[0].vInt.Value]; var playerIDKilledBy = unitTrackerEvent.Data.dictionary[2].optionalData != null ? (int)unitTrackerEvent.Data.dictionary[2].optionalData.vInt.Value : (int?)null; unitThatDied.TimeSpanDied = unitTrackerEvent.TimeSpan; unitThatDied.PlayerKilledBy = playerIDKilledBy.HasValue && playerIDKilledBy.Value > 0 && playerIDKilledBy.Value <= 10 ? replay.Players[playerIDKilledBy.Value - 1] : null; unitThatDied.PointDied = new Point { X = (int)unitTrackerEvent.Data.dictionary[3].vInt.Value, Y = (int)unitTrackerEvent.Data.dictionary[4].vInt.Value }; unitThatDied.UnitKilledBy = unitTrackerEvent.Data.dictionary[5].optionalData != null ? activeUnitsByIndex[(int)unitTrackerEvent.Data.dictionary[5].optionalData.vInt.Value] : null; // Sometimes 'PlayerIDKilledBy' will be outside of the range of players (1-10) // Minions that are killed by other minions or towers will have the 'team' that killed them in this field (11 or 12) // Some other units have interesting values I don't fully understand yet. For example, 'ItemCannonball' (the coins on Blackheart's Bay) will have 0 or 15 in this field. I'm guessing this is also which team acquires them, which may be useful // Other map objectives may also have this. I'll look into this more in the future. /* if (unitDiedEvent.PlayerIDKilledBy.HasValue && unitThatDied.PlayerKilledBy == null) Console.WriteLine(""); */ break; case ReplayTrackerEvents.TrackerEventType.UnitOwnerChangeEvent: var ownerChangeEvent = new OwnerChangeEvent { TimeSpanOwnerChanged = unitTrackerEvent.TimeSpan, Team = unitTrackerEvent.Data.dictionary[2].vInt.Value == 11 || unitTrackerEvent.Data.dictionary[2].vInt.Value == 12 ? (int)unitTrackerEvent.Data.dictionary[2].vInt.Value - 11 : (int?)null, PlayerNewOwner = unitTrackerEvent.Data.dictionary[2].vInt.Value > 0 && unitTrackerEvent.Data.dictionary[2].vInt.Value <= 10 ? replay.Players[unitTrackerEvent.Data.dictionary[2].vInt.Value - 1] : null }; if (!ownerChangeEvent.Team.HasValue && ownerChangeEvent.PlayerNewOwner != null) ownerChangeEvent.Team = ownerChangeEvent.PlayerNewOwner.Team; var unitOwnerChanged = activeUnitsByIndex[(int)unitTrackerEvent.Data.dictionary[0].vInt.Value]; unitOwnerChanged.OwnerChangeEvents.Add(ownerChangeEvent); if (unitOwnerChanged.PlayerControlledBy != null && UnitOwnerChangeProvidesLocationForOwner.ContainsKey(unitOwnerChanged.Name)) activeHeroUnits[unitOwnerChanged.PlayerControlledBy].Positions.Add(new Position { TimeSpan = ownerChangeEvent.TimeSpanOwnerChanged, Point = unitOwnerChanged.PointBorn }); break; case ReplayTrackerEvents.TrackerEventType.UnitPositionsEvent: var currentUnitIndex = (int)unitTrackerEvent.Data.dictionary[0].vInt.Value; for (var i = 0; i < unitTrackerEvent.Data.dictionary[1].array.Length; i++) { currentUnitIndex += (int)unitTrackerEvent.Data.dictionary[1].array[i++].vInt.Value; activeUnitsByIndex[currentUnitIndex].Positions.Add(new Position { TimeSpan = unitTrackerEvent.TimeSpan, Point = new Point { X = (int)unitTrackerEvent.Data.dictionary[1].array[i++].vInt.Value, Y = (int)unitTrackerEvent.Data.dictionary[1].array[i].vInt.Value } }); } break; } // Use 'CCmdUpdateTargetUnitEvent' to find an accurate location of units targeted // Excellent for finding frequent, accurate locations of heroes during team fights while (updateTargetUnitEventArrayIndex < updateTargetUnitEventArray.Length && unitTrackerEvent.TimeSpan > updateTargetUnitEventArray[updateTargetUnitEventArrayIndex].TimeSpan) if (activeUnitsByUnitID.ContainsKey((int)updateTargetUnitEventArray[updateTargetUnitEventArrayIndex++].data.array[2].unsignedInt.Value)) activeUnitsByUnitID[(int)updateTargetUnitEventArray[updateTargetUnitEventArrayIndex - 1].data.array[2].unsignedInt.Value].Positions.Add(new Position { TimeSpan = updateTargetUnitEventArray[updateTargetUnitEventArrayIndex - 1].TimeSpan, Point = Point.FromEventFormat( updateTargetUnitEventArray[updateTargetUnitEventArrayIndex - 1].data.array[6].array[0].unsignedInt.Value, updateTargetUnitEventArray[updateTargetUnitEventArrayIndex - 1].data.array[6].array[1].unsignedInt.Value) }); } } // For simplicity, I set extra fields on units that are not initially controlled by a player, and only have one owner change event foreach (var unitWithOneOwnerChange in replay.Units.Where(i => i.OwnerChangeEvents.Count == 1 && i.PlayerControlledBy == null)) { var singleOwnerChangeEvent = unitWithOneOwnerChange.OwnerChangeEvents.Single(); if (singleOwnerChangeEvent.PlayerNewOwner != null) { unitWithOneOwnerChange.PlayerControlledBy = singleOwnerChangeEvent.PlayerNewOwner; unitWithOneOwnerChange.TimeSpanAcquired = singleOwnerChangeEvent.TimeSpanOwnerChanged; unitWithOneOwnerChange.OwnerChangeEvents.Clear(); } } // Estimate Hero positions from CCmdEvent and CCmdUpdateTargetPointEvent (Movement points) { // Excluding heroes with multiple units like Lost Vikings, and Abathur with 'Ultimate Evolution' clones // It's okay to not estimate Abathur's position, as he rarely moves and we also get an accurate position each time he spawns a locust var playerToActiveHeroUnitIndexDictionary = replay.Players.Where(i => i.HeroUnits.Select(j => j.Name).Distinct().Count() == 1).ToDictionary(i => i, i => 0); // This is a list of 'Player', 'TimeSpan', and 'EventPosition' for each CCmdEvent where ability data is null and a position is included var playerCCmdEventLists = replay.GameEvents.Where(i => i.eventType == GameEventType.CCmdEvent && i.data.array[1] == null && i.data.array[2] != null && i.data.array[2].array.Length == 3 && playerToActiveHeroUnitIndexDictionary.ContainsKey(i.player)).Select(i => new { i.player, Position = new Position { TimeSpan = i.TimeSpan, Point = Point.FromEventFormat(i.data.array[2].array[0].unsignedInt.Value, i.data.array[2].array[1].unsignedInt.Value), IsEstimated = true } }) .GroupBy(i => i.player) .Select(i => new { Player = i.Key, Positions = i.Select(j => j.Position) // Union the CCmdUpdateTargetPointEvents for each Player .Union(replay.GameEvents.Where(j => j.player == i.Key && j.eventType == GameEventType.CCmdUpdateTargetPointEvent) .Select(j => new Position { TimeSpan = j.TimeSpan, Point = Point.FromEventFormat(j.data.array[0].unsignedInt.Value, j.data.array[1].unsignedInt.Value), IsEstimated = true })) // Take the single latest applicable CCmdEvent or CCmdUpdateTargetPointEvent if there are more than one in a second .GroupBy(j => (int)j.TimeSpan.TotalSeconds) .Select(j => j.OrderByDescending(k => k.TimeSpan).First()) .ToArray() }); // Find the applicable events for each Hero unit while they were alive var playerAndHeroCCmdEventLists = playerCCmdEventLists.Select(i => i.Player.HeroUnits.Select(j => new { HeroUnit = j, Positions = i.Positions.Where(k => k.TimeSpan > j.TimeSpanBorn && (!j.TimeSpanDied.HasValue || k.TimeSpan < j.TimeSpanDied.Value)).OrderBy(k => k.TimeSpan).ToArray() })); const double PlayerSpeedUnitsPerSecond = 5.0; foreach (var playerCCmdEventList in playerAndHeroCCmdEventLists) foreach (var heroCCmdEventList in playerCCmdEventList) { // Estimate the hero unit travelling to each intended destination // Only save one position per second, and prefer accurate positions // Heroes can have a lot more positions, and probably won't be useful more frequently than this var heroTargetLocationArray = heroCCmdEventList.HeroUnit.Positions.Union(new[] { new Position { TimeSpan = heroCCmdEventList.HeroUnit.TimeSpanBorn, Point = heroCCmdEventList.HeroUnit.PointBorn } }).Union(heroCCmdEventList.Positions).GroupBy(i => (int)i.TimeSpan.TotalSeconds).Select(i => i.OrderBy(j => j.IsEstimated).First()).OrderBy(i => i.TimeSpan).ToArray(); var currentEstimatedPosition = heroTargetLocationArray[0]; for (var i = 0; i < heroTargetLocationArray.Length - 1; i++) if (!heroTargetLocationArray[i + 1].IsEstimated) currentEstimatedPosition = heroTargetLocationArray[i + 1]; else { var percentageOfDistanceTravelledToTargetLocation = (heroTargetLocationArray[i + 1].TimeSpan - currentEstimatedPosition.TimeSpan).TotalSeconds * PlayerSpeedUnitsPerSecond / currentEstimatedPosition.Point.DistanceTo(heroTargetLocationArray[i + 1].Point); currentEstimatedPosition = new Position { TimeSpan = heroTargetLocationArray[i + 1].TimeSpan, Point = percentageOfDistanceTravelledToTargetLocation >= 1 ? heroTargetLocationArray[i + 1].Point : new Point { X = (int)((heroTargetLocationArray[i + 1].Point.X - currentEstimatedPosition.Point.X) * percentageOfDistanceTravelledToTargetLocation + currentEstimatedPosition.Point.X), Y = (int)((heroTargetLocationArray[i + 1].Point.Y - currentEstimatedPosition.Point.Y) * percentageOfDistanceTravelledToTargetLocation + currentEstimatedPosition.Point.Y) }, IsEstimated = true }; heroCCmdEventList.HeroUnit.Positions.Add(currentEstimatedPosition); } heroCCmdEventList.HeroUnit.Positions = heroCCmdEventList.HeroUnit.Positions.OrderBy(i => i.TimeSpan).ToList(); } } // Save no more than one position event per second per unit foreach (var unit in replay.Units.Where(i => i.Positions.Count > 0)) unit.Positions = unit.Positions.GroupBy(i => (int)i.TimeSpan.TotalSeconds).Select(i => i.OrderBy(j => j.IsEstimated).First()).OrderBy(i => i.TimeSpan).ToList(); // Add 'estimated' minion positions based on their fixed pathing // Without these positions, minions can appear to travel through walls straight across the map // These estimated positions are actually quite accurate, as minions always follow a path connecting each fort/keep in their lane var numberOfStructureTiers = replay.Units.Where(i => i.Name.StartsWith("TownTownHall")).Select(i => i.Name).Distinct().Count(); var uniqueTierName = replay.Units.First(i => i.Name.StartsWith("TownTownHall")).Name; var numberOfLanes = replay.Units.Count(i => i.Name == uniqueTierName && i.Team == 0); var minionWayPoints = replay.Units.Where(i => i.Name.StartsWith("TownTownHall")).Select(j => j.PointBorn).OrderBy(j => j.X).Skip(numberOfLanes).OrderByDescending(j => j.X).Skip(numberOfLanes).OrderBy(j => j.Y); for (var team = 0; team <= 1; team++) { // Gather all minion units for this team var minionUnits = replay.Units.Where(i => i.Team == team && i.Group == UnitGroup.Minions).ToArray(); // Each wave spawns together, but not necessarily from top to bottom // We will figure out what order the lanes are spawning in, and order by top to bottom later on var unitsPerLaneTemp = new List<Unit>[numberOfLanes]; for (var i = 0; i < unitsPerLaneTemp.Length; i++) unitsPerLaneTemp[i] = new List<Unit>(); var minionLaneOrderMinions = minionUnits.Where(i => i.Name == "WizardMinion").Take(numberOfLanes).ToArray(); var minionLaneOrder = new List<Tuple<int, int>>(); for (var i = 0; i < numberOfLanes; i++) minionLaneOrder.Add(new Tuple<int, int>(i, minionLaneOrderMinions[i].PointBorn.Y)); minionLaneOrder = minionLaneOrder.OrderBy(i => i.Item2).ToList(); // Group minion units by lane var currentIndex = 0; var minionUnitsPerWave = 7; while (currentIndex < minionUnits.Length) for (var i = 0; i < unitsPerLaneTemp.Length; i++) for (var j = 0; j < minionUnitsPerWave; j++) { if (currentIndex == minionUnits.Length) break; unitsPerLaneTemp[i].Add(minionUnits[currentIndex++]); // CatapultMinions don't seem to spawn exactly with their minion wave, which is strange // For now I will leave them out of this, which means they may appear to travel through walls if (currentIndex < minionUnits.Length && minionUnits[currentIndex].Name == "CatapultMinion") currentIndex++; } // Order the lanes by top to bottom var unitsPerLane = unitsPerLaneTemp.ToArray(); for (var i = 0; i < unitsPerLane.Length; i++) unitsPerLane[i] = unitsPerLaneTemp[minionLaneOrder[i].Item1]; for (var i = 0; i < numberOfLanes; i++) { // For each lane, take the forts in that lane, and see if the minions in that lane walked beyond this var currentLaneUnitsToAdjust = unitsPerLane[i].Where(j => j.Positions.Any() || j.TimeSpanDied.HasValue); var currentLaneWaypoints = minionWayPoints.Skip(numberOfStructureTiers * i).Take(numberOfStructureTiers); if (team == 0) currentLaneWaypoints = currentLaneWaypoints.OrderBy(j => j.X); else currentLaneWaypoints = currentLaneWaypoints.OrderByDescending(j => j.X); foreach (var laneUnit in currentLaneUnitsToAdjust) { var isLaneUnitModified = false; var beginningPosition = new Position { TimeSpan = laneUnit.TimeSpanBorn, Point = laneUnit.PointBorn }; var firstLaneUnitPosition = laneUnit.Positions.Any() ? laneUnit.Positions.First() : new Position { TimeSpan = laneUnit.TimeSpanDied.Value, Point = laneUnit.PointDied }; foreach (var laneWaypoint in currentLaneWaypoints) if ((team == 0 && firstLaneUnitPosition.Point.X > laneWaypoint.X) || team == 1 && firstLaneUnitPosition.Point.X < laneWaypoint.X) { var leg1Distance = beginningPosition.Point.DistanceTo(laneWaypoint); var newPosition = new Position { TimeSpan = beginningPosition.TimeSpan + TimeSpan.FromSeconds((long)((firstLaneUnitPosition.TimeSpan - beginningPosition.TimeSpan).TotalSeconds * (leg1Distance / (leg1Distance + laneWaypoint.DistanceTo(firstLaneUnitPosition.Point))))), Point = laneWaypoint }; laneUnit.Positions.Add(newPosition); beginningPosition = newPosition; isLaneUnitModified = true; } else break; if (isLaneUnitModified) laneUnit.Positions = laneUnit.Positions.OrderBy(j => j.TimeSpan).ToList(); } } } // Remove 'duplicate' positions that don't tell us anything foreach (var unit in replay.Units.Where(i => i.Positions.Count >= 3)) { var unitPositions = unit.Positions.ToArray(); for (var i = 1; i < unitPositions.Length - 1; i++) if (unitPositions[i].Point.X == unitPositions[i - 1].Point.X && unitPositions[i].Point.Y == unitPositions[i - 1].Point.Y && unitPositions[i].Point.X == unitPositions[i + 1].Point.X && unitPositions[i].Point.Y == unitPositions[i + 1].Point.Y) unit.Positions.Remove(unitPositions[i]); } }