update stats plugin to IPluginV2

This commit is contained in:
RaidMax 2023-02-11 21:01:28 -06:00
parent 7b8f6421aa
commit 66c0561e7f
19 changed files with 1388 additions and 1326 deletions

View File

@ -1,6 +1,5 @@
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -10,6 +9,7 @@ using Data.Models.Client;
using Data.Models.Client.Stats; using Data.Models.Client.Stats;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore; using SharedLibraryCore;
using Stats.Config;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Plugins.Stats.Cheat namespace IW4MAdmin.Plugins.Stats.Cheat
@ -37,6 +37,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
Dictionary<IW4Info.HitLocation, HitInfo> HitLocationCount; Dictionary<IW4Info.HitLocation, HitInfo> HitLocationCount;
double AngleDifferenceAverage; double AngleDifferenceAverage;
EFClientStatistics ClientStats; EFClientStatistics ClientStats;
private readonly StatsConfiguration _statsConfiguration;
long LastOffset; long LastOffset;
string LastWeapon; string LastWeapon;
ILogger Log; ILogger Log;
@ -55,7 +56,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
public double Offset { get; set; } public double Offset { get; set; }
}; };
public Detection(ILogger log, EFClientStatistics clientStats) public Detection(ILogger log, EFClientStatistics clientStats, StatsConfiguration statsConfiguration)
{ {
Log = log; Log = log;
HitLocationCount = new Dictionary<IW4Info.HitLocation, HitInfo>(); HitLocationCount = new Dictionary<IW4Info.HitLocation, HitInfo>();
@ -65,6 +66,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
} }
ClientStats = clientStats; ClientStats = clientStats;
_statsConfiguration = statsConfiguration;
Strain = new Strain(); Strain = new Strain();
Tracker = new ChangeTracking<EFACSnapshot>(); Tracker = new ChangeTracking<EFACSnapshot>();
TrackedHits = new List<EFClientKill>(); TrackedHits = new List<EFClientKill>();
@ -308,7 +310,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
bool shouldIgnoreDetection = false; bool shouldIgnoreDetection = false;
try try
{ {
shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[(Server.Game)hit.GameName][DetectionType.Recoil] shouldIgnoreDetection = _statsConfiguration.AnticheatConfiguration.IgnoredDetectionSpecification[(Server.Game)hit.GameName][DetectionType.Recoil]
.Any(_weaponRegex => Regex.IsMatch(hit.WeaponReference, _weaponRegex)); .Any(_weaponRegex => Regex.IsMatch(hit.WeaponReference, _weaponRegex));
} }
@ -340,7 +342,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
try try
{ {
shouldIgnoreDetection = false; shouldIgnoreDetection = false;
shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[(Server.Game)hit.GameName][DetectionType.Button] shouldIgnoreDetection = _statsConfiguration.AnticheatConfiguration.IgnoredDetectionSpecification[(Server.Game)hit.GameName][DetectionType.Button]
.Any(_weaponRegex => Regex.IsMatch(hit.WeaponReference, _weaponRegex)); .Any(_weaponRegex => Regex.IsMatch(hit.WeaponReference, _weaponRegex));
} }
@ -453,7 +455,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
try try
{ {
shouldIgnoreDetection = false; // reset previous value shouldIgnoreDetection = false; // reset previous value
shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[(Server.Game)hit.GameName][DetectionType.Chest] shouldIgnoreDetection = _statsConfiguration.AnticheatConfiguration.IgnoredDetectionSpecification[(Server.Game)hit.GameName][DetectionType.Chest]
.Any(_weaponRegex => Regex.IsMatch(hit.WeaponReference, _weaponRegex)); .Any(_weaponRegex => Regex.IsMatch(hit.WeaponReference, _weaponRegex));
} }

View File

@ -1,11 +1,11 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using SharedLibraryCore; using SharedLibraryCore.Events;
namespace IW4MAdmin.Plugins.Stats.Client.Abstractions namespace IW4MAdmin.Plugins.Stats.Client.Abstractions
{ {
public interface IClientStatisticCalculator public interface IClientStatisticCalculator
{ {
Task GatherDependencies(); Task GatherDependencies();
Task CalculateForEvent(GameEvent gameEvent); Task CalculateForEvent(CoreEvent coreEvent);
} }
} }

View File

@ -1,11 +1,11 @@
using IW4MAdmin.Plugins.Stats.Client.Game; using Data.Models;
using SharedLibraryCore; using IW4MAdmin.Plugins.Stats.Client.Game;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
namespace Stats.Client.Abstractions namespace Stats.Client.Abstractions
{ {
public interface IHitInfoBuilder public interface IHitInfoBuilder
{ {
HitInfo Build(string[] log, ParserRegex parserRegex, int entityId, bool isSelf, bool isVictim, Server.Game gameName); HitInfo Build(string[] log, ParserRegex parserRegex, int entityId, bool isSelf, bool isVictim, Reference.Game gameName);
} }
} }

View File

@ -1,10 +1,9 @@
using SharedLibraryCore; using Data.Models;
using Stats.Client.Game; using Stats.Client.Game;
namespace Stats.Client.Abstractions namespace Stats.Client.Abstractions;
{
public interface IWeaponNameParser public interface IWeaponNameParser
{ {
WeaponInfo Parse(string weaponName, Server.Game gameName); WeaponInfo Parse(string weaponName, Reference.Game gameName);
}
} }

View File

@ -14,13 +14,15 @@ using IW4MAdmin.Plugins.Stats.Client.Game;
using IW4MAdmin.Plugins.Stats.Helpers; using IW4MAdmin.Plugins.Stats.Helpers;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Events;
using SharedLibraryCore.Events.Game;
using SharedLibraryCore.Events.Management;
using Stats.Client.Abstractions; using Stats.Client.Abstractions;
using Stats.Client.Game; using Stats.Client.Game;
namespace IW4MAdmin.Plugins.Stats.Client namespace IW4MAdmin.Plugins.Stats.Client;
{
public class HitState public class HitState
{ {
public HitState() public HitState()
@ -46,8 +48,7 @@ namespace IW4MAdmin.Plugins.Stats.Client
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger<HitCalculator> _logger; private readonly ILogger<HitCalculator> _logger;
private readonly ConcurrentDictionary<int, HitState> _clientHitStatistics = private readonly ConcurrentDictionary<int, HitState> _clientHitStatistics = new();
new ConcurrentDictionary<int, HitState>();
private readonly SemaphoreSlim _onTransaction = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim _onTransaction = new SemaphoreSlim(1, 1);
@ -93,9 +94,9 @@ namespace IW4MAdmin.Plugins.Stats.Client
await _modCache.InitializeAsync(); await _modCache.InitializeAsync();
} }
public async Task CalculateForEvent(GameEvent gameEvent) public async Task CalculateForEvent(CoreEvent coreEvent)
{ {
if (gameEvent.Type == GameEvent.EventType.Connect) if (coreEvent is ClientStateInitializeEvent clientStateInitializeEvent)
{ {
// if no servers have been cached yet we need to pull them here // if no servers have been cached yet we need to pull them here
// as they could have gotten added after we've initialized // as they could have gotten added after we've initialized
@ -104,32 +105,32 @@ namespace IW4MAdmin.Plugins.Stats.Client
await _serverCache.InitializeAsync(); await _serverCache.InitializeAsync();
} }
gameEvent.Origin.SetAdditionalProperty(SessionScores, new List<(int, DateTime)>()); clientStateInitializeEvent.Client.SetAdditionalProperty(SessionScores, new List<(int, DateTime)>());
return; return;
} }
if (gameEvent.Type == GameEvent.EventType.Disconnect) if (coreEvent is ClientStateDisposeEvent clientStateDisposeEvent)
{ {
_clientHitStatistics.Remove(gameEvent.Origin.ClientId, out var state); _clientHitStatistics.Remove(clientStateDisposeEvent.Client.ClientId, out var state);
if (state == null) if (state == null)
{ {
_logger.LogWarning("No client hit state available for disconnecting client {client}", _logger.LogWarning("No client hit state available for disconnecting client {Client}",
gameEvent.Origin.ToString()); clientStateDisposeEvent.Client.ToString());
return; return;
} }
try try
{ {
await state.OnTransaction.WaitAsync(); await state.OnTransaction.WaitAsync();
HandleDisconnectCalculations(gameEvent.Origin, state); HandleDisconnectCalculations(clientStateDisposeEvent.Client, state);
await UpdateClientStatistics(gameEvent.Origin.ClientId, state); await UpdateClientStatistics(clientStateDisposeEvent.Client.ClientId, state);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Could not handle disconnect calculations for client {client}", _logger.LogError(ex, "Could not handle disconnect calculations for client {Client}",
gameEvent.Origin.ToString()); clientStateDisposeEvent.Client.ToString());
} }
finally finally
@ -143,37 +144,40 @@ namespace IW4MAdmin.Plugins.Stats.Client
return; return;
} }
if (gameEvent.Type == GameEvent.EventType.MapEnd) if (coreEvent is MatchEndEvent matchEndEvent)
{ {
foreach (var client in gameEvent.Owner.GetClientsAsList()) foreach (var client in matchEndEvent.Server.ConnectedClients)
{ {
var scores = client.GetAdditionalProperty<List<(int, DateTime)>>(SessionScores); var scores = client.GetAdditionalProperty<List<(int, DateTime)>>(SessionScores);
scores?.Add((client.GetAdditionalProperty<int?>(StatManager.ESTIMATED_SCORE) ?? client.Score, DateTime.Now)); scores?.Add((client.GetAdditionalProperty<int?>(StatManager.ESTIMATED_SCORE) ?? client.Score,
DateTime.Now));
} }
} }
if (gameEvent.Type != GameEvent.EventType.Kill && gameEvent.Type != GameEvent.EventType.Damage) var damageEvent = coreEvent as ClientKillEvent ?? coreEvent as ClientDamageEvent;
if (damageEvent is null)
{ {
return; return;
} }
var eventRegex = gameEvent.Type == GameEvent.EventType.Kill var eventRegex = damageEvent is ClientKillEvent
? gameEvent.Owner.EventParser.Configuration.Kill ? damageEvent.Owner.EventParser.Configuration.Kill
: gameEvent.Owner.EventParser.Configuration.Damage; : damageEvent.Owner.EventParser.Configuration.Damage;
var match = eventRegex.PatternMatcher.Match(gameEvent.Data); var match = eventRegex.PatternMatcher.Match(damageEvent.Data);
if (!match.Success) if (!match.Success)
{ {
_logger.LogWarning("Log for event type {type} does not match pattern {logLine}", gameEvent.Type, _logger.LogWarning("Log for event type {Type} does not match pattern {LogLine}", damageEvent.Type,
gameEvent.Data); damageEvent.Data);
return; return;
} }
var attackerHitInfo = _hitInfoBuilder.Build(match.Values.ToArray(), eventRegex, gameEvent.Origin.ClientId, var attackerHitInfo = _hitInfoBuilder.Build(match.Values.ToArray(), eventRegex, damageEvent.Attacker.ClientId,
gameEvent.Origin.ClientId == gameEvent.Target.ClientId, false, gameEvent.Owner.GameName); damageEvent.Attacker.ClientId == damageEvent.Victim.ClientId, false, damageEvent.Server.GameCode);
var victimHitInfo = _hitInfoBuilder.Build(match.Values.ToArray(), eventRegex, gameEvent.Target.ClientId, var victimHitInfo = _hitInfoBuilder.Build(match.Values.ToArray(), eventRegex, damageEvent.Victim.ClientId,
gameEvent.Origin.ClientId == gameEvent.Target.ClientId, true, gameEvent.Owner.GameName); damageEvent.Attacker.ClientId == damageEvent.Victim.ClientId, true, damageEvent.Server.GameCode);
foreach (var hitInfo in new[] {attackerHitInfo, victimHitInfo}) foreach (var hitInfo in new[] {attackerHitInfo, victimHitInfo})
{ {
@ -188,14 +192,14 @@ namespace IW4MAdmin.Plugins.Stats.Client
await _onTransaction.WaitAsync(); await _onTransaction.WaitAsync();
if (!_clientHitStatistics.ContainsKey(hitInfo.EntityId)) if (!_clientHitStatistics.ContainsKey(hitInfo.EntityId))
{ {
_logger.LogDebug("Starting to track hits for {client}", hitInfo.EntityId); _logger.LogDebug("Starting to track hits for {Client}", hitInfo.EntityId);
var clientHits = await GetHitsForClient(hitInfo.EntityId); var clientHits = await GetHitsForClient(hitInfo.EntityId);
_clientHitStatistics.TryAdd(hitInfo.EntityId, new HitState() _clientHitStatistics.TryAdd(hitInfo.EntityId, new HitState
{ {
Hits = clientHits, Hits = clientHits,
Server = (await _serverCache Server = await _serverCache
.FirstAsync(server => .FirstAsync(server =>
server.EndPoint == gameEvent.Owner.ToString() && server.HostName != null)) server.EndPoint == damageEvent.Server.Id && server.HostName != null)
}); });
} }
} }
@ -228,7 +232,7 @@ namespace IW4MAdmin.Plugins.Stats.Client
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Could not update hit calculations for {client}", hitInfo.EntityId); _logger.LogError(ex, "Could not update hit calculations for {Client}", hitInfo.EntityId);
} }
finally finally
@ -613,4 +617,3 @@ namespace IW4MAdmin.Plugins.Stats.Client
} }
} }
} }
}

View File

@ -3,14 +3,13 @@ using System.Linq;
using Data.Models; using Data.Models;
using IW4MAdmin.Plugins.Stats.Client.Game; using IW4MAdmin.Plugins.Stats.Client.Game;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using Stats.Client.Abstractions; using Stats.Client.Abstractions;
using Stats.Client.Game; using Stats.Client.Game;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Stats.Client namespace Stats.Client;
{
public class HitInfoBuilder : IHitInfoBuilder public class HitInfoBuilder : IHitInfoBuilder
{ {
private readonly IWeaponNameParser _weaponNameParser; private readonly IWeaponNameParser _weaponNameParser;
@ -24,7 +23,7 @@ namespace Stats.Client
} }
public HitInfo Build(string[] log, ParserRegex parserRegex, int entityId, bool isSelf, bool isVictim, public HitInfo Build(string[] log, ParserRegex parserRegex, int entityId, bool isSelf, bool isVictim,
Server.Game gameName) Reference.Game gameName)
{ {
var eventType = log[(uint)ParserRegex.GroupType.EventType].First(); var eventType = log[(uint)ParserRegex.GroupType.EventType].First();
HitType hitType; HitType hitType;
@ -75,10 +74,9 @@ namespace Stats.Client
MeansOfDeath = log.Length > parserRegex.GroupMapping[ParserRegex.GroupType.MeansOfDeath] MeansOfDeath = log.Length > parserRegex.GroupMapping[ParserRegex.GroupType.MeansOfDeath]
? log[parserRegex.GroupMapping[ParserRegex.GroupType.MeansOfDeath]] ? log[parserRegex.GroupMapping[ParserRegex.GroupType.MeansOfDeath]]
: "Unknown", : "Unknown",
Game = (Reference.Game) gameName Game = gameName
}; };
return hitInfo; return hitInfo;
} }
} }
}

View File

@ -3,7 +3,7 @@ using Stats.Client.Abstractions;
using Stats.Client.Game; using Stats.Client.Game;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using SharedLibraryCore; using Data.Models;
using Stats.Config; using Stats.Config;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -20,7 +20,7 @@ namespace Stats.Client
_config = config; _config = config;
} }
public WeaponInfo Parse(string weaponName, Server.Game gameName) public WeaponInfo Parse(string weaponName, Reference.Game gameName)
{ {
var configForGame = _config.WeaponNameParserConfigurations var configForGame = _config.WeaponNameParserConfigurations
?.FirstOrDefault(config => config.Game == gameName) ?? new WeaponNameParserConfiguration() ?.FirstOrDefault(config => config.Game == gameName) ?? new WeaponNameParserConfiguration()

View File

@ -12,14 +12,15 @@ using SharedLibraryCore.Interfaces;
using IW4MAdmin.Plugins.Stats.Helpers; using IW4MAdmin.Plugins.Stats.Helpers;
using Stats.Config; using Stats.Config;
namespace IW4MAdmin.Plugins.Stats.Commands namespace IW4MAdmin.Plugins.Stats.Commands;
{
class MostKillsCommand : Command class MostKillsCommand : Command
{ {
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly StatsConfiguration _statsConfig;
public MostKillsCommand(CommandConfiguration config, ITranslationLookup translationLookup, public MostKillsCommand(CommandConfiguration config, ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory) : base(config, translationLookup) IDatabaseContextFactory contextFactory, StatsConfiguration statsConfig) : base(config, translationLookup)
{ {
Name = "mostkills"; Name = "mostkills";
Description = translationLookup["PLUGINS_STATS_COMMANDS_MOSTKILLS_DESC"]; Description = translationLookup["PLUGINS_STATS_COMMANDS_MOSTKILLS_DESC"];
@ -27,11 +28,12 @@ namespace IW4MAdmin.Plugins.Stats.Commands
Permission = EFClient.Permission.User; Permission = EFClient.Permission.User;
_contextFactory = contextFactory; _contextFactory = contextFactory;
_statsConfig = statsConfig;
} }
public override async Task ExecuteAsync(GameEvent gameEvent) public override async Task ExecuteAsync(GameEvent gameEvent)
{ {
var mostKills = await GetMostKills(StatManager.GetIdForServer(gameEvent.Owner), Plugin.Config.Configuration(), var mostKills = await GetMostKills(StatManager.GetIdForServer(gameEvent.Owner), _statsConfig,
_contextFactory, _translationLookup); _contextFactory, _translationLookup);
if (!gameEvent.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix)) if (!gameEvent.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
{ {
@ -77,4 +79,3 @@ namespace IW4MAdmin.Plugins.Stats.Commands
.Prepend(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_MOSTKILLS_HEADER"]); .Prepend(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_MOSTKILLS_HEADER"]);
} }
} }
}

View File

@ -7,15 +7,18 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models.Client.Stats; using Data.Models.Client.Stats;
using IW4MAdmin.Plugins.Stats.Helpers;
using Stats.Config;
namespace IW4MAdmin.Plugins.Stats.Commands namespace IW4MAdmin.Plugins.Stats.Commands
{ {
public class ResetStats : Command public class ResetStats : Command
{ {
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly StatManager _statManager;
public ResetStats(CommandConfiguration config, ITranslationLookup translationLookup, public ResetStats(CommandConfiguration config, ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory) : base(config, translationLookup) IDatabaseContextFactory contextFactory, StatManager statManager) : base(config, translationLookup)
{ {
Name = "resetstats"; Name = "resetstats";
Description = translationLookup["PLUGINS_STATS_COMMANDS_RESET_DESC"]; Description = translationLookup["PLUGINS_STATS_COMMANDS_RESET_DESC"];
@ -25,6 +28,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
AllowImpersonation = true; AllowImpersonation = true;
_contextFactory = contextFactory; _contextFactory = contextFactory;
_statManager = statManager;
} }
public override async Task ExecuteAsync(GameEvent gameEvent) public override async Task ExecuteAsync(GameEvent gameEvent)
@ -53,7 +57,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
} }
// reset the cached version // reset the cached version
Plugin.Manager.ResetStats(gameEvent.Origin); _statManager.ResetStats(gameEvent.Origin);
gameEvent.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_RESET_SUCCESS"]); gameEvent.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_RESET_SUCCESS"]);
} }

View File

@ -11,15 +11,15 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{ {
public class TopStats : Command public class TopStats : Command
{ {
public static async Task<List<string>> GetTopStats(Server s, ITranslationLookup translationLookup) public static async Task<List<string>> GetTopStats(IGameServer server, ITranslationLookup translationLookup, StatManager statManager)
{ {
var serverId = StatManager.GetIdForServer(s); var serverId = StatManager.GetIdForServer(server);
var topStatsText = new List<string>() var topStatsText = new List<string>()
{ {
$"(Color::Accent)--{translationLookup["PLUGINS_STATS_COMMANDS_TOP_TEXT"]}--" $"(Color::Accent)--{translationLookup["PLUGINS_STATS_COMMANDS_TOP_TEXT"]}--"
}; };
var stats = await Plugin.Manager.GetTopStats(0, 5, serverId); var stats = await statManager.GetTopStats(0, 5, serverId);
var statsList = stats.Select((stats, index) => var statsList = stats.Select((stats, index) =>
translationLookup["COMMANDS_TOPSTATS_RESULT"] translationLookup["COMMANDS_TOPSTATS_RESULT"]
.FormatExt(index + 1, stats.Name, stats.KDR, stats.Performance)); .FormatExt(index + 1, stats.Name, stats.KDR, stats.Performance));
@ -39,8 +39,9 @@ namespace IW4MAdmin.Plugins.Stats.Commands
} }
private new readonly CommandConfiguration _config; private new readonly CommandConfiguration _config;
private readonly StatManager _statManager;
public TopStats(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, public TopStats(CommandConfiguration config, ITranslationLookup translationLookup, StatManager statManager) : base(config,
translationLookup) translationLookup)
{ {
Name = "topstats"; Name = "topstats";
@ -50,11 +51,12 @@ namespace IW4MAdmin.Plugins.Stats.Commands
RequiresTarget = false; RequiresTarget = false;
_config = config; _config = config;
_statManager = statManager;
} }
public override async Task ExecuteAsync(GameEvent gameEvent) public override async Task ExecuteAsync(GameEvent gameEvent)
{ {
var topStats = await GetTopStats(gameEvent.Owner, _translationLookup); var topStats = await GetTopStats(gameEvent.Owner, _translationLookup, _statManager);
if (!gameEvent.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix)) if (!gameEvent.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
{ {
await gameEvent.Origin.TellAsync(topStats, gameEvent.Owner.Manager.CancellationToken); await gameEvent.Origin.TellAsync(topStats, gameEvent.Owner.Manager.CancellationToken);

View File

@ -15,9 +15,10 @@ 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;
public ViewStatsCommand(CommandConfiguration config, ITranslationLookup translationLookup, public ViewStatsCommand(CommandConfiguration config, ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory) : base(config, translationLookup) IDatabaseContextFactory contextFactory, StatManager statManager) : base(config, translationLookup)
{ {
Name = "stats"; Name = "stats";
Description = translationLookup["PLUGINS_STATS_COMMANDS_VIEW_DESC"]; Description = translationLookup["PLUGINS_STATS_COMMANDS_VIEW_DESC"];
@ -34,6 +35,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
}; };
_contextFactory = contextFactory; _contextFactory = contextFactory;
_statManager = statManager;
} }
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent E)
@ -53,12 +55,12 @@ namespace IW4MAdmin.Plugins.Stats.Commands
var serverId = StatManager.GetIdForServer(E.Owner); var serverId = StatManager.GetIdForServer(E.Owner);
var totalRankedPlayers = await Plugin.Manager.GetTotalRankedPlayers(serverId); var totalRankedPlayers = await _statManager.GetTotalRankedPlayers(serverId);
// getting stats for a particular client // getting stats for a particular client
if (E.Target != null) if (E.Target != null)
{ {
var performanceRanking = await Plugin.Manager.GetClientOverallRanking(E.Target.ClientId, serverId); var performanceRanking = await _statManager.GetClientOverallRanking(E.Target.ClientId, serverId);
var performanceRankingString = performanceRanking == 0 var performanceRankingString = performanceRanking == 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}/{totalRankedPlayers}";
@ -87,7 +89,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
// getting self stats // getting self stats
else else
{ {
var performanceRanking = await Plugin.Manager.GetClientOverallRanking(E.Origin.ClientId, serverId); var performanceRanking = await _statManager.GetClientOverallRanking(E.Origin.ClientId, serverId);
var performanceRankingString = performanceRanking == 0 var performanceRankingString = performanceRanking == 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}/{totalRankedPlayers}";

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Data.Models;
using IW4MAdmin.Plugins.Stats.Config; using IW4MAdmin.Plugins.Stats.Config;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
@ -21,26 +22,26 @@ namespace Stats.Config
public WeaponNameParserConfiguration[] WeaponNameParserConfigurations { get; set; } = { public WeaponNameParserConfiguration[] WeaponNameParserConfigurations { get; set; } = {
new() new()
{ {
Game = Server.Game.IW3, Game = Reference.Game.IW3,
WeaponSuffix = "mp", WeaponSuffix = "mp",
Delimiters = new[] {'_'} Delimiters = new[] {'_'}
}, },
new() new()
{ {
Game = Server.Game.IW4, Game = Reference.Game.IW4,
WeaponSuffix = "mp", WeaponSuffix = "mp",
Delimiters = new[] {'_'} Delimiters = new[] {'_'}
}, },
new() new()
{ {
Game = Server.Game.IW5, Game = Reference.Game.IW5,
WeaponSuffix = "mp", WeaponSuffix = "mp",
WeaponPrefix = "iw5", WeaponPrefix = "iw5",
Delimiters = new[] {'_'} Delimiters = new[] {'_'}
}, },
new() new()
{ {
Game = Server.Game.T6, Game = Reference.Game.T6,
WeaponSuffix = "mp", WeaponSuffix = "mp",
Delimiters = new[] {'_', '+'} Delimiters = new[] {'_', '+'}
} }

View File

@ -1,10 +1,10 @@
using SharedLibraryCore; using Data.Models;
namespace Stats.Config namespace Stats.Config
{ {
public class WeaponNameParserConfiguration public class WeaponNameParserConfiguration
{ {
public Server.Game Game { get; set; } public Reference.Game Game { get; set; }
public char[] Delimiters { get; set; } public char[] Delimiters { get; set; }
public string WeaponSuffix { get; set; } public string WeaponSuffix { get; set; }
public string WeaponPrefix { get; set; } public string WeaponPrefix { get; set; }

View File

@ -2,6 +2,7 @@
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System.Collections.Generic; using System.Collections.Generic;
using SharedLibraryCore.Events.Game;
using EventGeneratorCallback = System.ValueTuple<string, string, using EventGeneratorCallback = System.ValueTuple<string, string,
System.Func<string, SharedLibraryCore.Interfaces.IEventParserConfiguration, System.Func<string, SharedLibraryCore.Interfaces.IEventParserConfiguration,
SharedLibraryCore.GameEvent, SharedLibraryCore.GameEvent,
@ -11,35 +12,41 @@ namespace IW4MAdmin.Plugins.Stats.Events
{ {
public class Script : IRegisterEvent public class Script : IRegisterEvent
{ {
private const string EVENT_SCRIPTKILL = "ScriptKill"; private const string EventScriptKill = "ScriptKill";
private const string EVENT_SCRIPTDAMAGE = "ScriptDamage"; private const string EventScriptDamage = "ScriptDamage";
private const string EVENT_JOINTEAM = "JoinTeam";
/// <summary> /// <summary>
/// this is a custom event printed out by _customcallbacks.gsc (used for anticheat) /// this is a custom event printed out by _customcallbacks.gsc (used for anticheat)
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
private EventGeneratorCallback ScriptKill() private static EventGeneratorCallback ScriptKill()
{ {
return (EVENT_SCRIPTKILL, EVENT_SCRIPTKILL, (string eventLine, IEventParserConfiguration config, GameEvent autoEvent) => return (EventScriptKill, EventScriptKill,
(eventLine, config, autoEvent) =>
{ {
string[] lineSplit = eventLine.Split(";"); var lineSplit = eventLine.Split(";");
if (lineSplit[1].IsBotGuid() || lineSplit[2].IsBotGuid()) if (lineSplit[1].IsBotGuid() || lineSplit[2].IsBotGuid())
{ {
return autoEvent; return autoEvent;
} }
long originId = lineSplit[1].ConvertGuidToLong(config.GuidNumberStyle, 1); var originId = lineSplit[1].ConvertGuidToLong(config.GuidNumberStyle, 1);
long targetId = lineSplit[2].ConvertGuidToLong(config.GuidNumberStyle, 1); var targetId = lineSplit[2].ConvertGuidToLong(config.GuidNumberStyle, 1);
autoEvent.Type = GameEvent.EventType.ScriptKill; var anticheatEvent = new AntiCheatDamageEvent
autoEvent.Origin = new EFClient() { NetworkId = originId }; {
autoEvent.Target = new EFClient() { NetworkId = targetId }; ScriptData = eventLine,
autoEvent.RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target; Type = GameEvent.EventType.ScriptKill,
autoEvent.GameTime = autoEvent.GameTime; Origin = new EFClient { NetworkId = originId },
Target = new EFClient { NetworkId = targetId },
RequiredEntity =
GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
GameTime = autoEvent.GameTime,
IsKill = true
};
return autoEvent; return anticheatEvent;
} }
); );
} }
@ -48,55 +55,34 @@ namespace IW4MAdmin.Plugins.Stats.Events
/// this is a custom event printed out by _customcallbacks.gsc (used for anticheat) /// this is a custom event printed out by _customcallbacks.gsc (used for anticheat)
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
private EventGeneratorCallback ScriptDamage() public EventGeneratorCallback ScriptDamage()
{ {
// this is a custom event printed out by _customcallbacks.gsc (used for anticheat) // this is a custom event printed out by _customcallbacks.gsc (used for anticheat)
return (EVENT_SCRIPTDAMAGE, EVENT_SCRIPTDAMAGE, (string eventLine, IEventParserConfiguration config, GameEvent autoEvent) => return (EventScriptDamage, EventScriptDamage,
(eventLine, config, autoEvent) =>
{ {
string[] lineSplit = eventLine.Split(";"); var lineSplit = eventLine.Split(";");
if (lineSplit[1].IsBotGuid() || lineSplit[2].IsBotGuid()) if (lineSplit[1].IsBotGuid() || lineSplit[2].IsBotGuid())
{ {
return autoEvent; return autoEvent;
} }
long originId = lineSplit[1].ConvertGuidToLong(config.GuidNumberStyle, 1); var originId = lineSplit[1].ConvertGuidToLong(config.GuidNumberStyle, 1);
long targetId = lineSplit[2].ConvertGuidToLong(config.GuidNumberStyle, 1); var targetId = lineSplit[2].ConvertGuidToLong(config.GuidNumberStyle, 1);
autoEvent.Type = GameEvent.EventType.ScriptDamage; var anticheatEvent = new AntiCheatDamageEvent
autoEvent.Origin = new EFClient() { NetworkId = originId };
autoEvent.Target = new EFClient() { NetworkId = targetId };
autoEvent.RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target;
return autoEvent;
}
);
}
/// <summary>
/// this is a custom event printed out by _customcallbacks.gsc (used for anticheat)
/// </summary>
/// <returns></returns>
private EventGeneratorCallback JoinTeam()
{ {
// this is a custom event printed out by _customcallbacks.gsc (used for anticheat) ScriptData = eventLine,
return (EVENT_JOINTEAM, EVENT_JOINTEAM, (string eventLine, IEventParserConfiguration config, GameEvent autoEvent) => Type = GameEvent.EventType.ScriptDamage,
{ Origin = new EFClient { NetworkId = originId },
string[] lineSplit = eventLine.Split(";"); Target = new EFClient { NetworkId = targetId },
RequiredEntity =
GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
GameTime = autoEvent.GameTime
};
if (lineSplit[1].IsBotGuid() || lineSplit[2].IsBotGuid()) return anticheatEvent;
{
return autoEvent;
}
long originId = lineSplit[1].ConvertGuidToLong(config.GuidNumberStyle, 1);
long targetId = lineSplit[2].ConvertGuidToLong(config.GuidNumberStyle, 1);
autoEvent.Type = GameEvent.EventType.JoinTeam;
autoEvent.Origin = new EFClient() { NetworkId = lineSplit[1].ConvertGuidToLong(config.GuidNumberStyle) };
autoEvent.RequiredEntity = GameEvent.EventRequiredEntity.Target;
return autoEvent;
} }
); );
} }
@ -105,8 +91,12 @@ namespace IW4MAdmin.Plugins.Stats.Events
new[] new[]
{ {
ScriptKill(), ScriptKill(),
ScriptDamage(), ScriptDamage()
JoinTeam()
}; };
} }
public class AntiCheatDamageEvent : GameScriptEvent
{
public bool IsKill { get; init; }
}
} }

View File

@ -7,7 +7,6 @@ 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.Config;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos;
@ -114,7 +113,7 @@ namespace Stats.Helpers
All = hitStats, All = hitStats,
Servers = _manager.GetServers() Servers = _manager.GetServers()
.Select(server => new ServerInfo .Select(server => new ServerInfo
{Name = server.Hostname, IPAddress = server.IP, Port = server.Port, 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 =>

View File

@ -46,7 +46,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
private readonly SemaphoreSlim _addPlayerWaiter = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim _addPlayerWaiter = new SemaphoreSlim(1, 1);
private readonly IServerDistributionCalculator _serverDistributionCalculator; private readonly IServerDistributionCalculator _serverDistributionCalculator;
public StatManager(ILogger<StatManager> logger, IManager mgr, IDatabaseContextFactory contextFactory, public StatManager(ILogger<StatManager> logger, IDatabaseContextFactory contextFactory,
StatsConfiguration statsConfig, StatsConfiguration statsConfig,
IServerDistributionCalculator serverDistributionCalculator) IServerDistributionCalculator serverDistributionCalculator)
{ {
@ -360,13 +360,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return finished; return finished;
} }
/// <summary> public async Task EnsureServerAdded(IGameServer gameServer, CancellationToken token)
/// Add a server to the StatManager server pool
/// </summary>
/// <param name="sv"></param>
public void AddServer(Server sv)
{ {
// insert the server if it does not exist
try try
{ {
if (serverModels == null) if (serverModels == null)
@ -374,76 +369,75 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
SetupServerIds(); SetupServerIds();
} }
long serverId = GetIdForServer(sv); var serverId = GetIdForServer(gameServer as Server);
EFServer server;
using var ctx = _contextFactory.CreateContext(enableTracking: false); await using var ctx = _contextFactory.CreateContext(enableTracking: false);
var serverSet = ctx.Set<EFServer>(); var serverSet = ctx.Set<EFServer>();
// get the server from the database if it exists, otherwise create and insert a new one // get the server from the database if it exists, otherwise create and insert a new one
server = serverSet.FirstOrDefault(s => s.ServerId == serverId); var cachedServerModel = await serverSet.FirstOrDefaultAsync(s => s.ServerId == serverId, token);
// the server might be using legacy server id // the server might be using legacy server id
if (server == null) if (cachedServerModel == null)
{ {
server = serverSet.FirstOrDefault(s => s.EndPoint == sv.ToString()); cachedServerModel = await serverSet.FirstOrDefaultAsync(s => s.EndPoint == gameServer.Id, token);
if (server != null) if (cachedServerModel != null)
{ {
// this provides a way to identify legacy server entries // this provides a way to identify legacy server entries
server.EndPoint = sv.ToString(); cachedServerModel.EndPoint = gameServer.Id;
ctx.Update(server); ctx.Update(cachedServerModel);
ctx.SaveChanges(); ctx.SaveChanges();
} }
} }
// server has never been added before // server has never been added before
if (server == null) if (cachedServerModel == null)
{ {
server = new EFServer() cachedServerModel = new EFServer
{ {
Port = sv.Port, Port = gameServer.ListenPort,
EndPoint = sv.ToString(), EndPoint = gameServer.Id,
ServerId = serverId, ServerId = serverId,
GameName = (Reference.Game?)sv.GameName, GameName = gameServer.GameCode,
HostName = sv.Hostname HostName = gameServer.ListenAddress
}; };
server = serverSet.Add(server).Entity; cachedServerModel = serverSet.Add(cachedServerModel).Entity;
// this doesn't need to be async as it's during initialization // this doesn't need to be async as it's during initialization
ctx.SaveChanges(); await ctx.SaveChangesAsync(token);
} }
// we want to set the gamename up if it's never been set, or it changed // we want to set the gamename up if it's never been set, or it changed
else if (!server.GameName.HasValue || server.GameName.Value != (Reference.Game)sv.GameName) else if (!cachedServerModel.GameName.HasValue || cachedServerModel.GameName.Value != gameServer.GameCode)
{ {
server.GameName = (Reference.Game)sv.GameName; cachedServerModel.GameName = gameServer.GameCode;
ctx.Entry(server).Property(_prop => _prop.GameName).IsModified = true; ctx.Entry(cachedServerModel).Property(property => property.GameName).IsModified = true;
ctx.SaveChanges(); await ctx.SaveChangesAsync(token);
} }
if (server.HostName == null || server.HostName != sv.Hostname) if (cachedServerModel.HostName == null || cachedServerModel.HostName != gameServer.ServerName)
{ {
server.HostName = sv.Hostname; cachedServerModel.HostName = gameServer.ServerName;
ctx.Entry(server).Property(_prop => _prop.HostName).IsModified = true; ctx.Entry(cachedServerModel).Property(property => property.HostName).IsModified = true;
ctx.SaveChanges(); await ctx.SaveChangesAsync(token);
} }
ctx.Entry(server).Property(_prop => _prop.IsPasswordProtected).IsModified = true; ctx.Entry(cachedServerModel).Property(property => property.IsPasswordProtected).IsModified = true;
server.IsPasswordProtected = !string.IsNullOrEmpty(sv.GamePassword); cachedServerModel.IsPasswordProtected = !string.IsNullOrEmpty(gameServer.GamePassword);
ctx.SaveChanges(); await ctx.SaveChangesAsync(token);
// check to see if the stats have ever been initialized // check to see if the stats have ever been initialized
var serverStats = InitializeServerStats(server.ServerId); var serverStats = InitializeServerStats(cachedServerModel.ServerId);
_servers.TryAdd(serverId, new ServerStats(server, serverStats, sv) _servers.TryAdd(serverId, new ServerStats(cachedServerModel, serverStats, gameServer as Server)
{ {
IsTeamBased = sv.Gametype != "dm" IsTeamBased = gameServer.Gametype != "dm"
}); });
} }
catch (Exception e) catch (Exception ex)
{ {
_log.LogError(e, "{message}", _log.LogError(ex, "{Message}",
Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_ERROR_ADD"]); Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_ERROR_ADD"]);
} }
} }
@ -552,7 +546,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
clientStats.SessionScore = pl.Score; clientStats.SessionScore = pl.Score;
clientStats.LastScore = pl.Score; clientStats.LastScore = pl.Score;
pl.SetAdditionalProperty(CLIENT_DETECTIONS_KEY, new Detection(_log, clientStats)); pl.SetAdditionalProperty(CLIENT_DETECTIONS_KEY, new Detection(_log, clientStats, _config));
_log.LogDebug("Added {client} to stats", pl.ToString()); _log.LogDebug("Added {client} to stats", pl.ToString());
return clientStats; return clientStats;
@ -586,41 +580,42 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
/// <summary> /// <summary>
/// Perform stat updates for disconnecting client /// Perform stat updates for disconnecting client
/// </summary> /// </summary>
/// <param name="pl">Disconnecting client</param> /// <param name="client">Disconnecting client</param>
/// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
public async Task RemovePlayer(EFClient pl) public async Task RemovePlayer(EFClient client, CancellationToken cancellationToken)
{ {
_log.LogDebug("Removing {client} from stats", pl.ToString()); _log.LogDebug("Removing {Client} from stats", client.ToString());
if (pl.CurrentServer == null) if (client.CurrentServer == null)
{ {
_log.LogWarning("Disconnecting client {client} is not on a server", pl.ToString()); _log.LogWarning("Disconnecting client {Client} is not on a server", client.ToString());
return; return;
} }
var serverId = GetIdForServer(pl.CurrentServer); var serverId = GetIdForServer(client.CurrentServer);
var serverStats = _servers[serverId].ServerStatistics; var serverStats = _servers[serverId].ServerStatistics;
// get individual client's stats // get individual client's stats
var clientStats = pl.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY); var clientStats = client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY);
// sync their stats before they leave // sync their stats before they leave
if (clientStats != null) if (clientStats != null)
{ {
clientStats = UpdateStats(clientStats, pl); clientStats = UpdateStats(clientStats, client);
await SaveClientStats(clientStats); await SaveClientStats(clientStats);
if (_config.EnableAdvancedMetrics) if (_config.EnableAdvancedMetrics)
{ {
await UpdateHistoricalRanking(pl.ClientId, clientStats, serverId); await UpdateHistoricalRanking(client.ClientId, clientStats, serverId);
} }
// increment the total play time // increment the total play time
serverStats.TotalPlayTime += pl.ConnectionLength; serverStats.TotalPlayTime += client.ConnectionLength;
pl.SetAdditionalProperty(CLIENT_STATS_KEY, null); client.SetAdditionalProperty(CLIENT_STATS_KEY, null);
} }
else else
{ {
_log.LogWarning("Disconnecting client {client} has not been added to stats", pl.ToString()); _log.LogWarning("Disconnecting client {Client} has not been added to stats", client.ToString());
} }
} }
@ -743,7 +738,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return; return;
} }
if (Plugin.Config.Configuration().StoreClientKills) if (_config.StoreClientKills)
{ {
var serverWaiter = _servers[serverId].OnSaving; var serverWaiter = _servers[serverId].OnSaving;
try try
@ -772,7 +767,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
} }
} }
if (Plugin.Config.Configuration().AnticheatConfiguration.Enable && !attacker.IsBot && if (_config.AnticheatConfiguration.Enable && !attacker.IsBot &&
attacker.ClientId != victim.ClientId) attacker.ClientId != victim.ClientId)
{ {
clientDetection.TrackedHits.Add(hit); clientDetection.TrackedHits.Add(hit);
@ -857,10 +852,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
private bool ShouldUseDetection(Server server, DetectionType detectionType, long clientId) private bool ShouldUseDetection(Server server, DetectionType detectionType, long clientId)
{ {
#pragma warning disable CS0612 #pragma warning disable CS0612
var serverDetectionTypes = Plugin.Config.Configuration().AnticheatConfiguration.ServerDetectionTypes; var serverDetectionTypes = _config.AnticheatConfiguration.ServerDetectionTypes;
#pragma warning restore CS0612 #pragma warning restore CS0612
var gameDetectionTypes = Plugin.Config.Configuration().AnticheatConfiguration.GameDetectionTypes; var gameDetectionTypes = _config.AnticheatConfiguration.GameDetectionTypes;
var ignoredClients = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredClientIds; var ignoredClients = _config.AnticheatConfiguration.IgnoredClientIds;
if (ignoredClients.Contains(clientId)) if (ignoredClients.Contains(clientId))
{ {
@ -1011,9 +1006,9 @@ 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 = attackerStats.ClientId != victimStats.ClientId
? StreakMessage.MessageOnStreak(attackerStats.KillStreak, attackerStats.DeathStreak) ? StreakMessage.MessageOnStreak(attackerStats.KillStreak, attackerStats.DeathStreak, _config)
: StreakMessage.MessageOnStreak(-1, -1); : StreakMessage.MessageOnStreak(-1, -1, _config);
if (streakMessage != string.Empty) if (streakMessage != string.Empty)
{ {
@ -1530,13 +1525,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return serverStats; return serverStats;
} }
public void ResetKillstreaks(Server sv) public void ResetKillstreaks(IGameServer gameServer)
{ {
foreach (var session in sv.GetClientsAsList() foreach (var session in gameServer.ConnectedClients
.Select(_client => new .Select(client => new
{ {
stat = _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY), stat = client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY),
detection = _client.GetAdditionalProperty<Detection>(CLIENT_DETECTIONS_KEY) detection = client.GetAdditionalProperty<Detection>(CLIENT_DETECTIONS_KEY)
})) }))
{ {
session.stat?.StartNewSession(); session.stat?.StartNewSession();
@ -1563,7 +1558,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
stats.EloRating = 200; stats.EloRating = 200;
} }
public async Task AddMessageAsync(int clientId, long serverId, bool sentIngame, string message) public async Task AddMessageAsync(int clientId, long serverId, bool sentIngame, string message,
CancellationToken cancellationToken)
{ {
// the web users can have no account // the web users can have no account
if (clientId < 1) if (clientId < 1)
@ -1571,8 +1567,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return; return;
} }
await using var ctx = _contextFactory.CreateContext(enableTracking: false); await using var context = _contextFactory.CreateContext(enableTracking: false);
ctx.Set<EFClientMessage>().Add(new EFClientMessage() context.Set<EFClientMessage>().Add(new EFClientMessage()
{ {
ClientId = clientId, ClientId = clientId,
Message = message, Message = message,
@ -1581,26 +1577,26 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
SentIngame = sentIngame SentIngame = sentIngame
}); });
await ctx.SaveChangesAsync(); await context.SaveChangesAsync(cancellationToken);
} }
public async Task Sync(Server sv) public async Task Sync(IGameServer gameServer, CancellationToken token)
{ {
long serverId = GetIdForServer(sv); var serverId = GetIdForServer(gameServer);
var waiter = _servers[serverId].OnSaving; var waiter = _servers[serverId].OnSaving;
try try
{ {
await waiter.WaitAsync(); await waiter.WaitAsync(token);
await using var ctx = _contextFactory.CreateContext(); await using var context = _contextFactory.CreateContext();
var serverStatsSet = ctx.Set<EFServerStatistics>(); var serverStatsSet = context.Set<EFServerStatistics>();
serverStatsSet.Update(_servers[serverId].ServerStatistics); serverStatsSet.Update(_servers[serverId].ServerStatistics);
await ctx.SaveChangesAsync(); await context.SaveChangesAsync(token);
foreach (var stats in sv.GetClientsAsList() foreach (var stats in gameServer.ConnectedClients
.Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY)) .Select(client => client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY))
.Where(_stats => _stats != null)) .Where(stats => stats != null))
{ {
await SaveClientStats(stats); await SaveClientStats(stats);
} }
@ -1608,9 +1604,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
await SaveHitCache(serverId); await SaveHitCache(serverId);
} }
catch (Exception e) catch (Exception ex)
{ {
_log.LogError(e, "There was a problem syncing server stats"); _log.LogError(ex, "There was a problem syncing server stats");
} }
finally finally
@ -1627,28 +1623,24 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
_servers[serverId].IsTeamBased = isTeamBased; _servers[serverId].IsTeamBased = isTeamBased;
} }
public static long GetIdForServer(Server server) public static long GetIdForServer(IGameServer gameServer)
{ {
if ($"{server.IP}:{server.Port.ToString()}" == "66.150.121.184:28965") if (gameServer.Id == "66.150.121.184:28965")
{ {
return 886229536; return 886229536;
} }
// todo: this is not stable and will need to be migrated again... // todo: this is not stable and will need to be migrated again...
long id = HashCode.Combine(server.IP, server.Port); long id = HashCode.Combine(gameServer.ListenAddress, gameServer.ListenPort);
id = id < 0 ? Math.Abs(id) : id; id = id < 0 ? Math.Abs(id) : id;
long? serverId;
serverId = serverModels.FirstOrDefault(_server => _server.ServerId == server.EndPoint || #pragma warning disable CS0618
_server.EndPoint == server.ToString() || var serverId = serverModels.FirstOrDefault(cachedServer => cachedServer.ServerId == gameServer.LegacyEndpoint ||
_server.ServerId == id)?.ServerId; #pragma warning restore CS0618
cachedServer.EndPoint == gameServer.ToString() ||
cachedServer.ServerId == id)?.ServerId;
if (!serverId.HasValue) return serverId ?? id;
{
return id;
}
return serverId.Value;
} }
} }
} }

View File

@ -1,29 +1,17 @@
using SharedLibraryCore; using System.Linq;
using SharedLibraryCore.Helpers; using Stats.Config;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IW4MAdmin.Plugins.Stats.Helpers namespace IW4MAdmin.Plugins.Stats.Helpers;
{
public class StreakMessage
{
/// <summary>
/// Get a message from the configuration encouraging or discouraging clients
/// </summary>
/// <param name="killStreak">how many kills the client has without dying</param>
/// <param name="deathStreak">how many deaths the client has without getting a kill</param>
/// <returns>message to send to the client</returns>
public static string MessageOnStreak(int killStreak, int deathStreak)
{
var killstreakMessage = Plugin.Config.Configuration().KillstreakMessages;
var deathstreakMessage = Plugin.Config.Configuration().DeathstreakMessages;
string message = killstreakMessage?.FirstOrDefault(m => m.Count == killStreak)?.Message; public static class StreakMessage
message = message ?? deathstreakMessage?.FirstOrDefault(m => m.Count == deathStreak)?.Message; {
public static string MessageOnStreak(int killStreak, int deathStreak, StatsConfiguration config)
{
var killstreakMessage = config.KillstreakMessages;
var deathstreakMessage = config.DeathstreakMessages;
var message = killstreakMessage?.FirstOrDefault(m => m.Count == killStreak)?.Message;
message ??= deathstreakMessage?.FirstOrDefault(m => m.Count == deathStreak)?.Message;
return message ?? ""; return message ?? "";
} }
} }
}

View File

@ -12,156 +12,210 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models;
using Data.Models.Client.Stats; using Data.Models.Client.Stats;
using Data.Models.Server; using Data.Models.Server;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using IW4MAdmin.Plugins.Stats.Client.Abstractions; using IW4MAdmin.Plugins.Stats.Client.Abstractions;
using IW4MAdmin.Plugins.Stats.Events;
using Microsoft.Extensions.DependencyInjection;
using SharedLibraryCore.Events.Game;
using SharedLibraryCore.Events.Management;
using SharedLibraryCore.Interfaces.Events;
using Stats.Client.Abstractions; using Stats.Client.Abstractions;
using Stats.Config; using Stats.Config;
using EFClient = SharedLibraryCore.Database.Models.EFClient; using EFClient = SharedLibraryCore.Database.Models.EFClient;
namespace IW4MAdmin.Plugins.Stats namespace IW4MAdmin.Plugins.Stats;
{
public class Plugin : IPlugin public class Plugin : IPluginV2
{ {
public string Name => "Simple Stats"; public string Name => "Simple Stats";
public string Version => Utilities.GetVersionAsString();
public float Version => (float)Utilities.GetVersionAsDouble();
public string Author => "RaidMax"; public string Author => "RaidMax";
public static StatManager Manager { get; private set; }
public static IManager ServerManager; public static IManager ServerManager;
public static IConfigurationHandler<StatsConfiguration> Config { get; private set; }
private readonly IDatabaseContextFactory _databaseContextFactory; private readonly IDatabaseContextFactory _databaseContextFactory;
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private readonly IMetaServiceV2 _metaService; private readonly IMetaServiceV2 _metaService;
private readonly IResourceQueryHelper<ChatSearchQuery, MessageResponse> _chatQueryHelper; private readonly IResourceQueryHelper<ChatSearchQuery, MessageResponse> _chatQueryHelper;
private readonly ILogger<StatManager> _managerLogger;
private readonly ILogger<Plugin> _logger; private readonly ILogger<Plugin> _logger;
private readonly List<IClientStatisticCalculator> _statCalculators; private readonly List<IClientStatisticCalculator> _statCalculators;
private readonly IServerDistributionCalculator _serverDistributionCalculator; private readonly IServerDistributionCalculator _serverDistributionCalculator;
private readonly IServerDataViewer _serverDataViewer; private readonly IServerDataViewer _serverDataViewer;
private readonly StatsConfiguration _statsConfig;
private readonly StatManager _statManager;
public Plugin(ILogger<Plugin> logger, IConfigurationHandlerFactory configurationHandlerFactory, IDatabaseContextFactory databaseContextFactory, public static void RegisterDependencies(IServiceCollection serviceCollection)
ITranslationLookup translationLookup, IMetaServiceV2 metaService, IResourceQueryHelper<ChatSearchQuery, MessageResponse> chatQueryHelper, ILogger<StatManager> managerLogger, {
IEnumerable<IClientStatisticCalculator> statCalculators, IServerDistributionCalculator serverDistributionCalculator, IServerDataViewer serverDataViewer) serviceCollection.AddConfiguration<StatsConfiguration>("StatsPluginSettings");
serviceCollection.AddSingleton<StatManager>();
}
public Plugin(ILogger<Plugin> logger, IDatabaseContextFactory databaseContextFactory,
ITranslationLookup translationLookup, IMetaServiceV2 metaService,
IResourceQueryHelper<ChatSearchQuery, MessageResponse> chatQueryHelper,
IEnumerable<IClientStatisticCalculator> statCalculators,
IServerDistributionCalculator serverDistributionCalculator, IServerDataViewer serverDataViewer,
StatsConfiguration statsConfig, StatManager statManager)
{ {
Config = configurationHandlerFactory.GetConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
_databaseContextFactory = databaseContextFactory; _databaseContextFactory = databaseContextFactory;
_translationLookup = translationLookup; _translationLookup = translationLookup;
_metaService = metaService; _metaService = metaService;
_chatQueryHelper = chatQueryHelper; _chatQueryHelper = chatQueryHelper;
_managerLogger = managerLogger;
_logger = logger; _logger = logger;
_statCalculators = statCalculators.ToList(); _statCalculators = statCalculators.ToList();
_serverDistributionCalculator = serverDistributionCalculator; _serverDistributionCalculator = serverDistributionCalculator;
_serverDataViewer = serverDataViewer; _serverDataViewer = serverDataViewer;
_statsConfig = statsConfig;
_statManager = statManager;
IGameServerEventSubscriptions.MonitoringStarted +=
async (monitorEvent, token) => await _statManager.EnsureServerAdded(monitorEvent.Server, token);
IGameServerEventSubscriptions.MonitoringStopped +=
async (monitorEvent, token) => await _statManager.Sync(monitorEvent.Server, token);
IManagementEventSubscriptions.ClientStateInitialized += async (clientEvent, token) =>
{
if (!_statsConfig.EnableAdvancedMetrics)
{
return;
} }
public async Task OnEventAsync(GameEvent gameEvent, Server server) foreach (var calculator in _statCalculators)
{ {
switch (gameEvent.Type) await calculator.CalculateForEvent(clientEvent);
{
case GameEvent.EventType.Start:
Manager.AddServer(server);
break;
case GameEvent.EventType.Disconnect:
await Manager.RemovePlayer(gameEvent.Origin);
break;
case GameEvent.EventType.Say:
if (!string.IsNullOrEmpty(gameEvent.Data) &&
gameEvent.Origin.ClientId > 1)
{
await Manager.AddMessageAsync(gameEvent.Origin.ClientId, StatManager.GetIdForServer(server), true, gameEvent.Data);
} }
break; };
case GameEvent.EventType.MapChange: IManagementEventSubscriptions.ClientStateDisposed +=
Manager.SetTeamBased(StatManager.GetIdForServer(server), server.Gametype != "dm"); async (clientEvent, token) =>
Manager.ResetKillstreaks(server); {
await Manager.Sync(server); await _statManager.RemovePlayer(clientEvent.Client, token);
break;
case GameEvent.EventType.MapEnd: if (!_statsConfig.EnableAdvancedMetrics)
Manager.ResetKillstreaks(server); {
await Manager.Sync(server); return;
break; }
case GameEvent.EventType.Command:
var shouldPersist = !string.IsNullOrEmpty(gameEvent.Data) && foreach (var calculator in _statCalculators)
gameEvent.Extra?.GetType().Name == "SayCommand"; {
await calculator.CalculateForEvent(clientEvent);
}
};
IGameEventSubscriptions.ClientMessaged += async (messageEvent, token) =>
{
if (!string.IsNullOrEmpty(messageEvent.Message) &&
messageEvent.Client.ClientId > 1)
{
await _statManager.AddMessageAsync(messageEvent.Client.ClientId,
StatManager.GetIdForServer(messageEvent.Server), true, messageEvent.Message, token);
}
};
IGameEventSubscriptions.MatchEnded += OnMatchEvent;
IGameEventSubscriptions.MatchStarted += OnMatchEvent;
IGameEventSubscriptions.ScriptEventTriggered += OnScriptEvent;
IGameEventSubscriptions.ClientKilled += OnClientKilled;
IGameEventSubscriptions.ClientDamaged += OnClientDamaged;
IManagementEventSubscriptions.ClientCommandExecuted += OnClientCommandExecute;
IManagementEventSubscriptions.Load += OnLoad;
}
private async Task OnClientKilled(ClientKillEvent killEvent, CancellationToken token)
{
if (!ShouldIgnoreEvent(killEvent.Attacker, killEvent.Victim))
{
// this treats "world" damage as self damage
if (IsWorldDamage(killEvent.Attacker))
{
killEvent.UpdateAttacker(killEvent.Victim);
}
await EnsureClientsAdded(killEvent.Attacker, killEvent.Victim);
await _statManager.AddStandardKill(killEvent.Attacker, killEvent.Victim);
if (!_statsConfig.EnableAdvancedMetrics)
{
return;
}
foreach (var calculator in _statCalculators)
{
await calculator.CalculateForEvent(killEvent);
}
}
}
private async Task OnClientDamaged(ClientDamageEvent damageEvent, CancellationToken token)
{
if (ShouldIgnoreEvent(damageEvent.Attacker, damageEvent.Victim))
{
return;
}
if (!_statsConfig.EnableAdvancedMetrics)
{
return;
}
// this treats "world" damage as self damage
if (IsWorldDamage(damageEvent.Attacker))
{
damageEvent.UpdateAttacker(damageEvent.Victim);
}
foreach (var calculator in _statCalculators)
{
await calculator.CalculateForEvent(damageEvent);
}
}
private async Task OnScriptEvent(GameScriptEvent scriptEvent, CancellationToken token)
{
if (scriptEvent is not AntiCheatDamageEvent antiCheatDamageEvent)
{
return;
}
var killInfo = scriptEvent.ScriptData?.Split(';') ?? Array.Empty<string>();
if ((scriptEvent.Server.IsLegacyGameIntegrationEnabled ||
ShouldOverrideAnticheatSetting(scriptEvent.Server)) && killInfo.Length >= 18 &&
!ShouldIgnoreEvent(antiCheatDamageEvent.Origin, antiCheatDamageEvent.Target))
{
// this treats "world" damage as self damage
if (IsWorldDamage(antiCheatDamageEvent.Origin))
{
antiCheatDamageEvent.Origin = antiCheatDamageEvent.Target;
}
await EnsureClientsAdded(antiCheatDamageEvent.Origin, antiCheatDamageEvent.Target);
await _statManager.AddScriptHit(!antiCheatDamageEvent.IsKill, antiCheatDamageEvent.CreatedAt.DateTime,
antiCheatDamageEvent.Origin,
antiCheatDamageEvent.Target,
StatManager.GetIdForServer(antiCheatDamageEvent.Server), antiCheatDamageEvent.Server.Map.Name,
killInfo[7], killInfo[8],
killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11],
killInfo[12], killInfo[13], killInfo[14], killInfo[15], killInfo[16], killInfo[17]);
}
}
private async Task OnClientCommandExecute(ClientExecuteCommandEvent commandEvent, CancellationToken token)
{
var shouldPersist = !string.IsNullOrEmpty(commandEvent.CommandText) && commandEvent.Command.Name == "say";
if (shouldPersist) if (shouldPersist)
{ {
await Manager.AddMessageAsync(gameEvent.Origin.ClientId, StatManager.GetIdForServer(server), false, gameEvent.Data); await _statManager.AddMessageAsync(commandEvent.Client.ClientId,
StatManager.GetIdForServer(commandEvent.Client.CurrentServer), false, commandEvent.CommandText, token);
} }
break;
case GameEvent.EventType.ScriptKill:
var killInfo = (gameEvent.Data != null) ? gameEvent.Data.Split(';') : Array.Empty<string>();
if ((server.CustomCallback || ShouldOverrideAnticheatSetting(server)) && killInfo.Length >= 18 && !ShouldIgnoreEvent(gameEvent.Origin, gameEvent.Target))
{
// this treats "world" damage as self damage
if (IsWorldDamage(gameEvent.Origin))
{
gameEvent.Origin = gameEvent.Target;
} }
await EnsureClientsAdded(gameEvent.Origin, gameEvent.Target); private async Task OnMatchEvent(GameEventV2 gameEvent, CancellationToken token)
await Manager.AddScriptHit(false, gameEvent.Time, gameEvent.Origin, gameEvent.Target, StatManager.GetIdForServer(server), server.CurrentMap.Name, killInfo[7], killInfo[8], {
killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13], killInfo[14], killInfo[15], killInfo[16], killInfo[17]); _statManager.SetTeamBased(StatManager.GetIdForServer(gameEvent.Server), gameEvent.Server.Gametype != "dm");
} _statManager.ResetKillstreaks(gameEvent.Server);
await _statManager.Sync(gameEvent.Server, token);
else if (!_statsConfig.EnableAdvancedMetrics)
{
_logger.LogDebug("Skipping script kill as it is ignored or data in customcallbacks is outdated/missing");
}
break;
case GameEvent.EventType.Kill:
if (!ShouldIgnoreEvent(gameEvent.Origin, gameEvent.Target))
{
// this treats "world" damage as self damage
if (IsWorldDamage(gameEvent.Origin))
{
gameEvent.Origin = gameEvent.Target;
}
await EnsureClientsAdded(gameEvent.Origin, gameEvent.Target);
await Manager.AddStandardKill(gameEvent.Origin, gameEvent.Target);
}
break;
case GameEvent.EventType.Damage:
if (!ShouldIgnoreEvent(gameEvent.Origin, gameEvent.Target))
{
// this treats "world" damage as self damage
if (IsWorldDamage(gameEvent.Origin))
{
gameEvent.Origin = gameEvent.Target;
}
Manager.AddDamageEvent(gameEvent.Data, gameEvent.Origin.ClientId, gameEvent.Target.ClientId, StatManager.GetIdForServer(server));
}
break;
case GameEvent.EventType.ScriptDamage:
killInfo = (gameEvent.Data != null) ? gameEvent.Data.Split(';') : new string[0];
if ((server.CustomCallback || ShouldOverrideAnticheatSetting(server)) && killInfo.Length >= 18 && !ShouldIgnoreEvent(gameEvent.Origin, gameEvent.Target))
{
// this treats "world" damage as self damage
if (IsWorldDamage(gameEvent.Origin))
{
gameEvent.Origin = gameEvent.Target;
}
await EnsureClientsAdded(gameEvent.Origin, gameEvent.Target);
await Manager.AddScriptHit(true, gameEvent.Time, gameEvent.Origin, gameEvent.Target, StatManager.GetIdForServer(server), server.CurrentMap.Name, killInfo[7], killInfo[8],
killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13], killInfo[14], killInfo[15], killInfo[16], killInfo[17]);
}
else
{
_logger.LogDebug("Skipping script damage as it is ignored or data in customcallbacks is outdated/missing");
}
break;
}
if (!Config.Configuration().EnableAdvancedMetrics)
{ {
return; return;
} }
@ -172,17 +226,8 @@ namespace IW4MAdmin.Plugins.Stats
} }
} }
public async Task OnLoadAsync(IManager manager) private async Task OnLoad(IManager manager, CancellationToken token)
{ {
await Config.BuildAsync();
// load custom configuration
if (Config.Configuration() == null)
{
Config.Set((StatsConfiguration)new StatsConfiguration().Generate());
}
Config.Configuration().ApplyMigration();
await Config.Save();
// register the topstats page // register the topstats page
// todo:generate the URL/Location instead of hardcoding // todo:generate the URL/Location instead of hardcoding
manager.GetPageList() manager.GetPageList()
@ -191,28 +236,37 @@ namespace IW4MAdmin.Plugins.Stats
"/Stats/TopPlayers"); "/Stats/TopPlayers");
// meta data info // meta data info
async Task<IEnumerable<InformationResponse>> GetStats(ClientPaginationRequest request, CancellationToken token = default) async Task<IEnumerable<InformationResponse>> GetStats(ClientPaginationRequest request,
CancellationToken token = default)
{ {
await using var ctx = _databaseContextFactory.CreateContext(enableTracking: false); await using var ctx = _databaseContextFactory.CreateContext(enableTracking: false);
IList<EFClientStatistics> clientStats = await ctx.Set<EFClientStatistics>().Where(c => c.ClientId == request.ClientId).ToListAsync(token); IList<EFClientStatistics> clientStats = await ctx.Set<EFClientStatistics>()
.Where(c => c.ClientId == request.ClientId).ToListAsync(token);
var kills = clientStats.Sum(c => c.Kills); var kills = clientStats.Sum(c => c.Kills);
var deaths = clientStats.Sum(c => c.Deaths); var deaths = clientStats.Sum(c => c.Deaths);
var kdr = Math.Round(kills / (double)deaths, 2); var kdr = Math.Round(kills / (double)deaths, 2);
var validPerformanceValues = clientStats.Where(c => c.Performance > 0).ToList(); var validPerformanceValues = clientStats.Where(c => c.Performance > 0).ToList();
var performancePlayTime = validPerformanceValues.Sum(s => s.TimePlayed); var performancePlayTime = validPerformanceValues.Sum(s => s.TimePlayed);
var performance = Math.Round(validPerformanceValues.Sum(c => c.Performance * c.TimePlayed / performancePlayTime), 2); var performance =
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 Manager.GetClientOverallRanking(request.ClientId); var overallRanking = await _statManager.GetClientOverallRanking(request.ClientId);
return new List<InformationResponse> return new List<InformationResponse>
{ {
new InformationResponse new InformationResponse
{ {
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"].FormatExt((overallRanking == 0 ? "--" : Value = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING_FORMAT"]
overallRanking.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))), .FormatExt(
(await _serverDataViewer.RankedClientsCountAsync(token: token)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) (overallRanking == 0
? "--"
: overallRanking.ToString("#,##0",
new System.Globalization.CultureInfo(Utilities.CurrentLocalization
.LocalizationName))),
(await _serverDataViewer.RankedClientsCountAsync(token: token)).ToString("#,##0",
new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))
), ),
Column = 0, Column = 0,
Order = 0, Order = 0,
@ -221,7 +275,8 @@ namespace IW4MAdmin.Plugins.Stats
new InformationResponse new InformationResponse
{ {
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KILLS"], Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KILLS"],
Value = kills.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), Value = kills.ToString("#,##0",
new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Column = 0, Column = 0,
Order = 1, Order = 1,
Type = MetaType.Information Type = MetaType.Information
@ -229,7 +284,8 @@ namespace IW4MAdmin.Plugins.Stats
new InformationResponse new InformationResponse
{ {
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_DEATHS"], Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_DEATHS"],
Value = deaths.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), Value = deaths.ToString("#,##0",
new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Column = 0, Column = 0,
Order = 2, Order = 2,
Type = MetaType.Information Type = MetaType.Information
@ -237,7 +293,8 @@ namespace IW4MAdmin.Plugins.Stats
new InformationResponse new InformationResponse
{ {
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KDR"], Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KDR"],
Value = kdr.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), Value = kdr.ToString(
new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Column = 0, Column = 0,
Order = 3, Order = 3,
Type = MetaType.Information Type = MetaType.Information
@ -245,7 +302,8 @@ namespace IW4MAdmin.Plugins.Stats
new InformationResponse new InformationResponse
{ {
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_PERFORMANCE"], Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_PERFORMANCE"],
Value = performance.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), Value = performance.ToString("#,##0",
new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Column = 0, Column = 0,
Order = 4, Order = 4,
Type = MetaType.Information Type = MetaType.Information
@ -253,7 +311,8 @@ namespace IW4MAdmin.Plugins.Stats
new InformationResponse new InformationResponse
{ {
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_META_SPM"], Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_META_SPM"],
Value = spm.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), Value = spm.ToString(
new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Column = 0, Column = 0,
Order = 5, Order = 5,
Type = MetaType.Information Type = MetaType.Information
@ -261,7 +320,8 @@ namespace IW4MAdmin.Plugins.Stats
}; };
} }
async Task<IEnumerable<InformationResponse>> GetAnticheatInfo(ClientPaginationRequest request, CancellationToken token = default) async Task<IEnumerable<InformationResponse>> GetAnticheatInfo(ClientPaginationRequest request,
CancellationToken token = default)
{ {
await using var context = _databaseContextFactory.CreateContext(enableTracking: false); await using var context = _databaseContextFactory.CreateContext(enableTracking: false);
IList<EFClientStatistics> clientStats = await context.Set<EFClientStatistics>() IList<EFClientStatistics> clientStats = await context.Set<EFClientStatistics>()
@ -280,24 +340,43 @@ namespace IW4MAdmin.Plugins.Stats
if (clientStats.Any(cs => cs.HitLocations.Count > 0)) if (clientStats.Any(cs => cs.HitLocations.Count > 0))
{ {
chestRatio = Math.Round((clientStats.Where(c => c.HitLocations.Count > 0).Sum(c => chestRatio = Math.Round((clientStats.Where(c => c.HitLocations.Count > 0).Sum(c =>
c.HitLocations.First(hl => hl.Location == (int)IW4Info.HitLocation.torso_upper).HitCount) / c.HitLocations.First(hl =>
hl.Location == (int)IW4Info.HitLocation.torso_upper).HitCount) /
(double)clientStats.Where(c => c.HitLocations.Count > 0) (double)clientStats.Where(c => c.HitLocations.Count > 0)
.Sum(c => c.HitLocations.Where(hl => hl.Location != (int)IW4Info.HitLocation.none).Sum(f => f.HitCount))) * 100.0, 0); .Sum(c => c.HitLocations
.Where(hl => hl.Location != (int)IW4Info.HitLocation.none)
.Sum(f => f.HitCount))) * 100.0, 0);
abdomenRatio = Math.Round((clientStats.Where(c => c.HitLocations.Count > 0).Sum(c => abdomenRatio = Math.Round((clientStats.Where(c => c.HitLocations.Count > 0).Sum(c =>
c.HitLocations.First(hl => hl.Location == (int)IW4Info.HitLocation.torso_lower).HitCount) / c.HitLocations.First(hl =>
(double)clientStats.Where(c => c.HitLocations.Count > 0).Sum(c => c.HitLocations.Where(hl => hl.Location != (int)IW4Info.HitLocation.none).Sum(f => f.HitCount))) * 100.0, 0); hl.Location == (int)IW4Info.HitLocation.torso_lower).HitCount) /
(double)clientStats.Where(c => c.HitLocations.Count > 0).Sum(c =>
c.HitLocations.Where(hl => hl.Location != (int)IW4Info.HitLocation.none)
.Sum(f => f.HitCount))) * 100.0, 0);
chestAbdomenRatio = Math.Round((clientStats.Where(c => c.HitLocations.Count > 0).Sum(cs => cs.HitLocations.First(hl => hl.Location == (int)IW4Info.HitLocation.torso_upper).HitCount) / chestAbdomenRatio = Math.Round((clientStats.Where(c => c.HitLocations.Count > 0).Sum(cs =>
(double)clientStats.Where(c => c.HitLocations.Count > 0).Sum(cs => cs.HitLocations.First(hl => hl.Location == (int)IW4Info.HitLocation.torso_lower).HitCount)) * 100.0, 0); cs.HitLocations.First(hl =>
hl.Location == (int)IW4Info.HitLocation.torso_upper).HitCount) /
(double)clientStats.Where(c => c.HitLocations.Count > 0).Sum(cs =>
cs.HitLocations.First(hl =>
hl.Location == (int)IW4Info.HitLocation.torso_lower)
.HitCount)) * 100.0, 0);
headRatio = Math.Round((clientStats.Where(c => c.HitLocations.Count > 0).Sum(cs => cs.HitLocations.First(hl => hl.Location == (int)IW4Info.HitLocation.head).HitCount) / headRatio = Math.Round((clientStats.Where(c => c.HitLocations.Count > 0).Sum(cs =>
cs.HitLocations.First(hl => hl.Location == (int)IW4Info.HitLocation.head)
.HitCount) /
(double)clientStats.Where(c => c.HitLocations.Count > 0) (double)clientStats.Where(c => c.HitLocations.Count > 0)
.Sum(c => c.HitLocations.Where(hl => hl.Location != (int)IW4Info.HitLocation.none).Sum(f => f.HitCount))) * 100.0, 0); .Sum(c => c.HitLocations
.Where(hl => hl.Location != (int)IW4Info.HitLocation.none)
.Sum(f => f.HitCount))) * 100.0, 0);
var validOffsets = clientStats.Where(c => c.HitLocations.Count(hl => hl.HitCount > 0) > 0).SelectMany(hl => hl.HitLocations).ToList(); var validOffsets = clientStats.Where(c => c.HitLocations.Count(hl => hl.HitCount > 0) > 0)
hitOffsetAverage = validOffsets.Sum(o => o.HitCount * o.HitOffsetAverage) / (double)validOffsets.Sum(o => o.HitCount); .SelectMany(hl => hl.HitLocations).ToList();
averageSnapValue = clientStats.Any(_stats => _stats.AverageSnapValue > 0) ? clientStats.Where(_stats => _stats.AverageSnapValue > 0).Average(_stat => _stat.AverageSnapValue) : 0; hitOffsetAverage = validOffsets.Sum(o => o.HitCount * o.HitOffsetAverage) /
(double)validOffsets.Sum(o => o.HitCount);
averageSnapValue = clientStats.Any(_stats => _stats.AverageSnapValue > 0)
? clientStats.Where(_stats => _stats.AverageSnapValue > 0).Average(_stat => _stat.AverageSnapValue)
: 0;
} }
return new List<InformationResponse> return new List<InformationResponse>
@ -305,7 +384,8 @@ namespace IW4MAdmin.Plugins.Stats
new InformationResponse() new InformationResponse()
{ {
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 1", Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 1",
Value = chestRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%', Value = chestRatio.ToString(
new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
Type = MetaType.Information, Type = MetaType.Information,
Order = 100, Order = 100,
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM1"], ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM1"],
@ -314,7 +394,8 @@ namespace IW4MAdmin.Plugins.Stats
new InformationResponse() new InformationResponse()
{ {
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 2", Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 2",
Value = abdomenRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%', Value = abdomenRatio.ToString(
new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
Type = MetaType.Information, Type = MetaType.Information,
Order = 101, Order = 101,
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM2"], ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM2"],
@ -323,7 +404,8 @@ namespace IW4MAdmin.Plugins.Stats
new InformationResponse() new InformationResponse()
{ {
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 3", Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 3",
Value = chestAbdomenRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%', Value = chestAbdomenRatio.ToString(
new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
Type = MetaType.Information, Type = MetaType.Information,
Order = 102, Order = 102,
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM3"], ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM3"],
@ -332,7 +414,8 @@ namespace IW4MAdmin.Plugins.Stats
new InformationResponse() new InformationResponse()
{ {
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 4", Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 4",
Value = headRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%', Value = headRatio.ToString(
new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
Type = MetaType.Information, Type = MetaType.Information,
Order = 103, Order = 103,
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM4"], ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM4"],
@ -342,7 +425,8 @@ namespace IW4MAdmin.Plugins.Stats
{ {
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 5", Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 5",
// todo: make sure this is wrapped somewhere else // todo: make sure this is wrapped somewhere else
Value = $"{Math.Round(((float)hitOffsetAverage), 4).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))}°", Value =
$"{Math.Round(((float)hitOffsetAverage), 4).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))}°",
Type = MetaType.Information, Type = MetaType.Information,
Order = 104, Order = 104,
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM5"], ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM5"],
@ -351,7 +435,8 @@ namespace IW4MAdmin.Plugins.Stats
new InformationResponse() new InformationResponse()
{ {
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 6", Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 6",
Value = Math.Round(maxStrain, 3).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), Value = Math.Round(maxStrain, 3)
.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Type = MetaType.Information, Type = MetaType.Information,
Order = 105, Order = 105,
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM6"], ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM6"],
@ -360,7 +445,8 @@ namespace IW4MAdmin.Plugins.Stats
new InformationResponse() new InformationResponse()
{ {
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 7", Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 7",
Value = Math.Round(averageSnapValue, 3).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), Value = Math.Round(averageSnapValue, 3)
.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Type = MetaType.Information, Type = MetaType.Information,
Order = 106, Order = 106,
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM7"], ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM7"],
@ -369,7 +455,8 @@ namespace IW4MAdmin.Plugins.Stats
}; };
} }
async Task<IEnumerable<MessageResponse>> GetMessages(ClientPaginationRequest request, CancellationToken token = default) async Task<IEnumerable<MessageResponse>> GetMessages(ClientPaginationRequest request,
CancellationToken token = default)
{ {
var query = new ChatSearchQuery var query = new ChatSearchQuery
{ {
@ -383,9 +470,10 @@ namespace IW4MAdmin.Plugins.Stats
return (await _chatQueryHelper.QueryResource(query)).Results; return (await _chatQueryHelper.QueryResource(query)).Results;
} }
if (Config.Configuration().AnticheatConfiguration.Enable) if (_statsConfig.AnticheatConfiguration.Enable)
{ {
_metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information, GetAnticheatInfo); _metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information,
GetAnticheatInfo);
} }
_metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information, GetStats); _metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information, GetStats);
@ -395,32 +483,38 @@ namespace IW4MAdmin.Plugins.Stats
{ {
await using var context = _databaseContextFactory.CreateContext(false); await using var context = _databaseContextFactory.CreateContext(false);
var kills = await context.Set<EFServerStatistics>().Where(s => s.Active).SumAsync(s => s.TotalKills); var kills = await context.Set<EFServerStatistics>().Where(s => s.Active).SumAsync(s => s.TotalKills);
return kills.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)); return kills.ToString("#,##0",
new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName));
} }
async Task<string> TotalPlayTime(Server server) async Task<string> TotalPlayTime(Server server)
{ {
await using var context = _databaseContextFactory.CreateContext(false); await using var context = _databaseContextFactory.CreateContext(false);
var playTime = await context.Set<EFServerStatistics>().Where(s => s.Active).SumAsync(s => s.TotalPlayTime); var playTime = await context.Set<EFServerStatistics>().Where(s => s.Active).SumAsync(s => s.TotalPlayTime);
return (playTime / 3600.0).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)); return (playTime / 3600.0).ToString("#,##0",
new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName));
} }
async Task<string> TopStats(Server s) async Task<string> TopStats(Server s)
{ {
// todo: this needs to needs to be updated when we DI the lookup // todo: this needs to needs to be updated when we DI the lookup
return string.Join(Environment.NewLine, await Commands.TopStats.GetTopStats(s, Utilities.CurrentLocalization.LocalizationIndex)); return string.Join(Environment.NewLine,
await Commands.TopStats.GetTopStats(s, Utilities.CurrentLocalization.LocalizationIndex, _statManager));
} }
async Task<string> MostPlayed(Server s) async Task<string> MostPlayed(Server s)
{ {
// todo: this needs to needs to be updated when we DI the lookup // todo: this needs to needs to be updated when we DI the lookup
return string.Join(Environment.NewLine, await Commands.MostPlayedCommand.GetMostPlayed(s, Utilities.CurrentLocalization.LocalizationIndex, _databaseContextFactory)); return string.Join(Environment.NewLine,
await Commands.MostPlayedCommand.GetMostPlayed(s, Utilities.CurrentLocalization.LocalizationIndex,
_databaseContextFactory));
} }
async Task<string> MostKills(Server gameServer) async Task<string> MostKills(Server gameServer)
{ {
return string.Join(Environment.NewLine, return string.Join(Environment.NewLine,
await Commands.MostKillsCommand.GetMostKills(StatManager.GetIdForServer(gameServer), Config.Configuration(), _databaseContextFactory, _translationLookup)); await Commands.MostKillsCommand.GetMostKills(StatManager.GetIdForServer(gameServer), _statsConfig,
_databaseContextFactory, _translationLookup));
} }
manager.GetMessageTokens().Add(new MessageToken("TOTALKILLS", TotalKills)); manager.GetMessageTokens().Add(new MessageToken("TOTALKILLS", TotalKills));
@ -429,7 +523,7 @@ namespace IW4MAdmin.Plugins.Stats
manager.GetMessageTokens().Add(new MessageToken("MOSTPLAYED", MostPlayed)); manager.GetMessageTokens().Add(new MessageToken("MOSTPLAYED", MostPlayed));
manager.GetMessageTokens().Add(new MessageToken("MOSTKILLS", MostKills)); manager.GetMessageTokens().Add(new MessageToken("MOSTKILLS", MostKills));
if (Config.Configuration().EnableAdvancedMetrics) if (_statsConfig.EnableAdvancedMetrics)
{ {
foreach (var calculator in _statCalculators) foreach (var calculator in _statCalculators)
{ {
@ -438,23 +532,9 @@ namespace IW4MAdmin.Plugins.Stats
} }
ServerManager = manager; ServerManager = manager;
Manager = new StatManager(_managerLogger, manager, _databaseContextFactory, Config.Configuration(), _serverDistributionCalculator);
await _serverDistributionCalculator.Initialize(); await _serverDistributionCalculator.Initialize();
} }
public Task OnTickAsync(Server server)
{
return Task.CompletedTask;
}
public async Task OnUnloadAsync()
{
foreach (var sv in ServerManager.GetServers())
{
await Manager.Sync(sv);
}
}
/// <summary> /// <summary>
/// Indicates if the event should be ignored /// Indicates if the event should be ignored
/// (If the client id or target id is not a real client or the target/origin is a bot and ignore bots is turned on) /// (If the client id or target id is not a real client or the target/origin is a bot and ignore bots is turned on)
@ -464,7 +544,7 @@ namespace IW4MAdmin.Plugins.Stats
/// <returns></returns> /// <returns></returns>
private bool ShouldIgnoreEvent(EFClient origin, EFClient target) private bool ShouldIgnoreEvent(EFClient origin, EFClient target)
{ {
return ((origin?.NetworkId == Utilities.WORLD_ID && target?.NetworkId == Utilities.WORLD_ID)); return origin?.NetworkId == Utilities.WORLD_ID && target?.NetworkId == Utilities.WORLD_ID;
} }
/// <summary> /// <summary>
@ -472,14 +552,16 @@ namespace IW4MAdmin.Plugins.Stats
/// </summary> /// </summary>
/// <param name="origin"></param> /// <param name="origin"></param>
/// <returns></returns> /// <returns></returns>
private bool IsWorldDamage(EFClient origin) => origin?.NetworkId == Utilities.WORLD_ID || origin?.ClientId == Utilities.WORLD_ID; private bool IsWorldDamage(EFClient origin) =>
origin?.NetworkId == Utilities.WORLD_ID || origin?.ClientId == Utilities.WORLD_ID;
/// <summary> /// <summary>
/// Indicates if we should try to use anticheat even if sv_customcallbacks is not defined /// Indicates if we should try to use anticheat even if sv_customcallbacks is not defined
/// </summary> /// </summary>
/// <param name="s"></param> /// <param name="gameServer"></param>
/// <returns></returns> /// <returns></returns>
private bool ShouldOverrideAnticheatSetting(Server s) => Config.Configuration().AnticheatConfiguration.Enable && s.GameName == Server.Game.IW5; private bool ShouldOverrideAnticheatSetting(IGameServer gameServer) => _statsConfig.AnticheatConfiguration.Enable &&
gameServer.GameCode == Reference.Game.IW5;
/// <summary> /// <summary>
/// Makes sure both clients are added /// Makes sure both clients are added
@ -489,12 +571,11 @@ namespace IW4MAdmin.Plugins.Stats
/// <returns></returns> /// <returns></returns>
private async Task EnsureClientsAdded(EFClient origin, EFClient target) private async Task EnsureClientsAdded(EFClient origin, EFClient target)
{ {
await Manager.AddPlayer(origin); await _statManager.AddPlayer(origin);
if (!origin.Equals(target)) if (!origin.Equals(target))
{ {
await Manager.AddPlayer(target); await _statManager.AddPlayer(target);
}
} }
} }
} }

View File

@ -17,7 +17,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.10.13.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2023.2.11.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">