mirror of
https://github.com/RaidMax/IW4M-Admin.git
synced 2025-06-10 23:31:13 -05:00
Additional zombie stast work
This commit is contained in:
@ -57,7 +57,7 @@ namespace Stats.Client
|
||||
|
||||
var iqPerformances = set
|
||||
.Where(s => s.Skill > 0)
|
||||
.Where(s => s.EloRating > 0)
|
||||
.Where(s => s.EloRating >= 0)
|
||||
.Where(s => s.Client.Level != EFClient.Permission.Banned);
|
||||
|
||||
foreach (var serverId in _serverIds)
|
||||
@ -71,30 +71,33 @@ namespace Stats.Client
|
||||
distributions.Add(serverId.ToString(), distributionParams);
|
||||
}
|
||||
|
||||
foreach (var server in _appConfig.Servers)
|
||||
foreach (var performanceBucketGroup in _appConfig.Servers.GroupBy(server => server.PerformanceBucket))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(server.PerformanceBucket))
|
||||
if (string.IsNullOrWhiteSpace(performanceBucketGroup.Key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var performanceBucket = performanceBucketGroup.Key;
|
||||
|
||||
var bucketConfig =
|
||||
_configuration.PerformanceBuckets.FirstOrDefault(bucket =>
|
||||
bucket.Name == server.PerformanceBucket) ?? new PerformanceBucketConfiguration();
|
||||
bucket.Name == performanceBucket) ?? new PerformanceBucketConfiguration();
|
||||
|
||||
var oldestPerf = DateTimeOffset.UtcNow - bucketConfig.RankingExpiration;
|
||||
var oldestPerf = DateTime.UtcNow - bucketConfig.RankingExpiration;
|
||||
var performances = await iqPerformances
|
||||
.Where(perf => perf.Server.PerformanceBucket == server.PerformanceBucket)
|
||||
.Where(perf => perf.Server.PerformanceBucket == performanceBucket)
|
||||
.Where(perf => perf.TimePlayed >= bucketConfig.ClientMinPlayTime.TotalSeconds)
|
||||
.Where(perf => perf.UpdatedAt >= oldestPerf)
|
||||
.Where(perf => perf.Skill < 999999)
|
||||
.Select(s => s.EloRating * 1 / 3.0 + s.Skill * 2 / 3.0)
|
||||
.ToListAsync(token);
|
||||
var distributionParams = performances.GenerateDistributionParameters();
|
||||
distributions.Add(server.PerformanceBucket, distributionParams);
|
||||
distributions.Add(performanceBucket, distributionParams);
|
||||
}
|
||||
|
||||
return distributions;
|
||||
}, DistributionCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromHours(1));
|
||||
}, DistributionCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(1) : TimeSpan.FromHours(1));
|
||||
|
||||
foreach (var server in _appConfig.Servers)
|
||||
{
|
||||
@ -117,7 +120,7 @@ namespace Stats.Client
|
||||
var zScore = await set
|
||||
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(validPlayTime, oldestStat))
|
||||
.Where(s => s.Skill > 0)
|
||||
.Where(s => s.EloRating > 0)
|
||||
.Where(s => s.EloRating >= 1)
|
||||
.Where(stat =>
|
||||
performanceBucket == null || performanceBucket == stat.Server.PerformanceBucket)
|
||||
.GroupBy(stat => stat.ClientId)
|
||||
@ -127,7 +130,7 @@ namespace Stats.Client
|
||||
|
||||
return zScore ?? 0;
|
||||
}, MaxZScoreCacheKey, new[] { server.PerformanceBucket },
|
||||
Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromMinutes(30));
|
||||
Utilities.IsDevelopment ? TimeSpan.FromMinutes(1) : TimeSpan.FromMinutes(30));
|
||||
|
||||
await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new[] { server.PerformanceBucket });
|
||||
}
|
||||
@ -199,6 +202,8 @@ namespace Stats.Client
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
value = Math.Max(1, value);
|
||||
|
||||
var zScore = (Math.Log(value) - sdParams.Mean) / sdParams.Sigma;
|
||||
return zScore;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Data.Models;
|
||||
using Data.Models.Client;
|
||||
using Data.Models.Client.Stats;
|
||||
using SharedLibraryCore.Dtos;
|
||||
@ -25,5 +26,6 @@ namespace Stats.Dtos
|
||||
public List<EFClientHitStatistic> ByAttachmentCombo { get; set; }
|
||||
public List<EFClientRankingHistory> Ratings { get; set; }
|
||||
public List<EFClientStatistics> LegacyStats { get; set; }
|
||||
public List<EFMeta> CustomMetrics { get; set; } = new();
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
using SharedLibraryCore.Dtos;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Data.Models;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Web.Dtos
|
||||
{
|
||||
@ -22,6 +22,7 @@ namespace IW4MAdmin.Plugins.Stats.Web.Dtos
|
||||
public List<PerformanceHistory> PerformanceHistory { get; set; }
|
||||
public double? ZScore { get; set; }
|
||||
public long? ServerId { get; set; }
|
||||
public List<EFMeta> Metrics { get; } = new();
|
||||
}
|
||||
|
||||
public class PerformanceHistory
|
||||
|
@ -110,25 +110,26 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
return 0;
|
||||
}
|
||||
|
||||
public Expression<Func<EFClientRankingHistory, bool>> GetNewRankingFunc(int? clientId = null,
|
||||
long? serverId = null)
|
||||
private Expression<Func<EFClientRankingHistory, bool>> GetNewRankingFunc(TimeSpan oldestStat, TimeSpan minPlayTime, long? serverId = null)
|
||||
{
|
||||
return (ranking) => ranking.ServerId == serverId
|
||||
&& ranking.Client.Level != Data.Models.Client.EFClient.Permission.Banned
|
||||
&& ranking.CreatedDateTime >= Extensions.FifteenDaysAgo()
|
||||
&& ranking.ZScore != null
|
||||
&& ranking.PerformanceMetric != null
|
||||
&& ranking.Newest
|
||||
&& ranking.Client.TotalConnectionTime >=
|
||||
_config.TopPlayersMinPlayTime;
|
||||
var oldestDate = DateTime.UtcNow - oldestStat;
|
||||
return ranking => ranking.ServerId == serverId
|
||||
&& ranking.Client.Level != Data.Models.Client.EFClient.Permission.Banned
|
||||
&& ranking.CreatedDateTime >= oldestDate
|
||||
&& ranking.ZScore != null
|
||||
&& ranking.PerformanceMetric != null
|
||||
&& ranking.Newest
|
||||
&& ranking.Client.TotalConnectionTime >= (int)minPlayTime.TotalSeconds;
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalRankedPlayers(long serverId)
|
||||
{
|
||||
var bucketConfig = await GetBucketConfig(serverId);
|
||||
|
||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||
|
||||
return await context.Set<EFClientRankingHistory>()
|
||||
.Where(GetNewRankingFunc(serverId: serverId))
|
||||
.Where(GetNewRankingFunc(bucketConfig.RankingExpiration, bucketConfig.ClientMinPlayTime, serverId: serverId))
|
||||
.CountAsync();
|
||||
}
|
||||
|
||||
@ -143,12 +144,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
public DateTime CreatedDateTime { get; set; }
|
||||
}
|
||||
|
||||
public async Task<List<TopStatsInfo>> GetNewTopStats(int start, int count, long? serverId = null)
|
||||
public async Task<List<TopStatsInfo>> GetNewTopStats(int start, int count, long? serverId = null, string performanceBucket = null)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
var bucketConfig = await GetBucketConfig(serverId);
|
||||
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
var clientIdsList = await context.Set<EFClientRankingHistory>()
|
||||
.Where(GetNewRankingFunc(serverId: serverId))
|
||||
.Where(GetNewRankingFunc(bucketConfig.RankingExpiration, bucketConfig.ClientMinPlayTime, serverId: serverId))
|
||||
.OrderByDescending(ranking => ranking.PerformanceMetric)
|
||||
.Select(ranking => ranking.ClientId)
|
||||
.Skip(start)
|
||||
@ -233,9 +235,77 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
.OrderBy(r => r.Ranking)
|
||||
.ToList();
|
||||
|
||||
foreach (var topStatsInfo in finished)
|
||||
{
|
||||
topStatsInfo.Metrics.AddRange(new EFMeta[]
|
||||
{
|
||||
new()
|
||||
{
|
||||
Extra = "Kills",
|
||||
Value = topStatsInfo.Kills.ToNumericalString(),
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KILLS"]
|
||||
},
|
||||
new()
|
||||
{
|
||||
Extra = "Deaths",
|
||||
Value = topStatsInfo.Deaths.ToNumericalString(),
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_DEATHS"]
|
||||
},
|
||||
new()
|
||||
{
|
||||
Extra = "KDR",
|
||||
Value = topStatsInfo.KDR.ToNumericalString(),
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KDR"]
|
||||
},
|
||||
new()
|
||||
{
|
||||
Extra = "TimePlayed",
|
||||
Value = topStatsInfo.TimePlayedValue.HumanizeForCurrentCulture(),
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_PLAYER"]
|
||||
},
|
||||
new()
|
||||
{
|
||||
Extra = "LastSeen",
|
||||
Value = topStatsInfo.LastSeenValue.HumanizeForCurrentCulture(),
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_LSEEN"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var customMetricFunc in Plugin.ServerManager.CustomStatsMetrics)
|
||||
{
|
||||
await customMetricFunc(finished.ToDictionary(kvp => kvp.ClientId, kvp => kvp.Metrics), serverId,
|
||||
performanceBucket, true);
|
||||
}
|
||||
|
||||
return finished;
|
||||
}
|
||||
|
||||
private async Task<PerformanceBucketConfiguration> GetBucketConfig(long? serverId)
|
||||
{
|
||||
var defaultConfig = new PerformanceBucketConfiguration
|
||||
{
|
||||
ClientMinPlayTime = TimeSpan.FromSeconds(_config.TopPlayersMinPlayTime),
|
||||
RankingExpiration = DateTime.UtcNow - Extensions.FifteenDaysAgo()
|
||||
};
|
||||
|
||||
if (serverId is null)
|
||||
{
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
var performanceBucket =
|
||||
(await _serverCache.FirstAsync(server => server.Id == serverId)).PerformanceBucket;
|
||||
|
||||
if (string.IsNullOrEmpty(performanceBucket))
|
||||
{
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
return _config.PerformanceBuckets.FirstOrDefault(bucket => bucket.Name == performanceBucket) ??
|
||||
defaultConfig;
|
||||
}
|
||||
|
||||
public async Task<List<TopStatsInfo>> GetTopStats(int start, int count, long? serverId = null)
|
||||
{
|
||||
if (_config.EnableAdvancedMetrics)
|
||||
@ -1179,54 +1249,41 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
public async Task UpdateHistoricalRanking(int clientId, EFClientStatistics clientStats, long serverId)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext();
|
||||
var minPlayTime = _config.TopPlayersMinPlayTime;
|
||||
var oldestStat = DateTimeOffset.UtcNow - Extensions.FifteenDaysAgo();
|
||||
var bucketConfig = await GetBucketConfig(serverId);
|
||||
|
||||
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 oldestStateDate = DateTime.UtcNow - oldestStat;
|
||||
await using var context = _contextFactory.CreateContext();
|
||||
var oldestStateDate = DateTime.UtcNow - bucketConfig.RankingExpiration;
|
||||
var performances = await context.Set<EFClientStatistics>()
|
||||
.AsNoTracking()
|
||||
.Include(stat => stat.Server)
|
||||
.Where(stat => stat.ClientId == clientId)
|
||||
.Where(stat => stat.ServerId != serverId) // ignore the one we're currently tracking
|
||||
.Where(stats => stats.UpdatedAt >= oldestStateDate)
|
||||
.Where(stats => stats.TimePlayed >= minPlayTime)
|
||||
.Where(stats => stats.TimePlayed >= (int)bucketConfig.ClientMinPlayTime.TotalSeconds)
|
||||
.ToListAsync();
|
||||
|
||||
if (clientStats.TimePlayed >= minPlayTime)
|
||||
if (clientStats.TimePlayed >= bucketConfig.ClientMinPlayTime.TotalSeconds)
|
||||
{
|
||||
await UpdateForServer(clientId, clientStats, context, minPlayTime, oldestStat, serverId);
|
||||
await UpdateForServer(clientId, clientStats, context, (int)bucketConfig.ClientMinPlayTime.TotalSeconds, bucketConfig.RankingExpiration, serverId);
|
||||
clientStats.Server = await _serverCache.FirstAsync(server => server.Id == serverId);
|
||||
performances.Add(clientStats);
|
||||
}
|
||||
|
||||
if (performances.Any(performance => performance.TimePlayed >= minPlayTime))
|
||||
if (performances.Any(performance => performance.TimePlayed >= (int)bucketConfig.ClientMinPlayTime.TotalSeconds))
|
||||
{
|
||||
await UpdateAggregateForServerOrBucket(clientId, clientStats, context, performances, minPlayTime,
|
||||
oldestStat, performanceBucket);
|
||||
await UpdateAggregateForServerOrBucket(clientId, clientStats, context, performances, bucketConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateAggregateForServerOrBucket(int clientId, EFClientStatistics clientStats, DatabaseContext context, List<EFClientStatistics> performances,
|
||||
int minPlayTime, TimeSpan oldestStat, string performanceBucket)
|
||||
private async Task UpdateAggregateForServerOrBucket(int clientId, EFClientStatistics clientStats, DatabaseContext context, List<EFClientStatistics> performances, PerformanceBucketConfiguration bucketConfig)
|
||||
{
|
||||
var aggregateZScore =
|
||||
performances.Where(performance => performance.Server.PerformanceBucket == performanceBucket)
|
||||
.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), minPlayTime);
|
||||
performances.Where(performance => performance.Server.PerformanceBucket == bucketConfig.Name)
|
||||
.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), (int)bucketConfig.ClientMinPlayTime.TotalSeconds);
|
||||
|
||||
int? aggregateRanking = await context.Set<EFClientStatistics>()
|
||||
.Where(stat => stat.ClientId != clientId)
|
||||
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(minPlayTime, oldestStat))
|
||||
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc((int)bucketConfig.ClientMinPlayTime.TotalSeconds, bucketConfig.RankingExpiration))
|
||||
.GroupBy(stat => stat.ClientId)
|
||||
.Where(group =>
|
||||
group.Sum(stat => stat.ZScore * stat.TimePlayed) / group.Sum(stat => stat.TimePlayed) >
|
||||
@ -1234,7 +1291,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
.Select(c => c.Key)
|
||||
.CountAsync();
|
||||
|
||||
var newPerformanceMetric = await _serverDistributionCalculator.GetRatingForZScore(aggregateZScore, performanceBucket);
|
||||
var newPerformanceMetric = await _serverDistributionCalculator.GetRatingForZScore(aggregateZScore, bucketConfig.Name);
|
||||
|
||||
if (newPerformanceMetric == null)
|
||||
{
|
||||
@ -1249,13 +1306,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
ZScore = aggregateZScore,
|
||||
Ranking = aggregateRanking,
|
||||
PerformanceMetric = newPerformanceMetric,
|
||||
PerformanceBucket = performanceBucket,
|
||||
PerformanceBucket = bucketConfig.Name,
|
||||
Newest = true,
|
||||
};
|
||||
|
||||
context.Add(aggregateRankingSnapshot);
|
||||
|
||||
await PruneOldRankings(context, clientId);
|
||||
await PruneOldRankings(context, clientId, performanceBucket: bucketConfig.Name);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
@ -1364,6 +1421,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
attackerStats.EloRating = Math.Max(0, Math.Round(attackerStats.EloRating, 2));
|
||||
victimStats.EloRating = Math.Max(0, Math.Round(victimStats.EloRating, 2));
|
||||
|
||||
var attackerEloRatingFunc =
|
||||
attacker.GetAdditionalProperty<Func<EFClient, EFClientStatistics, double>>("EloRatingFunction");
|
||||
|
||||
attackerStats.EloRating =
|
||||
attackerEloRatingFunc?.Invoke(attacker, attackerStats) ?? attackerStats.EloRating;
|
||||
|
||||
var victimEloRatingFunc =
|
||||
victim.GetAdditionalProperty<Func<EFClient, EFClientStatistics, double>>("EloRatingFunction");
|
||||
|
||||
victimStats.EloRating =
|
||||
attackerEloRatingFunc?.Invoke(victim, victimStats) ?? victimStats.EloRating;
|
||||
|
||||
// update after calculation
|
||||
attackerStats.TimePlayed += (int)(DateTime.UtcNow - attackerStats.LastActive).TotalSeconds;
|
||||
victimStats.TimePlayed += (int)(DateTime.UtcNow - victimStats.LastActive).TotalSeconds;
|
||||
@ -1428,7 +1497,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
? (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds
|
||||
: clientStats.TimePlayed + (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds;
|
||||
|
||||
double SPMAgainstPlayWeight = timeSinceLastCalc / Math.Min(600, (totalPlayTime / 60.0));
|
||||
double SPMAgainstPlayWeight = totalPlayTime == 0 ? killSpm : timeSinceLastCalc / Math.Min(600, (totalPlayTime / 60.0));
|
||||
|
||||
// calculate the new weight against average times the weight against play time
|
||||
clientStats.SPM = (killSpm * SPMAgainstPlayWeight) + (clientStats.SPM * (1 - SPMAgainstPlayWeight));
|
||||
@ -1446,7 +1515,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
skillFunction?.Invoke(client, clientStats) ?? Math.Round(clientStats.SPM * KDRWeight, 3);
|
||||
|
||||
// fixme: how does this happen?
|
||||
if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill))
|
||||
if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill) || double.IsInfinity(clientStats.Skill))
|
||||
{
|
||||
_log.LogWarning("clientStats SPM/Skill NaN {@killInfo}",
|
||||
new
|
||||
|
@ -1,10 +1,20 @@
|
||||
using Data.Models.Client.Stats;
|
||||
using System.Linq;
|
||||
using Data.Models.Client.Stats;
|
||||
|
||||
namespace Stats.Helpers
|
||||
{
|
||||
public static class WeaponNameExtensions
|
||||
{
|
||||
public static string RebuildWeaponName(this EFClientHitStatistic stat) =>
|
||||
$"{stat.Weapon?.Name}{string.Join("_", stat.WeaponAttachmentCombo?.Attachment1?.Name, stat.WeaponAttachmentCombo?.Attachment2?.Name, stat.WeaponAttachmentCombo?.Attachment3?.Name)}";
|
||||
public static string RebuildWeaponName(this EFClientHitStatistic stat)
|
||||
{
|
||||
var attachments =
|
||||
new[]
|
||||
{
|
||||
stat.WeaponAttachmentCombo?.Attachment1?.Name, stat.WeaponAttachmentCombo?.Attachment2?.Name,
|
||||
stat.WeaponAttachmentCombo?.Attachment3?.Name
|
||||
}.Where(a => !string.IsNullOrEmpty(a));
|
||||
|
||||
return $"{stat.Weapon?.Name?.Replace("zombie_", "").Replace("_zombie", "")}{string.Join("_", attachments)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -117,6 +117,7 @@ public class Plugin : IPluginV2
|
||||
}
|
||||
};
|
||||
IGameEventSubscriptions.MatchEnded += OnMatchEvent;
|
||||
IGameEventSubscriptions.RoundEnded += (roundEndedEvent, token) => _statManager.Sync(roundEndedEvent.Server, token);
|
||||
IGameEventSubscriptions.MatchStarted += OnMatchEvent;
|
||||
IGameEventSubscriptions.ScriptEventTriggered += OnScriptEvent;
|
||||
IGameEventSubscriptions.ClientKilled += OnClientKilled;
|
||||
|
Reference in New Issue
Block a user