From 962abcf833708f01fe729afb701302fc22722064 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Sat, 17 Feb 2024 15:17:42 -0600 Subject: [PATCH] Additional updates to support performance bucket --- Application/DefaultSettings.json | 1 + Application/Misc/ServerDataViewer.cs | 30 +++-- Plugins/Stats/Client/HitCalculator.cs | 34 +++++ .../Client/ServerDistributionCalculator.cs | 42 +++--- Plugins/Stats/Commands/ViewStats.cs | 30 +++-- Plugins/Stats/Dtos/AdvancedStatsInfo.cs | 1 + Plugins/Stats/Dtos/ClientRankingInfo.cs | 3 + .../Stats/Dtos/ClientRankingInfoRequest.cs | 3 + Plugins/Stats/Dtos/StatsInfoRequest.cs | 1 + .../AdvancedClientStatsResourceQueryHelper.cs | 122 ++++++++++++++---- Plugins/Stats/Helpers/StatManager.cs | 55 ++++---- Plugins/Stats/Plugin.cs | 30 ++++- SharedLibraryCore/Events/EventExtensions.cs | 8 +- .../Interfaces/IServerDataViewer.cs | 3 +- SharedLibraryCore/Utilities.cs | 6 +- .../Client/ClientStatisticsController.cs | 2 - .../Client/Legacy/StatsController.cs | 29 ++--- WebfrontCore/Startup.cs | 1 + 18 files changed, 273 insertions(+), 128 deletions(-) create mode 100644 Plugins/Stats/Dtos/ClientRankingInfo.cs create mode 100644 Plugins/Stats/Dtos/ClientRankingInfoRequest.cs diff --git a/Application/DefaultSettings.json b/Application/DefaultSettings.json index fb9deeb0..2e74415a 100644 --- a/Application/DefaultSettings.json +++ b/Application/DefaultSettings.json @@ -2659,6 +2659,7 @@ "gewehr43_upgraded": "G115 Compressor", "m1a1carbine_upgraded": "Widdershins RC-1", "m1garand_upgraded": "M1000", + "m1garand_gl_upgraded": "The Imploder", "mg42_upgraded": "Barracuda FU-A11", "mp40_upgraded": "The Afterburner", "ppsh_upgraded": "The Reaper", diff --git a/Application/Misc/ServerDataViewer.cs b/Application/Misc/ServerDataViewer.cs index b36222a7..4ee92242 100644 --- a/Application/Misc/ServerDataViewer.cs +++ b/Application/Misc/ServerDataViewer.cs @@ -8,6 +8,7 @@ using Data.Models; using Data.Models.Client; using Data.Models.Client.Stats; using Data.Models.Server; +using IW4MAdmin.Plugins.Stats.Helpers; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SharedLibraryCore; @@ -25,19 +26,21 @@ namespace IW4MAdmin.Application.Misc private readonly IDataValueCache _serverStatsCache; private readonly IDataValueCache> _clientHistoryCache; private readonly IDataValueCache _rankedClientsCache; + private readonly StatManager _statManager; private readonly TimeSpan? _cacheTimeSpan = Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10); public ServerDataViewer(ILogger logger, IDataValueCache snapshotCache, IDataValueCache serverStatsCache, - IDataValueCache> clientHistoryCache, IDataValueCache rankedClientsCache) + IDataValueCache> clientHistoryCache, IDataValueCache rankedClientsCache, StatManager statManager) { _logger = logger; _snapshotCache = snapshotCache; _serverStatsCache = serverStatsCache; _clientHistoryCache = clientHistoryCache; _rankedClientsCache = rankedClientsCache; + _statManager = statManager; } public async Task<(int?, DateTime?)> @@ -185,30 +188,31 @@ namespace IW4MAdmin.Application.Misc } } - public async Task RankedClientsCountAsync(long? serverId = null, CancellationToken token = default) + public async Task RankedClientsCountAsync(long? serverId = null, string performanceBucket = null, CancellationToken token = default) { _rankedClientsCache.SetCacheItem((set, ids, cancellationToken) => { long? id = null; + string bucket = null; if (ids.Any()) { id = (long?)ids.First(); } - - var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15); - return set - .Where(rating => rating.Newest) - .Where(rating => rating.ServerId == id) - .Where(rating => rating.CreatedDateTime >= fifteenDaysAgo) - .Where(rating => rating.Client.Level != EFClient.Permission.Banned) - .Where(rating => rating.Ranking != null) - .CountAsync(cancellationToken); - }, nameof(_rankedClientsCache), new object[] { serverId }, _cacheTimeSpan); + + if (ids.Count() == 2) + { + bucket = (string)ids.Last(); + } + + return _statManager.GetBucketConfig(serverId) + .ContinueWith(result => _statManager.GetTotalRankedPlayers(id, bucket), cancellationToken).Result; + + }, nameof(_rankedClientsCache), new object[] { serverId, performanceBucket }, _cacheTimeSpan); try { - return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), new object[] { serverId }, token); + return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), new object[] { serverId, performanceBucket }, token); } catch (Exception ex) { diff --git a/Plugins/Stats/Client/HitCalculator.cs b/Plugins/Stats/Client/HitCalculator.cs index 5af5c8b8..f5944015 100644 --- a/Plugins/Stats/Client/HitCalculator.cs +++ b/Plugins/Stats/Client/HitCalculator.cs @@ -17,6 +17,7 @@ using Microsoft.Extensions.Logging; using SharedLibraryCore.Database.Models; using SharedLibraryCore.Events; using SharedLibraryCore.Events.Game; +using SharedLibraryCore.Events.Game.GameScript.Zombie; using SharedLibraryCore.Events.Management; using Stats.Client.Abstractions; using Stats.Client.Game; @@ -109,6 +110,39 @@ public class HitCalculator : IClientStatisticCalculator return; } + if (coreEvent is RoundEndEvent roundEndEvent) + { + foreach (var client in roundEndEvent.Server.ConnectedClients) + { + if (!_clientHitStatistics.TryGetValue(client.ClientId, out var state)) + { + continue; + } + + try + { + await state.OnTransaction.WaitAsync(); + await UpdateClientStatistics(client.ClientId, state); + } + + catch (Exception ex) + { + _logger.LogError(ex, "Could not handle round end calculations for client {Client}", + client.ToString()); + } + + finally + { + if (state.OnTransaction.CurrentCount == 0) + { + state.OnTransaction.Release(); + } + } + } + + return; + } + if (coreEvent is ClientStateDisposeEvent clientStateDisposeEvent) { _clientHitStatistics.Remove(clientStateDisposeEvent.Client.ClientId, out var state); diff --git a/Plugins/Stats/Client/ServerDistributionCalculator.cs b/Plugins/Stats/Client/ServerDistributionCalculator.cs index a0e4735d..a7bce2b6 100644 --- a/Plugins/Stats/Client/ServerDistributionCalculator.cs +++ b/Plugins/Stats/Client/ServerDistributionCalculator.cs @@ -47,7 +47,7 @@ namespace Stats.Client public async Task Initialize() { await LoadServers(); - + _distributionCache.SetCacheItem(async (set, token) => { var validPlayTime = _configuration.TopPlayersMinPlayTime; @@ -71,14 +71,9 @@ namespace Stats.Client distributions.Add(serverId.ToString(), distributionParams); } - foreach (var performanceBucketGroup in _appConfig.Servers.GroupBy(server => server.PerformanceBucket)) + foreach (var performanceBucketGroup in _appConfig.Servers.Select(server => server.PerformanceBucket).Distinct()) { - if (string.IsNullOrWhiteSpace(performanceBucketGroup.Key)) - { - continue; - } - - var performanceBucket = performanceBucketGroup.Key; + var performanceBucket = performanceBucketGroup ?? "null"; var bucketConfig = _configuration.PerformanceBuckets.FirstOrDefault(bucket => @@ -99,19 +94,19 @@ namespace Stats.Client return distributions; }, DistributionCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(1) : TimeSpan.FromHours(1)); - foreach (var server in _appConfig.Servers) + foreach (var performanceBucket in _appConfig.Servers.Select(s => s.PerformanceBucket).Distinct()) { _maxZScoreCache.SetCacheItem(async (set, ids, token) => { var validPlayTime = _configuration.TopPlayersMinPlayTime; - var oldestStat = TimeSpan.FromSeconds(_configuration.TopPlayersMinPlayTime); - var performanceBucket = (string)ids.FirstOrDefault(); + var oldestStat = DateTime.UtcNow - Extensions.FifteenDaysAgo(); + var perfBucket = (string)ids.FirstOrDefault(); - if (!string.IsNullOrEmpty(performanceBucket)) + if (!string.IsNullOrEmpty(perfBucket)) { var bucketConfig = _configuration.PerformanceBuckets.FirstOrDefault(cfg => - cfg.Name == performanceBucket) ?? new PerformanceBucketConfiguration(); + cfg.Name == perfBucket) ?? new PerformanceBucketConfiguration(); validPlayTime = (int)bucketConfig.ClientMinPlayTime.TotalSeconds; oldestStat = bucketConfig.RankingExpiration; @@ -120,19 +115,18 @@ namespace Stats.Client var zScore = await set .Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(validPlayTime, oldestStat)) .Where(s => s.Skill > 0) - .Where(s => s.EloRating >= 1) - .Where(stat => - performanceBucket == null || performanceBucket == stat.Server.PerformanceBucket) + .Where(s => s.EloRating >= 0) + .Where(stat => perfBucket == stat.Server.PerformanceBucket) .GroupBy(stat => stat.ClientId) .Select(group => group.Sum(stat => stat.ZScore * stat.TimePlayed) / group.Sum(stat => stat.TimePlayed)) .MaxAsync(avgZScore => (double?)avgZScore, token); return zScore ?? 0; - }, MaxZScoreCacheKey, new[] { server.PerformanceBucket }, + }, MaxZScoreCacheKey, new[] { performanceBucket }, Utilities.IsDevelopment ? TimeSpan.FromMinutes(1) : TimeSpan.FromMinutes(30)); - await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new[] { server.PerformanceBucket }); + await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new[] { performanceBucket }); } await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken()); @@ -140,7 +134,7 @@ namespace Stats.Client /*foreach (var serverId in _serverIds) { await using var ctx = _contextFactory.CreateContext(enableTracking: true); - + var a = await ctx.Set() .Where(s => s.ServerId == serverId) //.Where(s=> s.ClientId == 216105) @@ -150,16 +144,16 @@ namespace Stats.Client .Where(s => s.TimePlayed >= 3600 * 3) .Where(s => s.UpdatedAt >= Extensions.FifteenDaysAgo()) .ToListAsync(); - + var b = a.Distinct(); - + foreach (var item in b) { await Plugin.Manager.UpdateHistoricalRanking(item.ClientId, item, item.ServerId); //item.ZScore = await GetZScoreForServer(serverId, item.Performance); //item.UpdatedAt = DateTime.UtcNow; } - + await ctx.SaveChangesAsync(); }*/ } @@ -186,7 +180,7 @@ namespace Stats.Client var serverParams = await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken()); Extensions.LogParams sdParams = null; - + if (serverId is not null && serverParams.TryGetValue(serverId.ToString(), out var sdParams1)) { sdParams = sdParams1; @@ -210,7 +204,7 @@ namespace Stats.Client public async Task GetRatingForZScore(double? value, string performanceBucket) { - var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new [] { performanceBucket }); + var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new[] { performanceBucket ?? "null" }); return maxZScore == 0 ? null : value.GetRatingForZScore(maxZScore); } } diff --git a/Plugins/Stats/Commands/ViewStats.cs b/Plugins/Stats/Commands/ViewStats.cs index 85c36279..3f7f9f1b 100644 --- a/Plugins/Stats/Commands/ViewStats.cs +++ b/Plugins/Stats/Commands/ViewStats.cs @@ -9,16 +9,17 @@ using SharedLibraryCore.Database.Models; using SharedLibraryCore.Configuration; using SharedLibraryCore.Interfaces; using SharedLibraryCore.Commands; +using Stats.Dtos; namespace IW4MAdmin.Plugins.Stats.Commands { public class ViewStatsCommand : Command { private readonly IDatabaseContextFactory _contextFactory; - private readonly StatManager _statManager; + private readonly IResourceQueryHelper _queryHelper; public ViewStatsCommand(CommandConfiguration config, ITranslationLookup translationLookup, - IDatabaseContextFactory contextFactory, StatManager statManager) : base(config, translationLookup) + IDatabaseContextFactory contextFactory, IResourceQueryHelper queryHelper) : base(config, translationLookup) { Name = "stats"; Description = translationLookup["PLUGINS_STATS_COMMANDS_VIEW_DESC"]; @@ -35,7 +36,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands }; _contextFactory = contextFactory; - _statManager = statManager; + _queryHelper = queryHelper; } public override async Task ExecuteAsync(GameEvent gameEvent) @@ -54,15 +55,19 @@ namespace IW4MAdmin.Plugins.Stats.Commands } var serverId = (gameEvent.Owner as IGameServer).LegacyDatabaseId; - var totalRankedPlayers = await _statManager.GetTotalRankedPlayers(serverId); // getting stats for a particular client if (gameEvent.Target != null) { - var performanceRanking = await _statManager.GetClientOverallRanking(gameEvent.Target.ClientId, serverId); - var performanceRankingString = performanceRanking == 0 + var performanceRanking = (await _queryHelper.QueryResource(new ClientRankingInfoRequest + { + ClientId = gameEvent.Target.ClientId, + ServerEndpoint = gameEvent.Owner.Id + })).Results.First(); + + var performanceRankingString = performanceRanking.CurrentRanking == 0 ? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"] - : $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} (Color::Accent)#{performanceRanking}/{totalRankedPlayers}"; + : $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} (Color::Accent)#{performanceRanking.CurrentRanking}/{performanceRanking.TotalRankedClients}"; // target is currently connected so we want their cached stats if they exist if (gameEvent.Owner.GetClientsAsList().Any(client => client.Equals(gameEvent.Target))) @@ -88,10 +93,15 @@ namespace IW4MAdmin.Plugins.Stats.Commands // getting self stats else { - var performanceRanking = await _statManager.GetClientOverallRanking(gameEvent.Origin.ClientId, serverId); - var performanceRankingString = performanceRanking == 0 + var performanceRanking = (await _queryHelper.QueryResource(new ClientRankingInfoRequest + { + ClientId = gameEvent.Origin.ClientId, + ServerEndpoint = gameEvent.Owner.Id + })).Results.First(); + + var performanceRankingString = performanceRanking.CurrentRanking == 0 ? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"] - : $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} (Color::Accent)#{performanceRanking}/{totalRankedPlayers}"; + : $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} (Color::Accent)#{performanceRanking.CurrentRanking}/{performanceRanking.TotalRankedClients}"; // check if current client is connected to the server if (gameEvent.Owner.GetClientsAsList().Any(client => client.Equals(gameEvent.Origin))) diff --git a/Plugins/Stats/Dtos/AdvancedStatsInfo.cs b/Plugins/Stats/Dtos/AdvancedStatsInfo.cs index 1a22ef54..316c7c58 100644 --- a/Plugins/Stats/Dtos/AdvancedStatsInfo.cs +++ b/Plugins/Stats/Dtos/AdvancedStatsInfo.cs @@ -27,5 +27,6 @@ namespace Stats.Dtos public List Ratings { get; set; } public List LegacyStats { get; set; } public List CustomMetrics { get; set; } = new(); + public string PerformanceBucket { get; set; } } } diff --git a/Plugins/Stats/Dtos/ClientRankingInfo.cs b/Plugins/Stats/Dtos/ClientRankingInfo.cs new file mode 100644 index 00000000..24645ca1 --- /dev/null +++ b/Plugins/Stats/Dtos/ClientRankingInfo.cs @@ -0,0 +1,3 @@ +namespace Stats.Dtos; + +public record ClientRankingInfo(int CurrentRanking, int TotalRankedClients, string PerformanceBucket); diff --git a/Plugins/Stats/Dtos/ClientRankingInfoRequest.cs b/Plugins/Stats/Dtos/ClientRankingInfoRequest.cs new file mode 100644 index 00000000..ea96c0ea --- /dev/null +++ b/Plugins/Stats/Dtos/ClientRankingInfoRequest.cs @@ -0,0 +1,3 @@ +namespace Stats.Dtos; + +public class ClientRankingInfoRequest : StatsInfoRequest; diff --git a/Plugins/Stats/Dtos/StatsInfoRequest.cs b/Plugins/Stats/Dtos/StatsInfoRequest.cs index 3340a06f..2e6fd77d 100644 --- a/Plugins/Stats/Dtos/StatsInfoRequest.cs +++ b/Plugins/Stats/Dtos/StatsInfoRequest.cs @@ -7,5 +7,6 @@ /// public int? ClientId { get; set; } public string ServerEndpoint { get; set; } + public string PerformanceBucket { get; set; } } } diff --git a/Plugins/Stats/Helpers/AdvancedClientStatsResourceQueryHelper.cs b/Plugins/Stats/Helpers/AdvancedClientStatsResourceQueryHelper.cs index 92fff0ec..d1f2abd8 100644 --- a/Plugins/Stats/Helpers/AdvancedClientStatsResourceQueryHelper.cs +++ b/Plugins/Stats/Helpers/AdvancedClientStatsResourceQueryHelper.cs @@ -7,40 +7,35 @@ using Data.Models; using Data.Models.Client; using Data.Models.Client.Stats; using IW4MAdmin.Plugins.Stats; +using IW4MAdmin.Plugins.Stats.Helpers; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SharedLibraryCore.Dtos; using SharedLibraryCore.Helpers; using SharedLibraryCore.Interfaces; using Stats.Dtos; -using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Stats.Helpers { - public class AdvancedClientStatsResourceQueryHelper : IResourceQueryHelper + public class AdvancedClientStatsResourceQueryHelper( + ILogger logger, + IDatabaseContextFactory contextFactory, + IServerDataViewer serverDataViewer, + StatManager statManager + ) + : IResourceQueryHelper, + IResourceQueryHelper { - private readonly IDatabaseContextFactory _contextFactory; - private readonly ILogger _logger; - private readonly IManager _manager; - - public AdvancedClientStatsResourceQueryHelper(ILogger logger, - IDatabaseContextFactory contextFactory, IManager manager) - { - _contextFactory = contextFactory; - _logger = logger; - _manager = manager; - } - public async Task> QueryResource(StatsInfoRequest query) { - await using var context = _contextFactory.CreateContext(enableTracking: false); + await using var context = contextFactory.CreateContext(enableTracking: false); long? serverId = null; if (!string.IsNullOrEmpty(query.ServerEndpoint)) { serverId = (await context.Servers - .Select(server => new {server.EndPoint, server.Id}) + .Select(server => new { server.EndPoint, server.Id }) .FirstOrDefaultAsync(server => server.EndPoint == query.ServerEndpoint)) ?.Id; } @@ -77,10 +72,18 @@ namespace Stats.Helpers .Where(r => r.ClientId == clientInfo.ClientId) .Where(r => r.ServerId == serverId) .Where(r => r.Ranking != null) + .Where(r => r.PerformanceBucket == query.PerformanceBucket) .OrderByDescending(r => r.CreatedDateTime) .Take(250) .ToListAsync(); + var rankingInfo = (await QueryResource(new ClientRankingInfoRequest + { + ClientId = query.ClientId, + ServerEndpoint = query.ServerEndpoint, + PerformanceBucket = query.PerformanceBucket + })).Results.First(); + var mostRecentRanking = ratings.FirstOrDefault(ranking => ranking.Newest); var ranking = mostRecentRanking?.Ranking + 1; @@ -90,7 +93,9 @@ namespace Stats.Helpers .Where(stat => serverId == null || stat.ServerId == serverId) .ToListAsync(); - if (mostRecentRanking != null && mostRecentRanking.CreatedDateTime < Extensions.FifteenDaysAgo()) + var bucketConfig = await statManager.GetBucketConfig(serverId); + + if (mostRecentRanking != null && mostRecentRanking.CreatedDateTime < DateTime.UtcNow - bucketConfig.RankingExpiration) { ranking = 0; } @@ -100,7 +105,7 @@ namespace Stats.Helpers ranking = null; } - var hitInfo = new AdvancedStatsInfo() + var hitInfo = new AdvancedStatsInfo { ServerId = serverId, Performance = mostRecentRanking?.PerformanceMetric, @@ -111,9 +116,12 @@ namespace Stats.Helpers Level = clientInfo.Level, Rating = mostRecentRanking?.PerformanceMetric, All = hitStats, - Servers = _manager.GetServers() + Servers = Plugin.ServerManager.GetServers() .Select(server => new ServerInfo - {Name = server.Hostname, IPAddress = server.ListenAddress, Port = server.ListenPort, Game = (Reference.Game)server.GameName}) + { + Name = server.Hostname, IPAddress = server.ListenAddress, Port = server.ListenPort, + Game = (Reference.Game)server.GameName + }) .Where(server => server.Game == clientInfo.GameName) .ToList(), Aggregate = hitStats.FirstOrDefault(hit => @@ -136,24 +144,82 @@ namespace Stats.Helpers Ratings = ratings, LegacyStats = legacyStats, Ranking = ranking, + TotalRankedClients = rankingInfo.TotalRankedClients, + PerformanceBucket = rankingInfo.PerformanceBucket }; - // todo: when nothign found - return new ResourceQueryHelperResult() + return new ResourceQueryHelperResult { - Results = new[] {hitInfo} + Results = new[] { hitInfo } }; } - public static Expression> GetRankingFunc(int minPlayTime, TimeSpan expiration, double? zScore = null, + public static Expression> GetRankingFunc(int minPlayTime, TimeSpan expiration, + double? zScore = null, long? serverId = null) { var oldestStat = DateTime.UtcNow.Subtract(expiration); return stats => (serverId == null || stats.ServerId == serverId) && - stats.UpdatedAt >= oldestStat && - stats.Client.Level != EFClient.Permission.Banned && - stats.TimePlayed >= minPlayTime - && (zScore == null || stats.ZScore > zScore); + stats.UpdatedAt >= oldestStat && + stats.Client.Level != EFClient.Permission.Banned && + stats.TimePlayed >= minPlayTime + && (zScore == null || stats.ZScore > zScore); + } + + public async Task> QueryResource(ClientRankingInfoRequest query) + { + await using var context = contextFactory.CreateContext(enableTracking: false); + + long? serverId = null; + + if (!string.IsNullOrEmpty(query.ServerEndpoint)) + { + serverId = Plugin.ServerManager.Servers.FirstOrDefault(server => server.Id == query.ServerEndpoint) + ?.LegacyDatabaseId; + } + + var currentRanking = 0; + int totalRankedClients; + string performanceBucket; + + if (string.IsNullOrEmpty(query.PerformanceBucket) && serverId is null) + { + var maxPerformance = await context.Set() + .Where(r => r.ClientId == query.ClientId) + .Where(r => r.Ranking != null) + .Where(r => r.ServerId == serverId) + .Where(rating => rating.Newest) + .GroupBy(rating => rating.PerformanceBucket) + .Select(grp => new { grp.Key, PerformanceMetric = grp.Max(rating => rating.Ranking) }) + .Where(grp => grp.PerformanceMetric != null) + .FirstOrDefaultAsync(); + + if (maxPerformance is null) + { + currentRanking = 0; + totalRankedClients = 0; + performanceBucket = null; + } + else + { + currentRanking = + await statManager.GetClientOverallRanking(query.ClientId!.Value, null, maxPerformance.Key); + totalRankedClients = await serverDataViewer.RankedClientsCountAsync(null, maxPerformance.Key); + performanceBucket = maxPerformance.Key; + } + } + else + { + performanceBucket = query.PerformanceBucket; + currentRanking = + await statManager.GetClientOverallRanking(query.ClientId!.Value, serverId, performanceBucket); + totalRankedClients = await serverDataViewer.RankedClientsCountAsync(serverId, performanceBucket); + } + + return new ResourceQueryHelperResult + { + Results = [new ClientRankingInfo(currentRanking, totalRankedClients, performanceBucket)] + }; } } } diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index 7e290055..fe433c7e 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -75,18 +75,23 @@ namespace IW4MAdmin.Plugins.Stats.Helpers /// gets a ranking across all servers for given client id /// /// client id of the player + /// + /// /// - public async Task GetClientOverallRanking(int clientId, long? serverId = null) + public async Task GetClientOverallRanking(int clientId, long? serverId = null, string performanceBucket = null) { await using var context = _contextFactory.CreateContext(enableTracking: false); if (_config.EnableAdvancedMetrics) { + var bucketConfig = await GetBucketConfig(null, performanceBucket); + var clientRanking = await context.Set() + .Where(GetNewRankingFunc(bucketConfig.RankingExpiration, bucketConfig.ClientMinPlayTime, serverId, performanceBucket)) .Where(r => r.ClientId == clientId) - .Where(r => r.ServerId == serverId) .Where(r => r.Newest) .FirstOrDefaultAsync(); + return clientRanking?.Ranking + 1 ?? 0; } @@ -110,7 +115,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers return 0; } - private Expression> GetNewRankingFunc(TimeSpan oldestStat, TimeSpan minPlayTime, long? serverId = null) + private Expression> GetNewRankingFunc(TimeSpan oldestStat, TimeSpan minPlayTime, long? serverId = null, string performanceBucket = null) { var oldestDate = DateTime.UtcNow - oldestStat; return ranking => ranking.ServerId == serverId @@ -119,12 +124,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers && ranking.ZScore != null && ranking.PerformanceMetric != null && ranking.Newest + && ranking.PerformanceBucket == performanceBucket && ranking.Client.TotalConnectionTime >= (int)minPlayTime.TotalSeconds; } - public async Task GetTotalRankedPlayers(long serverId) + public async Task GetTotalRankedPlayers(long? serverId = null, string performanceBucket = null) { - var bucketConfig = await GetBucketConfig(serverId); + var bucketConfig = await GetBucketConfig(serverId, performanceBucket); await using var context = _contextFactory.CreateContext(enableTracking: false); @@ -150,7 +156,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers await using var context = _contextFactory.CreateContext(false); var clientIdsList = await context.Set() - .Where(GetNewRankingFunc(bucketConfig.RankingExpiration, bucketConfig.ClientMinPlayTime, serverId: serverId)) + .Where(GetNewRankingFunc(bucketConfig.RankingExpiration, bucketConfig.ClientMinPlayTime, serverId: serverId, performanceBucket)) .OrderByDescending(ranking => ranking.PerformanceMetric) .Select(ranking => ranking.ClientId) .Skip(start) @@ -164,6 +170,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers var eachRank = await context.Set() .Where(ranking => ranking.ClientId == clientId) .Where(ranking => ranking.ServerId == serverId) + .Where(ranking => ranking.PerformanceBucket == performanceBucket) .OrderByDescending(ranking => ranking.CreatedDateTime) .Select(ranking => new RankingSnapshot { @@ -177,16 +184,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers }) .Take(60) .ToListAsync(); - - if (rankingsDict.ContainsKey(clientId)) + + if (!rankingsDict.TryAdd(clientId, eachRank)) { rankingsDict[clientId] = rankingsDict[clientId].Concat(eachRank).Distinct() .OrderByDescending(ranking => ranking.CreatedDateTime).ToList(); } - else - { - rankingsDict.Add(clientId, eachRank); - } } var statsInfo = await context.Set() @@ -224,7 +227,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers RatingChange = (rankingsDict[s.ClientId].Last().Ranking - rankingsDict[s.ClientId].First().Ranking) ?? 0, PerformanceHistory = rankingsDict[s.ClientId].Select(ranking => new PerformanceHistory - {Performance = ranking.PerformanceMetric ?? 0, OccurredAt = ranking.CreatedDateTime}) + { Performance = ranking.PerformanceMetric ?? 0, OccurredAt = ranking.CreatedDateTime }) .ToList(), TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"), TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed), @@ -281,7 +284,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers return finished; } - private async Task GetBucketConfig(long? serverId) + public async Task GetBucketConfig(long? serverId = null, + string bucketName = null) { var defaultConfig = new PerformanceBucketConfiguration { @@ -289,11 +293,17 @@ namespace IW4MAdmin.Plugins.Stats.Helpers RankingExpiration = DateTime.UtcNow - Extensions.FifteenDaysAgo() }; - if (serverId is null) + if (serverId is null && bucketName is null) { return defaultConfig; } + if (bucketName is not null) + { + return _config.PerformanceBuckets.FirstOrDefault(bucket => bucket.Name == bucketName) ?? + defaultConfig; + } + var performanceBucket = (await _serverCache.FirstAsync(server => server.Id == serverId)).PerformanceBucket; @@ -306,11 +316,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers defaultConfig; } - public async Task> GetTopStats(int start, int count, long? serverId = null) + public async Task> GetTopStats(int start, int count, long? serverId = null, string performanceBucket = null) { if (_config.EnableAdvancedMetrics) { - return await GetNewTopStats(start, count, serverId); + return await GetNewTopStats(start, count, serverId, performanceBucket); } await using var context = _contextFactory.CreateContext(enableTracking: false); @@ -355,7 +365,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers .Select(grp => new { grp.Key, - Ratings = grp.Select(r => new {r.Performance, r.Ranking, r.When}) + Ratings = grp.Select(r => new { r.Performance, r.Ranking, r.When }) }); var iqStatsInfo = (from stat in context.Set() @@ -393,7 +403,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking, PerformanceHistory = ratingInfo.First(r => r.Key == s.ClientId).Ratings.Count() > 1 ? ratingInfo.First(r => r.Key == s.ClientId).Ratings.OrderBy(r => r.When) - .Select(r => new PerformanceHistory {Performance = r.Performance, OccurredAt = r.When}) + .Select(r => new PerformanceHistory { Performance = r.Performance, OccurredAt = r.When }) .ToList() : new List { @@ -1007,7 +1017,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers victimStats.LastScore = estimatedVictimScore; // show encouragement/discouragement - var streakMessage = attackerStats.ClientId != victimStats.ClientId + var streakMessage = attacker.CurrentServer.IsZombieServer() ? string.Empty : attackerStats.ClientId != victimStats.ClientId ? StreakMessage.MessageOnStreak(attackerStats.KillStreak, attackerStats.DeathStreak, _config) : StreakMessage.MessageOnStreak(-1, -1, _config); @@ -1258,6 +1268,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers .Include(stat => stat.Server) .Where(stat => stat.ClientId == clientId) .Where(stat => stat.ServerId != serverId) // ignore the one we're currently tracking + .Where(stat => stat.Server.PerformanceBucket == bucketConfig.Name) .Where(stats => stats.UpdatedAt >= oldestStateDate) .Where(stats => stats.TimePlayed >= (int)bucketConfig.ClientMinPlayTime.TotalSeconds) .ToListAsync(); @@ -1278,11 +1289,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers private async Task UpdateAggregateForServerOrBucket(int clientId, EFClientStatistics clientStats, DatabaseContext context, List performances, PerformanceBucketConfiguration bucketConfig) { var aggregateZScore = - performances.Where(performance => performance.Server.PerformanceBucket == bucketConfig.Name) - .WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), (int)bucketConfig.ClientMinPlayTime.TotalSeconds); + performances.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), (int)bucketConfig.ClientMinPlayTime.TotalSeconds); int? aggregateRanking = await context.Set() .Where(stat => stat.ClientId != clientId) + .Where(stat => bucketConfig.Name == stat.Server.PerformanceBucket) .Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc((int)bucketConfig.ClientMinPlayTime.TotalSeconds, bucketConfig.RankingExpiration)) .GroupBy(stat => stat.ClientId) .Where(group => diff --git a/Plugins/Stats/Plugin.cs b/Plugins/Stats/Plugin.cs index 0d114f57..a9acb9ee 100644 --- a/Plugins/Stats/Plugin.cs +++ b/Plugins/Stats/Plugin.cs @@ -46,6 +46,7 @@ public class Plugin : IPluginV2 private readonly IServerDataViewer _serverDataViewer; private readonly StatsConfiguration _statsConfig; private readonly StatManager _statManager; + private readonly IResourceQueryHelper _queryHelper; public static void RegisterDependencies(IServiceCollection serviceCollection) { @@ -57,8 +58,9 @@ public class Plugin : IPluginV2 ITranslationLookup translationLookup, IMetaServiceV2 metaService, IResourceQueryHelper chatQueryHelper, IEnumerable statCalculators, - IServerDistributionCalculator serverDistributionCalculator, IServerDataViewer serverDataViewer, - StatsConfiguration statsConfig, StatManager statManager) + IServerDistributionCalculator serverDistributionCalculator, + StatsConfiguration statsConfig, StatManager statManager, + IResourceQueryHelper queryHelper) { _databaseContextFactory = databaseContextFactory; _translationLookup = translationLookup; @@ -70,6 +72,7 @@ public class Plugin : IPluginV2 _serverDataViewer = serverDataViewer; _statsConfig = statsConfig; _statManager = statManager; + _queryHelper = queryHelper; IGameServerEventSubscriptions.MonitoringStopped += async (monitorEvent, token) => await _statManager.Sync(monitorEvent.Server, token); @@ -117,7 +120,7 @@ public class Plugin : IPluginV2 } }; IGameEventSubscriptions.MatchEnded += OnMatchEvent; - IGameEventSubscriptions.RoundEnded += (roundEndedEvent, token) => _statManager.Sync(roundEndedEvent.Server, token); + IGameEventSubscriptions.RoundEnded += OnRoundEnded; IGameEventSubscriptions.MatchStarted += OnMatchEvent; IGameEventSubscriptions.ScriptEventTriggered += OnScriptEvent; IGameEventSubscriptions.ClientKilled += OnClientKilled; @@ -126,6 +129,16 @@ public class Plugin : IPluginV2 IManagementEventSubscriptions.Load += OnLoad; } + private async Task OnRoundEnded(RoundEndEvent roundEndedEvent, CancellationToken token) + { + await _statManager.Sync(roundEndedEvent.Server, token); + + foreach (var calculator in _statCalculators) + { + await calculator.CalculateForEvent(roundEndedEvent); + } + } + private async Task OnClientKilled(ClientKillEvent killEvent, CancellationToken token) { if (!ShouldIgnoreEvent(killEvent.Attacker, killEvent.Victim)) @@ -258,7 +271,10 @@ public class Plugin : IPluginV2 var performance = Math.Round(validPerformanceValues.Sum(c => c.Performance * c.TimePlayed / performancePlayTime), 2); var spm = Math.Round(clientStats.Sum(c => c.SPM) / clientStats.Count(c => c.SPM > 0), 1); - var overallRanking = await _statManager.GetClientOverallRanking(request.ClientId); + var ranking = (await _queryHelper.QueryResource(new ClientRankingInfoRequest + { + ClientId = request.ClientId, + })).Results.First(); return new List { @@ -267,12 +283,12 @@ public class Plugin : IPluginV2 Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"], Value = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING_FORMAT"] .FormatExt( - (overallRanking == 0 + (ranking.CurrentRanking == 0 ? "--" - : overallRanking.ToString("#,##0", + : ranking.CurrentRanking.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization .LocalizationName))), - (await _serverDataViewer.RankedClientsCountAsync(token: token)).ToString("#,##0", + ranking.TotalRankedClients.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) ), Column = 0, diff --git a/SharedLibraryCore/Events/EventExtensions.cs b/SharedLibraryCore/Events/EventExtensions.cs index 26e0e844..8507aaf4 100644 --- a/SharedLibraryCore/Events/EventExtensions.cs +++ b/SharedLibraryCore/Events/EventExtensions.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Serilog; namespace SharedLibraryCore.Events; @@ -31,8 +32,7 @@ public static class EventExtensions } catch (Exception ex) { - // todo: static logger - Console.WriteLine("InvokeAsync: " + ex); + Log.Error(ex, "InvokeAsync for event type {EventType} failed. Cancellation Token is None", typeof(TEventType).Name); } } @@ -46,8 +46,8 @@ public static class EventExtensions } catch (Exception ex) { - // todo: static logger - Console.WriteLine("InvokeAsync: " + ex); + Log.Error(ex, "InvokeAsync for event type {EventType} failed. IsCancellationRequested is {TokenStatus}", + typeof(TEventType).Name, tokenSource.Token.IsCancellationRequested); } } } diff --git a/SharedLibraryCore/Interfaces/IServerDataViewer.cs b/SharedLibraryCore/Interfaces/IServerDataViewer.cs index bd8b1be4..cfe0d30e 100644 --- a/SharedLibraryCore/Interfaces/IServerDataViewer.cs +++ b/SharedLibraryCore/Interfaces/IServerDataViewer.cs @@ -45,8 +45,9 @@ namespace SharedLibraryCore.Interfaces /// Retrieves the number of ranked clients for given server id /// /// ServerId to query on + /// /// CancellationToken /// - Task RankedClientsCountAsync(long? serverId = null, CancellationToken token = default); + Task RankedClientsCountAsync(long? serverId = null, string performanceBucket = null, CancellationToken token = default); } } diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index d738a021..38977aa8 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -219,9 +219,11 @@ namespace SharedLibraryCore /// /// /// - public static bool IsZombieServer(this Server server) + public static bool IsZombieServer(this Server server) => (server as IGameServer).IsZombieServer(); + + public static bool IsZombieServer(this IGameServer server) { - return new[] { Game.T4, Game.T5, Game.T6 }.Contains(server.GameName) && + return new[] { Reference.Game.T4, Reference.Game.T5, Reference.Game.T6 }.Contains(server.GameCode) && ZmGameTypes.Contains(server.Gametype.ToLower()); } diff --git a/WebfrontCore/Controllers/Client/ClientStatisticsController.cs b/WebfrontCore/Controllers/Client/ClientStatisticsController.cs index f330ede3..45096eff 100644 --- a/WebfrontCore/Controllers/Client/ClientStatisticsController.cs +++ b/WebfrontCore/Controllers/Client/ClientStatisticsController.cs @@ -54,8 +54,6 @@ namespace WebfrontCore.Controllers { await statMetricFunc(new Dictionary> { { id, hitInfo.CustomMetrics } }, matchedServerId, null, false); } - - hitInfo.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, token); return View("~/Views/Client/Statistics/Advanced.cshtml", hitInfo); } diff --git a/WebfrontCore/Controllers/Client/Legacy/StatsController.cs b/WebfrontCore/Controllers/Client/Legacy/StatsController.cs index 24fa0494..d5f84102 100644 --- a/WebfrontCore/Controllers/Client/Legacy/StatsController.cs +++ b/WebfrontCore/Controllers/Client/Legacy/StatsController.cs @@ -1,25 +1,24 @@ -using IW4MAdmin.Plugins.Stats; -using IW4MAdmin.Plugins.Stats.Helpers; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using SharedLibraryCore; -using SharedLibraryCore.Dtos; -using SharedLibraryCore.Dtos.Meta.Responses; -using SharedLibraryCore.Interfaces; -using Stats.Dtos; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using ILogger = Microsoft.Extensions.Logging.ILogger; using Data.Abstractions; +using IW4MAdmin.Plugins.Stats.Helpers; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using SharedLibraryCore; +using SharedLibraryCore.Dtos; +using SharedLibraryCore.Dtos.Meta.Responses; +using SharedLibraryCore.Interfaces; using Stats.Config; +using Stats.Dtos; using WebfrontCore.QueryHelpers.Models; +using ILogger = Microsoft.Extensions.Logging.ILogger; -namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers +namespace WebfrontCore.Controllers.Client.Legacy { public class StatsController : BaseController { @@ -62,7 +61,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers matchedServerId = server.LegacyDatabaseId; } - ViewBag.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, token); + ViewBag.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, null, token); ViewBag.ServerId = matchedServerId; return View("~/Views/Client/Statistics/Index.cshtml", _manager.GetServers() diff --git a/WebfrontCore/Startup.cs b/WebfrontCore/Startup.cs index 6af15888..8e88ce81 100644 --- a/WebfrontCore/Startup.cs +++ b/WebfrontCore/Startup.cs @@ -113,6 +113,7 @@ namespace WebfrontCore services.AddTransient, FindClientRequestValidator>(); services.AddSingleton, ClientService>(); services.AddSingleton, StatsResourceQueryHelper>(); + services.AddSingleton, AdvancedClientStatsResourceQueryHelper>(); services .AddSingleton, AdvancedClientStatsResourceQueryHelper>();