diff --git a/Application/Application.csproj b/Application/Application.csproj index 39f2879d2..00b23616f 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -25,13 +25,13 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + @@ -39,7 +39,6 @@ true true Latest - diff --git a/Application/Main.cs b/Application/Main.cs index b192ae7cd..3460dfc7d 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -68,7 +68,10 @@ namespace IW4MAdmin.Application private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e) { ServerManager?.Stop(); - await ApplicationTask; + if (ApplicationTask != null) + { + await ApplicationTask; + } } /// diff --git a/Plugins/ScriptPlugins/ParserCoD4x.js b/Plugins/ScriptPlugins/ParserCoD4x.js index 3a7358159..982acd44d 100644 --- a/Plugins/ScriptPlugins/ParserCoD4x.js +++ b/Plugins/ScriptPlugins/ParserCoD4x.js @@ -25,12 +25,12 @@ var plugin = { rconParser.Configuration.Dvar.AddMapping(110, 4); // dvar info rconParser.Configuration.GuidNumberStyle = 7; // Integer rconParser.Configuration.NoticeLineSeparator = '. '; // CoD4x does not support \n in the client notice - rconParser.Version = 'CoD4 X - win_mingw-x86 build 963 Mar 12 2019'; + rconParser.Version = 'CoD4 X - win_mingw-x86 build 1056 Dec 12 2020'; rconParser.GameName = 1; // IW3 eventParser.Configuration.GameDirectory = 'main'; eventParser.Configuration.GuidNumberStyle = 7; // Integer - eventParser.Version = 'CoD4 X - win_mingw-x86 build 963 Mar 12 2019'; + eventParser.Version = 'CoD4 X - win_mingw-x86 build 1056 Dec 12 2020'; eventParser.GameName = 1; // IW3 eventParser.URLProtocolFormat = 'cod4://{{ip}}:{{port}}'; }, diff --git a/Plugins/Stats/Commands/ResetStats.cs b/Plugins/Stats/Commands/ResetStats.cs index ef4637c13..320b319c4 100644 --- a/Plugins/Stats/Commands/ResetStats.cs +++ b/Plugins/Stats/Commands/ResetStats.cs @@ -23,40 +23,44 @@ namespace IW4MAdmin.Plugins.Stats.Commands Permission = EFClient.Permission.User; RequiresTarget = false; AllowImpersonation = true; + + _contextFactory = contextFactory; } - public override async Task ExecuteAsync(GameEvent E) + public override async Task ExecuteAsync(GameEvent gameEvent) { - if (E.Origin.ClientNumber >= 0) + if (gameEvent.Origin.ClientNumber >= 0) { + var serverId = Helpers.StatManager.GetIdForServer(gameEvent.Owner); - long serverId = Helpers.StatManager.GetIdForServer(E.Owner); - - EFClientStatistics clientStats; await using var context = _contextFactory.CreateContext(); - clientStats = await context.Set() - .Where(s => s.ClientId == E.Origin.ClientId) + var clientStats = await context.Set() + .Where(s => s.ClientId == gameEvent.Origin.ClientId) .Where(s => s.ServerId == serverId) - .FirstAsync(); - - clientStats.Deaths = 0; - clientStats.Kills = 0; - clientStats.SPM = 0.0; - clientStats.Skill = 0.0; - clientStats.TimePlayed = 0; - // todo: make this more dynamic - clientStats.EloRating = 200.0; + .FirstOrDefaultAsync(); + + // want to prevent resetting stats before they've gotten any kills + if (clientStats != null) + { + clientStats.Deaths = 0; + clientStats.Kills = 0; + clientStats.SPM = 0.0; + clientStats.Skill = 0.0; + clientStats.TimePlayed = 0; + // todo: make this more dynamic + clientStats.EloRating = 200.0; + await context.SaveChangesAsync(); + } // reset the cached version - Plugin.Manager.ResetStats(E.Origin); + Plugin.Manager.ResetStats(gameEvent.Origin); - await context.SaveChangesAsync(); - E.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_RESET_SUCCESS"]); + gameEvent.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_RESET_SUCCESS"]); } else { - E.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_RESET_FAIL"]); + gameEvent.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_RESET_FAIL"]); } } } diff --git a/Plugins/Stats/Commands/ViewStats.cs b/Plugins/Stats/Commands/ViewStats.cs index 676c1b375..2ace6997c 100644 --- a/Plugins/Stats/Commands/ViewStats.cs +++ b/Plugins/Stats/Commands/ViewStats.cs @@ -15,11 +15,10 @@ namespace IW4MAdmin.Plugins.Stats.Commands public class ViewStatsCommand : Command { private readonly IDatabaseContextFactory _contextFactory; - - public ViewStatsCommand(CommandConfiguration config, ITranslationLookup translationLookup, + + public ViewStatsCommand(CommandConfiguration config, ITranslationLookup translationLookup, IDatabaseContextFactory contextFactory) : base(config, translationLookup) { - Name = "stats"; Description = translationLookup["PLUGINS_STATS_COMMANDS_VIEW_DESC"]; Alias = "xlrstats"; @@ -33,17 +32,14 @@ namespace IW4MAdmin.Plugins.Stats.Commands Required = false } }; - - _config = config; + _contextFactory = contextFactory; } - private readonly CommandConfiguration _config; - public override async Task ExecuteAsync(GameEvent E) { string statLine; - EFClientStatistics pStats; + EFClientStatistics pStats = null; if (E.Data.Length > 0 && E.Target == null) { @@ -55,48 +51,67 @@ namespace IW4MAdmin.Plugins.Stats.Commands } } - long serverId = StatManager.GetIdForServer(E.Owner); - + var serverId = StatManager.GetIdForServer(E.Owner); + // getting stats for a particular client if (E.Target != null) { - int performanceRanking = await Plugin.Manager.GetClientOverallRanking(E.Target.ClientId); - string performanceRankingString = performanceRanking == 0 ? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"] : $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}"; + var performanceRanking = await Plugin.Manager.GetClientOverallRanking(E.Target.ClientId); + var performanceRankingString = performanceRanking == 0 + ? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"] + : $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}"; - if (E.Owner.GetClientsAsList().Any(_client => _client.Equals(E.Target))) + // target is currently connected so we want their cached stats if they exist + if (E.Owner.GetClientsAsList().Any(client => client.Equals(E.Target))) { pStats = E.Target.GetAdditionalProperty(StatManager.CLIENT_STATS_KEY); } - else + // target is not connected so we want to look up via database + if (pStats == null) { await using var context = _contextFactory.CreateContext(false); - pStats = (await context.Set().FirstAsync(c => c.ServerId == serverId && c.ClientId == E.Target.ClientId)); + pStats = (await context.Set() + .FirstOrDefaultAsync(c => c.ServerId == serverId && c.ClientId == E.Target.ClientId)); } - statLine = $"^5{pStats.Kills} ^7{_translationLookup["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{_translationLookup["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{_translationLookup["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()} | {performanceRankingString}"; + + // if it's still null then they've not gotten a kill or death yet + statLine = pStats == null + ? _translationLookup["PLUGINS_STATS_COMMANDS_NOTAVAILABLE"] + : $"^5{pStats.Kills} ^7{_translationLookup["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{_translationLookup["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{_translationLookup["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()} | {performanceRankingString}"; } + // getting self stats else { - int performanceRanking = await Plugin.Manager.GetClientOverallRanking(E.Origin.ClientId); - string performanceRankingString = performanceRanking == 0 ? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"] : $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}"; + var performanceRanking = await Plugin.Manager.GetClientOverallRanking(E.Origin.ClientId); + var performanceRankingString = performanceRanking == 0 + ? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"] + : $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}"; - if (E.Owner.GetClientsAsList().Any(_client => _client.Equals(E.Origin))) + // check if current client is connected to the server + if (E.Owner.GetClientsAsList().Any(client => client.Equals(E.Origin))) { pStats = E.Origin.GetAdditionalProperty(StatManager.CLIENT_STATS_KEY); } - else + // happens if the user has not gotten a kill/death since connecting + if (pStats == null) { await using var context = _contextFactory.CreateContext(false); - pStats = (await context.Set().FirstAsync(c => c.ServerId == serverId && c.ClientId == E.Origin.ClientId)); + pStats = (await context.Set() + .FirstOrDefaultAsync(c => c.ServerId == serverId && c.ClientId == E.Origin.ClientId)); } - statLine = $"^5{pStats.Kills} ^7{_translationLookup["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{_translationLookup["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{_translationLookup["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()} | {performanceRankingString}"; + + // if it's still null then they've not gotten a kill or death yet + statLine = pStats == null + ? _translationLookup["PLUGINS_STATS_COMMANDS_NOTAVAILABLE"] + : $"^5{pStats.Kills} ^7{_translationLookup["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{_translationLookup["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{_translationLookup["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()} | {performanceRankingString}"; } if (E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix)) { - string name = E.Target == null ? E.Origin.Name : E.Target.Name; + 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); } @@ -112,4 +127,4 @@ namespace IW4MAdmin.Plugins.Stats.Commands } } } -} +} \ No newline at end of file diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index 52dd6de1a..b9df54b9f 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -1161,6 +1161,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers public void ResetStats(EFClient client) { var stats = client.GetAdditionalProperty(CLIENT_STATS_KEY); + + // the cached stats have not been loaded yet + if (stats == null) + { + return; + } + stats.Kills = 0; stats.Deaths = 0; stats.SPM = 0; diff --git a/SharedLibraryCore/Commands/NativeCommands.cs b/SharedLibraryCore/Commands/NativeCommands.cs index e4d0fa023..6b8d0dbd0 100644 --- a/SharedLibraryCore/Commands/NativeCommands.cs +++ b/SharedLibraryCore/Commands/NativeCommands.cs @@ -725,6 +725,12 @@ namespace SharedLibraryCore.Commands gameEvent.Origin.Tell($"{_translationLookup["COMMANDS_SETLEVEL_STEPPEDDISABLED"]} ^5{gameEvent.Target.Name}"); return; } + + else if (gameEvent.Target.Level == Permission.Flagged) + { + gameEvent.Origin.Tell(_translationLookup["COMMANDS_SETLEVEL_FLAGGED"].FormatExt(gameEvent.Target.Name)); + return; + } // stepped privilege is enabled, but the new level is too high else if (steppedPrivileges && !canPromoteSteppedPriv) @@ -748,9 +754,19 @@ namespace SharedLibraryCore.Commands if (result.Failed) { + // user is the same level + if (result.FailReason == GameEvent.EventFailReason.Invalid) + { + gameEvent.Origin.Tell(_translationLookup["COMMANDS_SETLEVEL_INVALID"] + .FormatExt(gameEvent.Target.Name, newPerm.ToString())); + return; + } + using (LogContext.PushProperty("Server", gameEvent.Origin.CurrentServer?.ToString())) { - logger.LogWarning("Failed to set level of client {origin}", gameEvent.Origin.ToString()); + logger.LogWarning("Failed to set level of client {origin} {reason}", + gameEvent.Origin.ToString(), + result.FailReason); } gameEvent.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]); return; diff --git a/SharedLibraryCore/SharedLibraryCore.csproj b/SharedLibraryCore/SharedLibraryCore.csproj index 4a54280fa..55b4e4336 100644 --- a/SharedLibraryCore/SharedLibraryCore.csproj +++ b/SharedLibraryCore/SharedLibraryCore.csproj @@ -28,35 +28,35 @@ - + - - - + + + all runtime; build; native; contentfiles - - - - - - + + + + + + - + - + - + diff --git a/Tests/ApplicationTests/ApplicationTests.csproj b/Tests/ApplicationTests/ApplicationTests.csproj index 1c28ad4bf..69f4000ef 100644 --- a/Tests/ApplicationTests/ApplicationTests.csproj +++ b/Tests/ApplicationTests/ApplicationTests.csproj @@ -6,9 +6,9 @@ - + - + all diff --git a/Tests/ApplicationTests/CommandTests.cs b/Tests/ApplicationTests/CommandTests.cs index cc3ed8cf3..363cb5ec3 100644 --- a/Tests/ApplicationTests/CommandTests.cs +++ b/Tests/ApplicationTests/CommandTests.cs @@ -532,6 +532,31 @@ namespace ApplicationTests Assert.IsNotEmpty(mockEventHandler.Events.Where(_event => _event.Type == GameEvent.EventType.Tell)); Assert.IsNotEmpty(mockEventHandler.Events.Where(_event => _event.Type == GameEvent.EventType.ChangePermission && !_event.Failed)); } + + [Test] + public async Task Test_SetLevelFail_WhenFlagged() + { + var server = serviceProvider.GetRequiredService(); + var cmd = serviceProvider.GetRequiredService(); + var origin = ClientGenerators.CreateBasicClient(server); + origin.Level = Permission.Owner; + var target = ClientGenerators.CreateBasicClient(server); + target.Level = Permission.Flagged; + + var gameEvent = new GameEvent() + { + Target = target, + Origin = origin, + Data = "Banned", + Owner = server, + }; + + await cmd.ExecuteAsync(gameEvent); + + Assert.AreEqual(Permission.Flagged, target.Level); + Assert.IsNotEmpty(mockEventHandler.Events.Where(_event => _event.Type == GameEvent.EventType.Tell)); + Assert.IsEmpty(mockEventHandler.Events.Where(_event => _event.Type == GameEvent.EventType.ChangePermission)); + } #endregion #region PREFIX_PROCESSING diff --git a/WebfrontCore/WebfrontCore.csproj b/WebfrontCore/WebfrontCore.csproj index 5cb08065e..d65527eeb 100644 --- a/WebfrontCore/WebfrontCore.csproj +++ b/WebfrontCore/WebfrontCore.csproj @@ -68,8 +68,8 @@ - - + +