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 @@
-
-
+
+