1
0
mirror of https://github.com/RaidMax/IW4M-Admin.git synced 2025-06-07 21:58:06 -05:00

Additional updates to support performance bucket

This commit is contained in:
RaidMax 2024-02-17 15:17:42 -06:00
parent e1461582fa
commit 962abcf833
18 changed files with 273 additions and 128 deletions

View File

@ -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",

View File

@ -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<EFClient, (int, int)> _serverStatsCache;
private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache;
private readonly IDataValueCache<EFClientRankingHistory, int> _rankedClientsCache;
private readonly StatManager _statManager;
private readonly TimeSpan? _cacheTimeSpan =
Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10);
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache,
IDataValueCache<EFClient, (int, int)> serverStatsCache,
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache, IDataValueCache<EFClientRankingHistory, int> rankedClientsCache)
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache, IDataValueCache<EFClientRankingHistory, int> 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<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default)
public async Task<int> 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)
{

View File

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

View File

@ -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<EFClientStatistics>()
.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<double?> 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);
}
}

View File

@ -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<ClientRankingInfoRequest, ClientRankingInfo> _queryHelper;
public ViewStatsCommand(CommandConfiguration config, ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory, StatManager statManager) : base(config, translationLookup)
IDatabaseContextFactory contextFactory, IResourceQueryHelper<ClientRankingInfoRequest, ClientRankingInfo> 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)))

View File

@ -27,5 +27,6 @@ namespace Stats.Dtos
public List<EFClientRankingHistory> Ratings { get; set; }
public List<EFClientStatistics> LegacyStats { get; set; }
public List<EFMeta> CustomMetrics { get; set; } = new();
public string PerformanceBucket { get; set; }
}
}

View File

@ -0,0 +1,3 @@
namespace Stats.Dtos;
public record ClientRankingInfo(int CurrentRanking, int TotalRankedClients, string PerformanceBucket);

View File

@ -0,0 +1,3 @@
namespace Stats.Dtos;
public class ClientRankingInfoRequest : StatsInfoRequest;

View File

@ -7,5 +7,6 @@
/// </summary>
public int? ClientId { get; set; }
public string ServerEndpoint { get; set; }
public string PerformanceBucket { get; set; }
}
}

View File

@ -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<StatsInfoRequest, AdvancedStatsInfo>
public class AdvancedClientStatsResourceQueryHelper(
ILogger<AdvancedClientStatsResourceQueryHelper> logger,
IDatabaseContextFactory contextFactory,
IServerDataViewer serverDataViewer,
StatManager statManager
)
: IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo>,
IResourceQueryHelper<ClientRankingInfoRequest, ClientRankingInfo>
{
private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger;
private readonly IManager _manager;
public AdvancedClientStatsResourceQueryHelper(ILogger<AdvancedClientStatsResourceQueryHelper> logger,
IDatabaseContextFactory contextFactory, IManager manager)
{
_contextFactory = contextFactory;
_logger = logger;
_manager = manager;
}
public async Task<ResourceQueryHelperResult<AdvancedStatsInfo>> 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<AdvancedStatsInfo>()
return new ResourceQueryHelperResult<AdvancedStatsInfo>
{
Results = new[] {hitInfo}
Results = new[] { hitInfo }
};
}
public static Expression<Func<EFClientStatistics, bool>> GetRankingFunc(int minPlayTime, TimeSpan expiration, double? zScore = null,
public static Expression<Func<EFClientStatistics, bool>> 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<ResourceQueryHelperResult<ClientRankingInfo>> 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<EFClientRankingHistory>()
.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<ClientRankingInfo>
{
Results = [new ClientRankingInfo(currentRanking, totalRankedClients, performanceBucket)]
};
}
}
}

View File

@ -75,18 +75,23 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
/// gets a ranking across all servers for given client id
/// </summary>
/// <param name="clientId">client id of the player</param>
/// <param name="serverId"></param>
/// <param name="performanceBucket"></param>
/// <returns></returns>
public async Task<int> GetClientOverallRanking(int clientId, long? serverId = null)
public async Task<int> 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<EFClientRankingHistory>()
.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<Func<EFClientRankingHistory, bool>> GetNewRankingFunc(TimeSpan oldestStat, TimeSpan minPlayTime, long? serverId = null)
private Expression<Func<EFClientRankingHistory, bool>> 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<int> GetTotalRankedPlayers(long serverId)
public async Task<int> 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<EFClientRankingHistory>()
.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<EFClientRankingHistory>()
.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<EFClientStatistics>()
@ -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<PerformanceBucketConfiguration> GetBucketConfig(long? serverId)
public async Task<PerformanceBucketConfiguration> 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<List<TopStatsInfo>> GetTopStats(int start, int count, long? serverId = null)
public async Task<List<TopStatsInfo>> 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<EFClientStatistics>()
@ -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<PerformanceHistory>
{
@ -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<EFClientStatistics> 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<EFClientStatistics>()
.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 =>

View File

@ -46,6 +46,7 @@ public class Plugin : IPluginV2
private readonly IServerDataViewer _serverDataViewer;
private readonly StatsConfiguration _statsConfig;
private readonly StatManager _statManager;
private readonly IResourceQueryHelper<ClientRankingInfoRequest, ClientRankingInfo> _queryHelper;
public static void RegisterDependencies(IServiceCollection serviceCollection)
{
@ -57,8 +58,9 @@ public class Plugin : IPluginV2
ITranslationLookup translationLookup, IMetaServiceV2 metaService,
IResourceQueryHelper<ChatSearchQuery, MessageResponse> chatQueryHelper,
IEnumerable<IClientStatisticCalculator> statCalculators,
IServerDistributionCalculator serverDistributionCalculator, IServerDataViewer serverDataViewer,
StatsConfiguration statsConfig, StatManager statManager)
IServerDistributionCalculator serverDistributionCalculator,
StatsConfiguration statsConfig, StatManager statManager,
IResourceQueryHelper<ClientRankingInfoRequest, ClientRankingInfo> 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<InformationResponse>
{
@ -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,

View File

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

View File

@ -45,8 +45,9 @@ namespace SharedLibraryCore.Interfaces
/// Retrieves the number of ranked clients for given server id
/// </summary>
/// <param name="serverId">ServerId to query on</param>
/// <param name="performanceBucket"></param>
/// <param name="token">CancellationToken</param>
/// <returns></returns>
Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default);
Task<int> RankedClientsCountAsync(long? serverId = null, string performanceBucket = null, CancellationToken token = default);
}
}

View File

@ -219,9 +219,11 @@ namespace SharedLibraryCore
/// </summary>
/// <param name="server"></param>
/// <returns></returns>
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());
}

View File

@ -54,8 +54,6 @@ namespace WebfrontCore.Controllers
{
await statMetricFunc(new Dictionary<int, List<EFMeta>> { { id, hitInfo.CustomMetrics } }, matchedServerId, null, false);
}
hitInfo.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, token);
return View("~/Views/Client/Statistics/Advanced.cshtml", hitInfo);
}

View File

@ -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()

View File

@ -113,6 +113,7 @@ namespace WebfrontCore
services.AddTransient<IValidator<FindClientRequest>, FindClientRequestValidator>();
services.AddSingleton<IResourceQueryHelper<FindClientRequest, FindClientResult>, ClientService>();
services.AddSingleton<IResourceQueryHelper<StatsInfoRequest, StatsInfoResult>, StatsResourceQueryHelper>();
services.AddSingleton<IResourceQueryHelper<ClientRankingInfoRequest, ClientRankingInfo>, AdvancedClientStatsResourceQueryHelper>();
services
.AddSingleton<IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo>,
AdvancedClientStatsResourceQueryHelper>();