예제 #1
0
        public async Task <IActionResult> GetTroopsSummary(int fangMinCats, int fangMaxPop)
        {
            //  Dear jesus this is such a mess

            context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

            if (!CurrentUserIsAdmin)
            {
                var authRecord = MakeFailedAuthRecord("User is not admin");
                context.Add(authRecord);
                await context.SaveChangesAsync();

                return(Unauthorized());
            }

            //  This is a mess because of different classes for Player, CurrentPlayer, etc

            //  Get all CurrentVillages from the user's tribe - list of (Player, CurrentVillage)
            //  (This returns a lot of data and will be slow)
            var tribeVillages = await(
                from player in CurrentSets.Player
                join user in CurrentSets.User on player.PlayerId equals user.PlayerId
                join village in CurrentSets.Village on player.PlayerId equals village.PlayerId
                join currentVillage in CurrentSets.CurrentVillage
                on village.VillageId equals currentVillage.VillageId
                into currentVillage
                where user.Enabled && !user.IsReadOnly
                where player.TribeId == CurrentTribeId || !Configuration.Security.RestrictAccessWithinTribes
                select new { player, villageId = village.VillageId, currentVillage = currentVillage.FirstOrDefault(), X = village.X.Value, Y = village.Y.Value }
                ).ToListAsync();

            var currentPlayers = await(
                //  Get all CurrentPlayer data for the user's tribe (separate from global 'Player' table
                //      so we can also output stats for players that haven't uploaded anything yet)
                from currentPlayer in CurrentSets.CurrentPlayer
                join player in CurrentSets.Player on currentPlayer.PlayerId equals player.PlayerId
                join user in CurrentSets.User on player.PlayerId equals user.PlayerId
                where user.Enabled && !user.IsReadOnly
                where player.TribeId == CurrentTribeId || !Configuration.Security.RestrictAccessWithinTribes
                select currentPlayer
                ).ToListAsync();

            var uploadHistory = await(
                //  Get user upload history
                from history in context.UserUploadHistory
                join user in CurrentSets.User on history.Uid equals user.Uid
                join player in CurrentSets.Player on user.PlayerId equals player.PlayerId
                where player.TribeId == CurrentTribeId || !Configuration.Security.RestrictAccessWithinTribes
                where user.Enabled && !user.IsReadOnly
                select new { playerId = player.PlayerId, history }
                ).ToListAsync();

            var enemyVillages = await(
                //  Get enemy villages
                from tribe in CurrentSets.EnemyTribe
                join player in CurrentSets.Player on tribe.EnemyTribeId equals player.TribeId
                join village in CurrentSets.Village on player.PlayerId equals village.PlayerId
                select new { village.VillageId, X = village.X.Value, Y = village.Y.Value }
                ).ToListAsync();

            // Need to load armies separately since `join into` doesn't work right with .Include()
            var armyIds = tribeVillages
                          .Where(v => v.currentVillage != null)
                          .SelectMany(v => new[] {
                v.currentVillage.ArmyAtHomeId,
                v.currentVillage.ArmyOwnedId,
                v.currentVillage.ArmyRecentLossesId,
                v.currentVillage.ArmyStationedId,
                v.currentVillage.ArmySupportingId,
                v.currentVillage.ArmyTravelingId
            })
                          .Where(id => id != null)
                          .Select(id => id.Value)
                          .ToList();

            var allArmies = await context.CurrentArmy.Where(a => armyIds.Contains(a.ArmyId)).ToDictionaryAsync(a => a.ArmyId, a => a);

            foreach (var village in tribeVillages.Where(v => v.currentVillage != null))
            {
                Scaffold.CurrentArmy FindArmy(long?armyId) => armyId == null ? null : allArmies.GetValueOrDefault(armyId.Value);

                var cv = village.currentVillage;
                cv.ArmyAtHome       = FindArmy(cv.ArmyAtHomeId);
                cv.ArmyOwned        = FindArmy(cv.ArmyOwnedId);
                cv.ArmyRecentLosses = FindArmy(cv.ArmyRecentLossesId);
                cv.ArmyStationed    = FindArmy(cv.ArmyStationedId);
                cv.ArmySupporting   = FindArmy(cv.ArmySupportingId);
                cv.ArmyTraveling    = FindArmy(cv.ArmyTravelingId);
            }

            var currentPlayerIds = currentPlayers.Select(p => p.PlayerId).ToList();

            var villageIds         = tribeVillages.Select(v => v.villageId).Distinct().ToList();
            var attackedVillageIds = await Profile("Get incomings", () => (
                                                       from command in CurrentSets.Command
                                                       where villageIds.Contains(command.TargetVillageId) && command.IsAttack && command.LandsAt > CurrentServerTime && !command.IsReturning
                                                       where !currentPlayerIds.Contains(command.SourcePlayerId)
                                                       select command.TargetVillageId
                                                       ).ToListAsync());

            var attackCommands = await Profile("Get attack details", () => (
                                                   from command in CurrentSets.Command.Include(c => c.Army)
                                                   where villageIds.Contains(command.SourceVillageId) && command.IsAttack && command.LandsAt > CurrentServerTime
                                                   where command.TargetPlayerId != null
                                                   select new { command.SourceVillageId, command.Army }
                                                   ).ToListAsync());

            var attackingVillageIds = attackCommands.Select(c => c.SourceVillageId).ToList();

            var tribeIds = tribeVillages.Select(tv => tv.player.TribeId)
                           .Where(tid => tid != null)
                           .Distinct()
                           .Select(tid => tid.Value)
                           .ToList();

            //  Collect villages grouped by owner
            var villagesByPlayer = tribeVillages
                                   .Select(v => v.player)
                                   .Distinct()
                                   .ToDictionary(
                p => p,
                p => tribeVillages.Where(v => v.player == p)
                .Select(tv => tv.currentVillage)
                .Where(cv => cv != null)
                .ToList()
                );

            var villageIdsByPlayer = villagesByPlayer.ToDictionary(
                kvp => kvp.Key,
                kvp => kvp.Value.Select(v => v.VillageId).ToList()
                );

            var uploadHistoryByPlayer = uploadHistory
                                        .Select(h => h.playerId)
                                        .Distinct()
                                        .ToDictionary(
                p => p,
                p => uploadHistory.Where(h => h.playerId == p)
                .Select(h => h.history)
                .FirstOrDefault()
                );

            //  Get all support data for the tribe
            //  'villageIds' tends to be large, so this will be a slow query
            var villagesSupport = await(
                from support in CurrentSets.CurrentVillageSupport
                .Include(s => s.SupportingArmy)
                where villageIds.Contains(support.SourceVillageId)
                select support
                ).ToListAsync();



            //  Get support data by player Id, and sorted by target tribe ID
            var playersById = tribeVillages.Select(tv => tv.player).Distinct().ToDictionary(p => p.PlayerId, p => p);

            var tribeIdsByVillage = tribeVillages.ToDictionary(
                v => v.villageId,
                v => v.player.TribeId ?? -1
                );

            //  Get tribes being supported that are not from vault
            var nonTribeVillageIds = villagesSupport.Select(s => s.TargetVillageId).Distinct().Except(villageIds).ToList();

            var nonTribeTargetTribesByVillageId = await(
                from village in CurrentSets.Village
                join player in CurrentSets.Player on village.PlayerId equals player.PlayerId
                join ally in CurrentSets.Ally on player.TribeId equals ally.TribeId
                where nonTribeVillageIds.Contains(village.VillageId)
                select new { village.VillageId, ally.TribeId }
                ).ToDictionaryAsync(d => d.VillageId, d => d.TribeId);

            foreach (var entry in nonTribeTargetTribesByVillageId)
            {
                tribeIdsByVillage.Add(entry.Key, entry.Value);
            }

            tribeIds = tribeIds.Concat(nonTribeTargetTribesByVillageId.Values.Distinct()).Distinct().ToList();

            var villagesSupportByPlayerId = new Dictionary <long, List <Scaffold.CurrentVillageSupport> >();
            var villagesSupportByPlayerIdByTargetTribeId = new Dictionary <long, Dictionary <long, List <Scaffold.CurrentVillageSupport> > >();


            //  Only check support with players that have registered villas
            foreach (var player in currentPlayers.Where(p => playersById.ContainsKey(p.PlayerId)))
            {
                var supportFromPlayer = villagesSupport.Where(
                    s => villageIdsByPlayer[playersById[player.PlayerId]].Contains(s.SourceVillageId)
                    ).ToList();

                villagesSupportByPlayerId.Add(player.PlayerId, supportFromPlayer);

                var supportByTribe = tribeIds.ToDictionary(tid => tid, _ => new List <Scaffold.CurrentVillageSupport>());
                supportByTribe.Add(-1, new List <Scaffold.CurrentVillageSupport>());

                foreach (var support in supportFromPlayer)
                {
                    var targetTribeId = tribeIdsByVillage.GetValueOrDefault(support.TargetVillageId, -1);
                    supportByTribe[targetTribeId].Add(support);
                }

                villagesSupportByPlayerIdByTargetTribeId.Add(player.PlayerId, supportByTribe);
            }

            var numIncomingsByPlayer      = new Dictionary <long, int>();
            var numAttacksByPlayer        = new Dictionary <long, int>();
            var numAttackingFangsByPlayer = new Dictionary <long, int>();
            var villageOwnerIdById        = tribeVillages.ToDictionary(v => v.villageId, v => v.player.PlayerId);

            foreach (var target in attackedVillageIds)
            {
                var playerId = villageOwnerIdById[target];
                if (!numIncomingsByPlayer.ContainsKey(playerId))
                {
                    numIncomingsByPlayer[playerId] = 0;
                }
                numIncomingsByPlayer[playerId]++;
            }

            foreach (var source in attackingVillageIds)
            {
                var playerId = villageOwnerIdById[source];
                if (!numAttacksByPlayer.ContainsKey(playerId))
                {
                    numAttacksByPlayer[playerId] = 0;
                }
                numAttacksByPlayer[playerId]++;
            }

            bool IsFang(JSON.Army army, bool ignorePop = false) =>
            army != null &&
            army.ContainsKey(JSON.TroopType.Catapult) &&
            army[JSON.TroopType.Catapult] >= fangMinCats &&
            (ignorePop || ArmyStats.CalculateTotalPopulation(army, ArmyStats.OffensiveTroopTypes) <= fangMaxPop);

            foreach (var command in attackCommands)
            {
                var playerId = villageOwnerIdById[command.SourceVillageId];
                if (!numAttackingFangsByPlayer.ContainsKey(playerId))
                {
                    numAttackingFangsByPlayer[playerId] = 0;
                }

                if (IsFang(command.Army))
                {
                    numAttackingFangsByPlayer[playerId]++;
                }
            }

            var villagesNearEnemy = new HashSet <long>();

            foreach (var village in tribeVillages)
            {
                var nearbyEnemyVillage = enemyVillages.FirstOrDefault(v =>
                {
                    var distance = Model.Coordinate.Distance(v.X, v.Y, village.X, village.Y);
                    return(distance < 10);
                });

                if (nearbyEnemyVillage != null)
                {
                    villagesNearEnemy.Add(village.villageId);
                }
            }

            var maxNoblesByPlayer = currentPlayers.ToDictionary(p => p.PlayerId, p => p.CurrentPossibleNobles);

            //  Get tribe labels
            var tribeNames = await(
                from tribe in CurrentSets.Ally
                where tribeIds.Contains(tribe.TribeId)
                select new { tribe.Tag, tribe.TribeId }
                ).ToListAsync();

            var tribeNamesById = tribeNames.ToDictionary(tn => tn.TribeId, tn => tn.Tag.UrlDecode());

            var jsonData = new List <JSON.PlayerSummary>();

            foreach (var kvp in villagesByPlayer.OrderBy(kvp => kvp.Key.TribeId).ThenBy(kvp => kvp.Key.PlayerName))
            {
                var    player         = kvp.Key;
                String playerName     = player.PlayerName;
                String tribeName      = tribeNamesById.GetValueOrDefault(player.TribeId ?? -1);
                var    playerVillages = kvp.Value;

                var playerHistory = uploadHistoryByPlayer.GetValueOrDefault(player.PlayerId);
                var playerSummary = new JSON.PlayerSummary
                {
                    PlayerName          = playerName.UrlDecode(),
                    PlayerId            = player.PlayerId,
                    TribeName           = tribeName,
                    UploadedAt          = playerHistory?.LastUploadedTroopsAt ?? new DateTime(),
                    UploadedReportsAt   = playerHistory?.LastUploadedReportsAt ?? new DateTime(),
                    UploadedIncomingsAt = playerHistory?.LastUploadedIncomingsAt ?? new DateTime(),
                    UploadedCommandsAt  = playerHistory?.LastUploadedCommandsAt ?? new DateTime(),
                    NumNobles           = playerVillages.Select(v => v.ArmyOwned?.Snob ?? 0).Sum(),
                    NumIncomings        = numIncomingsByPlayer.GetValueOrDefault(player.PlayerId, 0),
                    NumAttackCommands   = numAttacksByPlayer.GetValueOrDefault(player.PlayerId, 0),
                    FangsTraveling      = numAttackingFangsByPlayer.GetValueOrDefault(player.PlayerId, 0)
                };

                playerSummary.UploadAge = CurrentServerTime - playerSummary.UploadedAt;

                if (maxNoblesByPlayer.ContainsKey(player.PlayerId))
                {
                    playerSummary.MaxPossibleNobles = maxNoblesByPlayer[player.PlayerId];
                }

                //  General army data
                foreach (var village in playerVillages.Where(v => v.ArmyOwned != null && v.ArmyTraveling != null && v.ArmyAtHome != null))
                {
                    var armyOwned     = ArmyConvert.ArmyToJson(village.ArmyOwned);
                    var armyTraveling = ArmyConvert.ArmyToJson(village.ArmyTraveling);
                    var armyAtHome    = ArmyConvert.ArmyToJson(village.ArmyAtHome);

                    if (IsFang(armyAtHome, true) && !ArmyStats.IsNuke(armyOwned, 0.75))
                    {
                        playerSummary.FangsOwned++;
                    }

                    if (ArmyStats.IsOffensive(village.ArmyOwned))
                    {
                        playerSummary.NumOffensiveVillages++;

                        var offensivePower = BattleSimulator.TotalAttackPower(armyOwned);

                        if (ArmyStats.IsNuke(armyOwned))
                        {
                            playerSummary.NukesOwned++;
                        }
                        else if (ArmyStats.IsNuke(armyOwned, 0.75))
                        {
                            playerSummary.ThreeQuarterNukesOwned++;
                        }
                        else if (ArmyStats.IsNuke(armyOwned, 0.5))
                        {
                            playerSummary.HalfNukesOwned++;
                        }
                        else if (ArmyStats.IsNuke(armyOwned, 0.25))
                        {
                            playerSummary.QuarterNukesOwned++;
                        }

                        if (ArmyStats.IsNuke(armyTraveling))
                        {
                            playerSummary.NukesTraveling++;
                        }
                        else if (IsFang(armyTraveling))
                        {
                            playerSummary.FangsTraveling++;
                        }
                    }
                    else
                    {
                        playerSummary.NumDefensiveVillages++;

                        var ownedDefensivePower     = BattleSimulator.TotalDefensePower(armyOwned);
                        var atHomeDefensivePower    = BattleSimulator.TotalDefensePower(armyAtHome);
                        var travelingDefensivePower = BattleSimulator.TotalDefensePower(armyTraveling);

                        playerSummary.DVsAtHome += atHomeDefensivePower / (float)ArmyStats.FullDVDefensivePower;
                        if (!villagesNearEnemy.Contains(village.VillageId))
                        {
                            playerSummary.DVsAtHomeBackline += atHomeDefensivePower / (float)ArmyStats.FullDVDefensivePower;
                        }

                        playerSummary.DVsOwned     += ownedDefensivePower / (float)ArmyStats.FullDVDefensivePower;
                        playerSummary.DVsTraveling += travelingDefensivePower / (float)ArmyStats.FullDVDefensivePower;
                    }
                }

                //  Support data
                var playerSupport = villagesSupportByPlayerId.GetValueOrDefault(player.PlayerId);
                if (playerSupport != null)
                {
                    //  Support where the target is one of the players' own villages
                    foreach (var support in playerSupport.Where(s => playerVillages.Any(v => v.VillageId == s.TargetVillageId)))
                    {
                        playerSummary.DVsSupportingSelf += BattleSimulator.TotalDefensePower(support.SupportingArmy) / (float)ArmyStats.FullDVDefensivePower;
                    }

                    //  Support where the target isn't any of the players' own villages
                    foreach (var support in playerSupport.Where(s => playerVillages.All(v => v.VillageId != s.TargetVillageId)))
                    {
                        playerSummary.DVsSupportingOthers += BattleSimulator.TotalDefensePower(support.SupportingArmy) / (float)ArmyStats.FullDVDefensivePower;
                    }

                    playerSummary.SupportPopulationByTargetTribe = new Dictionary <string, int>();

                    foreach (var(tribeId, supportToTribe) in villagesSupportByPlayerIdByTargetTribeId[player.PlayerId])
                    {
                        var supportedTribeName     = tribeNamesById.GetValueOrDefault(tribeId, Translate("UNKNOWN"));
                        var totalSupportPopulation = 0;
                        foreach (var support in supportToTribe)
                        {
                            totalSupportPopulation += ArmyStats.CalculateTotalPopulation(ArmyConvert.ArmyToJson(support.SupportingArmy));
                        }

                        playerSummary.SupportPopulationByTargetTribe.Add(supportedTribeName, totalSupportPopulation);
                    }
                }

                jsonData.Add(playerSummary);
            }

            return(Ok(jsonData));
        }
        public async Task <Dictionary <String, UserStats> > GenerateHighScores(Scaffold.VaultContext context, int worldId, int accessGroupId, CancellationToken ct)
        {
            context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

            logger.LogDebug("Generating high scores for world {0}", worldId);

            var CurrentSets = new
            {
                ActiveUser            = context.User.Active().FromWorld(worldId).FromAccessGroup(accessGroupId),
                Player                = context.Player.FromWorld(worldId),
                Village               = context.Village.FromWorld(worldId),
                CurrentVillage        = context.CurrentVillage.FromWorld(worldId).FromAccessGroup(accessGroupId),
                Ally                  = context.Ally.FromWorld(worldId),
                CurrentVillageSupport = context.CurrentVillageSupport.FromWorld(worldId).FromAccessGroup(accessGroupId),
                Command               = context.Command.FromWorld(worldId).FromAccessGroup(accessGroupId),
                Report                = context.Report.FromWorld(worldId).FromAccessGroup(accessGroupId),
                EnemyTribe            = context.EnemyTribe.FromWorld(worldId).FromAccessGroup(accessGroupId)
            };

            var serverSettings = await context.WorldSettings.Where(s => s.WorldId == worldId).FirstOrDefaultAsync();

            var lastWeek = serverSettings.ServerTime - TimeSpan.FromDays(7);

            logger.LogDebug("Running data queries...");

            var tribePlayers = await(
                from user in CurrentSets.ActiveUser
                join player in CurrentSets.Player on user.PlayerId equals player.PlayerId
                select new { player.PlayerName, player.PlayerId }
                ).ToListAsync(ct);

            var tribeVillas = await(
                from user in CurrentSets.ActiveUser
                join player in CurrentSets.Player on user.PlayerId equals player.PlayerId
                join village in CurrentSets.Village on player.PlayerId equals village.PlayerId
                join currentVillage in CurrentSets.CurrentVillage
                on village.VillageId equals currentVillage.VillageId
                where currentVillage.ArmyAtHomeId != null && currentVillage.ArmyTravelingId != null
                select new { X = village.X.Value, Y = village.Y.Value, player.PlayerId, village.VillageId, currentVillage.ArmyAtHome, currentVillage.ArmyTraveling }
                ).ToListAsync(ct);

            var tribeSupport = await(
                from user in CurrentSets.ActiveUser
                join player in CurrentSets.Player on user.PlayerId equals player.PlayerId
                join village in CurrentSets.Village on player.PlayerId equals village.PlayerId
                join support in CurrentSets.CurrentVillageSupport
                on village.VillageId equals support.SourceVillageId
                select new { player.PlayerId, support.TargetVillageId, support.SupportingArmy }
                ).ToListAsync(ct);

            var tribeAttackCommands = await(
                from user in CurrentSets.ActiveUser
                join player in CurrentSets.Player on user.PlayerId equals player.PlayerId
                join command in CurrentSets.Command on player.PlayerId equals command.SourcePlayerId
                where command.ArmyId != null
                where command.LandsAt > lastWeek
                where command.IsAttack
                where command.TargetPlayerId != null
                select new { command.CommandId, command.SourcePlayerId, command.LandsAt, command.TargetVillageId, command.Army }
                ).ToListAsync(ct);

            var tribeSupportCommands = await(
                from user in CurrentSets.ActiveUser
                join player in CurrentSets.Player on user.PlayerId equals player.PlayerId
                join command in CurrentSets.Command on player.PlayerId equals command.SourcePlayerId
                where command.ArmyId != null
                where command.LandsAt > lastWeek
                where !command.IsAttack
                where command.TargetPlayerId != null
                select new SlimSupportCommand {
                SourcePlayerId = command.SourcePlayerId, TargetPlayerId = command.TargetPlayerId.Value, TargetVillageId = command.TargetVillageId, LandsAt = command.LandsAt
            }
                ).ToListAsync(ct);

            var tribeAttackingReports = await(
                from user in CurrentSets.ActiveUser
                join player in CurrentSets.Player on user.PlayerId equals player.PlayerId
                join report in CurrentSets.Report on player.PlayerId equals report.AttackerPlayerId
                where report.OccuredAt > lastWeek
                where report.AttackerArmy != null
                where report.DefenderPlayerId != null
                select new SlimReport {
                AttackerArmy = report.AttackerArmy, ReportId = report.ReportId, OccuredAt = report.OccuredAt, AttackerPlayerId = report.AttackerPlayerId.Value, DefenderVillageId = report.DefenderVillageId
            }
                ).ToListAsync(ct);

            var tribeDefendingReports = await(
                from user in CurrentSets.ActiveUser
                join player in CurrentSets.Player on user.PlayerId equals player.PlayerId
                join report in CurrentSets.Report on player.PlayerId equals report.DefenderPlayerId
                where report.OccuredAt > lastWeek
                where report.AttackerArmy != null
                select new SlimReport {
                AttackerArmy = report.AttackerArmy, DefenderVillageId = report.DefenderVillageId, ReportId = report.ReportId, OccuredAt = report.OccuredAt
            }
                ).ToListAsync(ct);

            var enemyVillas = await(
                from enemy in CurrentSets.EnemyTribe
                join player in CurrentSets.Player on enemy.EnemyTribeId equals player.TribeId
                join village in CurrentSets.Village on player.PlayerId equals village.PlayerId
                select new { X = village.X.Value, Y = village.Y.Value }
                ).ToListAsync(ct);

            if (ct.IsCancellationRequested)
            {
                return(null);
            }

            logger.LogDebug("Finished data queries");

            var tribeVillageIds = tribeVillas.Select(v => v.VillageId).ToList();

            var supportedVillageIds = tribeSupport.Select(s => s.TargetVillageId).Distinct().ToList();
            var villageTribeIds     = await(
                from village in CurrentSets.Village
                join player in CurrentSets.Player on village.PlayerId equals player.PlayerId
                join tribe in CurrentSets.Ally on player.TribeId equals tribe.TribeId
                where supportedVillageIds.Contains(village.VillageId)
                select new { village.VillageId, tribe.TribeId }
                ).ToDictionaryAsync(d => d.VillageId, d => d.TribeId, ct);

            if (ct.IsCancellationRequested)
            {
                return(null);
            }

            var supportedTribeIds = villageTribeIds.Values.Distinct().ToList();
            var tribeInfo         = await(
                from tribe in CurrentSets.Ally
                where supportedTribeIds.Contains(tribe.TribeId)
                select new { tribe.TribeId, tribe.TribeName, tribe.Tag }
                ).ToDictionaryAsync(d => d.TribeId, d => new { Name = d.TribeName, d.Tag }, ct);

            if (ct.IsCancellationRequested)
            {
                return(null);
            }

            logger.LogDebug("Finished supplemental queries");

            var defenseReportsWithNobles           = tribeDefendingReports.Where(r => r.AttackerArmy.Snob > 0).OrderBy(r => r.OccuredAt).ToList();
            var defenseNobleReportsByTargetVillage = defenseReportsWithNobles.GroupBy(r => r.DefenderVillageId).ToDictionary(g => g.Key, g => g.ToList());
            var possibleSnipesByTargetVillage      = tribeSupportCommands
                                                     .Where(c => defenseNobleReportsByTargetVillage.Keys.Contains(c.TargetVillageId))
                                                     .GroupBy(c => c.TargetVillageId, c => c)
                                                     .ToDictionary(g => g.Key, g => g.ToList());

            var numSnipesByPlayer = tribePlayers.ToDictionary(p => p.PlayerId, p => 0);

            foreach ((var villageId, var possibleSnipes) in possibleSnipesByTargetVillage.Tupled())
            {
                var attacksToVillage = defenseNobleReportsByTargetVillage[villageId];

                foreach (var snipe in possibleSnipes)
                {
                    var earlierReport = attacksToVillage.LastOrDefault(r => r.OccuredAt <= snipe.LandsAt);
                    var laterReport   = attacksToVillage.FirstOrDefault(r => r.OccuredAt > snipe.LandsAt);

                    if (laterReport == null)
                    {
                        continue;
                    }

                    if (earlierReport != null)
                    {
                        //  Check if between two nobles that landed at around the same time
                        if (laterReport.OccuredAt - earlierReport.OccuredAt < TimeSpan.FromMilliseconds(500))
                        {
                            numSnipesByPlayer[snipe.SourcePlayerId]++;
                        }
                    }
                    else if (laterReport.OccuredAt - snipe.LandsAt < TimeSpan.FromMilliseconds(1000))
                    {
                        // Landed before
                        numSnipesByPlayer[snipe.SourcePlayerId]++;
                    }
                }
            }



            var supportByTargetTribe = tribePlayers.ToDictionary(p => p.PlayerId, p => supportedTribeIds.ToDictionary(t => t, t => new List <Scaffold.CurrentArmy>()));

            foreach (var support in tribeSupport.Where(s => villageTribeIds.ContainsKey(s.TargetVillageId) && !tribeVillageIds.Contains(s.TargetVillageId)))
            {
                supportByTargetTribe[support.PlayerId][villageTribeIds[support.TargetVillageId]].Add(support.SupportingArmy);
            }

            logger.LogDebug("Sorted support by tribe");


            var reportsBySourcePlayer = tribePlayers.ToDictionary(p => p.PlayerId, _ => new Dictionary <long, List <SlimReport> >());

            foreach (var report in tribeAttackingReports)
            {
                var playerReports = reportsBySourcePlayer[report.AttackerPlayerId];
                if (!playerReports.ContainsKey(report.OccuredAt.Ticks))
                {
                    playerReports.Add(report.OccuredAt.Ticks, new List <SlimReport>());
                }
                playerReports[report.OccuredAt.Ticks].Add(report);
            }

            logger.LogDebug("Sorted reports by source player");

            var commandArmiesWithReports = new Dictionary <Scaffold.CommandArmy, SlimReport>();

            foreach (var command in tribeAttackCommands.Where(cmd => reportsBySourcePlayer.ContainsKey(cmd.SourcePlayerId)))
            {
                var matchingReport = reportsBySourcePlayer[command.SourcePlayerId].GetValueOrDefault(command.LandsAt.Ticks)?.FirstOrDefault(c => c.DefenderVillageId == command.TargetVillageId);
                if (matchingReport != null)
                {
                    commandArmiesWithReports.Add(command.Army, matchingReport);
                }
            }

            logger.LogDebug("Gathered commands with associated reports");

            var reportsWithoutCommands = tribeAttackingReports.Except(commandArmiesWithReports.Values).ToList();

            var usedAttackArmies = tribeAttackCommands
                                   .Select(c => new { c.SourcePlayerId, Army = (Army)c.Army })
                                   .Concat(reportsWithoutCommands.Select(r => new { SourcePlayerId = r.AttackerPlayerId, Army = (Army)r.AttackerArmy }))
                                   .ToList();

            logger.LogDebug("Gathered used attack armies");

            var usedAttackArmiesByPlayer = tribePlayers.ToDictionary(p => p.PlayerId, p => new List <Army>());

            foreach (var army in usedAttackArmies)
            {
                usedAttackArmiesByPlayer[army.SourcePlayerId].Add(army.Army);
            }

            logger.LogDebug("Sorted attack armies by player");

            var villagesByPlayer = tribeVillas.GroupBy(v => v.PlayerId).ToDictionary(g => g.Key, g => g.ToList());

            var armiesNearEnemy = new HashSet <long>();
            var enemyMap        = new Spatial.Quadtree(enemyVillas.Select(v => new Coordinate {
                X = v.X, Y = v.Y
            }));

            foreach (var village in tribeVillas.Where(v => enemyMap.ContainsInRange(new Coordinate {
                X = v.X, Y = v.Y
            }, 10)))
            {
                armiesNearEnemy.Add(village.ArmyAtHome.ArmyId);
            }

            logger.LogDebug("Collected armies near enemies");

            var result = new Dictionary <String, UserStats>();

            foreach (var player in tribePlayers)
            {
                if (ct.IsCancellationRequested)
                {
                    break;
                }

                var playerVillages = villagesByPlayer.GetValueOrDefault(player.PlayerId);
                var playerArmies   = usedAttackArmiesByPlayer[player.PlayerId];

                int numFangs = 0, numNukes = 0, numFakes = 0;
                foreach (var army in playerArmies)
                {
                    if (ArmyStats.IsFake(army))
                    {
                        numFakes++;
                    }
                    if (ArmyStats.IsNuke(army))
                    {
                        numNukes++;
                    }
                    if (ArmyStats.IsFang(army))
                    {
                        numFangs++;
                    }
                }

                var playerResult = new UserStats
                {
                    FangsInPastWeek   = numFangs,
                    NukesInPastWeek   = numNukes,
                    FakesInPastWeek   = numFakes,
                    SnipesInPastWeek  = numSnipesByPlayer[player.PlayerId],
                    BacklineDVsAtHome = playerVillages?.Where(v => !armiesNearEnemy.Contains(v.ArmyAtHome.ArmyId)).Sum(v => BattleSimulator.TotalDefensePower(v.ArmyAtHome) / (float)ArmyStats.FullDVDefensivePower) ?? 0,
                    DVsAtHome         = playerVillages?.Sum(v => BattleSimulator.TotalDefensePower(v.ArmyAtHome) / (float)ArmyStats.FullDVDefensivePower) ?? 0,
                    DVsTraveling      = playerVillages?.Sum(v => BattleSimulator.TotalDefensePower(v.ArmyTraveling) / (float)ArmyStats.FullDVDefensivePower) ?? 0,
                    PopPerTribe       = supportByTargetTribe[player.PlayerId].Where(kvp => kvp.Value.Count > 0).ToDictionary(
                        kvp => tribeInfo[kvp.Key].Tag.UrlDecode(),
                        kvp => kvp.Value.Sum(a => BattleSimulator.TotalDefensePower(a) / (float)ArmyStats.FullDVDefensivePower)
                        )
                };

                result.Add(player.PlayerName.UrlDecode(), playerResult);
            }

            logger.LogDebug("Generated result data");

            return(result);
        }