mirror of
https://github.com/RaidMax/IW4M-Admin.git
synced 2025-06-09 23:00:57 -05:00
Additional updates to support performance bucket
This commit is contained in:
@ -2659,6 +2659,7 @@
|
|||||||
"gewehr43_upgraded": "G115 Compressor",
|
"gewehr43_upgraded": "G115 Compressor",
|
||||||
"m1a1carbine_upgraded": "Widdershins RC-1",
|
"m1a1carbine_upgraded": "Widdershins RC-1",
|
||||||
"m1garand_upgraded": "M1000",
|
"m1garand_upgraded": "M1000",
|
||||||
|
"m1garand_gl_upgraded": "The Imploder",
|
||||||
"mg42_upgraded": "Barracuda FU-A11",
|
"mg42_upgraded": "Barracuda FU-A11",
|
||||||
"mp40_upgraded": "The Afterburner",
|
"mp40_upgraded": "The Afterburner",
|
||||||
"ppsh_upgraded": "The Reaper",
|
"ppsh_upgraded": "The Reaper",
|
||||||
|
@ -8,6 +8,7 @@ using Data.Models;
|
|||||||
using Data.Models.Client;
|
using Data.Models.Client;
|
||||||
using Data.Models.Client.Stats;
|
using Data.Models.Client.Stats;
|
||||||
using Data.Models.Server;
|
using Data.Models.Server;
|
||||||
|
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
@ -25,19 +26,21 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
|
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
|
||||||
private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache;
|
private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache;
|
||||||
private readonly IDataValueCache<EFClientRankingHistory, int> _rankedClientsCache;
|
private readonly IDataValueCache<EFClientRankingHistory, int> _rankedClientsCache;
|
||||||
|
private readonly StatManager _statManager;
|
||||||
|
|
||||||
private readonly TimeSpan? _cacheTimeSpan =
|
private readonly TimeSpan? _cacheTimeSpan =
|
||||||
Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10);
|
Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10);
|
||||||
|
|
||||||
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache,
|
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache,
|
||||||
IDataValueCache<EFClient, (int, int)> serverStatsCache,
|
IDataValueCache<EFClient, (int, int)> serverStatsCache,
|
||||||
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache, IDataValueCache<EFClientRankingHistory, int> rankedClientsCache)
|
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache, IDataValueCache<EFClientRankingHistory, int> rankedClientsCache, StatManager statManager)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_snapshotCache = snapshotCache;
|
_snapshotCache = snapshotCache;
|
||||||
_serverStatsCache = serverStatsCache;
|
_serverStatsCache = serverStatsCache;
|
||||||
_clientHistoryCache = clientHistoryCache;
|
_clientHistoryCache = clientHistoryCache;
|
||||||
_rankedClientsCache = rankedClientsCache;
|
_rankedClientsCache = rankedClientsCache;
|
||||||
|
_statManager = statManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(int?, DateTime?)>
|
public async Task<(int?, DateTime?)>
|
||||||
@ -185,30 +188,31 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default)
|
public async Task<int> RankedClientsCountAsync(long? serverId = null, string performanceBucket = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
_rankedClientsCache.SetCacheItem((set, ids, cancellationToken) =>
|
_rankedClientsCache.SetCacheItem((set, ids, cancellationToken) =>
|
||||||
{
|
{
|
||||||
long? id = null;
|
long? id = null;
|
||||||
|
string bucket = null;
|
||||||
|
|
||||||
if (ids.Any())
|
if (ids.Any())
|
||||||
{
|
{
|
||||||
id = (long?)ids.First();
|
id = (long?)ids.First();
|
||||||
}
|
}
|
||||||
|
|
||||||
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
|
if (ids.Count() == 2)
|
||||||
return set
|
{
|
||||||
.Where(rating => rating.Newest)
|
bucket = (string)ids.Last();
|
||||||
.Where(rating => rating.ServerId == id)
|
}
|
||||||
.Where(rating => rating.CreatedDateTime >= fifteenDaysAgo)
|
|
||||||
.Where(rating => rating.Client.Level != EFClient.Permission.Banned)
|
return _statManager.GetBucketConfig(serverId)
|
||||||
.Where(rating => rating.Ranking != null)
|
.ContinueWith(result => _statManager.GetTotalRankedPlayers(id, bucket), cancellationToken).Result;
|
||||||
.CountAsync(cancellationToken);
|
|
||||||
}, nameof(_rankedClientsCache), new object[] { serverId }, _cacheTimeSpan);
|
}, nameof(_rankedClientsCache), new object[] { serverId, performanceBucket }, _cacheTimeSpan);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), new object[] { serverId }, token);
|
return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), new object[] { serverId, performanceBucket }, token);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -17,6 +17,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using SharedLibraryCore.Database.Models;
|
using SharedLibraryCore.Database.Models;
|
||||||
using SharedLibraryCore.Events;
|
using SharedLibraryCore.Events;
|
||||||
using SharedLibraryCore.Events.Game;
|
using SharedLibraryCore.Events.Game;
|
||||||
|
using SharedLibraryCore.Events.Game.GameScript.Zombie;
|
||||||
using SharedLibraryCore.Events.Management;
|
using SharedLibraryCore.Events.Management;
|
||||||
using Stats.Client.Abstractions;
|
using Stats.Client.Abstractions;
|
||||||
using Stats.Client.Game;
|
using Stats.Client.Game;
|
||||||
@ -109,6 +110,39 @@ public class HitCalculator : IClientStatisticCalculator
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (coreEvent is RoundEndEvent roundEndEvent)
|
||||||
|
{
|
||||||
|
foreach (var client in roundEndEvent.Server.ConnectedClients)
|
||||||
|
{
|
||||||
|
if (!_clientHitStatistics.TryGetValue(client.ClientId, out var state))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await state.OnTransaction.WaitAsync();
|
||||||
|
await UpdateClientStatistics(client.ClientId, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Could not handle round end calculations for client {Client}",
|
||||||
|
client.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (state.OnTransaction.CurrentCount == 0)
|
||||||
|
{
|
||||||
|
state.OnTransaction.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (coreEvent is ClientStateDisposeEvent clientStateDisposeEvent)
|
if (coreEvent is ClientStateDisposeEvent clientStateDisposeEvent)
|
||||||
{
|
{
|
||||||
_clientHitStatistics.Remove(clientStateDisposeEvent.Client.ClientId, out var state);
|
_clientHitStatistics.Remove(clientStateDisposeEvent.Client.ClientId, out var state);
|
||||||
|
@ -47,7 +47,7 @@ namespace Stats.Client
|
|||||||
public async Task Initialize()
|
public async Task Initialize()
|
||||||
{
|
{
|
||||||
await LoadServers();
|
await LoadServers();
|
||||||
|
|
||||||
_distributionCache.SetCacheItem(async (set, token) =>
|
_distributionCache.SetCacheItem(async (set, token) =>
|
||||||
{
|
{
|
||||||
var validPlayTime = _configuration.TopPlayersMinPlayTime;
|
var validPlayTime = _configuration.TopPlayersMinPlayTime;
|
||||||
@ -71,14 +71,9 @@ namespace Stats.Client
|
|||||||
distributions.Add(serverId.ToString(), distributionParams);
|
distributions.Add(serverId.ToString(), distributionParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var performanceBucketGroup in _appConfig.Servers.GroupBy(server => server.PerformanceBucket))
|
foreach (var performanceBucketGroup in _appConfig.Servers.Select(server => server.PerformanceBucket).Distinct())
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(performanceBucketGroup.Key))
|
var performanceBucket = performanceBucketGroup ?? "null";
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var performanceBucket = performanceBucketGroup.Key;
|
|
||||||
|
|
||||||
var bucketConfig =
|
var bucketConfig =
|
||||||
_configuration.PerformanceBuckets.FirstOrDefault(bucket =>
|
_configuration.PerformanceBuckets.FirstOrDefault(bucket =>
|
||||||
@ -99,19 +94,19 @@ namespace Stats.Client
|
|||||||
return distributions;
|
return distributions;
|
||||||
}, DistributionCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(1) : TimeSpan.FromHours(1));
|
}, DistributionCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(1) : TimeSpan.FromHours(1));
|
||||||
|
|
||||||
foreach (var server in _appConfig.Servers)
|
foreach (var performanceBucket in _appConfig.Servers.Select(s => s.PerformanceBucket).Distinct())
|
||||||
{
|
{
|
||||||
_maxZScoreCache.SetCacheItem(async (set, ids, token) =>
|
_maxZScoreCache.SetCacheItem(async (set, ids, token) =>
|
||||||
{
|
{
|
||||||
var validPlayTime = _configuration.TopPlayersMinPlayTime;
|
var validPlayTime = _configuration.TopPlayersMinPlayTime;
|
||||||
var oldestStat = TimeSpan.FromSeconds(_configuration.TopPlayersMinPlayTime);
|
var oldestStat = DateTime.UtcNow - Extensions.FifteenDaysAgo();
|
||||||
var performanceBucket = (string)ids.FirstOrDefault();
|
var perfBucket = (string)ids.FirstOrDefault();
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(performanceBucket))
|
if (!string.IsNullOrEmpty(perfBucket))
|
||||||
{
|
{
|
||||||
var bucketConfig =
|
var bucketConfig =
|
||||||
_configuration.PerformanceBuckets.FirstOrDefault(cfg =>
|
_configuration.PerformanceBuckets.FirstOrDefault(cfg =>
|
||||||
cfg.Name == performanceBucket) ?? new PerformanceBucketConfiguration();
|
cfg.Name == perfBucket) ?? new PerformanceBucketConfiguration();
|
||||||
|
|
||||||
validPlayTime = (int)bucketConfig.ClientMinPlayTime.TotalSeconds;
|
validPlayTime = (int)bucketConfig.ClientMinPlayTime.TotalSeconds;
|
||||||
oldestStat = bucketConfig.RankingExpiration;
|
oldestStat = bucketConfig.RankingExpiration;
|
||||||
@ -120,19 +115,18 @@ namespace Stats.Client
|
|||||||
var zScore = await set
|
var zScore = await set
|
||||||
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(validPlayTime, oldestStat))
|
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(validPlayTime, oldestStat))
|
||||||
.Where(s => s.Skill > 0)
|
.Where(s => s.Skill > 0)
|
||||||
.Where(s => s.EloRating >= 1)
|
.Where(s => s.EloRating >= 0)
|
||||||
.Where(stat =>
|
.Where(stat => perfBucket == stat.Server.PerformanceBucket)
|
||||||
performanceBucket == null || performanceBucket == stat.Server.PerformanceBucket)
|
|
||||||
.GroupBy(stat => stat.ClientId)
|
.GroupBy(stat => stat.ClientId)
|
||||||
.Select(group =>
|
.Select(group =>
|
||||||
group.Sum(stat => stat.ZScore * stat.TimePlayed) / group.Sum(stat => stat.TimePlayed))
|
group.Sum(stat => stat.ZScore * stat.TimePlayed) / group.Sum(stat => stat.TimePlayed))
|
||||||
.MaxAsync(avgZScore => (double?)avgZScore, token);
|
.MaxAsync(avgZScore => (double?)avgZScore, token);
|
||||||
|
|
||||||
return zScore ?? 0;
|
return zScore ?? 0;
|
||||||
}, MaxZScoreCacheKey, new[] { server.PerformanceBucket },
|
}, MaxZScoreCacheKey, new[] { performanceBucket },
|
||||||
Utilities.IsDevelopment ? TimeSpan.FromMinutes(1) : TimeSpan.FromMinutes(30));
|
Utilities.IsDevelopment ? TimeSpan.FromMinutes(1) : TimeSpan.FromMinutes(30));
|
||||||
|
|
||||||
await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new[] { server.PerformanceBucket });
|
await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new[] { performanceBucket });
|
||||||
}
|
}
|
||||||
|
|
||||||
await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
|
await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
|
||||||
@ -140,7 +134,7 @@ namespace Stats.Client
|
|||||||
/*foreach (var serverId in _serverIds)
|
/*foreach (var serverId in _serverIds)
|
||||||
{
|
{
|
||||||
await using var ctx = _contextFactory.CreateContext(enableTracking: true);
|
await using var ctx = _contextFactory.CreateContext(enableTracking: true);
|
||||||
|
|
||||||
var a = await ctx.Set<EFClientStatistics>()
|
var a = await ctx.Set<EFClientStatistics>()
|
||||||
.Where(s => s.ServerId == serverId)
|
.Where(s => s.ServerId == serverId)
|
||||||
//.Where(s=> s.ClientId == 216105)
|
//.Where(s=> s.ClientId == 216105)
|
||||||
@ -150,16 +144,16 @@ namespace Stats.Client
|
|||||||
.Where(s => s.TimePlayed >= 3600 * 3)
|
.Where(s => s.TimePlayed >= 3600 * 3)
|
||||||
.Where(s => s.UpdatedAt >= Extensions.FifteenDaysAgo())
|
.Where(s => s.UpdatedAt >= Extensions.FifteenDaysAgo())
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var b = a.Distinct();
|
var b = a.Distinct();
|
||||||
|
|
||||||
foreach (var item in b)
|
foreach (var item in b)
|
||||||
{
|
{
|
||||||
await Plugin.Manager.UpdateHistoricalRanking(item.ClientId, item, item.ServerId);
|
await Plugin.Manager.UpdateHistoricalRanking(item.ClientId, item, item.ServerId);
|
||||||
//item.ZScore = await GetZScoreForServer(serverId, item.Performance);
|
//item.ZScore = await GetZScoreForServer(serverId, item.Performance);
|
||||||
//item.UpdatedAt = DateTime.UtcNow;
|
//item.UpdatedAt = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.SaveChangesAsync();
|
await ctx.SaveChangesAsync();
|
||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
@ -186,7 +180,7 @@ namespace Stats.Client
|
|||||||
|
|
||||||
var serverParams = await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
|
var serverParams = await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
|
||||||
Extensions.LogParams sdParams = null;
|
Extensions.LogParams sdParams = null;
|
||||||
|
|
||||||
if (serverId is not null && serverParams.TryGetValue(serverId.ToString(), out var sdParams1))
|
if (serverId is not null && serverParams.TryGetValue(serverId.ToString(), out var sdParams1))
|
||||||
{
|
{
|
||||||
sdParams = sdParams1;
|
sdParams = sdParams1;
|
||||||
@ -210,7 +204,7 @@ namespace Stats.Client
|
|||||||
|
|
||||||
public async Task<double?> GetRatingForZScore(double? value, string performanceBucket)
|
public async Task<double?> GetRatingForZScore(double? value, string performanceBucket)
|
||||||
{
|
{
|
||||||
var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new [] { performanceBucket });
|
var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new[] { performanceBucket ?? "null" });
|
||||||
return maxZScore == 0 ? null : value.GetRatingForZScore(maxZScore);
|
return maxZScore == 0 ? null : value.GetRatingForZScore(maxZScore);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,16 +9,17 @@ using SharedLibraryCore.Database.Models;
|
|||||||
using SharedLibraryCore.Configuration;
|
using SharedLibraryCore.Configuration;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using SharedLibraryCore.Commands;
|
using SharedLibraryCore.Commands;
|
||||||
|
using Stats.Dtos;
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Stats.Commands
|
namespace IW4MAdmin.Plugins.Stats.Commands
|
||||||
{
|
{
|
||||||
public class ViewStatsCommand : Command
|
public class ViewStatsCommand : Command
|
||||||
{
|
{
|
||||||
private readonly IDatabaseContextFactory _contextFactory;
|
private readonly IDatabaseContextFactory _contextFactory;
|
||||||
private readonly StatManager _statManager;
|
private readonly IResourceQueryHelper<ClientRankingInfoRequest, ClientRankingInfo> _queryHelper;
|
||||||
|
|
||||||
public ViewStatsCommand(CommandConfiguration config, ITranslationLookup translationLookup,
|
public ViewStatsCommand(CommandConfiguration config, ITranslationLookup translationLookup,
|
||||||
IDatabaseContextFactory contextFactory, StatManager statManager) : base(config, translationLookup)
|
IDatabaseContextFactory contextFactory, IResourceQueryHelper<ClientRankingInfoRequest, ClientRankingInfo> queryHelper) : base(config, translationLookup)
|
||||||
{
|
{
|
||||||
Name = "stats";
|
Name = "stats";
|
||||||
Description = translationLookup["PLUGINS_STATS_COMMANDS_VIEW_DESC"];
|
Description = translationLookup["PLUGINS_STATS_COMMANDS_VIEW_DESC"];
|
||||||
@ -35,7 +36,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
};
|
};
|
||||||
|
|
||||||
_contextFactory = contextFactory;
|
_contextFactory = contextFactory;
|
||||||
_statManager = statManager;
|
_queryHelper = queryHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task ExecuteAsync(GameEvent gameEvent)
|
public override async Task ExecuteAsync(GameEvent gameEvent)
|
||||||
@ -54,15 +55,19 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
}
|
}
|
||||||
|
|
||||||
var serverId = (gameEvent.Owner as IGameServer).LegacyDatabaseId;
|
var serverId = (gameEvent.Owner as IGameServer).LegacyDatabaseId;
|
||||||
var totalRankedPlayers = await _statManager.GetTotalRankedPlayers(serverId);
|
|
||||||
|
|
||||||
// getting stats for a particular client
|
// getting stats for a particular client
|
||||||
if (gameEvent.Target != null)
|
if (gameEvent.Target != null)
|
||||||
{
|
{
|
||||||
var performanceRanking = await _statManager.GetClientOverallRanking(gameEvent.Target.ClientId, serverId);
|
var performanceRanking = (await _queryHelper.QueryResource(new ClientRankingInfoRequest
|
||||||
var performanceRankingString = performanceRanking == 0
|
{
|
||||||
|
ClientId = gameEvent.Target.ClientId,
|
||||||
|
ServerEndpoint = gameEvent.Owner.Id
|
||||||
|
})).Results.First();
|
||||||
|
|
||||||
|
var performanceRankingString = performanceRanking.CurrentRanking == 0
|
||||||
? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"]
|
? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"]
|
||||||
: $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} (Color::Accent)#{performanceRanking}/{totalRankedPlayers}";
|
: $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} (Color::Accent)#{performanceRanking.CurrentRanking}/{performanceRanking.TotalRankedClients}";
|
||||||
|
|
||||||
// target is currently connected so we want their cached stats if they exist
|
// target is currently connected so we want their cached stats if they exist
|
||||||
if (gameEvent.Owner.GetClientsAsList().Any(client => client.Equals(gameEvent.Target)))
|
if (gameEvent.Owner.GetClientsAsList().Any(client => client.Equals(gameEvent.Target)))
|
||||||
@ -88,10 +93,15 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
// getting self stats
|
// getting self stats
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var performanceRanking = await _statManager.GetClientOverallRanking(gameEvent.Origin.ClientId, serverId);
|
var performanceRanking = (await _queryHelper.QueryResource(new ClientRankingInfoRequest
|
||||||
var performanceRankingString = performanceRanking == 0
|
{
|
||||||
|
ClientId = gameEvent.Origin.ClientId,
|
||||||
|
ServerEndpoint = gameEvent.Owner.Id
|
||||||
|
})).Results.First();
|
||||||
|
|
||||||
|
var performanceRankingString = performanceRanking.CurrentRanking == 0
|
||||||
? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"]
|
? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"]
|
||||||
: $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} (Color::Accent)#{performanceRanking}/{totalRankedPlayers}";
|
: $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} (Color::Accent)#{performanceRanking.CurrentRanking}/{performanceRanking.TotalRankedClients}";
|
||||||
|
|
||||||
// check if current client is connected to the server
|
// check if current client is connected to the server
|
||||||
if (gameEvent.Owner.GetClientsAsList().Any(client => client.Equals(gameEvent.Origin)))
|
if (gameEvent.Owner.GetClientsAsList().Any(client => client.Equals(gameEvent.Origin)))
|
||||||
|
@ -27,5 +27,6 @@ namespace Stats.Dtos
|
|||||||
public List<EFClientRankingHistory> Ratings { get; set; }
|
public List<EFClientRankingHistory> Ratings { get; set; }
|
||||||
public List<EFClientStatistics> LegacyStats { get; set; }
|
public List<EFClientStatistics> LegacyStats { get; set; }
|
||||||
public List<EFMeta> CustomMetrics { get; set; } = new();
|
public List<EFMeta> CustomMetrics { get; set; } = new();
|
||||||
|
public string PerformanceBucket { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3
Plugins/Stats/Dtos/ClientRankingInfo.cs
Normal file
3
Plugins/Stats/Dtos/ClientRankingInfo.cs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
namespace Stats.Dtos;
|
||||||
|
|
||||||
|
public record ClientRankingInfo(int CurrentRanking, int TotalRankedClients, string PerformanceBucket);
|
3
Plugins/Stats/Dtos/ClientRankingInfoRequest.cs
Normal file
3
Plugins/Stats/Dtos/ClientRankingInfoRequest.cs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
namespace Stats.Dtos;
|
||||||
|
|
||||||
|
public class ClientRankingInfoRequest : StatsInfoRequest;
|
@ -7,5 +7,6 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? ClientId { get; set; }
|
public int? ClientId { get; set; }
|
||||||
public string ServerEndpoint { get; set; }
|
public string ServerEndpoint { get; set; }
|
||||||
|
public string PerformanceBucket { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,40 +7,35 @@ using Data.Models;
|
|||||||
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.Helpers;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SharedLibraryCore.Dtos;
|
using SharedLibraryCore.Dtos;
|
||||||
using SharedLibraryCore.Helpers;
|
using SharedLibraryCore.Helpers;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using Stats.Dtos;
|
using Stats.Dtos;
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
|
||||||
|
|
||||||
namespace Stats.Helpers
|
namespace Stats.Helpers
|
||||||
{
|
{
|
||||||
public class AdvancedClientStatsResourceQueryHelper : IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo>
|
public class AdvancedClientStatsResourceQueryHelper(
|
||||||
|
ILogger<AdvancedClientStatsResourceQueryHelper> logger,
|
||||||
|
IDatabaseContextFactory contextFactory,
|
||||||
|
IServerDataViewer serverDataViewer,
|
||||||
|
StatManager statManager
|
||||||
|
)
|
||||||
|
: IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo>,
|
||||||
|
IResourceQueryHelper<ClientRankingInfoRequest, ClientRankingInfo>
|
||||||
{
|
{
|
||||||
private readonly IDatabaseContextFactory _contextFactory;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly IManager _manager;
|
|
||||||
|
|
||||||
public AdvancedClientStatsResourceQueryHelper(ILogger<AdvancedClientStatsResourceQueryHelper> logger,
|
|
||||||
IDatabaseContextFactory contextFactory, IManager manager)
|
|
||||||
{
|
|
||||||
_contextFactory = contextFactory;
|
|
||||||
_logger = logger;
|
|
||||||
_manager = manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ResourceQueryHelperResult<AdvancedStatsInfo>> QueryResource(StatsInfoRequest query)
|
public async Task<ResourceQueryHelperResult<AdvancedStatsInfo>> QueryResource(StatsInfoRequest query)
|
||||||
{
|
{
|
||||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
await using var context = contextFactory.CreateContext(enableTracking: false);
|
||||||
|
|
||||||
long? serverId = null;
|
long? serverId = null;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(query.ServerEndpoint))
|
if (!string.IsNullOrEmpty(query.ServerEndpoint))
|
||||||
{
|
{
|
||||||
serverId = (await context.Servers
|
serverId = (await context.Servers
|
||||||
.Select(server => new {server.EndPoint, server.Id})
|
.Select(server => new { server.EndPoint, server.Id })
|
||||||
.FirstOrDefaultAsync(server => server.EndPoint == query.ServerEndpoint))
|
.FirstOrDefaultAsync(server => server.EndPoint == query.ServerEndpoint))
|
||||||
?.Id;
|
?.Id;
|
||||||
}
|
}
|
||||||
@ -77,10 +72,18 @@ namespace Stats.Helpers
|
|||||||
.Where(r => r.ClientId == clientInfo.ClientId)
|
.Where(r => r.ClientId == clientInfo.ClientId)
|
||||||
.Where(r => r.ServerId == serverId)
|
.Where(r => r.ServerId == serverId)
|
||||||
.Where(r => r.Ranking != null)
|
.Where(r => r.Ranking != null)
|
||||||
|
.Where(r => r.PerformanceBucket == query.PerformanceBucket)
|
||||||
.OrderByDescending(r => r.CreatedDateTime)
|
.OrderByDescending(r => r.CreatedDateTime)
|
||||||
.Take(250)
|
.Take(250)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
var rankingInfo = (await QueryResource(new ClientRankingInfoRequest
|
||||||
|
{
|
||||||
|
ClientId = query.ClientId,
|
||||||
|
ServerEndpoint = query.ServerEndpoint,
|
||||||
|
PerformanceBucket = query.PerformanceBucket
|
||||||
|
})).Results.First();
|
||||||
|
|
||||||
var mostRecentRanking = ratings.FirstOrDefault(ranking => ranking.Newest);
|
var mostRecentRanking = ratings.FirstOrDefault(ranking => ranking.Newest);
|
||||||
var ranking = mostRecentRanking?.Ranking + 1;
|
var ranking = mostRecentRanking?.Ranking + 1;
|
||||||
|
|
||||||
@ -90,7 +93,9 @@ namespace Stats.Helpers
|
|||||||
.Where(stat => serverId == null || stat.ServerId == serverId)
|
.Where(stat => serverId == null || stat.ServerId == serverId)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
if (mostRecentRanking != null && mostRecentRanking.CreatedDateTime < Extensions.FifteenDaysAgo())
|
var bucketConfig = await statManager.GetBucketConfig(serverId);
|
||||||
|
|
||||||
|
if (mostRecentRanking != null && mostRecentRanking.CreatedDateTime < DateTime.UtcNow - bucketConfig.RankingExpiration)
|
||||||
{
|
{
|
||||||
ranking = 0;
|
ranking = 0;
|
||||||
}
|
}
|
||||||
@ -100,7 +105,7 @@ namespace Stats.Helpers
|
|||||||
ranking = null;
|
ranking = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hitInfo = new AdvancedStatsInfo()
|
var hitInfo = new AdvancedStatsInfo
|
||||||
{
|
{
|
||||||
ServerId = serverId,
|
ServerId = serverId,
|
||||||
Performance = mostRecentRanking?.PerformanceMetric,
|
Performance = mostRecentRanking?.PerformanceMetric,
|
||||||
@ -111,9 +116,12 @@ namespace Stats.Helpers
|
|||||||
Level = clientInfo.Level,
|
Level = clientInfo.Level,
|
||||||
Rating = mostRecentRanking?.PerformanceMetric,
|
Rating = mostRecentRanking?.PerformanceMetric,
|
||||||
All = hitStats,
|
All = hitStats,
|
||||||
Servers = _manager.GetServers()
|
Servers = Plugin.ServerManager.GetServers()
|
||||||
.Select(server => new ServerInfo
|
.Select(server => new ServerInfo
|
||||||
{Name = server.Hostname, IPAddress = server.ListenAddress, Port = server.ListenPort, Game = (Reference.Game)server.GameName})
|
{
|
||||||
|
Name = server.Hostname, IPAddress = server.ListenAddress, Port = server.ListenPort,
|
||||||
|
Game = (Reference.Game)server.GameName
|
||||||
|
})
|
||||||
.Where(server => server.Game == clientInfo.GameName)
|
.Where(server => server.Game == clientInfo.GameName)
|
||||||
.ToList(),
|
.ToList(),
|
||||||
Aggregate = hitStats.FirstOrDefault(hit =>
|
Aggregate = hitStats.FirstOrDefault(hit =>
|
||||||
@ -136,24 +144,82 @@ namespace Stats.Helpers
|
|||||||
Ratings = ratings,
|
Ratings = ratings,
|
||||||
LegacyStats = legacyStats,
|
LegacyStats = legacyStats,
|
||||||
Ranking = ranking,
|
Ranking = ranking,
|
||||||
|
TotalRankedClients = rankingInfo.TotalRankedClients,
|
||||||
|
PerformanceBucket = rankingInfo.PerformanceBucket
|
||||||
};
|
};
|
||||||
|
|
||||||
// todo: when nothign found
|
return new ResourceQueryHelperResult<AdvancedStatsInfo>
|
||||||
return new ResourceQueryHelperResult<AdvancedStatsInfo>()
|
|
||||||
{
|
{
|
||||||
Results = new[] {hitInfo}
|
Results = new[] { hitInfo }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Expression<Func<EFClientStatistics, bool>> GetRankingFunc(int minPlayTime, TimeSpan expiration, double? zScore = null,
|
public static Expression<Func<EFClientStatistics, bool>> GetRankingFunc(int minPlayTime, TimeSpan expiration,
|
||||||
|
double? zScore = null,
|
||||||
long? serverId = null)
|
long? serverId = null)
|
||||||
{
|
{
|
||||||
var oldestStat = DateTime.UtcNow.Subtract(expiration);
|
var oldestStat = DateTime.UtcNow.Subtract(expiration);
|
||||||
return stats => (serverId == null || stats.ServerId == serverId) &&
|
return stats => (serverId == null || stats.ServerId == serverId) &&
|
||||||
stats.UpdatedAt >= oldestStat &&
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ResourceQueryHelperResult<ClientRankingInfo>> QueryResource(ClientRankingInfoRequest query)
|
||||||
|
{
|
||||||
|
await using var context = contextFactory.CreateContext(enableTracking: false);
|
||||||
|
|
||||||
|
long? serverId = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(query.ServerEndpoint))
|
||||||
|
{
|
||||||
|
serverId = Plugin.ServerManager.Servers.FirstOrDefault(server => server.Id == query.ServerEndpoint)
|
||||||
|
?.LegacyDatabaseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentRanking = 0;
|
||||||
|
int totalRankedClients;
|
||||||
|
string performanceBucket;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(query.PerformanceBucket) && serverId is null)
|
||||||
|
{
|
||||||
|
var maxPerformance = await context.Set<EFClientRankingHistory>()
|
||||||
|
.Where(r => r.ClientId == query.ClientId)
|
||||||
|
.Where(r => r.Ranking != null)
|
||||||
|
.Where(r => r.ServerId == serverId)
|
||||||
|
.Where(rating => rating.Newest)
|
||||||
|
.GroupBy(rating => rating.PerformanceBucket)
|
||||||
|
.Select(grp => new { grp.Key, PerformanceMetric = grp.Max(rating => rating.Ranking) })
|
||||||
|
.Where(grp => grp.PerformanceMetric != null)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (maxPerformance is null)
|
||||||
|
{
|
||||||
|
currentRanking = 0;
|
||||||
|
totalRankedClients = 0;
|
||||||
|
performanceBucket = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentRanking =
|
||||||
|
await statManager.GetClientOverallRanking(query.ClientId!.Value, null, maxPerformance.Key);
|
||||||
|
totalRankedClients = await serverDataViewer.RankedClientsCountAsync(null, maxPerformance.Key);
|
||||||
|
performanceBucket = maxPerformance.Key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
performanceBucket = query.PerformanceBucket;
|
||||||
|
currentRanking =
|
||||||
|
await statManager.GetClientOverallRanking(query.ClientId!.Value, serverId, performanceBucket);
|
||||||
|
totalRankedClients = await serverDataViewer.RankedClientsCountAsync(serverId, performanceBucket);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ResourceQueryHelperResult<ClientRankingInfo>
|
||||||
|
{
|
||||||
|
Results = [new ClientRankingInfo(currentRanking, totalRankedClients, performanceBucket)]
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,18 +75,23 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
/// gets a ranking across all servers for given client id
|
/// gets a ranking across all servers for given client id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="clientId">client id of the player</param>
|
/// <param name="clientId">client id of the player</param>
|
||||||
|
/// <param name="serverId"></param>
|
||||||
|
/// <param name="performanceBucket"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<int> GetClientOverallRanking(int clientId, long? serverId = null)
|
public async Task<int> GetClientOverallRanking(int clientId, long? serverId = null, string performanceBucket = null)
|
||||||
{
|
{
|
||||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||||
|
|
||||||
if (_config.EnableAdvancedMetrics)
|
if (_config.EnableAdvancedMetrics)
|
||||||
{
|
{
|
||||||
|
var bucketConfig = await GetBucketConfig(null, performanceBucket);
|
||||||
|
|
||||||
var clientRanking = await context.Set<EFClientRankingHistory>()
|
var clientRanking = await context.Set<EFClientRankingHistory>()
|
||||||
|
.Where(GetNewRankingFunc(bucketConfig.RankingExpiration, bucketConfig.ClientMinPlayTime, serverId, performanceBucket))
|
||||||
.Where(r => r.ClientId == clientId)
|
.Where(r => r.ClientId == clientId)
|
||||||
.Where(r => r.ServerId == serverId)
|
|
||||||
.Where(r => r.Newest)
|
.Where(r => r.Newest)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
return clientRanking?.Ranking + 1 ?? 0;
|
return clientRanking?.Ranking + 1 ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,7 +115,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Expression<Func<EFClientRankingHistory, bool>> GetNewRankingFunc(TimeSpan oldestStat, TimeSpan minPlayTime, long? serverId = null)
|
private Expression<Func<EFClientRankingHistory, bool>> GetNewRankingFunc(TimeSpan oldestStat, TimeSpan minPlayTime, long? serverId = null, string performanceBucket = null)
|
||||||
{
|
{
|
||||||
var oldestDate = DateTime.UtcNow - oldestStat;
|
var oldestDate = DateTime.UtcNow - oldestStat;
|
||||||
return ranking => ranking.ServerId == serverId
|
return ranking => ranking.ServerId == serverId
|
||||||
@ -119,12 +124,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
&& ranking.ZScore != null
|
&& ranking.ZScore != null
|
||||||
&& ranking.PerformanceMetric != null
|
&& ranking.PerformanceMetric != null
|
||||||
&& ranking.Newest
|
&& ranking.Newest
|
||||||
|
&& ranking.PerformanceBucket == performanceBucket
|
||||||
&& ranking.Client.TotalConnectionTime >= (int)minPlayTime.TotalSeconds;
|
&& ranking.Client.TotalConnectionTime >= (int)minPlayTime.TotalSeconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> GetTotalRankedPlayers(long serverId)
|
public async Task<int> GetTotalRankedPlayers(long? serverId = null, string performanceBucket = null)
|
||||||
{
|
{
|
||||||
var bucketConfig = await GetBucketConfig(serverId);
|
var bucketConfig = await GetBucketConfig(serverId, performanceBucket);
|
||||||
|
|
||||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||||
|
|
||||||
@ -150,7 +156,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
await using var context = _contextFactory.CreateContext(false);
|
await using var context = _contextFactory.CreateContext(false);
|
||||||
var clientIdsList = await context.Set<EFClientRankingHistory>()
|
var clientIdsList = await context.Set<EFClientRankingHistory>()
|
||||||
.Where(GetNewRankingFunc(bucketConfig.RankingExpiration, bucketConfig.ClientMinPlayTime, serverId: serverId))
|
.Where(GetNewRankingFunc(bucketConfig.RankingExpiration, bucketConfig.ClientMinPlayTime, serverId: serverId, performanceBucket))
|
||||||
.OrderByDescending(ranking => ranking.PerformanceMetric)
|
.OrderByDescending(ranking => ranking.PerformanceMetric)
|
||||||
.Select(ranking => ranking.ClientId)
|
.Select(ranking => ranking.ClientId)
|
||||||
.Skip(start)
|
.Skip(start)
|
||||||
@ -164,6 +170,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
var eachRank = await context.Set<EFClientRankingHistory>()
|
var eachRank = await context.Set<EFClientRankingHistory>()
|
||||||
.Where(ranking => ranking.ClientId == clientId)
|
.Where(ranking => ranking.ClientId == clientId)
|
||||||
.Where(ranking => ranking.ServerId == serverId)
|
.Where(ranking => ranking.ServerId == serverId)
|
||||||
|
.Where(ranking => ranking.PerformanceBucket == performanceBucket)
|
||||||
.OrderByDescending(ranking => ranking.CreatedDateTime)
|
.OrderByDescending(ranking => ranking.CreatedDateTime)
|
||||||
.Select(ranking => new RankingSnapshot
|
.Select(ranking => new RankingSnapshot
|
||||||
{
|
{
|
||||||
@ -177,16 +184,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
})
|
})
|
||||||
.Take(60)
|
.Take(60)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
if (rankingsDict.ContainsKey(clientId))
|
if (!rankingsDict.TryAdd(clientId, eachRank))
|
||||||
{
|
{
|
||||||
rankingsDict[clientId] = rankingsDict[clientId].Concat(eachRank).Distinct()
|
rankingsDict[clientId] = rankingsDict[clientId].Concat(eachRank).Distinct()
|
||||||
.OrderByDescending(ranking => ranking.CreatedDateTime).ToList();
|
.OrderByDescending(ranking => ranking.CreatedDateTime).ToList();
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
rankingsDict.Add(clientId, eachRank);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var statsInfo = await context.Set<EFClientStatistics>()
|
var statsInfo = await context.Set<EFClientStatistics>()
|
||||||
@ -224,7 +227,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
RatingChange = (rankingsDict[s.ClientId].Last().Ranking -
|
RatingChange = (rankingsDict[s.ClientId].Last().Ranking -
|
||||||
rankingsDict[s.ClientId].First().Ranking) ?? 0,
|
rankingsDict[s.ClientId].First().Ranking) ?? 0,
|
||||||
PerformanceHistory = rankingsDict[s.ClientId].Select(ranking => new PerformanceHistory
|
PerformanceHistory = rankingsDict[s.ClientId].Select(ranking => new PerformanceHistory
|
||||||
{Performance = ranking.PerformanceMetric ?? 0, OccurredAt = ranking.CreatedDateTime})
|
{ Performance = ranking.PerformanceMetric ?? 0, OccurredAt = ranking.CreatedDateTime })
|
||||||
.ToList(),
|
.ToList(),
|
||||||
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
|
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
|
||||||
TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed),
|
TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed),
|
||||||
@ -281,7 +284,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
return finished;
|
return finished;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<PerformanceBucketConfiguration> GetBucketConfig(long? serverId)
|
public async Task<PerformanceBucketConfiguration> GetBucketConfig(long? serverId = null,
|
||||||
|
string bucketName = null)
|
||||||
{
|
{
|
||||||
var defaultConfig = new PerformanceBucketConfiguration
|
var defaultConfig = new PerformanceBucketConfiguration
|
||||||
{
|
{
|
||||||
@ -289,11 +293,17 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
RankingExpiration = DateTime.UtcNow - Extensions.FifteenDaysAgo()
|
RankingExpiration = DateTime.UtcNow - Extensions.FifteenDaysAgo()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (serverId is null)
|
if (serverId is null && bucketName is null)
|
||||||
{
|
{
|
||||||
return defaultConfig;
|
return defaultConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bucketName is not null)
|
||||||
|
{
|
||||||
|
return _config.PerformanceBuckets.FirstOrDefault(bucket => bucket.Name == bucketName) ??
|
||||||
|
defaultConfig;
|
||||||
|
}
|
||||||
|
|
||||||
var performanceBucket =
|
var performanceBucket =
|
||||||
(await _serverCache.FirstAsync(server => server.Id == serverId)).PerformanceBucket;
|
(await _serverCache.FirstAsync(server => server.Id == serverId)).PerformanceBucket;
|
||||||
|
|
||||||
@ -306,11 +316,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
defaultConfig;
|
defaultConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<TopStatsInfo>> GetTopStats(int start, int count, long? serverId = null)
|
public async Task<List<TopStatsInfo>> GetTopStats(int start, int count, long? serverId = null, string performanceBucket = null)
|
||||||
{
|
{
|
||||||
if (_config.EnableAdvancedMetrics)
|
if (_config.EnableAdvancedMetrics)
|
||||||
{
|
{
|
||||||
return await GetNewTopStats(start, count, serverId);
|
return await GetNewTopStats(start, count, serverId, performanceBucket);
|
||||||
}
|
}
|
||||||
|
|
||||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||||
@ -355,7 +365,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
.Select(grp => new
|
.Select(grp => new
|
||||||
{
|
{
|
||||||
grp.Key,
|
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>()
|
var iqStatsInfo = (from stat in context.Set<EFClientStatistics>()
|
||||||
@ -393,7 +403,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking,
|
ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking,
|
||||||
PerformanceHistory = ratingInfo.First(r => r.Key == s.ClientId).Ratings.Count() > 1
|
PerformanceHistory = ratingInfo.First(r => r.Key == s.ClientId).Ratings.Count() > 1
|
||||||
? ratingInfo.First(r => r.Key == s.ClientId).Ratings.OrderBy(r => r.When)
|
? ratingInfo.First(r => r.Key == s.ClientId).Ratings.OrderBy(r => r.When)
|
||||||
.Select(r => new PerformanceHistory {Performance = r.Performance, OccurredAt = r.When})
|
.Select(r => new PerformanceHistory { Performance = r.Performance, OccurredAt = r.When })
|
||||||
.ToList()
|
.ToList()
|
||||||
: new List<PerformanceHistory>
|
: new List<PerformanceHistory>
|
||||||
{
|
{
|
||||||
@ -1007,7 +1017,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
victimStats.LastScore = estimatedVictimScore;
|
victimStats.LastScore = estimatedVictimScore;
|
||||||
|
|
||||||
// show encouragement/discouragement
|
// show encouragement/discouragement
|
||||||
var streakMessage = attackerStats.ClientId != victimStats.ClientId
|
var streakMessage = attacker.CurrentServer.IsZombieServer() ? string.Empty : attackerStats.ClientId != victimStats.ClientId
|
||||||
? StreakMessage.MessageOnStreak(attackerStats.KillStreak, attackerStats.DeathStreak, _config)
|
? StreakMessage.MessageOnStreak(attackerStats.KillStreak, attackerStats.DeathStreak, _config)
|
||||||
: StreakMessage.MessageOnStreak(-1, -1, _config);
|
: StreakMessage.MessageOnStreak(-1, -1, _config);
|
||||||
|
|
||||||
@ -1258,6 +1268,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
.Include(stat => stat.Server)
|
.Include(stat => stat.Server)
|
||||||
.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(stat => stat.Server.PerformanceBucket == bucketConfig.Name)
|
||||||
.Where(stats => stats.UpdatedAt >= oldestStateDate)
|
.Where(stats => stats.UpdatedAt >= oldestStateDate)
|
||||||
.Where(stats => stats.TimePlayed >= (int)bucketConfig.ClientMinPlayTime.TotalSeconds)
|
.Where(stats => stats.TimePlayed >= (int)bucketConfig.ClientMinPlayTime.TotalSeconds)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
@ -1278,11 +1289,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
private async Task UpdateAggregateForServerOrBucket(int clientId, EFClientStatistics clientStats, DatabaseContext context, List<EFClientStatistics> performances, PerformanceBucketConfiguration bucketConfig)
|
private async Task UpdateAggregateForServerOrBucket(int clientId, EFClientStatistics clientStats, DatabaseContext context, List<EFClientStatistics> performances, PerformanceBucketConfiguration bucketConfig)
|
||||||
{
|
{
|
||||||
var aggregateZScore =
|
var aggregateZScore =
|
||||||
performances.Where(performance => performance.Server.PerformanceBucket == bucketConfig.Name)
|
performances.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), (int)bucketConfig.ClientMinPlayTime.TotalSeconds);
|
||||||
.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), (int)bucketConfig.ClientMinPlayTime.TotalSeconds);
|
|
||||||
|
|
||||||
int? aggregateRanking = await context.Set<EFClientStatistics>()
|
int? aggregateRanking = await context.Set<EFClientStatistics>()
|
||||||
.Where(stat => stat.ClientId != clientId)
|
.Where(stat => stat.ClientId != clientId)
|
||||||
|
.Where(stat => bucketConfig.Name == stat.Server.PerformanceBucket)
|
||||||
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc((int)bucketConfig.ClientMinPlayTime.TotalSeconds, bucketConfig.RankingExpiration))
|
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc((int)bucketConfig.ClientMinPlayTime.TotalSeconds, bucketConfig.RankingExpiration))
|
||||||
.GroupBy(stat => stat.ClientId)
|
.GroupBy(stat => stat.ClientId)
|
||||||
.Where(group =>
|
.Where(group =>
|
||||||
|
@ -46,6 +46,7 @@ public class Plugin : IPluginV2
|
|||||||
private readonly IServerDataViewer _serverDataViewer;
|
private readonly IServerDataViewer _serverDataViewer;
|
||||||
private readonly StatsConfiguration _statsConfig;
|
private readonly StatsConfiguration _statsConfig;
|
||||||
private readonly StatManager _statManager;
|
private readonly StatManager _statManager;
|
||||||
|
private readonly IResourceQueryHelper<ClientRankingInfoRequest, ClientRankingInfo> _queryHelper;
|
||||||
|
|
||||||
public static void RegisterDependencies(IServiceCollection serviceCollection)
|
public static void RegisterDependencies(IServiceCollection serviceCollection)
|
||||||
{
|
{
|
||||||
@ -57,8 +58,9 @@ public class Plugin : IPluginV2
|
|||||||
ITranslationLookup translationLookup, IMetaServiceV2 metaService,
|
ITranslationLookup translationLookup, IMetaServiceV2 metaService,
|
||||||
IResourceQueryHelper<ChatSearchQuery, MessageResponse> chatQueryHelper,
|
IResourceQueryHelper<ChatSearchQuery, MessageResponse> chatQueryHelper,
|
||||||
IEnumerable<IClientStatisticCalculator> statCalculators,
|
IEnumerable<IClientStatisticCalculator> statCalculators,
|
||||||
IServerDistributionCalculator serverDistributionCalculator, IServerDataViewer serverDataViewer,
|
IServerDistributionCalculator serverDistributionCalculator,
|
||||||
StatsConfiguration statsConfig, StatManager statManager)
|
StatsConfiguration statsConfig, StatManager statManager,
|
||||||
|
IResourceQueryHelper<ClientRankingInfoRequest, ClientRankingInfo> queryHelper)
|
||||||
{
|
{
|
||||||
_databaseContextFactory = databaseContextFactory;
|
_databaseContextFactory = databaseContextFactory;
|
||||||
_translationLookup = translationLookup;
|
_translationLookup = translationLookup;
|
||||||
@ -70,6 +72,7 @@ public class Plugin : IPluginV2
|
|||||||
_serverDataViewer = serverDataViewer;
|
_serverDataViewer = serverDataViewer;
|
||||||
_statsConfig = statsConfig;
|
_statsConfig = statsConfig;
|
||||||
_statManager = statManager;
|
_statManager = statManager;
|
||||||
|
_queryHelper = queryHelper;
|
||||||
|
|
||||||
IGameServerEventSubscriptions.MonitoringStopped +=
|
IGameServerEventSubscriptions.MonitoringStopped +=
|
||||||
async (monitorEvent, token) => await _statManager.Sync(monitorEvent.Server, token);
|
async (monitorEvent, token) => await _statManager.Sync(monitorEvent.Server, token);
|
||||||
@ -117,7 +120,7 @@ public class Plugin : IPluginV2
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
IGameEventSubscriptions.MatchEnded += OnMatchEvent;
|
IGameEventSubscriptions.MatchEnded += OnMatchEvent;
|
||||||
IGameEventSubscriptions.RoundEnded += (roundEndedEvent, token) => _statManager.Sync(roundEndedEvent.Server, token);
|
IGameEventSubscriptions.RoundEnded += OnRoundEnded;
|
||||||
IGameEventSubscriptions.MatchStarted += OnMatchEvent;
|
IGameEventSubscriptions.MatchStarted += OnMatchEvent;
|
||||||
IGameEventSubscriptions.ScriptEventTriggered += OnScriptEvent;
|
IGameEventSubscriptions.ScriptEventTriggered += OnScriptEvent;
|
||||||
IGameEventSubscriptions.ClientKilled += OnClientKilled;
|
IGameEventSubscriptions.ClientKilled += OnClientKilled;
|
||||||
@ -126,6 +129,16 @@ public class Plugin : IPluginV2
|
|||||||
IManagementEventSubscriptions.Load += OnLoad;
|
IManagementEventSubscriptions.Load += OnLoad;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task OnRoundEnded(RoundEndEvent roundEndedEvent, CancellationToken token)
|
||||||
|
{
|
||||||
|
await _statManager.Sync(roundEndedEvent.Server, token);
|
||||||
|
|
||||||
|
foreach (var calculator in _statCalculators)
|
||||||
|
{
|
||||||
|
await calculator.CalculateForEvent(roundEndedEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task OnClientKilled(ClientKillEvent killEvent, CancellationToken token)
|
private async Task OnClientKilled(ClientKillEvent killEvent, CancellationToken token)
|
||||||
{
|
{
|
||||||
if (!ShouldIgnoreEvent(killEvent.Attacker, killEvent.Victim))
|
if (!ShouldIgnoreEvent(killEvent.Attacker, killEvent.Victim))
|
||||||
@ -258,7 +271,10 @@ public class Plugin : IPluginV2
|
|||||||
var performance =
|
var performance =
|
||||||
Math.Round(validPerformanceValues.Sum(c => c.Performance * c.TimePlayed / performancePlayTime), 2);
|
Math.Round(validPerformanceValues.Sum(c => c.Performance * c.TimePlayed / performancePlayTime), 2);
|
||||||
var spm = Math.Round(clientStats.Sum(c => c.SPM) / clientStats.Count(c => c.SPM > 0), 1);
|
var spm = Math.Round(clientStats.Sum(c => c.SPM) / clientStats.Count(c => c.SPM > 0), 1);
|
||||||
var overallRanking = await _statManager.GetClientOverallRanking(request.ClientId);
|
var ranking = (await _queryHelper.QueryResource(new ClientRankingInfoRequest
|
||||||
|
{
|
||||||
|
ClientId = request.ClientId,
|
||||||
|
})).Results.First();
|
||||||
|
|
||||||
return new List<InformationResponse>
|
return new List<InformationResponse>
|
||||||
{
|
{
|
||||||
@ -267,12 +283,12 @@ public class Plugin : IPluginV2
|
|||||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"],
|
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"],
|
||||||
Value = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING_FORMAT"]
|
Value = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING_FORMAT"]
|
||||||
.FormatExt(
|
.FormatExt(
|
||||||
(overallRanking == 0
|
(ranking.CurrentRanking == 0
|
||||||
? "--"
|
? "--"
|
||||||
: overallRanking.ToString("#,##0",
|
: ranking.CurrentRanking.ToString("#,##0",
|
||||||
new System.Globalization.CultureInfo(Utilities.CurrentLocalization
|
new System.Globalization.CultureInfo(Utilities.CurrentLocalization
|
||||||
.LocalizationName))),
|
.LocalizationName))),
|
||||||
(await _serverDataViewer.RankedClientsCountAsync(token: token)).ToString("#,##0",
|
ranking.TotalRankedClients.ToString("#,##0",
|
||||||
new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))
|
new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))
|
||||||
),
|
),
|
||||||
Column = 0,
|
Column = 0,
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
namespace SharedLibraryCore.Events;
|
namespace SharedLibraryCore.Events;
|
||||||
|
|
||||||
@ -31,8 +32,7 @@ public static class EventExtensions
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// todo: static logger
|
Log.Error(ex, "InvokeAsync for event type {EventType} failed. Cancellation Token is None", typeof(TEventType).Name);
|
||||||
Console.WriteLine("InvokeAsync: " + ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,8 +46,8 @@ public static class EventExtensions
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// todo: static logger
|
Log.Error(ex, "InvokeAsync for event type {EventType} failed. IsCancellationRequested is {TokenStatus}",
|
||||||
Console.WriteLine("InvokeAsync: " + ex);
|
typeof(TEventType).Name, tokenSource.Token.IsCancellationRequested);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,8 +45,9 @@ namespace SharedLibraryCore.Interfaces
|
|||||||
/// Retrieves the number of ranked clients for given server id
|
/// Retrieves the number of ranked clients for given server id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverId">ServerId to query on</param>
|
/// <param name="serverId">ServerId to query on</param>
|
||||||
|
/// <param name="performanceBucket"></param>
|
||||||
/// <param name="token">CancellationToken</param>
|
/// <param name="token">CancellationToken</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default);
|
Task<int> RankedClientsCountAsync(long? serverId = null, string performanceBucket = null, CancellationToken token = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,9 +219,11 @@ namespace SharedLibraryCore
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="server"></param>
|
/// <param name="server"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static bool IsZombieServer(this Server server)
|
public static bool IsZombieServer(this Server server) => (server as IGameServer).IsZombieServer();
|
||||||
|
|
||||||
|
public static bool IsZombieServer(this IGameServer server)
|
||||||
{
|
{
|
||||||
return new[] { Game.T4, Game.T5, Game.T6 }.Contains(server.GameName) &&
|
return new[] { Reference.Game.T4, Reference.Game.T5, Reference.Game.T6 }.Contains(server.GameCode) &&
|
||||||
ZmGameTypes.Contains(server.Gametype.ToLower());
|
ZmGameTypes.Contains(server.Gametype.ToLower());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,8 +54,6 @@ namespace WebfrontCore.Controllers
|
|||||||
{
|
{
|
||||||
await statMetricFunc(new Dictionary<int, List<EFMeta>> { { id, hitInfo.CustomMetrics } }, matchedServerId, null, false);
|
await statMetricFunc(new Dictionary<int, List<EFMeta>> { { id, hitInfo.CustomMetrics } }, matchedServerId, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
hitInfo.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, token);
|
|
||||||
|
|
||||||
return View("~/Views/Client/Statistics/Advanced.cshtml", hitInfo);
|
return View("~/Views/Client/Statistics/Advanced.cshtml", hitInfo);
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,24 @@
|
|||||||
using IW4MAdmin.Plugins.Stats;
|
using System;
|
||||||
using IW4MAdmin.Plugins.Stats.Helpers;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using SharedLibraryCore;
|
|
||||||
using SharedLibraryCore.Dtos;
|
|
||||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
|
||||||
using SharedLibraryCore.Interfaces;
|
|
||||||
using Stats.Dtos;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
|
||||||
using Data.Abstractions;
|
using Data.Abstractions;
|
||||||
|
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Dtos;
|
||||||
|
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
using Stats.Config;
|
using Stats.Config;
|
||||||
|
using Stats.Dtos;
|
||||||
using WebfrontCore.QueryHelpers.Models;
|
using WebfrontCore.QueryHelpers.Models;
|
||||||
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
namespace WebfrontCore.Controllers.Client.Legacy
|
||||||
{
|
{
|
||||||
public class StatsController : BaseController
|
public class StatsController : BaseController
|
||||||
{
|
{
|
||||||
@ -62,7 +61,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
|||||||
matchedServerId = server.LegacyDatabaseId;
|
matchedServerId = server.LegacyDatabaseId;
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewBag.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, token);
|
ViewBag.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, null, token);
|
||||||
ViewBag.ServerId = matchedServerId;
|
ViewBag.ServerId = matchedServerId;
|
||||||
|
|
||||||
return View("~/Views/Client/Statistics/Index.cshtml", _manager.GetServers()
|
return View("~/Views/Client/Statistics/Index.cshtml", _manager.GetServers()
|
||||||
|
@ -113,6 +113,7 @@ namespace WebfrontCore
|
|||||||
services.AddTransient<IValidator<FindClientRequest>, FindClientRequestValidator>();
|
services.AddTransient<IValidator<FindClientRequest>, FindClientRequestValidator>();
|
||||||
services.AddSingleton<IResourceQueryHelper<FindClientRequest, FindClientResult>, ClientService>();
|
services.AddSingleton<IResourceQueryHelper<FindClientRequest, FindClientResult>, ClientService>();
|
||||||
services.AddSingleton<IResourceQueryHelper<StatsInfoRequest, StatsInfoResult>, StatsResourceQueryHelper>();
|
services.AddSingleton<IResourceQueryHelper<StatsInfoRequest, StatsInfoResult>, StatsResourceQueryHelper>();
|
||||||
|
services.AddSingleton<IResourceQueryHelper<ClientRankingInfoRequest, ClientRankingInfo>, AdvancedClientStatsResourceQueryHelper>();
|
||||||
services
|
services
|
||||||
.AddSingleton<IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo>,
|
.AddSingleton<IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo>,
|
||||||
AdvancedClientStatsResourceQueryHelper>();
|
AdvancedClientStatsResourceQueryHelper>();
|
||||||
|
Reference in New Issue
Block a user