update server distribution calculations to account for performance bucket
This commit is contained in:
parent
7d67a3dfc9
commit
02eca5637f
@ -5,7 +5,7 @@ namespace Stats.Client.Abstractions
|
|||||||
public interface IServerDistributionCalculator
|
public interface IServerDistributionCalculator
|
||||||
{
|
{
|
||||||
Task Initialize();
|
Task Initialize();
|
||||||
Task<double> GetZScoreForServer(long serverId, double value);
|
Task<double> GetZScoreForServerOrBucket(double value, long? serverId = null, string performanceBucket = null);
|
||||||
Task<double?> GetRatingForZScore(double? value);
|
Task<double?> GetRatingForZScore(double? value, string performanceBucket);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,9 @@ using Data.Abstractions;
|
|||||||
using Data.Models.Client;
|
using Data.Models.Client;
|
||||||
using Data.Models.Client.Stats;
|
using Data.Models.Client.Stats;
|
||||||
using IW4MAdmin.Plugins.Stats;
|
using IW4MAdmin.Plugins.Stats;
|
||||||
using IW4MAdmin.Plugins.Stats.Config;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Configuration;
|
||||||
using Stats.Client.Abstractions;
|
using Stats.Client.Abstractions;
|
||||||
using Stats.Config;
|
using Stats.Config;
|
||||||
using Stats.Helpers;
|
using Stats.Helpers;
|
||||||
@ -21,76 +20,119 @@ namespace Stats.Client
|
|||||||
{
|
{
|
||||||
private readonly IDatabaseContextFactory _contextFactory;
|
private readonly IDatabaseContextFactory _contextFactory;
|
||||||
|
|
||||||
private readonly IDataValueCache<EFClientStatistics, Dictionary<long, Extensions.LogParams>>
|
private readonly IDataValueCache<EFClientStatistics, Dictionary<string, Extensions.LogParams>>
|
||||||
_distributionCache;
|
_distributionCache;
|
||||||
|
|
||||||
private readonly IDataValueCache<EFClientStatistics, double>
|
private readonly IDataValueCache<EFClientStatistics, double> _maxZScoreCache;
|
||||||
_maxZScoreCache;
|
|
||||||
|
|
||||||
private readonly IConfigurationHandler<StatsConfiguration> _configurationHandler;
|
private readonly StatsConfiguration _configuration;
|
||||||
private readonly List<long> _serverIds = new List<long>();
|
private readonly ApplicationConfiguration _appConfig;
|
||||||
|
private readonly List<long> _serverIds = new();
|
||||||
|
|
||||||
private const string DistributionCacheKey = nameof(DistributionCacheKey);
|
private const string DistributionCacheKey = nameof(DistributionCacheKey);
|
||||||
private const string MaxZScoreCacheKey = nameof(MaxZScoreCacheKey);
|
private const string MaxZScoreCacheKey = nameof(MaxZScoreCacheKey);
|
||||||
|
|
||||||
public ServerDistributionCalculator(IDatabaseContextFactory contextFactory,
|
public ServerDistributionCalculator(IDatabaseContextFactory contextFactory,
|
||||||
IDataValueCache<EFClientStatistics, Dictionary<long, Extensions.LogParams>> distributionCache,
|
IDataValueCache<EFClientStatistics, Dictionary<string, Extensions.LogParams>> distributionCache,
|
||||||
IDataValueCache<EFClientStatistics, double> maxZScoreCache,
|
IDataValueCache<EFClientStatistics, double> maxZScoreCache,
|
||||||
IConfigurationHandlerFactory configFactory)
|
StatsConfiguration config, ApplicationConfiguration appConfig)
|
||||||
{
|
{
|
||||||
_contextFactory = contextFactory;
|
_contextFactory = contextFactory;
|
||||||
_distributionCache = distributionCache;
|
_distributionCache = distributionCache;
|
||||||
_maxZScoreCache = maxZScoreCache;
|
_maxZScoreCache = maxZScoreCache;
|
||||||
_configurationHandler = configFactory.GetConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
|
_configuration = config;
|
||||||
|
_appConfig = appConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Initialize()
|
public async Task Initialize()
|
||||||
{
|
{
|
||||||
await LoadServers();
|
await LoadServers();
|
||||||
_distributionCache.SetCacheItem((async (set, token) =>
|
|
||||||
|
_distributionCache.SetCacheItem(async (set, token) =>
|
||||||
{
|
{
|
||||||
await _configurationHandler.BuildAsync();
|
var validPlayTime = _configuration.TopPlayersMinPlayTime;
|
||||||
var validPlayTime = _configurationHandler.Configuration()?.TopPlayersMinPlayTime ?? 3600 * 3;
|
var distributions = new Dictionary<string, Extensions.LogParams>();
|
||||||
|
|
||||||
var distributions = new Dictionary<long, Extensions.LogParams>();
|
|
||||||
|
|
||||||
await LoadServers();
|
await LoadServers();
|
||||||
|
|
||||||
|
var iqPerformances = set
|
||||||
|
.Where(s => s.Skill > 0)
|
||||||
|
.Where(s => s.EloRating > 0)
|
||||||
|
.Where(s => s.Client.Level != EFClient.Permission.Banned);
|
||||||
|
|
||||||
foreach (var serverId in _serverIds)
|
foreach (var serverId in _serverIds)
|
||||||
{
|
{
|
||||||
var performance = await set
|
var performances = await iqPerformances.Where(s => s.ServerId == serverId)
|
||||||
.Where(s => s.ServerId == serverId)
|
|
||||||
.Where(s => s.Skill > 0)
|
|
||||||
.Where(s => s.EloRating > 0)
|
|
||||||
.Where(s => s.Client.Level != EFClient.Permission.Banned)
|
|
||||||
.Where(s => s.TimePlayed >= validPlayTime)
|
.Where(s => s.TimePlayed >= validPlayTime)
|
||||||
.Where(s => s.UpdatedAt >= Extensions.FifteenDaysAgo())
|
.Where(s => s.UpdatedAt >= Extensions.FifteenDaysAgo())
|
||||||
.Select(s => s.EloRating * 1 / 3.0 + s.Skill * 2 / 3.0).ToListAsync();
|
.Select(s => s.EloRating * 1 / 3.0 + s.Skill * 2 / 3.0)
|
||||||
var distributionParams = performance.GenerateDistributionParameters();
|
.ToListAsync(token);
|
||||||
distributions.Add(serverId, distributionParams);
|
var distributionParams = performances.GenerateDistributionParameters();
|
||||||
|
distributions.Add(serverId.ToString(), distributionParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var server in _appConfig.Servers)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(server.PerformanceBucket))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bucketConfig =
|
||||||
|
_configuration.PerformanceBuckets.FirstOrDefault(bucket =>
|
||||||
|
bucket.Name == server.PerformanceBucket) ?? new PerformanceBucketConfiguration();
|
||||||
|
|
||||||
|
var oldestPerf = DateTimeOffset.UtcNow - bucketConfig.RankingExpiration;
|
||||||
|
var performances = await iqPerformances
|
||||||
|
.Where(perf => perf.Server.PerformanceBucket == server.PerformanceBucket)
|
||||||
|
.Where(perf => perf.TimePlayed >= bucketConfig.ClientMinPlayTime.TotalSeconds)
|
||||||
|
.Where(perf => perf.UpdatedAt >= oldestPerf)
|
||||||
|
.Select(s => s.EloRating * 1 / 3.0 + s.Skill * 2 / 3.0)
|
||||||
|
.ToListAsync(token);
|
||||||
|
var distributionParams = performances.GenerateDistributionParameters();
|
||||||
|
distributions.Add(server.PerformanceBucket, distributionParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
return distributions;
|
return distributions;
|
||||||
}), DistributionCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromHours(1));
|
}, DistributionCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromHours(1));
|
||||||
|
|
||||||
_maxZScoreCache.SetCacheItem(async (set, token) =>
|
foreach (var server in _appConfig.Servers)
|
||||||
{
|
{
|
||||||
await _configurationHandler.BuildAsync();
|
_maxZScoreCache.SetCacheItem(async (set, ids, token) =>
|
||||||
var validPlayTime = _configurationHandler.Configuration()?.TopPlayersMinPlayTime ?? 3600 * 3;
|
{
|
||||||
|
var validPlayTime = _configuration.TopPlayersMinPlayTime;
|
||||||
|
var oldestStat = TimeSpan.FromSeconds(_configuration.TopPlayersMinPlayTime);
|
||||||
|
var performanceBucket = (string)ids.FirstOrDefault();
|
||||||
|
|
||||||
var zScore = await set
|
if (!string.IsNullOrEmpty(performanceBucket))
|
||||||
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(validPlayTime))
|
{
|
||||||
.Where(s => s.Skill > 0)
|
var bucketConfig =
|
||||||
.Where(s => s.EloRating > 0)
|
_configuration.PerformanceBuckets.FirstOrDefault(cfg =>
|
||||||
.GroupBy(stat => stat.ClientId)
|
cfg.Name == performanceBucket) ?? new PerformanceBucketConfiguration();
|
||||||
.Select(group =>
|
|
||||||
group.Sum(stat => stat.ZScore * stat.TimePlayed) / group.Sum(stat => stat.TimePlayed))
|
validPlayTime = (int)bucketConfig.ClientMinPlayTime.TotalSeconds;
|
||||||
.MaxAsync(avgZScore => (double?) avgZScore, token);
|
oldestStat = bucketConfig.RankingExpiration;
|
||||||
return zScore ?? 0;
|
}
|
||||||
}, MaxZScoreCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromMinutes(30));
|
|
||||||
|
var zScore = await set
|
||||||
|
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(validPlayTime, oldestStat))
|
||||||
|
.Where(s => s.Skill > 0)
|
||||||
|
.Where(s => s.EloRating > 0)
|
||||||
|
.Where(stat =>
|
||||||
|
performanceBucket == null || performanceBucket == 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 },
|
||||||
|
Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromMinutes(30));
|
||||||
|
|
||||||
|
await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new[] { server.PerformanceBucket });
|
||||||
|
}
|
||||||
|
|
||||||
await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
|
await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
|
||||||
await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new CancellationToken());
|
|
||||||
|
|
||||||
/*foreach (var serverId in _serverIds)
|
/*foreach (var serverId in _serverIds)
|
||||||
{
|
{
|
||||||
@ -131,16 +173,28 @@ namespace Stats.Client
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<double> GetZScoreForServer(long serverId, double value)
|
public async Task<double> GetZScoreForServerOrBucket(double value, long? serverId = null,
|
||||||
|
string performanceBucket = null)
|
||||||
{
|
{
|
||||||
var serverParams = await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
|
if (serverId is null && performanceBucket is null)
|
||||||
if (!serverParams.ContainsKey(serverId))
|
|
||||||
{
|
{
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sdParams = serverParams[serverId];
|
var serverParams = await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
|
||||||
if (sdParams.Sigma == 0)
|
Extensions.LogParams sdParams = null;
|
||||||
|
|
||||||
|
if (serverId is not null && serverParams.TryGetValue(serverId.ToString(), out var sdParams1))
|
||||||
|
{
|
||||||
|
sdParams = sdParams1;
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (performanceBucket is not null && serverParams.TryGetValue(performanceBucket, out var sdParams2))
|
||||||
|
{
|
||||||
|
sdParams = sdParams2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sdParams is null || sdParams.Sigma == 0)
|
||||||
{
|
{
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
@ -149,9 +203,9 @@ namespace Stats.Client
|
|||||||
return zScore;
|
return zScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<double?> GetRatingForZScore(double? value)
|
public async Task<double?> GetRatingForZScore(double? value, string performanceBucket)
|
||||||
{
|
{
|
||||||
var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new CancellationToken());
|
var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new [] { performanceBucket });
|
||||||
return maxZScore == 0 ? null : value.GetRatingForZScore(maxZScore);
|
return maxZScore == 0 ? null : value.GetRatingForZScore(maxZScore);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
Plugins/Stats/Config/PerformanceBucketConfiguration.cs
Normal file
10
Plugins/Stats/Config/PerformanceBucketConfiguration.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Stats.Config;
|
||||||
|
|
||||||
|
public class PerformanceBucketConfiguration
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public TimeSpan ClientMinPlayTime { get; set; } = TimeSpan.FromHours(3);
|
||||||
|
public TimeSpan RankingExpiration { get; set; } = TimeSpan.FromDays(15);
|
||||||
|
}
|
@ -19,6 +19,8 @@ namespace Stats.Config
|
|||||||
public int MostKillsClientLimit { get; set; } = 5;
|
public int MostKillsClientLimit { get; set; } = 5;
|
||||||
public bool EnableAdvancedMetrics { get; set; } = true;
|
public bool EnableAdvancedMetrics { get; set; } = true;
|
||||||
|
|
||||||
|
public List<PerformanceBucketConfiguration> PerformanceBuckets { get; set; } = new();
|
||||||
|
|
||||||
public WeaponNameParserConfiguration[] WeaponNameParserConfigurations { get; set; } = {
|
public WeaponNameParserConfiguration[] WeaponNameParserConfigurations { get; set; } = {
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
|
@ -145,11 +145,12 @@ namespace Stats.Helpers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Expression<Func<EFClientStatistics, bool>> GetRankingFunc(int minPlayTime, double? zScore = null,
|
public static Expression<Func<EFClientStatistics, bool>> GetRankingFunc(int minPlayTime, TimeSpan expiration, double? zScore = null,
|
||||||
long? serverId = null)
|
long? serverId = null)
|
||||||
{
|
{
|
||||||
return (stats) => (serverId == null || stats.ServerId == serverId) &&
|
var oldestStat = DateTimeOffset.UtcNow.Subtract(expiration);
|
||||||
stats.UpdatedAt >= Extensions.FifteenDaysAgo() &&
|
return stats => (serverId == null || stats.ServerId == serverId) &&
|
||||||
|
stats.UpdatedAt >= oldestStat &&
|
||||||
stats.Client.Level != EFClient.Permission.Banned &&
|
stats.Client.Level != EFClient.Permission.Banned &&
|
||||||
stats.TimePlayed >= minPlayTime
|
stats.TimePlayed >= minPlayTime
|
||||||
&& (zScore == null || stats.ZScore > zScore);
|
&& (zScore == null || stats.ZScore > zScore);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using IW4MAdmin.Plugins.Stats.Cheat;
|
using IW4MAdmin.Plugins.Stats.Cheat;
|
||||||
using IW4MAdmin.Plugins.Stats.Config;
|
|
||||||
using IW4MAdmin.Plugins.Stats.Web.Dtos;
|
using IW4MAdmin.Plugins.Stats.Web.Dtos;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
@ -1185,93 +1184,122 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
{
|
{
|
||||||
await using var context = _contextFactory.CreateContext();
|
await using var context = _contextFactory.CreateContext();
|
||||||
var minPlayTime = _config.TopPlayersMinPlayTime;
|
var minPlayTime = _config.TopPlayersMinPlayTime;
|
||||||
|
var oldestStat = DateTimeOffset.UtcNow - Extensions.FifteenDaysAgo();
|
||||||
|
|
||||||
|
var performanceBucket =
|
||||||
|
(await _serverCache.FirstAsync(server => server.Id == serverId)).PerformanceBucket;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(performanceBucket))
|
||||||
|
{
|
||||||
|
var bucketConfig = _config.PerformanceBuckets.FirstOrDefault(cfg => cfg.Name == performanceBucket) ??
|
||||||
|
new PerformanceBucketConfiguration();
|
||||||
|
|
||||||
|
minPlayTime = (int)bucketConfig.ClientMinPlayTime.TotalSeconds;
|
||||||
|
oldestStat = bucketConfig.RankingExpiration;
|
||||||
|
}
|
||||||
|
|
||||||
var performances = await context.Set<EFClientStatistics>()
|
var performances = await context.Set<EFClientStatistics>()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(stat => stat.ClientId == clientId)
|
.Where(stat => stat.ClientId == clientId)
|
||||||
.Where(stat => stat.ServerId != serverId) // ignore the one we're currently tracking
|
.Where(stat => stat.ServerId != serverId) // ignore the one we're currently tracking
|
||||||
.Where(stats => stats.UpdatedAt >= Extensions.FifteenDaysAgo())
|
.Where(stats => stats.UpdatedAt >= DateTimeOffset.UtcNow - oldestStat)
|
||||||
.Where(stats => stats.TimePlayed >= minPlayTime)
|
.Where(stats => stats.TimePlayed >= minPlayTime)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
if (clientStats.TimePlayed >= minPlayTime)
|
if (clientStats.TimePlayed >= minPlayTime)
|
||||||
{
|
{
|
||||||
clientStats.ZScore = await _serverDistributionCalculator.GetZScoreForServer(serverId,
|
await UpdateForServer(clientId, clientStats, context, minPlayTime, oldestStat, serverId);
|
||||||
clientStats.Performance);
|
|
||||||
|
|
||||||
var serverRanking = await context.Set<EFClientStatistics>()
|
|
||||||
.Where(stats => stats.ClientId != clientStats.ClientId)
|
|
||||||
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(
|
|
||||||
_config.TopPlayersMinPlayTime, clientStats.ZScore, serverId))
|
|
||||||
.CountAsync();
|
|
||||||
|
|
||||||
var serverRankingSnapshot = new EFClientRankingHistory
|
|
||||||
{
|
|
||||||
ClientId = clientId,
|
|
||||||
ServerId = serverId,
|
|
||||||
ZScore = clientStats.ZScore,
|
|
||||||
Ranking = serverRanking,
|
|
||||||
PerformanceMetric = clientStats.Performance,
|
|
||||||
Newest = true
|
|
||||||
};
|
|
||||||
|
|
||||||
context.Add(serverRankingSnapshot);
|
|
||||||
await PruneOldRankings(context, clientId, serverId);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
|
|
||||||
performances.Add(clientStats);
|
performances.Add(clientStats);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (performances.Any(performance => performance.TimePlayed >= minPlayTime))
|
if (performances.Any(performance => performance.TimePlayed >= minPlayTime))
|
||||||
{
|
{
|
||||||
var aggregateZScore =
|
await UpdateAggregateForServerOrBucket(clientId, clientStats, context, performances, minPlayTime,
|
||||||
performances.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), minPlayTime);
|
oldestStat, performanceBucket);
|
||||||
|
|
||||||
int? aggregateRanking = await context.Set<EFClientStatistics>()
|
|
||||||
.Where(stat => stat.ClientId != clientId)
|
|
||||||
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(minPlayTime))
|
|
||||||
.GroupBy(stat => stat.ClientId)
|
|
||||||
.Where(group =>
|
|
||||||
group.Sum(stat => stat.ZScore * stat.TimePlayed) / group.Sum(stat => stat.TimePlayed) >
|
|
||||||
aggregateZScore)
|
|
||||||
.Select(c => c.Key)
|
|
||||||
.CountAsync();
|
|
||||||
|
|
||||||
var newPerformanceMetric = await _serverDistributionCalculator.GetRatingForZScore(aggregateZScore);
|
|
||||||
|
|
||||||
if (newPerformanceMetric == null)
|
|
||||||
{
|
|
||||||
_log.LogWarning("Could not determine performance metric for {Client} {AggregateZScore}",
|
|
||||||
clientStats.Client?.ToString(), aggregateZScore);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var aggregateRankingSnapshot = new EFClientRankingHistory
|
|
||||||
{
|
|
||||||
ClientId = clientId,
|
|
||||||
ZScore = aggregateZScore,
|
|
||||||
Ranking = aggregateRanking,
|
|
||||||
PerformanceMetric = newPerformanceMetric,
|
|
||||||
Newest = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
context.Add(aggregateRankingSnapshot);
|
|
||||||
|
|
||||||
await PruneOldRankings(context, clientId);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PruneOldRankings(DatabaseContext context, int clientId, long? serverId = null)
|
private async Task UpdateAggregateForServerOrBucket(int clientId, EFClientStatistics clientStats, DatabaseContext context, List<EFClientStatistics> performances,
|
||||||
|
int minPlayTime, TimeSpan oldestStat, string performanceBucket)
|
||||||
|
{
|
||||||
|
var aggregateZScore =
|
||||||
|
performances.Where(performance => performance.Server.PerformanceBucket == performanceBucket)
|
||||||
|
.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), minPlayTime);
|
||||||
|
|
||||||
|
int? aggregateRanking = await context.Set<EFClientStatistics>()
|
||||||
|
.Where(stat => stat.ClientId != clientId)
|
||||||
|
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(minPlayTime, oldestStat))
|
||||||
|
.GroupBy(stat => stat.ClientId)
|
||||||
|
.Where(group =>
|
||||||
|
group.Sum(stat => stat.ZScore * stat.TimePlayed) / group.Sum(stat => stat.TimePlayed) >
|
||||||
|
aggregateZScore)
|
||||||
|
.Select(c => c.Key)
|
||||||
|
.CountAsync();
|
||||||
|
|
||||||
|
var newPerformanceMetric = await _serverDistributionCalculator.GetRatingForZScore(aggregateZScore, performanceBucket);
|
||||||
|
|
||||||
|
if (newPerformanceMetric == null)
|
||||||
|
{
|
||||||
|
_log.LogWarning("Could not determine performance metric for {Client} {AggregateZScore}",
|
||||||
|
clientStats.Client?.ToString(), aggregateZScore);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var aggregateRankingSnapshot = new EFClientRankingHistory
|
||||||
|
{
|
||||||
|
ClientId = clientId,
|
||||||
|
ZScore = aggregateZScore,
|
||||||
|
Ranking = aggregateRanking,
|
||||||
|
PerformanceMetric = newPerformanceMetric,
|
||||||
|
PerformanceBucket = performanceBucket,
|
||||||
|
Newest = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Add(aggregateRankingSnapshot);
|
||||||
|
|
||||||
|
await PruneOldRankings(context, clientId);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateForServer(int clientId, EFClientStatistics clientStats, DatabaseContext context,
|
||||||
|
int minPlayTime, TimeSpan oldestStat, long? serverId = null)
|
||||||
|
{
|
||||||
|
clientStats.ZScore =
|
||||||
|
await _serverDistributionCalculator.GetZScoreForServerOrBucket(clientStats.Performance, serverId);
|
||||||
|
|
||||||
|
var serverRanking = await context.Set<EFClientStatistics>()
|
||||||
|
.Where(stats => stats.ClientId != clientStats.ClientId)
|
||||||
|
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(minPlayTime, oldestStat,
|
||||||
|
clientStats.ZScore, serverId))
|
||||||
|
.CountAsync();
|
||||||
|
|
||||||
|
var serverRankingSnapshot = new EFClientRankingHistory
|
||||||
|
{
|
||||||
|
ClientId = clientId,
|
||||||
|
ServerId = serverId,
|
||||||
|
ZScore = clientStats.ZScore,
|
||||||
|
Ranking = serverRanking,
|
||||||
|
PerformanceMetric = clientStats.Performance,
|
||||||
|
Newest = true
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Add(serverRankingSnapshot);
|
||||||
|
await PruneOldRankings(context, clientId, serverId);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PruneOldRankings(DatabaseContext context, int clientId, long? serverId = null, string performanceBucket = null)
|
||||||
{
|
{
|
||||||
var totalRankingEntries = await context.Set<EFClientRankingHistory>()
|
var totalRankingEntries = await context.Set<EFClientRankingHistory>()
|
||||||
.Where(r => r.ClientId == clientId)
|
.Where(r => r.ClientId == clientId)
|
||||||
.Where(r => r.ServerId == serverId)
|
.Where(r => r.ServerId == serverId)
|
||||||
|
.Where(r => r.PerformanceBucket == performanceBucket)
|
||||||
.CountAsync();
|
.CountAsync();
|
||||||
|
|
||||||
var mostRecent = await context.Set<EFClientRankingHistory>()
|
var mostRecent = await context.Set<EFClientRankingHistory>()
|
||||||
.Where(r => r.ClientId == clientId)
|
.Where(r => r.ClientId == clientId)
|
||||||
.Where(r => r.ServerId == serverId)
|
.Where(r => r.ServerId == serverId)
|
||||||
|
.Where(r => r.PerformanceBucket == performanceBucket)
|
||||||
.FirstOrDefaultAsync(r => r.Newest);
|
.FirstOrDefaultAsync(r => r.Newest);
|
||||||
|
|
||||||
if (mostRecent != null)
|
if (mostRecent != null)
|
||||||
@ -1287,6 +1315,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
var lastRating = await context.Set<EFClientRankingHistory>()
|
var lastRating = await context.Set<EFClientRankingHistory>()
|
||||||
.Where(r => r.ClientId == clientId)
|
.Where(r => r.ClientId == clientId)
|
||||||
.Where(r => r.ServerId == serverId)
|
.Where(r => r.ServerId == serverId)
|
||||||
|
.Where(r => r.PerformanceBucket == performanceBucket)
|
||||||
.OrderBy(r => r.CreatedDateTime)
|
.OrderBy(r => r.CreatedDateTime)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
@ -1413,7 +1442,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
}
|
}
|
||||||
|
|
||||||
clientStats.SPM = Math.Round(clientStats.SPM, 3);
|
clientStats.SPM = Math.Round(clientStats.SPM, 3);
|
||||||
clientStats.Skill = Math.Round((clientStats.SPM * KDRWeight), 3);
|
var skillFunction = client.GetAdditionalProperty<Func<EFClient, EFClientStatistics, double>>("SkillFunction");
|
||||||
|
|
||||||
|
clientStats.Skill =
|
||||||
|
skillFunction?.Invoke(client, clientStats) ?? Math.Round(clientStats.SPM * KDRWeight, 3);
|
||||||
|
|
||||||
// fixme: how does this happen?
|
// fixme: how does this happen?
|
||||||
if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill))
|
if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill))
|
||||||
@ -1424,7 +1456,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
killSPM = killSpm, KDRWeight, totalPlayTime, SPMAgainstPlayWeight, clientStats, scoreDifference
|
killSPM = killSpm, KDRWeight, totalPlayTime, SPMAgainstPlayWeight, clientStats, scoreDifference
|
||||||
});
|
});
|
||||||
clientStats.SPM = 0;
|
clientStats.SPM = 0;
|
||||||
clientStats.Skill = 0;
|
if (skillFunction is null)
|
||||||
|
{
|
||||||
|
clientStats.Skill = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clientStats.LastStatCalculation = DateTime.UtcNow;
|
clientStats.LastStatCalculation = DateTime.UtcNow;
|
||||||
|
@ -54,6 +54,7 @@ namespace SharedLibraryCore.Configuration
|
|||||||
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_CUSTOM_HOSTNAME")]
|
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_CUSTOM_HOSTNAME")]
|
||||||
[ConfigurationOptional]
|
[ConfigurationOptional]
|
||||||
public string CustomHostname { get; set; }
|
public string CustomHostname { get; set; }
|
||||||
|
public string PerformanceBucket { get; set; }
|
||||||
|
|
||||||
public IBaseConfiguration Generate()
|
public IBaseConfiguration Generate()
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user