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); }