public static void ParseUnitData(Replay replay) { // Get array of units from 'UnitBornEvent' replay.Units = replay.TrackerEvents.Where(i => i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitBornEvent).Select(i => new Unit { UnitID = Unit.GetUnitID((int)i.Data.dictionary[0].vInt.Value, (int)i.Data.dictionary[1].vInt.Value), Name = i.Data.dictionary[2].blobText, Group = Unit.UnitGroupDictionary.ContainsKey(i.Data.dictionary[2].blobText) ? Unit.UnitGroupDictionary[i.Data.dictionary[2].blobText] : Unit.UnitGroup.Unknown, TimeSpanBorn = i.TimeSpan, Team = i.Data.dictionary[3].vInt.Value == 11 || i.Data.dictionary[3].vInt.Value == 12 ? (int)i.Data.dictionary[3].vInt.Value - 11 : i.Data.dictionary[3].vInt.Value > 0 && i.Data.dictionary[3].vInt.Value <= 10 ? replay.Players[i.Data.dictionary[3].vInt.Value - 1].Team : (int?)null, PlayerControlledBy = i.Data.dictionary[3].vInt.Value > 0 && i.Data.dictionary[3].vInt.Value <= 10 ? replay.Players[i.Data.dictionary[3].vInt.Value - 1] : null, PointBorn = new Point { X = (int)i.Data.dictionary[5].vInt.Value, Y = (int)i.Data.dictionary[6].vInt.Value } }) .ToList(); // Add in information on unit deaths from 'UnitDiedEvent' var unitsDictionary = replay.Units.ToDictionary(i => i.UnitID, i => i); foreach (var unitDiedEvent in replay.TrackerEvents.Where(i => i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitDiedEvent).Select(i => new { UnitID = Unit.GetUnitID((int)i.Data.dictionary[0].vInt.Value, (int)i.Data.dictionary[1].vInt.Value), TimeSpanDied = i.TimeSpan, PlayerIDKilledBy = i.Data.dictionary[2].optionalData != null ? (int)i.Data.dictionary[2].optionalData.vInt.Value : (int?)null, PointDied = new Point { X = (int)i.Data.dictionary[3].vInt.Value, Y = (int)i.Data.dictionary[4].vInt.Value }, UnitKilledBy = i.Data.dictionary[5].optionalData != null ? unitsDictionary[Unit.GetUnitID((int)i.Data.dictionary[5].optionalData.vInt.Value, (int)i.Data.dictionary[6].optionalData.vInt.Value)] : null })) { var unitThatDied = unitsDictionary[unitDiedEvent.UnitID]; unitThatDied.TimeSpanDied = unitDiedEvent.TimeSpanDied; unitThatDied.PlayerKilledBy = unitDiedEvent.PlayerIDKilledBy.HasValue && unitDiedEvent.PlayerIDKilledBy.Value > 0 && unitDiedEvent.PlayerIDKilledBy.Value <= 10 ? replay.Players[unitDiedEvent.PlayerIDKilledBy.Value - 1] : null; unitThatDied.PointDied = unitDiedEvent.PointDied; unitThatDied.UnitKilledBy = unitDiedEvent.UnitKilledBy; // Sometimes 'PlayerIDKilledBy' will be outside of the range of players (1-10) // Minions that are killed by other minions or towers will have the 'team' that killed them in this field (11 or 12) // Some other units have interesting values I don't fully understand yet. For example, 'ItemCannonball' (the coins on Blackheart's Bay) will have 0 or 15 in this field. I'm guessing this is also which team acquires them, which may be useful // Other map objectives may also have this. I'll look into this more in the future. /* if (unitDiedEvent.PlayerIDKilledBy.HasValue && unitThatDied.PlayerKilledBy == null) Console.WriteLine(""); */ } // Add in information on unit ownership changes from 'UnitOwnerChangeEvent' (For example, players grabbing regen globes or a player grabbing a Garden Terror) foreach (var unitOwnerChangeEvent in replay.TrackerEvents.Where(i => i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitOwnerChangeEvent).Select(i => new { UnitID = Unit.GetUnitID((int)i.Data.dictionary[0].vInt.Value, (int)i.Data.dictionary[1].vInt.Value), TimeSpanOwnerChanged = i.TimeSpan, Team = i.Data.dictionary[2].vInt.Value == 11 || i.Data.dictionary[2].vInt.Value == 12 ? (int)i.Data.dictionary[2].vInt.Value - 11 : (int?)null, PlayerNewOwner = i.Data.dictionary[2].vInt.Value > 0 && i.Data.dictionary[2].vInt.Value <= 10 ? replay.Players[i.Data.dictionary[2].vInt.Value - 1] : null })) unitsDictionary[unitOwnerChangeEvent.UnitID].OwnerChangeEvents.Add(new OwnerChangeEvent { TimeSpanOwnerChanged = unitOwnerChangeEvent.TimeSpanOwnerChanged, Team = unitOwnerChangeEvent.Team ?? (unitOwnerChangeEvent.PlayerNewOwner != null ? unitOwnerChangeEvent.PlayerNewOwner.Team : (int?)null), PlayerNewOwner = unitOwnerChangeEvent.PlayerNewOwner }); // For simplicity, I set extra fields on units that are not initially controlled by a player, and only have one owner change event foreach (var unitWithOneOwnerChange in replay.Units.Where(i => i.OwnerChangeEvents.Count() == 1 && i.PlayerControlledBy == null)) { var singleOwnerChangeEvent = unitWithOneOwnerChange.OwnerChangeEvents.Single(); if (singleOwnerChangeEvent.PlayerNewOwner != null) { unitWithOneOwnerChange.PlayerControlledBy = singleOwnerChangeEvent.PlayerNewOwner; unitWithOneOwnerChange.TimeSpanAcquired = singleOwnerChangeEvent.TimeSpanOwnerChanged; unitWithOneOwnerChange.OwnerChangeEvents.Clear(); } } // Add in information from the 'UnitPositionEvent' // We need to go through the replay file in order because unit IDs are recycled var activeUnits = new Dictionary<int, Unit>(); foreach (var unitPositionEvent in replay.TrackerEvents.Where(i => i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitBornEvent || i.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitPositionsEvent).OrderBy(i => i.TimeSpan)) if (unitPositionEvent.TrackerEventType == ReplayTrackerEvents.TrackerEventType.UnitBornEvent) activeUnits[(int)unitPositionEvent.Data.dictionary[0].vInt.Value] = unitsDictionary[Unit.GetUnitID((int)unitPositionEvent.Data.dictionary[0].vInt.Value, (int)unitPositionEvent.Data.dictionary[1].vInt.Value)]; else { var currentUnitIndex = (int)unitPositionEvent.Data.dictionary[0].vInt.Value; for (var i = 0; i < unitPositionEvent.Data.dictionary[1].array.Length; i++) { currentUnitIndex += (int)unitPositionEvent.Data.dictionary[1].array[i++].vInt.Value; activeUnits[currentUnitIndex].Positions.Add(new Position { TimeSpan = unitPositionEvent.TimeSpan, Point = new Point { X = (int)unitPositionEvent.Data.dictionary[1].array[i++].vInt.Value, Y = (int)unitPositionEvent.Data.dictionary[1].array[i].vInt.Value } }); } } // Add an array of Hero units to each player // Currently I'm only getting single heroes (Lost Vikings not yet supported) var earlyGameTimeSpan = new TimeSpan(0, 0, 10); var heroUnitsDictionary = replay.Players.Where(i => replay.Units.Count(j => j.TimeSpanBorn < earlyGameTimeSpan && j.PlayerControlledBy == i && j.Name.StartsWith("Hero")) == 1).ToDictionary(i => i, i => replay.Units.Single(j => j.TimeSpanBorn < earlyGameTimeSpan && j.PlayerControlledBy == i && j.Name.StartsWith("Hero"))); foreach (var player in replay.Players) if (heroUnitsDictionary.ContainsKey(player)) player.HeroUnits = new[] { heroUnitsDictionary[player] }; // Add derived hero positions from associated unit born/acquired/died info // These are accurate positions: Picking up regen globes, spawning Locusts, etc // For Abathur locusts, we need to make sure they aren't spawning from a locust nest (Level 20 talent) var abathurLocustUnits = replay.Units.Where(i => i.Name == "AbathurLocustNormal" || i.Name == "AbathurLocustAssaultStrain" || i.Name == "AbathurLocustBombardStrain").ToList(); if (abathurLocustUnits.Any() && replay.Units.Any(i => i.Name == "AbathurLocustNest")) { var abathurLocustNests = replay.Units.Where(i => i.Name == "AbathurLocustNest"); foreach (var abathurLocustUnit in abathurLocustUnits.ToArray()) if (abathurLocustNests.Any(i => i.TimeSpanBorn <= abathurLocustUnit.TimeSpanBorn && (!i.TimeSpanDied.HasValue || i.TimeSpanDied >= abathurLocustUnit.TimeSpanBorn) && i.PointBorn.DistanceTo(abathurLocustUnit.PointBorn) <= 3)) abathurLocustUnits.Remove(abathurLocustUnit); } foreach (var unit in replay.Units.Where(i => Unit.UnitBornProvidesLocationForOwner.ContainsKey(i.Name) || i.Group == Unit.UnitGroup.HeroTalentSelection).Union(abathurLocustUnits).Where(i => heroUnitsDictionary.ContainsKey(i.PlayerControlledBy))) heroUnitsDictionary[unit.PlayerControlledBy].Positions.Add(new Position { TimeSpan = unit.TimeSpanBorn, Point = unit.PointBorn }); foreach (var unit in replay.Units.Where(i => Unit.UnitOwnerChangeProvidesLocationForOwner.ContainsKey(i.Name) && i.PlayerControlledBy != null).Where(i => heroUnitsDictionary.ContainsKey(i.PlayerControlledBy))) heroUnitsDictionary[unit.PlayerControlledBy].Positions.Add(new Position { TimeSpan = unit.TimeSpanAcquired.Value, Point = unit.PointBorn }); // Use 'CCmdUpdateTargetUnitEvent' to find an accurate location of units targeted // Excellent for finding frequent, accurate locations of heroes during team fights foreach (var updateTargetUnitEvent in replay.GameEvents.Where(i => i.eventType == GameEventType.CCmdUpdateTargetUnitEvent)) if (replay.Units.Any(i => i.UnitID == (int)updateTargetUnitEvent.data.array[2].unsignedInt.Value)) replay.Units.Single(i => i.UnitID == (int)updateTargetUnitEvent.data.array[2].unsignedInt.Value).Positions.Add(new Position { TimeSpan = updateTargetUnitEvent.TimeSpan, Point = Point.FromEventFormat( updateTargetUnitEvent.data.array[6].array[0].unsignedInt.Value, updateTargetUnitEvent.data.array[6].array[1].unsignedInt.Value) }); // Add in 'accurate' positions for each player's death, which sends them to their spawn point // Special Exceptions: // Uther: Level 20 respawn talent: Doesn't display the death animation when respawning, so probably doesn't count as a death in this situation. This is actually probably the best situation for us // Diablo: Fast respawn if he has enough souls. Not yet able to detect when this occurs // Murky: Respawns to his egg if his egg is alive when he dies // Lost Vikings: Individual Vikings spawn 25% faster per their trait, and 50% faster with a talent, but currently we aren't able to track their deaths individually foreach (var player in replay.Players.Where(i => i.HeroUnits.Length == 1 && i.Deaths.Length > 0)) { var fullTimerDeaths = new List<TimeSpan>(); if (player.HeroUnits[0].Name == "HeroMurky") { // Gather a list of the eggs Murky has placed throughout the game var murkyEggs = replay.Units.Where(i => i.PlayerControlledBy == player && i.Name == "MurkyRespawnEgg").OrderBy(i => i.TimeSpanBorn).ToArray(); var currentEggIndex = 0; foreach (var murkyDeath in player.Deaths) { // If Murky respawns at the egg, it will be 5 seconds after his death var murkyRespawnFromEggTimeSpan = murkyDeath.Add(TimeSpan.FromSeconds(5)); for (; currentEggIndex < murkyEggs.Length; currentEggIndex++) { if (murkyRespawnFromEggTimeSpan > murkyEggs[currentEggIndex].TimeSpanDied && currentEggIndex < murkyEggs.Length + 1) continue; // Check to see if there is an egg alive when Murky would respawn if (murkyRespawnFromEggTimeSpan >= murkyEggs[currentEggIndex].TimeSpanBorn && (!murkyEggs[currentEggIndex].TimeSpanDied.HasValue || murkyRespawnFromEggTimeSpan <= murkyEggs[currentEggIndex].TimeSpanDied.Value)) for (; murkyRespawnFromEggTimeSpan >= murkyDeath; murkyRespawnFromEggTimeSpan = murkyRespawnFromEggTimeSpan.Add(TimeSpan.FromSeconds(-1))) player.HeroUnits[0].Positions.Add(new Position { TimeSpan = murkyRespawnFromEggTimeSpan, Point = murkyEggs[currentEggIndex].PointBorn, IsEstimated = false }); else // Murky did not respawn at egg - give him the normal death timer fullTimerDeaths.Add(murkyDeath); break; } } } else fullTimerDeaths.AddRange(player.Deaths); // Normal death timer deaths // This is all deaths for most heroes, and Murky deaths if he didn't respawn from his egg if (fullTimerDeaths.Count != 0) { // Add a 'Position' at the player spawn when the death occurs player.HeroUnits[0].Positions.AddRange(fullTimerDeaths.Select(i => new Position { TimeSpan = i, Point = player.HeroUnits[0].PointBorn, IsEstimated = false })); // Add a 'Position' at the player spawn when the hero respawns if (player.HeroUnits[0].Name == "HeroDiablo") // Currently not able to tell if Diablo has a fast respawn - because of this we just always assume he does respawn quickly player.HeroUnits[0].Positions.AddRange(fullTimerDeaths.Select(i => new Position { TimeSpan = i.Add(TimeSpan.FromSeconds(5)), Point = player.HeroUnits[0].PointBorn, IsEstimated = false })); else { var currentTeamLevelMilestoneIndex = 1; foreach (var playerDeath in fullTimerDeaths) for (; currentTeamLevelMilestoneIndex < replay.TeamLevelMilestones[player.Team].Length; currentTeamLevelMilestoneIndex++) { Position spawnPosition = null; if (playerDeath < replay.TeamLevelMilestones[player.Team][currentTeamLevelMilestoneIndex]) spawnPosition = new Position { TimeSpan = playerDeath.Add(TimeSpan.FromSeconds(HeroDeathTimersByTeamLevelInSecondsForTalentLevels[currentTeamLevelMilestoneIndex - 1])), Point = player.HeroUnits[0].PointBorn, IsEstimated = false }; else if (currentTeamLevelMilestoneIndex == replay.TeamLevelMilestones[player.Team].Length - 1) spawnPosition = new Position { TimeSpan = playerDeath.Add(TimeSpan.FromSeconds(HeroDeathTimersByTeamLevelInSecondsForTalentLevels[currentTeamLevelMilestoneIndex])), Point = player.HeroUnits[0].PointBorn, IsEstimated = false }; if (spawnPosition != null) { var deathTimeSpan = playerDeath; while (deathTimeSpan < spawnPosition.TimeSpan) { // Add a 'Position' at the player spawn for every second the player is dead, to make sure we don't add 'estimated' positions during this time player.HeroUnits[0].Positions.Add(new Position { TimeSpan = deathTimeSpan, Point = player.HeroUnits[0].PointBorn, IsEstimated = false }); deathTimeSpan = deathTimeSpan.Add(TimeSpan.FromSeconds(1)); } player.HeroUnits[0].Positions.Add(spawnPosition); break; } } } } player.HeroUnits[0].Positions = player.HeroUnits[0].Positions.OrderBy(i => i.TimeSpan).ToList(); } // Estimate Hero positions from CCmdEvent and CCmdUpdateTargetPointEvent (Movement points) { // List of Hero units (Excluding heroes with multiple units like Lost Vikings - not sure how to handle those) // This is different from the above dictionary in that it excludes Abathur if he chooses the clone hero talent // It's okay to not estimate Abathur's position, as he rarely moves and we also get an accurate position each time he spawns a locust heroUnitsDictionary = replay.Players.Where(i => replay.Units.Count(j => j.PlayerControlledBy == i && j.Name.StartsWith("Hero")) == 1).ToDictionary(i => i, i => replay.Units.Single(j => j.PlayerControlledBy == i && j.Name.StartsWith("Hero"))); // This is a list of 'HeroUnit', 'TimeSpan', and 'EventPosition' for each CCmdEvent where ability data is null and a position is included var heroCCmdEventLists = replay.GameEvents.Where(i => i.eventType == GameEventType.CCmdEvent && i.data.array[1] == null && i.data.array[2] != null && i.data.array[2].array.Length == 3 && heroUnitsDictionary.ContainsKey(i.player)).Select(i => new { HeroUnit = heroUnitsDictionary[i.player], Position = new Position { TimeSpan = i.TimeSpan, Point = Point.FromEventFormat(i.data.array[2].array[0].unsignedInt.Value, i.data.array[2].array[1].unsignedInt.Value), IsEstimated = true } }) .GroupBy(i => i.HeroUnit) .Select(i => new { HeroUnit = i.Key, // Take the latest applicable CCmdEvent or CCmdUpdateTargetPointEvent if there are more than one in a second Positions = i.Select(j => j.Position).Union(replay.GameEvents.Where(j => j.player == i.Key.PlayerControlledBy && j.eventType == GameEventType.CCmdUpdateTargetPointEvent).Select(j => new Position { TimeSpan = j.TimeSpan, Point = Point.FromEventFormat(j.data.array[0].unsignedInt.Value, j.data.array[1].unsignedInt.Value), IsEstimated = true })).GroupBy(j => (int)j.TimeSpan.TotalSeconds).Select(j => j.OrderByDescending(k => k.TimeSpan).First()).OrderBy(j => j.TimeSpan).ToArray() }); const double PlayerSpeedUnitsPerSecond = 5.0; foreach (var heroCCmdEventList in heroCCmdEventLists) { // Estimate the hero unit travelling to each intended destination // Only save one position per second, and prefer accurate positions // Heroes can have a lot more positions, and probably won't be useful more frequently than this var heroTargetLocationArray = heroCCmdEventList.HeroUnit.Positions.Union(new[] { new Position { TimeSpan = heroCCmdEventList.HeroUnit.TimeSpanBorn, Point = heroCCmdEventList.HeroUnit.PointBorn } }).Union(heroCCmdEventList.Positions).GroupBy(i => (int)i.TimeSpan.TotalSeconds).Select(i => i.OrderBy(j => j.IsEstimated).First()).OrderBy(i => i.TimeSpan).ToArray(); var currentEstimatedPosition = heroTargetLocationArray[0]; for (var i = 0; i < heroTargetLocationArray.Length - 1; i++) if (!heroTargetLocationArray[i + 1].IsEstimated) currentEstimatedPosition = heroTargetLocationArray[i + 1]; else { var percentageOfDistanceTravelledToTargetLocation = (heroTargetLocationArray[i + 1].TimeSpan - currentEstimatedPosition.TimeSpan).TotalSeconds * PlayerSpeedUnitsPerSecond / currentEstimatedPosition.Point.DistanceTo(heroTargetLocationArray[i + 1].Point); currentEstimatedPosition = new Position { TimeSpan = heroTargetLocationArray[i + 1].TimeSpan, Point = percentageOfDistanceTravelledToTargetLocation >= 1 ? heroTargetLocationArray[i + 1].Point : new Point { X = (int)((heroTargetLocationArray[i + 1].Point.X - currentEstimatedPosition.Point.X) * percentageOfDistanceTravelledToTargetLocation + currentEstimatedPosition.Point.X), Y = (int)((heroTargetLocationArray[i + 1].Point.Y - currentEstimatedPosition.Point.Y) * percentageOfDistanceTravelledToTargetLocation + currentEstimatedPosition.Point.Y) }, IsEstimated = true }; heroCCmdEventList.HeroUnit.Positions.Add(currentEstimatedPosition); } heroCCmdEventList.HeroUnit.Positions = heroCCmdEventList.HeroUnit.Positions.OrderBy(i => i.TimeSpan).ToList(); } } foreach (var unit in replay.Units.Where(i => i.Positions.Any())) { // Save no more than one position event per second per unit unit.Positions = unit.Positions.GroupBy(i => (int)i.TimeSpan.TotalSeconds).Select(i => i.OrderBy(j => j.IsEstimated).First()).OrderBy(i => i.TimeSpan).ToList(); // If this is a Hero unit, adjust the 'PointDied' and 'TimeSpanDied' to the last position // Currently Hero units stop receiving tracker event updates after their first death if (unit.Group == Unit.UnitGroup.Hero) { var finalPosition = unit.Positions.Last(); unit.PointDied = finalPosition.Point; unit.TimeSpanDied = finalPosition.TimeSpan; } } // Add 'estimated' minion positions based on their fixed pathing // Without these positions, minions can appear to travel through walls straight across the map // These estimated positions are actually quite accurate, as minions always follow a path connecting each fort/keep in their lane var numberOfStructureTiers = replay.Units.Where(i => i.Name.StartsWith("TownTownHall")).Select(i => i.Name).Distinct().Count(); var uniqueTierName = replay.Units.First(i => i.Name.StartsWith("TownTownHall")).Name; var numberOfLanes = replay.Units.Count(i => i.Name == uniqueTierName && i.Team == 0); var minionWayPoints = replay.Units.Where(i => i.Name.StartsWith("TownTownHall")).Select(j => j.PointBorn).OrderBy(j => j.X).Skip(numberOfLanes).OrderByDescending(j => j.X).Skip(numberOfLanes).OrderBy(j => j.Y); for (var team = 0; team <= 1; team++) { // Gather all minion units for this team var minionUnits = replay.Units.Where(i => i.Team == team && i.Group == Unit.UnitGroup.Minions).ToArray(); // Each wave spawns together, but not necessarily from top to bottom // We will figure out what order the lanes are spawning in, and order by top to bottom later on var unitsPerLaneTemp = new List<Unit>[numberOfLanes]; for (var i = 0; i < unitsPerLaneTemp.Length; i++) unitsPerLaneTemp[i] = new List<Unit>(); var minionLaneOrderMinions = minionUnits.Where(i => i.Name == "WizardMinion").Take(numberOfLanes).ToArray(); var minionLaneOrder = new List<Tuple<int, int>>(); // *change* try { for (var i = 0; i < numberOfLanes; i++) minionLaneOrder.Add(new Tuple<int, int>(i, minionLaneOrderMinions[i].PointBorn.Y)); } catch (Exception ) { } minionLaneOrder = minionLaneOrder.OrderBy(i => i.Item2).ToList(); // Group minion units by lane var currentIndex = 0; var minionUnitsPerWave = 7; while (currentIndex < minionUnits.Length) for (var i = 0; i < unitsPerLaneTemp.Length; i++) for (var j = 0; j < minionUnitsPerWave; j++) { if (currentIndex == minionUnits.Length) break; unitsPerLaneTemp[i].Add(minionUnits[currentIndex++]); // CatapultMinions don't seem to spawn exactly with their minion wave, which is strange // For now I will leave them out of this, which means they may appear to travel through walls if (currentIndex < minionUnits.Length && minionUnits[currentIndex].Name == "CatapultMinion") currentIndex++; } // Order the lanes by top to bottom var unitsPerLane = unitsPerLaneTemp.ToArray(); // *change* try { for (var i = 0; i < unitsPerLane.Length; i++) unitsPerLane[i] = unitsPerLaneTemp[minionLaneOrder[i].Item1]; } catch (Exception ) { } for (var i = 0; i < numberOfLanes; i++) { // For each lane, take the forts in that lane, and see if the minions in that lane walked beyond this var currentLaneUnitsToAdjust = unitsPerLane[i].Where(j => j.Positions.Any() || j.TimeSpanDied.HasValue); var currentLaneWaypoints = minionWayPoints.Skip(numberOfStructureTiers * i).Take(numberOfStructureTiers); if (team == 0) currentLaneWaypoints = currentLaneWaypoints.OrderBy(j => j.X); else currentLaneWaypoints = currentLaneWaypoints.OrderByDescending(j => j.X); foreach (var laneUnit in currentLaneUnitsToAdjust) { var isLaneUnitModified = false; var beginningPosition = new Position { TimeSpan = laneUnit.TimeSpanBorn, Point = laneUnit.PointBorn }; var firstLaneUnitPosition = laneUnit.Positions.Any() ? laneUnit.Positions.First() : new Position { TimeSpan = laneUnit.TimeSpanDied.Value, Point = laneUnit.PointDied }; foreach (var laneWaypoint in currentLaneWaypoints) if ((team == 0 && firstLaneUnitPosition.Point.X > laneWaypoint.X) || team == 1 && firstLaneUnitPosition.Point.X < laneWaypoint.X) { var leg1Distance = beginningPosition.Point.DistanceTo(laneWaypoint); var newPosition = new Position { TimeSpan = beginningPosition.TimeSpan + TimeSpan.FromSeconds((long)((firstLaneUnitPosition.TimeSpan - beginningPosition.TimeSpan).TotalSeconds * (leg1Distance / (leg1Distance + laneWaypoint.DistanceTo(firstLaneUnitPosition.Point))))), Point = laneWaypoint }; laneUnit.Positions.Add(newPosition); beginningPosition = newPosition; isLaneUnitModified = true; } else break; if (isLaneUnitModified) laneUnit.Positions = laneUnit.Positions.OrderBy(j => j.TimeSpan).ToList(); } } } // Remove 'duplicate' positions that don't tell us anything foreach (var unit in replay.Units.Where(i => i.Positions.Count >= 3)) { var unitPositions = unit.Positions.ToArray(); for (var i = 1; i < unitPositions.Length - 1; i++) if (unitPositions[i].Point.X == unitPositions[i - 1].Point.X && unitPositions[i].Point.Y == unitPositions[i - 1].Point.Y && unitPositions[i].Point.X == unitPositions[i + 1].Point.X && unitPositions[i].Point.Y == unitPositions[i + 1].Point.Y) unit.Positions.Remove(unitPositions[i]); } }
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]); } }