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];
 }
Exemple #2
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]);
            }
        }