1
0
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:
RaidMax
2024-02-11 22:10:12 -06:00
parent 122b6dc79d
commit e1461582fa
45 changed files with 7663 additions and 292 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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