From d9d5a56ab0c52876d0930b9c48032235ade3226e Mon Sep 17 00:00:00 2001 From: RaidMax Date: Wed, 5 Apr 2023 10:15:10 -0500 Subject: [PATCH] update stats plugin for server caching and better DI usage --- Plugins/Stats/Commands/MostKillsCommand.cs | 3 +- Plugins/Stats/Commands/MostPlayedCommand.cs | 4 +- Plugins/Stats/Commands/ResetStats.cs | 3 +- Plugins/Stats/Commands/TopStats.cs | 6 +- Plugins/Stats/Commands/ViewStats.cs | 45 ++++---- Plugins/Stats/Helpers/StatManager.cs | 110 +++----------------- Plugins/Stats/Plugin.cs | 22 ++-- Plugins/Stats/Stats.csproj | 2 +- 8 files changed, 56 insertions(+), 139 deletions(-) diff --git a/Plugins/Stats/Commands/MostKillsCommand.cs b/Plugins/Stats/Commands/MostKillsCommand.cs index 92336ab0b..ebbd744fc 100644 --- a/Plugins/Stats/Commands/MostKillsCommand.cs +++ b/Plugins/Stats/Commands/MostKillsCommand.cs @@ -9,7 +9,6 @@ using Data.Models.Client.Stats; using SharedLibraryCore.Database.Models; using SharedLibraryCore.Configuration; using SharedLibraryCore.Interfaces; -using IW4MAdmin.Plugins.Stats.Helpers; using Stats.Config; namespace IW4MAdmin.Plugins.Stats.Commands; @@ -33,7 +32,7 @@ class MostKillsCommand : Command public override async Task ExecuteAsync(GameEvent gameEvent) { - var mostKills = await GetMostKills(StatManager.GetIdForServer(gameEvent.Owner), _statsConfig, + var mostKills = await GetMostKills((gameEvent.Owner as IGameServer).LegacyDatabaseId, _statsConfig, _contextFactory, _translationLookup); if (!gameEvent.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix)) { diff --git a/Plugins/Stats/Commands/MostPlayedCommand.cs b/Plugins/Stats/Commands/MostPlayedCommand.cs index 4ff0f3db1..ec3ff74e2 100644 --- a/Plugins/Stats/Commands/MostPlayedCommand.cs +++ b/Plugins/Stats/Commands/MostPlayedCommand.cs @@ -15,10 +15,10 @@ namespace IW4MAdmin.Plugins.Stats.Commands { class MostPlayedCommand : Command { - public static async Task> GetMostPlayed(Server s, ITranslationLookup translationLookup, + public static async Task> GetMostPlayed(IGameServer gameServer, ITranslationLookup translationLookup, IDatabaseContextFactory contextFactory) { - var serverId = StatManager.GetIdForServer(s); + var serverId = gameServer.LegacyDatabaseId; var mostPlayed = new List { diff --git a/Plugins/Stats/Commands/ResetStats.cs b/Plugins/Stats/Commands/ResetStats.cs index 4b21cea8d..b1f162977 100644 --- a/Plugins/Stats/Commands/ResetStats.cs +++ b/Plugins/Stats/Commands/ResetStats.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using Data.Abstractions; using Data.Models.Client.Stats; using IW4MAdmin.Plugins.Stats.Helpers; -using Stats.Config; namespace IW4MAdmin.Plugins.Stats.Commands { @@ -35,7 +34,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands { if (gameEvent.Origin.ClientNumber >= 0) { - var serverId = Helpers.StatManager.GetIdForServer(gameEvent.Owner); + var serverId = (gameEvent.Owner as IGameServer).LegacyDatabaseId; await using var context = _contextFactory.CreateContext(); var clientStats = await context.Set() diff --git a/Plugins/Stats/Commands/TopStats.cs b/Plugins/Stats/Commands/TopStats.cs index fb1a920d8..b3d5b1349 100644 --- a/Plugins/Stats/Commands/TopStats.cs +++ b/Plugins/Stats/Commands/TopStats.cs @@ -13,8 +13,8 @@ namespace IW4MAdmin.Plugins.Stats.Commands { public static async Task> GetTopStats(IGameServer server, ITranslationLookup translationLookup, StatManager statManager) { - var serverId = StatManager.GetIdForServer(server); - var topStatsText = new List() + var serverId = server.LegacyDatabaseId; + var topStatsText = new List { $"(Color::Accent)--{translationLookup["PLUGINS_STATS_COMMANDS_TOP_TEXT"]}--" }; @@ -29,7 +29,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands // no one qualified if (topStatsText.Count == 1) { - topStatsText = new List() + topStatsText = new List { translationLookup["PLUGINS_STATS_TEXT_NOQUALIFY"] }; diff --git a/Plugins/Stats/Commands/ViewStats.cs b/Plugins/Stats/Commands/ViewStats.cs index b0de9fd27..85c362796 100644 --- a/Plugins/Stats/Commands/ViewStats.cs +++ b/Plugins/Stats/Commands/ViewStats.cs @@ -38,37 +38,36 @@ namespace IW4MAdmin.Plugins.Stats.Commands _statManager = statManager; } - public override async Task ExecuteAsync(GameEvent E) + public override async Task ExecuteAsync(GameEvent gameEvent) { string statLine; EFClientStatistics pStats = null; - if (E.Data.Length > 0 && E.Target == null) + if (gameEvent.Data.Length > 0 && gameEvent.Target == null) { - E.Target = E.Owner.GetClientByName(E.Data).FirstOrDefault(); + gameEvent.Target = gameEvent.Owner.GetClientByName(gameEvent.Data).FirstOrDefault(); - if (E.Target == null) + if (gameEvent.Target == null) { - E.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_FAIL"]); + gameEvent.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_FAIL"]); } } - var serverId = StatManager.GetIdForServer(E.Owner); - + var serverId = (gameEvent.Owner as IGameServer).LegacyDatabaseId; var totalRankedPlayers = await _statManager.GetTotalRankedPlayers(serverId); // getting stats for a particular client - if (E.Target != null) + if (gameEvent.Target != null) { - var performanceRanking = await _statManager.GetClientOverallRanking(E.Target.ClientId, serverId); + var performanceRanking = await _statManager.GetClientOverallRanking(gameEvent.Target.ClientId, serverId); var performanceRankingString = performanceRanking == 0 ? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"] : $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} (Color::Accent)#{performanceRanking}/{totalRankedPlayers}"; // target is currently connected so we want their cached stats if they exist - if (E.Owner.GetClientsAsList().Any(client => client.Equals(E.Target))) + if (gameEvent.Owner.GetClientsAsList().Any(client => client.Equals(gameEvent.Target))) { - pStats = E.Target.GetAdditionalProperty(StatManager.CLIENT_STATS_KEY); + pStats = gameEvent.Target.GetAdditionalProperty(StatManager.CLIENT_STATS_KEY); } // target is not connected so we want to look up via database @@ -76,7 +75,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands { await using var context = _contextFactory.CreateContext(false); pStats = await context.Set() - .FirstOrDefaultAsync(c => c.ServerId == serverId && c.ClientId == E.Target.ClientId); + .FirstOrDefaultAsync(c => c.ServerId == serverId && c.ClientId == gameEvent.Target.ClientId); } // if it's still null then they've not gotten a kill or death yet @@ -89,15 +88,15 @@ namespace IW4MAdmin.Plugins.Stats.Commands // getting self stats else { - var performanceRanking = await _statManager.GetClientOverallRanking(E.Origin.ClientId, serverId); + var performanceRanking = await _statManager.GetClientOverallRanking(gameEvent.Origin.ClientId, serverId); var performanceRankingString = performanceRanking == 0 ? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"] : $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} (Color::Accent)#{performanceRanking}/{totalRankedPlayers}"; // check if current client is connected to the server - if (E.Owner.GetClientsAsList().Any(client => client.Equals(E.Origin))) + if (gameEvent.Owner.GetClientsAsList().Any(client => client.Equals(gameEvent.Origin))) { - pStats = E.Origin.GetAdditionalProperty(StatManager.CLIENT_STATS_KEY); + pStats = gameEvent.Origin.GetAdditionalProperty(StatManager.CLIENT_STATS_KEY); } // happens if the user has not gotten a kill/death since connecting @@ -105,7 +104,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands { await using var context = _contextFactory.CreateContext(false); pStats = (await context.Set() - .FirstOrDefaultAsync(c => c.ServerId == serverId && c.ClientId == E.Origin.ClientId)); + .FirstOrDefaultAsync(c => c.ServerId == serverId && c.ClientId == gameEvent.Origin.ClientId)); } // if it's still null then they've not gotten a kill or death yet @@ -115,21 +114,21 @@ namespace IW4MAdmin.Plugins.Stats.Commands pStats.KDR, pStats.Performance, performanceRankingString); } - if (E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix)) + if (gameEvent.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix)) { - var name = E.Target == null ? E.Origin.Name : E.Target.Name; - E.Owner.Broadcast(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(name)); - E.Owner.Broadcast(statLine); + var name = gameEvent.Target == null ? gameEvent.Origin.Name : gameEvent.Target.Name; + gameEvent.Owner.Broadcast(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(name)); + gameEvent.Owner.Broadcast(statLine); } else { - if (E.Target != null) + if (gameEvent.Target != null) { - E.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(E.Target.Name)); + gameEvent.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(gameEvent.Target.Name)); } - E.Origin.Tell(statLine); + gameEvent.Origin.Tell(statLine); } } } diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index e93252cde..eec64dd71 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -39,22 +39,23 @@ namespace IW4MAdmin.Plugins.Stats.Helpers private readonly ILogger _log; private readonly IDatabaseContextFactory _contextFactory; private readonly StatsConfiguration _config; - private static List serverModels; public static string CLIENT_STATS_KEY = "ClientStats"; public static string CLIENT_DETECTIONS_KEY = "ClientDetections"; public static string ESTIMATED_SCORE = "EstimatedScore"; - private readonly SemaphoreSlim _addPlayerWaiter = new SemaphoreSlim(1, 1); + private readonly SemaphoreSlim _addPlayerWaiter = new(1, 1); private readonly IServerDistributionCalculator _serverDistributionCalculator; + private readonly ILookupCache _serverCache; public StatManager(ILogger logger, IDatabaseContextFactory contextFactory, StatsConfiguration statsConfig, - IServerDistributionCalculator serverDistributionCalculator) + IServerDistributionCalculator serverDistributionCalculator, ILookupCache serverCache) { _servers = new ConcurrentDictionary(); _log = logger; _contextFactory = contextFactory; _config = statsConfig; _serverDistributionCalculator = serverDistributionCalculator; + _serverCache = serverCache; } ~StatManager() @@ -62,12 +63,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers _addPlayerWaiter.Dispose(); } - private void SetupServerIds() - { - using var ctx = _contextFactory.CreateContext(enableTracking: false); - serverModels = ctx.Set().ToList(); - } - public Expression> GetRankingFunc(long? serverId = null) { var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15); @@ -364,72 +359,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers { try { - if (serverModels == null) - { - SetupServerIds(); - } - - var serverId = GetIdForServer(gameServer as Server); - - await using var ctx = _contextFactory.CreateContext(enableTracking: false); - var serverSet = ctx.Set(); - // get the server from the database if it exists, otherwise create and insert a new one - var cachedServerModel = await serverSet.FirstOrDefaultAsync(s => s.ServerId == serverId, token); - - // the server might be using legacy server id - if (cachedServerModel == null) - { - cachedServerModel = await serverSet.FirstOrDefaultAsync(s => s.EndPoint == gameServer.Id, token); - - if (cachedServerModel != null) - { - // this provides a way to identify legacy server entries - cachedServerModel.EndPoint = gameServer.Id; - ctx.Update(cachedServerModel); - ctx.SaveChanges(); - } - } - - // server has never been added before - if (cachedServerModel == null) - { - cachedServerModel = new EFServer - { - Port = gameServer.ListenPort, - EndPoint = gameServer.Id, - ServerId = serverId, - GameName = gameServer.GameCode, - HostName = gameServer.ListenAddress - }; - - cachedServerModel = serverSet.Add(cachedServerModel).Entity; - // this doesn't need to be async as it's during initialization - await ctx.SaveChangesAsync(token); - } - - // we want to set the gamename up if it's never been set, or it changed - else if (!cachedServerModel.GameName.HasValue || cachedServerModel.GameName.Value != gameServer.GameCode) - { - cachedServerModel.GameName = gameServer.GameCode; - ctx.Entry(cachedServerModel).Property(property => property.GameName).IsModified = true; - await ctx.SaveChangesAsync(token); - } - - if (cachedServerModel.HostName == null || cachedServerModel.HostName != gameServer.ServerName) - { - cachedServerModel.HostName = gameServer.ServerName; - ctx.Entry(cachedServerModel).Property(property => property.HostName).IsModified = true; - await ctx.SaveChangesAsync(token); - } - - ctx.Entry(cachedServerModel).Property(property => property.IsPasswordProtected).IsModified = true; - cachedServerModel.IsPasswordProtected = !string.IsNullOrEmpty(gameServer.GamePassword); - await ctx.SaveChangesAsync(token); - // check to see if the stats have ever been initialized - var serverStats = InitializeServerStats(cachedServerModel.ServerId); + var cachedServer = + await _serverCache.FirstAsync(cachedServer => cachedServer.EndPoint == gameServer.Id); + var serverStats = InitializeServerStats(gameServer.LegacyDatabaseId); - _servers.TryAdd(serverId, new ServerStats(cachedServerModel, serverStats, gameServer as Server) + _servers.TryAdd(cachedServer.ServerId, new ServerStats(cachedServer, serverStats, gameServer as Server) { IsTeamBased = gameServer.Gametype != "dm" }); @@ -459,7 +394,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers try { await _addPlayerWaiter.WaitAsync(); - long serverId = GetIdForServer(pl.CurrentServer); + var serverId = (pl.CurrentServer as IGameServer).LegacyDatabaseId; if (!_servers.ContainsKey(serverId)) { @@ -593,7 +528,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers return; } - var serverId = GetIdForServer(client.CurrentServer); + var serverId = (client.CurrentServer as IGameServer).LegacyDatabaseId; var serverStats = _servers[serverId].ServerStatistics; // get individual client's stats @@ -954,7 +889,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers public async Task AddStandardKill(EFClient attacker, EFClient victim) { - var serverId = GetIdForServer(attacker.CurrentServer); + var serverId = (attacker.CurrentServer as IGameServer).LegacyDatabaseId; var attackerStats = attacker.GetAdditionalProperty(CLIENT_STATS_KEY); var victimStats = victim.GetAdditionalProperty(CLIENT_STATS_KEY); @@ -1582,8 +1517,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers public async Task Sync(IGameServer gameServer, CancellationToken token) { - var serverId = GetIdForServer(gameServer); - + var serverId = gameServer.LegacyDatabaseId; var waiter = _servers[serverId].OnSaving; try { @@ -1622,25 +1556,5 @@ namespace IW4MAdmin.Plugins.Stats.Helpers { _servers[serverId].IsTeamBased = isTeamBased; } - - public static long GetIdForServer(IGameServer gameServer) - { - if (gameServer.Id == "66.150.121.184:28965") - { - return 886229536; - } - - // todo: this is not stable and will need to be migrated again... - long id = HashCode.Combine(gameServer.ListenAddress, gameServer.ListenPort); - id = id < 0 ? Math.Abs(id) : id; - -#pragma warning disable CS0618 - var serverId = serverModels.FirstOrDefault(cachedServer => cachedServer.ServerId == gameServer.LegacyEndpoint || -#pragma warning restore CS0618 - cachedServer.EndPoint == gameServer.ToString() || - cachedServer.ServerId == id)?.ServerId; - - return serverId ?? id; - } } } diff --git a/Plugins/Stats/Plugin.cs b/Plugins/Stats/Plugin.cs index 6be93505c..ee8ccc7da 100644 --- a/Plugins/Stats/Plugin.cs +++ b/Plugins/Stats/Plugin.cs @@ -71,8 +71,6 @@ public class Plugin : IPluginV2 _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) => @@ -97,6 +95,13 @@ public class Plugin : IPluginV2 return; } + if (clientEvent.Client.ClientId == 0) + { + _logger.LogWarning("No client id for {Client}, so we are not doing any stat calculation", + clientEvent.Client.ToString()); + return; + } + foreach (var calculator in _statCalculators) { await calculator.CalculateForEvent(clientEvent); @@ -108,7 +113,7 @@ public class Plugin : IPluginV2 messageEvent.Client.ClientId > 1) { await _statManager.AddMessageAsync(messageEvent.Client.ClientId, - StatManager.GetIdForServer(messageEvent.Server), true, messageEvent.Message, token); + messageEvent.Server.LegacyDatabaseId, true, messageEvent.Message, token); } }; IGameEventSubscriptions.MatchEnded += OnMatchEvent; @@ -191,7 +196,7 @@ public class Plugin : IPluginV2 await _statManager.AddScriptHit(!antiCheatDamageEvent.IsKill, antiCheatDamageEvent.CreatedAt.DateTime, antiCheatDamageEvent.Origin, antiCheatDamageEvent.Target, - StatManager.GetIdForServer(antiCheatDamageEvent.Server), antiCheatDamageEvent.Server.Map.Name, + antiCheatDamageEvent.Server.LegacyDatabaseId, 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]); @@ -205,13 +210,14 @@ public class Plugin : IPluginV2 if (shouldPersist) { await _statManager.AddMessageAsync(commandEvent.Client.ClientId, - StatManager.GetIdForServer(commandEvent.Client.CurrentServer), false, commandEvent.CommandText, token); + (commandEvent.Client.CurrentServer as IGameServer).LegacyDatabaseId, false, + commandEvent.CommandText, token); } } private async Task OnMatchEvent(GameEventV2 gameEvent, CancellationToken token) { - _statManager.SetTeamBased(StatManager.GetIdForServer(gameEvent.Server), gameEvent.Server.Gametype != "dm"); + _statManager.SetTeamBased(gameEvent.Server.LegacyDatabaseId, gameEvent.Server.Gametype != "dm"); _statManager.ResetKillstreaks(gameEvent.Server); await _statManager.Sync(gameEvent.Server, token); @@ -510,10 +516,10 @@ public class Plugin : IPluginV2 _databaseContextFactory)); } - async Task MostKills(Server gameServer) + async Task MostKills(IGameServer gameServer) { return string.Join(Environment.NewLine, - await Commands.MostKillsCommand.GetMostKills(StatManager.GetIdForServer(gameServer), _statsConfig, + await Commands.MostKillsCommand.GetMostKills(gameServer.LegacyDatabaseId, _statsConfig, _databaseContextFactory, _translationLookup)); } diff --git a/Plugins/Stats/Stats.csproj b/Plugins/Stats/Stats.csproj index ac12a59d5..1a8321ac2 100644 --- a/Plugins/Stats/Stats.csproj +++ b/Plugins/Stats/Stats.csproj @@ -17,7 +17,7 @@ - +