mirror of
https://github.com/RaidMax/IW4M-Admin.git
synced 2025-06-10 15:20:48 -05:00
huge commit for advanced stats feature.
broke data out into its own library. may be breaking changes with existing plugins
This commit is contained in:
@ -1,10 +1,8 @@
|
||||
using IW4MAdmin.Plugins.Stats.Cheat;
|
||||
using IW4MAdmin.Plugins.Stats.Config;
|
||||
using IW4MAdmin.Plugins.Stats.Models;
|
||||
using IW4MAdmin.Plugins.Stats.Web.Dtos;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
@ -14,8 +12,18 @@ using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Context;
|
||||
using Data.Models;
|
||||
using Data.Models.Client;
|
||||
using Data.Models.Client.Stats;
|
||||
using Data.Models.Server;
|
||||
using Humanizer.Localisation;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Stats.Client.Abstractions;
|
||||
using Stats.Helpers;
|
||||
using static IW4MAdmin.Plugins.Stats.Cheat.Detection;
|
||||
using EFClient = SharedLibraryCore.Database.Models.EFClient;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
@ -30,21 +38,25 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
private static List<EFServer> serverModels;
|
||||
public static string CLIENT_STATS_KEY = "ClientStats";
|
||||
public static string CLIENT_DETECTIONS_KEY = "ClientDetections";
|
||||
private readonly SemaphoreSlim _addPlayerWaiter = new SemaphoreSlim(1, 1);
|
||||
private readonly SemaphoreSlim _addPlayerWaiter = new SemaphoreSlim(1, 1);
|
||||
private readonly IServerDistributionCalculator _serverDistributionCalculator;
|
||||
|
||||
public StatManager(ILogger<StatManager> logger, IManager mgr, IDatabaseContextFactory contextFactory, IConfigurationHandler<StatsConfiguration> configHandler)
|
||||
public StatManager(ILogger<StatManager> logger, IManager mgr, IDatabaseContextFactory contextFactory,
|
||||
IConfigurationHandler<StatsConfiguration> configHandler,
|
||||
IServerDistributionCalculator serverDistributionCalculator)
|
||||
{
|
||||
_servers = new ConcurrentDictionary<long, ServerStats>();
|
||||
_log = logger;
|
||||
_contextFactory = contextFactory;
|
||||
_configHandler = configHandler;
|
||||
_serverDistributionCalculator = serverDistributionCalculator;
|
||||
}
|
||||
|
||||
~StatManager()
|
||||
{
|
||||
_addPlayerWaiter.Dispose();
|
||||
}
|
||||
|
||||
|
||||
private void SetupServerIds()
|
||||
{
|
||||
using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
||||
@ -55,10 +67,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
|
||||
return (r) => r.ServerId == serverId &&
|
||||
r.When > fifteenDaysAgo &&
|
||||
r.RatingHistory.Client.Level != EFClient.Permission.Banned &&
|
||||
r.Newest &&
|
||||
r.ActivityAmount >= _configHandler.Configuration().TopPlayersMinPlayTime;
|
||||
r.When > fifteenDaysAgo &&
|
||||
r.RatingHistory.Client.Level != EFClient.Permission.Banned &&
|
||||
r.Newest &&
|
||||
r.ActivityAmount >= _configHandler.Configuration().TopPlayersMinPlayTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -66,13 +78,23 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
/// </summary>
|
||||
/// <param name="clientId">client id of the player</param>
|
||||
/// <returns></returns>
|
||||
public async Task<int> GetClientOverallRanking(int clientId)
|
||||
public async Task<int> GetClientOverallRanking(int clientId, long? serverId = null)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||
|
||||
if (_configHandler.Configuration().EnableAdvancedMetrics)
|
||||
{
|
||||
var clientRanking = await context.Set<EFClientRankingHistory>()
|
||||
.Where(r => r.ClientId == clientId)
|
||||
.Where(r => r.ServerId == serverId)
|
||||
.Where(r => r.Newest)
|
||||
.FirstOrDefaultAsync();
|
||||
return clientRanking?.Ranking + 1 ?? 0;
|
||||
}
|
||||
|
||||
var clientPerformance = await context.Set<EFRating>()
|
||||
.Where(r => r.RatingHistory.ClientId == clientId)
|
||||
.Where(r => r.ServerId == null)
|
||||
.Where(r => r.ServerId == serverId)
|
||||
.Where(r => r.Newest)
|
||||
.Select(r => r.Performance)
|
||||
.FirstOrDefaultAsync();
|
||||
@ -90,25 +112,115 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
return 0;
|
||||
}
|
||||
|
||||
public async Task<List<TopStatsInfo>> GetNewTopStats(int start, int count, long? serverId = null)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
|
||||
var clientIdsList = await context.Set<EFClientRankingHistory>()
|
||||
.Where(ranking => ranking.ServerId == serverId)
|
||||
.Where(ranking => ranking.Client.Level != Data.Models.Client.EFClient.Permission.Banned)
|
||||
.Where(ranking => ranking.Client.LastConnection >= Extensions.FifteenDaysAgo())
|
||||
.Where(ranking => ranking.ZScore != null)
|
||||
.Where(ranking => ranking.PerformanceMetric != null)
|
||||
.Where(ranking => ranking.Newest)
|
||||
.Where(ranking =>
|
||||
ranking.Client.TotalConnectionTime >= _configHandler.Configuration().TopPlayersMinPlayTime)
|
||||
.OrderByDescending(ranking => ranking.PerformanceMetric)
|
||||
.Select(ranking => ranking.ClientId)
|
||||
.Skip(start)
|
||||
.Take(count)
|
||||
.ToListAsync();
|
||||
|
||||
var rankings = await context.Set<EFClientRankingHistory>()
|
||||
.Where(ranking => clientIdsList.Contains(ranking.ClientId))
|
||||
.Where(ranking => ranking.ServerId == serverId)
|
||||
.Select(ranking => new
|
||||
{
|
||||
ranking.ClientId,
|
||||
ranking.Client.CurrentAlias.Name,
|
||||
ranking.Client.LastConnection,
|
||||
ranking.PerformanceMetric,
|
||||
ranking.ZScore,
|
||||
ranking.Ranking,
|
||||
ranking.CreatedDateTime
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var rankingsDict = rankings.GroupBy(rank => rank.ClientId)
|
||||
.ToDictionary(rank => rank.Key, rank => rank.OrderBy(r => r.CreatedDateTime).ToList());
|
||||
|
||||
var statsInfo = await context.Set<EFClientStatistics>()
|
||||
.Where(stat => clientIdsList.Contains(stat.ClientId))
|
||||
.Where(stat => stat.TimePlayed > 0)
|
||||
.Where(stat => stat.Kills > 0 || stat.Deaths > 0)
|
||||
.Where(stat => serverId == null || stat.ServerId == serverId)
|
||||
.GroupBy(stat => stat.ClientId)
|
||||
.Select(s => new
|
||||
{
|
||||
ClientId = s.Key,
|
||||
Kills = s.Sum(c => c.Kills),
|
||||
Deaths = s.Sum(c => c.Deaths),
|
||||
KDR = s.Sum(c => (c.Kills / (double) (c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) /
|
||||
s.Sum(c => c.TimePlayed),
|
||||
TotalTimePlayed = s.Sum(c => c.TimePlayed),
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var finished = statsInfo
|
||||
.OrderByDescending(stat => rankingsDict[stat.ClientId].Last().PerformanceMetric)
|
||||
.Select((s, index) => new TopStatsInfo()
|
||||
{
|
||||
ClientId = s.ClientId,
|
||||
Id = (int?) serverId ?? 0,
|
||||
Deaths = s.Deaths,
|
||||
Kills = s.Kills,
|
||||
KDR = Math.Round(s.KDR, 2),
|
||||
LastSeen = (DateTime.UtcNow - rankingsDict[s.ClientId].First().LastConnection)
|
||||
.HumanizeForCurrentCulture(1, TimeUnit.Week, TimeUnit.Second, ",", false),
|
||||
LastSeenValue = (DateTime.UtcNow - rankingsDict[s.ClientId].First().LastConnection),
|
||||
Name = rankingsDict[s.ClientId].First().Name,
|
||||
Performance = Math.Round(rankingsDict[s.ClientId].Last().PerformanceMetric ?? 0, 2),
|
||||
RatingChange = (rankingsDict[s.ClientId].First().Ranking -
|
||||
rankingsDict[s.ClientId].Last().Ranking) ?? 0,
|
||||
PerformanceHistory = rankingsDict[s.ClientId].Select(ranking => ranking.PerformanceMetric ?? 0).ToList(),
|
||||
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
|
||||
TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed),
|
||||
Ranking = index + start + 1,
|
||||
ZScore = rankingsDict[s.ClientId].Last().ZScore,
|
||||
ServerId = serverId
|
||||
})
|
||||
.OrderBy(r => r.Ranking)
|
||||
.ToList();
|
||||
|
||||
return finished;
|
||||
}
|
||||
|
||||
public async Task<List<TopStatsInfo>> GetTopStats(int start, int count, long? serverId = null)
|
||||
{
|
||||
if (_configHandler.Configuration().EnableAdvancedMetrics)
|
||||
{
|
||||
return await GetNewTopStats(start, count, serverId);
|
||||
}
|
||||
|
||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||
// setup the query for the clients within the given rating range
|
||||
var iqClientRatings = (from rating in context.Set<EFRating>()
|
||||
.Where(GetRankingFunc(serverId))
|
||||
select new
|
||||
{
|
||||
rating.RatingHistory.ClientId,
|
||||
rating.RatingHistory.Client.CurrentAlias.Name,
|
||||
rating.RatingHistory.Client.LastConnection,
|
||||
rating.Performance,
|
||||
})
|
||||
.Where(GetRankingFunc(serverId))
|
||||
select new
|
||||
{
|
||||
rating.RatingHistory.ClientId,
|
||||
rating.RatingHistory.Client.CurrentAlias.Name,
|
||||
rating.RatingHistory.Client.LastConnection,
|
||||
rating.Performance,
|
||||
})
|
||||
.OrderByDescending(c => c.Performance)
|
||||
.Skip(start)
|
||||
.Take(count);
|
||||
|
||||
// materialized list
|
||||
var clientRatings = await iqClientRatings.ToListAsync();
|
||||
var clientRatings = (await iqClientRatings.ToListAsync())
|
||||
.GroupBy(rating => rating.ClientId) // prevent duplicate keys
|
||||
.Select(group => group.FirstOrDefault());
|
||||
|
||||
// get all the unique client ids that are in the top stats
|
||||
var clientIds = clientRatings
|
||||
@ -117,59 +229,67 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
.ToList();
|
||||
|
||||
var iqRatingInfo = from rating in context.Set<EFRating>()
|
||||
where clientIds.Contains(rating.RatingHistory.ClientId)
|
||||
where rating.ServerId == serverId
|
||||
select new
|
||||
{
|
||||
rating.Ranking,
|
||||
rating.Performance,
|
||||
rating.RatingHistory.ClientId,
|
||||
rating.When
|
||||
};
|
||||
where clientIds.Contains(rating.RatingHistory.ClientId)
|
||||
where rating.ServerId == serverId
|
||||
select new
|
||||
{
|
||||
rating.Ranking,
|
||||
rating.Performance,
|
||||
rating.RatingHistory.ClientId,
|
||||
rating.When
|
||||
};
|
||||
|
||||
var ratingInfo = (await iqRatingInfo.ToListAsync())
|
||||
.GroupBy(r => r.ClientId)
|
||||
.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>()
|
||||
where clientIds.Contains(stat.ClientId)
|
||||
where stat.Kills > 0 || stat.Deaths > 0
|
||||
where serverId == null ? true : stat.ServerId == serverId
|
||||
group stat by stat.ClientId into s
|
||||
select new
|
||||
{
|
||||
ClientId = s.Key,
|
||||
Kills = s.Sum(c => c.Kills),
|
||||
Deaths = s.Sum(c => c.Deaths),
|
||||
KDR = s.Sum(c => (c.Kills / (double)(c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) / s.Sum(c => c.TimePlayed),
|
||||
TotalTimePlayed = s.Sum(c => c.TimePlayed),
|
||||
});
|
||||
where clientIds.Contains(stat.ClientId)
|
||||
where stat.Kills > 0 || stat.Deaths > 0
|
||||
where serverId == null || stat.ServerId == serverId
|
||||
group stat by stat.ClientId
|
||||
into s
|
||||
select new
|
||||
{
|
||||
ClientId = s.Key,
|
||||
Kills = s.Sum(c => c.Kills),
|
||||
Deaths = s.Sum(c => c.Deaths),
|
||||
KDR = s.Sum(c => (c.Kills / (double) (c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) /
|
||||
s.Sum(c => c.TimePlayed),
|
||||
TotalTimePlayed = s.Sum(c => c.TimePlayed),
|
||||
});
|
||||
|
||||
var topPlayers = await iqStatsInfo.ToListAsync();
|
||||
|
||||
var clientRatingsDict = clientRatings.ToDictionary(r => r.ClientId);
|
||||
var finished = topPlayers.Select(s => new TopStatsInfo()
|
||||
{
|
||||
ClientId = s.ClientId,
|
||||
Id = (int?)serverId ?? 0,
|
||||
Deaths = s.Deaths,
|
||||
Kills = s.Kills,
|
||||
KDR = Math.Round(s.KDR, 2),
|
||||
LastSeen = (DateTime.UtcNow - clientRatingsDict[s.ClientId].LastConnection).HumanizeForCurrentCulture(),
|
||||
Name = clientRatingsDict[s.ClientId].Name,
|
||||
Performance = Math.Round(clientRatingsDict[s.ClientId].Performance, 2),
|
||||
RatingChange = ratingInfo.First(r => r.Key == s.ClientId).Ratings.First().Ranking - 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 => r.Performance).ToList() :
|
||||
new List<double>() { clientRatingsDict[s.ClientId].Performance, clientRatingsDict[s.ClientId].Performance },
|
||||
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
|
||||
})
|
||||
.OrderByDescending(r => r.Performance)
|
||||
.ToList();
|
||||
{
|
||||
ClientId = s.ClientId,
|
||||
Id = (int?) serverId ?? 0,
|
||||
Deaths = s.Deaths,
|
||||
Kills = s.Kills,
|
||||
KDR = Math.Round(s.KDR, 2),
|
||||
LastSeen = (DateTime.UtcNow - clientRatingsDict[s.ClientId].LastConnection)
|
||||
.HumanizeForCurrentCulture(),
|
||||
LastSeenValue = DateTime.UtcNow - clientRatingsDict[s.ClientId].LastConnection,
|
||||
Name = clientRatingsDict[s.ClientId].Name,
|
||||
Performance = Math.Round(clientRatingsDict[s.ClientId].Performance, 2),
|
||||
RatingChange = ratingInfo.First(r => r.Key == s.ClientId).Ratings.First().Ranking -
|
||||
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 => r.Performance).ToList()
|
||||
: new List<double>()
|
||||
{clientRatingsDict[s.ClientId].Performance, clientRatingsDict[s.ClientId].Performance},
|
||||
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
|
||||
TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed)
|
||||
})
|
||||
.OrderByDescending(r => r.Performance)
|
||||
.ToList();
|
||||
|
||||
// set the ranking numerically
|
||||
int i = start + 1;
|
||||
@ -226,7 +346,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
Port = sv.Port,
|
||||
EndPoint = sv.ToString(),
|
||||
ServerId = serverId,
|
||||
GameName = sv.GameName,
|
||||
GameName = (Reference.Game?) sv.GameName,
|
||||
HostName = sv.Hostname
|
||||
};
|
||||
|
||||
@ -236,9 +356,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
|
||||
// we want to set the gamename up if it's never been set, or it changed
|
||||
else if (!server.GameName.HasValue || server.GameName.HasValue && server.GameName.Value != sv.GameName)
|
||||
else if (!server.GameName.HasValue || server.GameName.Value != (Reference.Game) sv.GameName)
|
||||
{
|
||||
server.GameName = sv.GameName;
|
||||
server.GameName = (Reference.Game) sv.GameName;
|
||||
ctx.Entry(server).Property(_prop => _prop.GameName).IsModified = true;
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
@ -265,7 +385,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
_log.LogError(e, "{message}", Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_ERROR_ADD"]);
|
||||
_log.LogError(e, "{message}",
|
||||
Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_ERROR_ADD"]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -277,7 +398,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
public async Task<EFClientStatistics> AddPlayer(EFClient pl)
|
||||
{
|
||||
var existingStats = pl.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY);
|
||||
|
||||
|
||||
if (existingStats != null)
|
||||
{
|
||||
return existingStats;
|
||||
@ -322,7 +443,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
Active = true,
|
||||
HitCount = 0,
|
||||
Location = hl
|
||||
Location = (int) hl
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
@ -342,7 +463,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
Active = true,
|
||||
HitCount = 0,
|
||||
Location = hl
|
||||
Location = (int) hl
|
||||
})
|
||||
.ToList();
|
||||
|
||||
@ -404,7 +525,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
return;
|
||||
}
|
||||
|
||||
long serverId = GetIdForServer(pl.CurrentServer);
|
||||
var serverId = GetIdForServer(pl.CurrentServer);
|
||||
var serverStats = _servers[serverId].ServerStatistics;
|
||||
|
||||
// get individual client's stats
|
||||
@ -414,9 +535,14 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
clientStats = UpdateStats(clientStats);
|
||||
await SaveClientStats(clientStats);
|
||||
if (_configHandler.Configuration().EnableAdvancedMetrics)
|
||||
{
|
||||
await UpdateHistoricalRanking(pl.ClientId, clientStats, serverId);
|
||||
}
|
||||
|
||||
// increment the total play time
|
||||
serverStats.TotalPlayTime += pl.ConnectionLength;
|
||||
pl.SetAdditionalProperty(CLIENT_STATS_KEY, null);
|
||||
}
|
||||
|
||||
else
|
||||
@ -440,8 +566,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
/// Process stats for kill event
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task AddScriptHit(bool isDamage, DateTime time, EFClient attacker, EFClient victim, long serverId, string map, string hitLoc, string type,
|
||||
string damage, string weapon, string killOrigin, string deathOrigin, string viewAngles, string offset, string isKillstreakKill, string Ads,
|
||||
public async Task AddScriptHit(bool isDamage, DateTime time, EFClient attacker, EFClient victim, long serverId,
|
||||
string map, string hitLoc, string type,
|
||||
string damage, string weapon, string killOrigin, string deathOrigin, string viewAngles, string offset,
|
||||
string isKillstreakKill, string Ads,
|
||||
string fraction, string visibilityPercentage, string snapAngles, string isAlive, string lastAttackTime)
|
||||
{
|
||||
Vector3 vDeathOrigin = null;
|
||||
@ -478,25 +606,26 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
ServerId = serverId,
|
||||
DeathOrigin = vDeathOrigin,
|
||||
KillOrigin = vKillOrigin,
|
||||
DeathType = ParseEnum<IW4Info.MeansOfDeath>.Get(type, typeof(IW4Info.MeansOfDeath)),
|
||||
DeathType = (int) ParseEnum<IW4Info.MeansOfDeath>.Get(type, typeof(IW4Info.MeansOfDeath)),
|
||||
Damage = int.Parse(damage),
|
||||
HitLoc = ParseEnum<IW4Info.HitLocation>.Get(hitLoc, typeof(IW4Info.HitLocation)),
|
||||
Weapon = ParseEnum<IW4Info.WeaponName>.Get(weapon, typeof(IW4Info.WeaponName)),
|
||||
HitLoc = (int) ParseEnum<IW4Info.HitLocation>.Get(hitLoc, typeof(IW4Info.HitLocation)),
|
||||
Weapon = (int) ParseEnum<IW4Info.WeaponName>.Get(weapon, typeof(IW4Info.WeaponName)),
|
||||
ViewAngles = vViewAngles,
|
||||
TimeOffset = long.Parse(offset),
|
||||
When = time,
|
||||
IsKillstreakKill = isKillstreakKill[0] != '0',
|
||||
AdsPercent = float.Parse(Ads, System.Globalization.CultureInfo.InvariantCulture),
|
||||
Fraction = double.Parse(fraction, System.Globalization.CultureInfo.InvariantCulture),
|
||||
VisibilityPercentage = double.Parse(visibilityPercentage, System.Globalization.CultureInfo.InvariantCulture),
|
||||
VisibilityPercentage = double.Parse(visibilityPercentage,
|
||||
System.Globalization.CultureInfo.InvariantCulture),
|
||||
IsKill = !isDamage,
|
||||
AnglesList = snapshotAngles,
|
||||
IsAlive = isAlive == "1",
|
||||
TimeSinceLastAttack = long.Parse(lastAttackTime),
|
||||
GameName = attacker.CurrentServer.GameName
|
||||
GameName = (int) attacker.CurrentServer.GameName
|
||||
};
|
||||
|
||||
if (hit.HitLoc == IW4Info.HitLocation.shield)
|
||||
if (hit.HitLoc == (int) IW4Info.HitLocation.shield)
|
||||
{
|
||||
// we don't care about shield hits
|
||||
return;
|
||||
@ -509,9 +638,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
await waiter.WaitAsync(Utilities.DefaultCommandTimeout, Plugin.ServerManager.CancellationToken);
|
||||
|
||||
// increment their hit count
|
||||
if (hit.DeathType == IW4Info.MeansOfDeath.MOD_PISTOL_BULLET ||
|
||||
hit.DeathType == IW4Info.MeansOfDeath.MOD_RIFLE_BULLET ||
|
||||
hit.DeathType == IW4Info.MeansOfDeath.MOD_HEAD_SHOT)
|
||||
if (hit.DeathType == (int) IW4Info.MeansOfDeath.MOD_PISTOL_BULLET ||
|
||||
hit.DeathType == (int) IW4Info.MeansOfDeath.MOD_RIFLE_BULLET ||
|
||||
hit.DeathType == (int) IW4Info.MeansOfDeath.MOD_HEAD_SHOT)
|
||||
{
|
||||
clientStats.HitLocations.First(hl => hl.Location == hit.HitLoc).HitCount += 1;
|
||||
}
|
||||
@ -550,7 +679,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
if (Plugin.Config.Configuration().AnticheatConfiguration.Enable && !attacker.IsBot && attacker.ClientId != victim.ClientId)
|
||||
if (Plugin.Config.Configuration().AnticheatConfiguration.Enable && !attacker.IsBot &&
|
||||
attacker.ClientId != victim.ClientId)
|
||||
{
|
||||
clientDetection.TrackedHits.Add(hit);
|
||||
|
||||
@ -573,7 +703,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
await ApplyPenalty(result, attacker);
|
||||
}
|
||||
|
||||
if (clientDetection.Tracker.HasChanges && result.ClientPenalty != EFPenalty.PenaltyType.Any)
|
||||
if (clientDetection.Tracker.HasChanges &&
|
||||
result.ClientPenalty != EFPenalty.PenaltyType.Any)
|
||||
{
|
||||
await SaveTrackedSnapshots(clientDetection);
|
||||
|
||||
@ -590,10 +721,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
catch (TaskCanceledException) { }
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "Could not save hit or anti-cheat info {@attacker} {@victim} {server}", attacker, victim, serverId);
|
||||
_log.LogError(ex, "Could not save hit or anti-cheat info {@attacker} {@victim} {server}", attacker,
|
||||
victim, serverId);
|
||||
}
|
||||
|
||||
finally
|
||||
@ -605,16 +739,17 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
private DetectionPenaltyResult DeterminePenaltyResult(IEnumerable<DetectionPenaltyResult> results, EFClient client)
|
||||
private DetectionPenaltyResult DeterminePenaltyResult(IEnumerable<DetectionPenaltyResult> results,
|
||||
EFClient client)
|
||||
{
|
||||
// allow disabling of certain detection types
|
||||
results = results.Where(_result => ShouldUseDetection(client.CurrentServer, _result.Type, client.ClientId));
|
||||
return results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Ban) ??
|
||||
results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ??
|
||||
new DetectionPenaltyResult()
|
||||
{
|
||||
ClientPenalty = EFPenalty.PenaltyType.Any,
|
||||
};
|
||||
results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ??
|
||||
new DetectionPenaltyResult()
|
||||
{
|
||||
ClientPenalty = EFPenalty.PenaltyType.Any,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task SaveHitCache(long serverId)
|
||||
@ -647,7 +782,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -668,13 +802,16 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
new EFPenalty()
|
||||
{
|
||||
AutomatedOffense = penalty.Type == Detection.DetectionType.Bone ?
|
||||
$"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" :
|
||||
$"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}",
|
||||
AutomatedOffense = penalty.Type == Detection.DetectionType.Bone
|
||||
? $"{penalty.Type}-{(int) penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}"
|
||||
: $"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}",
|
||||
}
|
||||
};
|
||||
|
||||
await attacker.Ban(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_CHEAT_DETECTED"], penaltyClient, false).WaitAsync(Utilities.DefaultCommandTimeout, attacker.CurrentServer.Manager.CancellationToken);
|
||||
await attacker
|
||||
.Ban(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_CHEAT_DETECTED"],
|
||||
penaltyClient, false).WaitAsync(Utilities.DefaultCommandTimeout,
|
||||
attacker.CurrentServer.Manager.CancellationToken);
|
||||
break;
|
||||
case EFPenalty.PenaltyType.Flag:
|
||||
if (attacker.Level != EFClient.Permission.User)
|
||||
@ -682,9 +819,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
break;
|
||||
}
|
||||
|
||||
string flagReason = penalty.Type == Cheat.Detection.DetectionType.Bone ?
|
||||
$"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" :
|
||||
$"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}";
|
||||
string flagReason = penalty.Type == Cheat.Detection.DetectionType.Bone
|
||||
? $"{penalty.Type}-{(int) penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}"
|
||||
: $"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}";
|
||||
|
||||
penaltyClient.AdministeredPenalties = new List<EFPenalty>()
|
||||
{
|
||||
@ -694,7 +831,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
};
|
||||
|
||||
await attacker.Flag(flagReason, penaltyClient, new TimeSpan(168, 0, 0)).WaitAsync(Utilities.DefaultCommandTimeout, attacker.CurrentServer.Manager.CancellationToken);
|
||||
await attacker.Flag(flagReason, penaltyClient, new TimeSpan(168, 0, 0))
|
||||
.WaitAsync(Utilities.DefaultCommandTimeout, attacker.CurrentServer.Manager.CancellationToken);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -708,6 +846,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
ctx.Add(change);
|
||||
}
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
@ -743,9 +882,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
victimStats.LastScore = victim.Score;
|
||||
|
||||
// show encouragement/discouragement
|
||||
string streakMessage = (attackerStats.ClientId != victimStats.ClientId) ?
|
||||
StreakMessage.MessageOnStreak(attackerStats.KillStreak, attackerStats.DeathStreak) :
|
||||
StreakMessage.MessageOnStreak(-1, -1);
|
||||
string streakMessage = (attackerStats.ClientId != victimStats.ClientId)
|
||||
? StreakMessage.MessageOnStreak(attackerStats.KillStreak, attackerStats.DeathStreak)
|
||||
: StreakMessage.MessageOnStreak(-1, -1);
|
||||
|
||||
if (streakMessage != string.Empty)
|
||||
{
|
||||
@ -768,15 +907,26 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
|
||||
// update their performance
|
||||
if ((DateTime.UtcNow - attackerStats.LastStatHistoryUpdate).TotalMinutes >= 2.5)
|
||||
if ((DateTime.UtcNow - attackerStats.LastStatHistoryUpdate).TotalMinutes >=
|
||||
(Utilities.IsDevelopment ? 0.5 : _configHandler.Configuration().EnableAdvancedMetrics ? 10.0 : 2.5))
|
||||
{
|
||||
try
|
||||
{
|
||||
// kill event is not designated as blocking, so we should be able to enter and exit
|
||||
// we need to make this thread safe because we can potentially have kills qualify
|
||||
// for stat history update, but one is already processing that invalidates the original
|
||||
await attackerStats.ProcessingHit.WaitAsync(Utilities.DefaultCommandTimeout, Plugin.ServerManager.CancellationToken);
|
||||
await UpdateStatHistory(attacker, attackerStats);
|
||||
await attackerStats.ProcessingHit.WaitAsync(Utilities.DefaultCommandTimeout,
|
||||
Plugin.ServerManager.CancellationToken);
|
||||
if (_configHandler.Configuration().EnableAdvancedMetrics)
|
||||
{
|
||||
await UpdateHistoricalRanking(attacker.ClientId, attackerStats, serverId);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
await UpdateStatHistory(attacker, attackerStats);
|
||||
}
|
||||
|
||||
attackerStats.LastStatHistoryUpdate = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
@ -796,14 +946,14 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the invidual and average stat history for a client
|
||||
/// Update the individual and average stat history for a client
|
||||
/// </summary>
|
||||
/// <param name="client">client to update</param>
|
||||
/// <param name="clientStats">stats of client that is being updated</param>
|
||||
/// <returns></returns>
|
||||
public async Task UpdateStatHistory(EFClient client, EFClientStatistics clientStats)
|
||||
{
|
||||
int currentSessionTime = (int)(DateTime.UtcNow - client.LastConnection).TotalSeconds;
|
||||
int currentSessionTime = (int) (DateTime.UtcNow - client.LastConnection).TotalSeconds;
|
||||
|
||||
// don't update their stat history if they haven't played long
|
||||
if (currentSessionTime < 60)
|
||||
@ -816,18 +966,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
await using var ctx = _contextFactory.CreateContext(enableTracking: true);
|
||||
// select the rating history for client
|
||||
var iqHistoryLink = from history in ctx.Set<EFClientRatingHistory>()
|
||||
.Include(h => h.Ratings)
|
||||
where history.ClientId == client.ClientId
|
||||
select history;
|
||||
.Include(h => h.Ratings)
|
||||
where history.ClientId == client.ClientId
|
||||
select history;
|
||||
|
||||
// get the client ratings
|
||||
var clientHistory = await iqHistoryLink
|
||||
.FirstOrDefaultAsync() ?? new EFClientRatingHistory()
|
||||
{
|
||||
Active = true,
|
||||
ClientId = client.ClientId,
|
||||
Ratings = new List<EFRating>()
|
||||
};
|
||||
{
|
||||
Active = true,
|
||||
ClientId = client.ClientId,
|
||||
Ratings = new List<EFRating>()
|
||||
};
|
||||
|
||||
// it's the first time they've played
|
||||
if (clientHistory.RatingHistoryId == 0)
|
||||
@ -836,13 +986,14 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
|
||||
#region INDIVIDUAL_SERVER_PERFORMANCE
|
||||
|
||||
// get the client ranking for the current server
|
||||
int individualClientRanking = await ctx.Set<EFRating>()
|
||||
.Where(GetRankingFunc(clientStats.ServerId))
|
||||
// ignore themselves in the query
|
||||
.Where(c => c.RatingHistory.ClientId != client.ClientId)
|
||||
.Where(c => c.Performance > clientStats.Performance)
|
||||
.CountAsync() + 1;
|
||||
.Where(GetRankingFunc(clientStats.ServerId))
|
||||
// ignore themselves in the query
|
||||
.Where(c => c.RatingHistory.ClientId != client.ClientId)
|
||||
.Where(c => c.Performance > clientStats.Performance)
|
||||
.CountAsync() + 1;
|
||||
|
||||
// limit max history per server to 40
|
||||
if (clientHistory.Ratings.Count(r => r.ServerId == clientStats.ServerId) >= 40)
|
||||
@ -887,16 +1038,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
ctx.Add(newServerRating);
|
||||
|
||||
#endregion
|
||||
|
||||
#region OVERALL_RATING
|
||||
|
||||
// select all performance & time played for current client
|
||||
var iqClientStats = from stats in ctx.Set<EFClientStatistics>()
|
||||
where stats.ClientId == client.ClientId
|
||||
where stats.ServerId != clientStats.ServerId
|
||||
select new
|
||||
{
|
||||
stats.Performance,
|
||||
stats.TimePlayed
|
||||
};
|
||||
where stats.ClientId == client.ClientId
|
||||
where stats.ServerId != clientStats.ServerId
|
||||
select new
|
||||
{
|
||||
stats.Performance,
|
||||
stats.TimePlayed
|
||||
};
|
||||
|
||||
var clientStatsList = await iqClientStats.ToListAsync();
|
||||
|
||||
@ -908,7 +1061,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
});
|
||||
|
||||
// weight the overall performance based on play time
|
||||
double performanceAverage = clientStatsList.Sum(p => (p.Performance * p.TimePlayed)) / clientStatsList.Sum(p => p.TimePlayed);
|
||||
double performanceAverage = clientStatsList.Sum(p => (p.Performance * p.TimePlayed)) /
|
||||
clientStatsList.Sum(p => p.TimePlayed);
|
||||
|
||||
// shouldn't happen but just in case the sum of time played is 0
|
||||
if (double.IsNaN(performanceAverage))
|
||||
@ -917,10 +1071,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
|
||||
int overallClientRanking = await ctx.Set<EFRating>()
|
||||
.Where(GetRankingFunc())
|
||||
.Where(r => r.RatingHistory.ClientId != client.ClientId)
|
||||
.Where(r => r.Performance > performanceAverage)
|
||||
.CountAsync() + 1;
|
||||
.Where(GetRankingFunc())
|
||||
.Where(r => r.RatingHistory.ClientId != client.ClientId)
|
||||
.Where(r => r.Performance > performanceAverage)
|
||||
.CountAsync() + 1;
|
||||
|
||||
// limit max average history to 40
|
||||
if (clientHistory.Ratings.Count(r => r.ServerId == null) >= 40)
|
||||
@ -962,11 +1116,112 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
};
|
||||
|
||||
ctx.Add(averageRating);
|
||||
|
||||
#endregion
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateHistoricalRanking(int clientId, EFClientStatistics clientStats, long serverId)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext();
|
||||
|
||||
var performances = await context.Set<EFClientStatistics>()
|
||||
.AsNoTracking()
|
||||
.Where(stat => stat.ClientId == clientId)
|
||||
.Where(stat => stat.ServerId != serverId) // ignore the one we're currently tracking
|
||||
.Where(stats => stats.UpdatedAt >= Extensions.FifteenDaysAgo())
|
||||
.Where(stats => stats.TimePlayed >= _configHandler.Configuration().TopPlayersMinPlayTime)
|
||||
.ToListAsync();
|
||||
|
||||
if (clientStats.TimePlayed >= _configHandler.Configuration().TopPlayersMinPlayTime)
|
||||
{
|
||||
clientStats.ZScore = await _serverDistributionCalculator.GetZScoreForServer(serverId,
|
||||
clientStats.Performance);
|
||||
|
||||
var serverRanking = await context.Set<EFClientStatistics>()
|
||||
.Where(stats => stats.ClientId != clientStats.ClientId)
|
||||
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(
|
||||
_configHandler.Configuration().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);
|
||||
}
|
||||
|
||||
if (performances.Any(performance => performance.TimePlayed >= _configHandler.Configuration().TopPlayersMinPlayTime))
|
||||
{
|
||||
var aggregateZScore = performances.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), _configHandler.Configuration().TopPlayersMinPlayTime);
|
||||
|
||||
int? aggregateRanking = await context.Set<EFClientStatistics>()
|
||||
.Where(stat => stat.ClientId != clientId)
|
||||
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(_configHandler.Configuration()
|
||||
.TopPlayersMinPlayTime))
|
||||
.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 aggregateRankingSnapshot = new EFClientRankingHistory()
|
||||
{
|
||||
ClientId = clientId,
|
||||
ZScore = aggregateZScore,
|
||||
Ranking = aggregateRanking,
|
||||
PerformanceMetric = await _serverDistributionCalculator.GetRatingForZScore(aggregateZScore),
|
||||
Newest = true,
|
||||
};
|
||||
|
||||
context.Add(aggregateRankingSnapshot);
|
||||
|
||||
await PruneOldRankings(context, clientId);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PruneOldRankings(DatabaseContext context, int clientId, long? serverId = null)
|
||||
{
|
||||
var totalRankingEntries = await context.Set<EFClientRankingHistory>()
|
||||
.Where(r => r.ClientId == clientId)
|
||||
.Where(r => r.ServerId == serverId)
|
||||
.CountAsync();
|
||||
|
||||
var mostRecent = await context.Set<EFClientRankingHistory>()
|
||||
.Where(r => r.ClientId == clientId)
|
||||
.Where(r => r.ServerId == serverId)
|
||||
.FirstOrDefaultAsync(r => r.Newest);
|
||||
|
||||
if (mostRecent != null)
|
||||
{
|
||||
mostRecent.Newest = false;
|
||||
context.Update(mostRecent);
|
||||
}
|
||||
|
||||
if (totalRankingEntries > EFClientRankingHistory.MaxRankingCount)
|
||||
{
|
||||
var lastRating = await context.Set<EFClientRankingHistory>()
|
||||
.Where(r => r.ClientId == clientId)
|
||||
.Where(r => r.ServerId == serverId)
|
||||
.OrderBy(r => r.CreatedDateTime)
|
||||
.FirstOrDefaultAsync();
|
||||
context.Remove(lastRating);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs the incrementation of kills and deaths for client statistics
|
||||
/// </summary>
|
||||
@ -994,6 +1249,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
attackerStats = UpdateStats(attackerStats);
|
||||
|
||||
#region DEPRECATED
|
||||
|
||||
/* var validAttackerLobbyRatings = Servers[attackerStats.ServerId].PlayerStats
|
||||
.Where(cs => cs.Value.ClientId != attackerStats.ClientId)
|
||||
.Where(cs =>
|
||||
@ -1017,10 +1273,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
double victimLobbyRating = validVictimLobbyRatings.Count() > 0 ?
|
||||
validVictimLobbyRatings.Average(cs => cs.Value.EloRating) :
|
||||
victimStats.EloRating;*/
|
||||
|
||||
#endregion
|
||||
|
||||
// calculate elo
|
||||
double attackerEloDifference = Math.Log(Math.Max(1, victimStats.EloRating)) - Math.Log(Math.Max(1, attackerStats.EloRating));
|
||||
double attackerEloDifference = Math.Log(Math.Max(1, victimStats.EloRating)) -
|
||||
Math.Log(Math.Max(1, attackerStats.EloRating));
|
||||
double winPercentage = 1.0 / (1 + Math.Pow(10, attackerEloDifference / Math.E));
|
||||
|
||||
// double victimEloDifference = Math.Log(Math.Max(1, attackerStats.EloRating)) - Math.Log(Math.Max(1, victimStats.EloRating));
|
||||
@ -1033,8 +1291,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
victimStats.EloRating = Math.Max(0, Math.Round(victimStats.EloRating, 2));
|
||||
|
||||
// update after calculation
|
||||
attackerStats.TimePlayed += (int)(DateTime.UtcNow - attackerStats.LastActive).TotalSeconds;
|
||||
victimStats.TimePlayed += (int)(DateTime.UtcNow - victimStats.LastActive).TotalSeconds;
|
||||
attackerStats.TimePlayed += (int) (DateTime.UtcNow - attackerStats.LastActive).TotalSeconds;
|
||||
victimStats.TimePlayed += (int) (DateTime.UtcNow - victimStats.LastActive).TotalSeconds;
|
||||
attackerStats.LastActive = DateTime.UtcNow;
|
||||
victimStats.LastActive = DateTime.UtcNow;
|
||||
}
|
||||
@ -1072,7 +1330,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
|
||||
double killSPM = scoreDifference / timeSinceLastCalc;
|
||||
double spmMultiplier = 2.934 * Math.Pow(_servers[clientStats.ServerId].TeamCount(clientStats.Team == IW4Info.Team.Allies ? IW4Info.Team.Axis : IW4Info.Team.Allies), -0.454);
|
||||
double spmMultiplier = 2.934 *
|
||||
Math.Pow(
|
||||
_servers[clientStats.ServerId]
|
||||
.TeamCount((IW4Info.Team) clientStats.Team == IW4Info.Team.Allies
|
||||
? IW4Info.Team.Axis
|
||||
: IW4Info.Team.Allies), -0.454);
|
||||
killSPM *= Math.Max(1, spmMultiplier);
|
||||
|
||||
// update this for ac tracking
|
||||
@ -1080,15 +1343,17 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
// calculate how much the KDR should weigh
|
||||
// 1.637 is a Eddie-Generated number that weights the KDR nicely
|
||||
double currentKDR = clientStats.SessionDeaths == 0 ? clientStats.SessionKills : clientStats.SessionKills / clientStats.SessionDeaths;
|
||||
double currentKDR = clientStats.SessionDeaths == 0
|
||||
? clientStats.SessionKills
|
||||
: clientStats.SessionKills / clientStats.SessionDeaths;
|
||||
double alpha = Math.Sqrt(2) / Math.Min(600, Math.Max(clientStats.Kills + clientStats.Deaths, 1));
|
||||
clientStats.RollingWeightedKDR = (alpha * currentKDR) + (1.0 - alpha) * clientStats.KDR;
|
||||
double KDRWeight = Math.Round(Math.Pow(clientStats.RollingWeightedKDR, 1.637 / Math.E), 3);
|
||||
|
||||
// calculate the weight of the new play time against last 10 hours of gameplay
|
||||
int totalPlayTime = (clientStats.TimePlayed == 0) ?
|
||||
(int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds :
|
||||
clientStats.TimePlayed + (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds;
|
||||
int totalPlayTime = (clientStats.TimePlayed == 0)
|
||||
? (int) (DateTime.UtcNow - clientStats.LastActive).TotalSeconds
|
||||
: clientStats.TimePlayed + (int) (DateTime.UtcNow - clientStats.LastActive).TotalSeconds;
|
||||
|
||||
double SPMAgainstPlayWeight = timeSinceLastCalc / Math.Min(600, (totalPlayTime / 60.0));
|
||||
|
||||
@ -1107,13 +1372,15 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
// fixme: how does this happen?
|
||||
if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill))
|
||||
{
|
||||
_log.LogWarning("clientStats SPM/Skill NaN {@killInfo}", new {killSPM, KDRWeight, totalPlayTime, SPMAgainstPlayWeight, clientStats, scoreDifference});
|
||||
_log.LogWarning("clientStats SPM/Skill NaN {@killInfo}",
|
||||
new {killSPM, KDRWeight, totalPlayTime, SPMAgainstPlayWeight, clientStats, scoreDifference});
|
||||
clientStats.SPM = 0;
|
||||
clientStats.Skill = 0;
|
||||
}
|
||||
|
||||
clientStats.LastStatCalculation = DateTime.UtcNow;
|
||||
//clientStats.LastScore = clientStats.SessionScore;
|
||||
clientStats.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
return clientStats;
|
||||
}
|
||||
@ -1161,13 +1428,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
public void ResetStats(EFClient client)
|
||||
{
|
||||
var stats = client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY);
|
||||
|
||||
|
||||
// the cached stats have not been loaded yet
|
||||
if (stats == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
stats.Kills = 0;
|
||||
stats.Deaths = 0;
|
||||
stats.SPM = 0;
|
||||
@ -1253,8 +1520,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
long? serverId;
|
||||
|
||||
serverId = serverModels.FirstOrDefault(_server => _server.ServerId == server.EndPoint ||
|
||||
_server.EndPoint == server.ToString() ||
|
||||
_server.ServerId == id)?.ServerId;
|
||||
_server.EndPoint == server.ToString() ||
|
||||
_server.ServerId == id)?.ServerId;
|
||||
|
||||
if (!serverId.HasValue)
|
||||
{
|
||||
@ -1264,4 +1531,4 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
return serverId.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user