diff --git a/Plugins/Stats/Cheat/Detection.cs b/Plugins/Stats/Cheat/Detection.cs index 55f70b02f..9c2c36870 100644 --- a/Plugins/Stats/Cheat/Detection.cs +++ b/Plugins/Stats/Cheat/Detection.cs @@ -20,7 +20,8 @@ namespace IW4MAdmin.Plugins.Stats.Cheat Offset, Strain, Recoil, - Snap + Snap, + Button }; public ChangeTracking Tracker { get; private set; } @@ -38,11 +39,12 @@ namespace IW4MAdmin.Plugins.Stats.Cheat ILogger Log; Strain Strain; readonly DateTime ConnectionTime = DateTime.UtcNow; - private double sessionAverageRecoilAmount; + private double mapAverageRecoilAmount; private double sessionAverageSnapAmount; private int sessionSnapHits; private EFClientKill lastHit; private int validRecoilHitCount; + private int validButtonHitCount; private class HitInfo { @@ -282,18 +284,30 @@ namespace IW4MAdmin.Plugins.Stats.Cheat #region RECOIL float hitRecoilAverage = 0; - if (!Plugin.Config.Configuration().RecoilessWeapons.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex))) + bool shouldIgnoreDetection = false; + try + { + shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[hit.GameName][DetectionType.Recoil] + .Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex)); + } + + catch (KeyNotFoundException) + { + + } + + if (!shouldIgnoreDetection) { validRecoilHitCount++; hitRecoilAverage = (hit.AnglesList.Sum(_angle => _angle.Z) + hit.ViewAngles.Z) / (hit.AnglesList.Count + 1); - sessionAverageRecoilAmount = (sessionAverageRecoilAmount * (validRecoilHitCount - 1) + hitRecoilAverage) / validRecoilHitCount; + mapAverageRecoilAmount = (mapAverageRecoilAmount * (validRecoilHitCount - 1) + hitRecoilAverage) / validRecoilHitCount; - if (validRecoilHitCount >= Thresholds.LowSampleMinKills && Kills > Thresholds.LowSampleMinKillsRecoil && sessionAverageRecoilAmount == 0) + if (validRecoilHitCount >= Thresholds.LowSampleMinKills && Kills > Thresholds.LowSampleMinKillsRecoil && mapAverageRecoilAmount == 0) { results.Add(new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Ban, - Value = sessionAverageRecoilAmount, + Value = mapAverageRecoilAmount, HitCount = HitCount, Type = DetectionType.Recoil }); @@ -301,6 +315,37 @@ namespace IW4MAdmin.Plugins.Stats.Cheat } #endregion + #region BUTTON + try + { + shouldIgnoreDetection = false; + shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[hit.GameName][DetectionType.Button] + .Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex)); + } + + catch (KeyNotFoundException) + { + + } + + if (!shouldIgnoreDetection) + { + validButtonHitCount++; + } + + double lastDiff = hit.TimeOffset - hit.TimeSinceLastAttack; + if (validButtonHitCount > 0 && lastDiff <= 0) + { + results.Add(new DetectionPenaltyResult() + { + ClientPenalty = EFPenalty.PenaltyType.Ban, + Value = lastDiff, + HitCount = HitCount, + Type = DetectionType.Button + }); + } + #endregion + #region SESSION_RATIOS if (Kills >= Thresholds.LowSampleMinKills) { @@ -384,7 +429,19 @@ namespace IW4MAdmin.Plugins.Stats.Cheat #region CHEST_ABDOMEN_RATIO_SESSION int chestHits = HitLocationCount[IW4Info.HitLocation.torso_upper].Count; - if (chestHits >= Thresholds.MediumSampleMinKills) + try + { + shouldIgnoreDetection = false; // reset previous value + shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[hit.GameName][DetectionType.Chest] + .Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex)); + } + + catch (KeyNotFoundException) + { + + } + + if (chestHits >= Thresholds.MediumSampleMinKills && !shouldIgnoreDetection) { double marginOfError = Thresholds.GetMarginOfError(chestHits); double lerpAmount = Math.Min(1.0, (chestHits - Thresholds.MediumSampleMinKills) / (double)(Thresholds.HighSampleMinKills - Thresholds.LowSampleMinKills)); @@ -466,5 +523,11 @@ namespace IW4MAdmin.Plugins.Stats.Cheat return results; } + + public void OnMapChange() + { + mapAverageRecoilAmount = 0; + validRecoilHitCount = 0; + } } } diff --git a/Plugins/Stats/Config/AnticheatConfiguration.cs b/Plugins/Stats/Config/AnticheatConfiguration.cs new file mode 100644 index 000000000..a85e29221 --- /dev/null +++ b/Plugins/Stats/Config/AnticheatConfiguration.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using static IW4MAdmin.Plugins.Stats.Cheat.Detection; +using static SharedLibraryCore.Server; + +namespace Stats.Config +{ + public class AnticheatConfiguration + { + public bool Enable { get; set; } + public IDictionary ServerDetectionTypes { get; set; } = new Dictionary(); + public IList IgnoredClientIds { get; set; } = new List(); + public IDictionary> IgnoredDetectionSpecification{ get; set; } = new Dictionary> + { + { + Game.IW4, new Dictionary + { + { DetectionType.Chest, new[] { "m21.+" } }, + { DetectionType.Recoil, new[] { "ranger.*_mp", "model1887.*_mp", ".+shotgun.*_mp" } }, + { DetectionType.Button, new[] { ".*akimbo.*" } } + } + } + }; + } +} diff --git a/Plugins/Stats/Config/StatsConfiguration.cs b/Plugins/Stats/Config/StatsConfiguration.cs index 3d14423ae..c4d091b3f 100644 --- a/Plugins/Stats/Config/StatsConfiguration.cs +++ b/Plugins/Stats/Config/StatsConfiguration.cs @@ -1,6 +1,7 @@ using SharedLibraryCore; using SharedLibraryCore.Interfaces; using Stats.Config; +using System; using System.Collections.Generic; using static IW4MAdmin.Plugins.Stats.Cheat.Detection; @@ -8,21 +9,41 @@ namespace IW4MAdmin.Plugins.Stats.Config { public class StatsConfiguration : IBaseConfiguration { - public bool EnableAntiCheat { get; set; } + [Obsolete] + public bool? EnableAntiCheat { get; set; } public List KillstreakMessages { get; set; } public List DeathstreakMessages { get; set; } - public List RecoilessWeapons { get; set; } public int TopPlayersMinPlayTime { get; set; } public bool StoreClientKills { get; set; } public int MostKillsMaxInactivityDays { get; set; } = 30; public int MostKillsClientLimit { get; set; } = 5; - public IDictionary DetectionDistributions { get; set; } + [Obsolete] public IDictionary ServerDetectionTypes { get; set; } + public AnticheatConfiguration AnticheatConfiguration { get; set; } = new AnticheatConfiguration(); + +#pragma warning disable CS0612 // Type or member is obsolete + public void ApplyMigration() + { + if (ServerDetectionTypes != null) + { + AnticheatConfiguration.ServerDetectionTypes = ServerDetectionTypes; + } + + ServerDetectionTypes = null; + + if (EnableAntiCheat != null) + { + AnticheatConfiguration.Enable = EnableAntiCheat.Value; + } + + EnableAntiCheat = null; + } +#pragma warning restore CS0612 // Type or member is obsolete public string Name() => "StatsPluginSettings"; public IBaseConfiguration Generate() { - EnableAntiCheat = Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_SETUP_ENABLEAC"]); + AnticheatConfiguration.Enable = Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_SETUP_ENABLEAC"]); KillstreakMessages = new List() { new StreakMessageConfiguration(){ @@ -57,16 +78,9 @@ namespace IW4MAdmin.Plugins.Stats.Config }, }; - RecoilessWeapons = new List() - { - "ranger.*_mp", - "model1887.*_mp", - ".+shotgun.*_mp" - }; - TopPlayersMinPlayTime = 3600 * 3; StoreClientKills = false; - + return this; } } diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index bdb588cee..c5b921bde 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -481,7 +481,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers IsKill = !isDamage, AnglesList = snapshotAngles, IsAlive = isAlive == "1", - TimeSinceLastAttack = long.Parse(lastAttackTime) + TimeSinceLastAttack = long.Parse(lastAttackTime), + GameName = attacker.CurrentServer.GameName }; if (hit.HitLoc == IW4Info.HitLocation.shield) @@ -539,7 +540,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers } } - if (Plugin.Config.Configuration().EnableAntiCheat && !attacker.IsBot && attacker.ClientId != victim.ClientId) + if (Plugin.Config.Configuration().AnticheatConfiguration.Enable && !attacker.IsBot && attacker.ClientId != victim.ClientId) { clientDetection.TrackedHits.Add(hit); @@ -555,10 +556,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers if (oldestHit.IsAlive) { - var result = DeterminePenaltyResult(clientDetection.ProcessHit(oldestHit), attacker.CurrentServer.EndPoint); -#if !DEBUG - await ApplyPenalty(result, attacker); -#endif + var result = DeterminePenaltyResult(clientDetection.ProcessHit(oldestHit), attacker); + + if (Utilities.IsDevelopment) + { + await ApplyPenalty(result, attacker); + } if (clientDetection.Tracker.HasChanges && result.ClientPenalty != EFPenalty.PenaltyType.Any) { @@ -594,10 +597,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers } } - private DetectionPenaltyResult DeterminePenaltyResult(IEnumerable results, long serverId) + private DetectionPenaltyResult DeterminePenaltyResult(IEnumerable results, EFClient client) { // allow disabling of certain detection types - results = results.Where(_result => ShouldUseDetection(serverId, _result.Type)); + results = results.Where(_result => ShouldUseDetection(client.CurrentServer, _result.Type, client.ClientId)); return results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Ban) ?? results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ?? new DetectionPenaltyResult() @@ -617,21 +620,24 @@ namespace IW4MAdmin.Plugins.Stats.Helpers } } - private bool ShouldUseDetection(long serverId, DetectionType detectionType) + private bool ShouldUseDetection(Server server, DetectionType detectionType, long clientId) { - var detectionTypes = Plugin.Config.Configuration().ServerDetectionTypes; + bool shouldRun = true; + var detectionTypes = Plugin.Config.Configuration().AnticheatConfiguration.ServerDetectionTypes; + var ignoredClients = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredClientIds; - if (detectionTypes == null) + try { - return true; + shouldRun &= !detectionTypes[server.EndPoint].Contains(detectionType); } - if (!detectionTypes.ContainsKey(serverId)) + catch (KeyNotFoundException) { - return true; + } - return detectionTypes[serverId].Contains(detectionType); + shouldRun &= !ignoredClients.Any(_clientId => _clientId == clientId); + return shouldRun; } async Task ApplyPenalty(DetectionPenaltyResult penalty, EFClient attacker) @@ -1139,10 +1145,15 @@ namespace IW4MAdmin.Plugins.Stats.Helpers public void ResetKillstreaks(Server sv) { - foreach (var stat in sv.GetClientsAsList() - .Select(_client => _client.GetAdditionalProperty(CLIENT_STATS_KEY))) + foreach (var session in sv.GetClientsAsList() + .Select(_client => new + { + stat = _client.GetAdditionalProperty(CLIENT_STATS_KEY), + detection = _client.GetAdditionalProperty(CLIENT_DETECTIONS_KEY) + })) { - stat?.StartNewSession(); + session.stat?.StartNewSession(); + session.detection?.OnMapChange(); } } diff --git a/Plugins/Stats/Models/EFClientKill.cs b/Plugins/Stats/Models/EFClientKill.cs index 72a58db4c..29f6dcc65 100644 --- a/Plugins/Stats/Models/EFClientKill.cs +++ b/Plugins/Stats/Models/EFClientKill.cs @@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations.Schema; using SharedLibraryCore.Helpers; using System.ComponentModel.DataAnnotations; using System.Collections.Generic; +using static SharedLibraryCore.Server; namespace IW4MAdmin.Plugins.Stats.Models { @@ -44,6 +45,8 @@ namespace IW4MAdmin.Plugins.Stats.Models public float AdsPercent { get; set; } [NotMapped] public List AnglesList { get; set; } + [NotMapped] + public Game GameName { get; set; } /// /// Indicates if the attacker was alive after last captured angle diff --git a/Plugins/Stats/Plugin.cs b/Plugins/Stats/Plugin.cs index e4a9d2028..995eacf86 100644 --- a/Plugins/Stats/Plugin.cs +++ b/Plugins/Stats/Plugin.cs @@ -182,8 +182,9 @@ namespace IW4MAdmin.Plugins.Stats if (Config.Configuration() == null) { Config.Set((StatsConfiguration)new StatsConfiguration().Generate()); - await Config.Save(); } + Config.Configuration().ApplyMigration(); + await Config.Save(); // register the topstats page // todo:generate the URL/Location instead of hardcoding @@ -405,7 +406,7 @@ namespace IW4MAdmin.Plugins.Stats return (await _chatQueryHelper.QueryResource(query)).Results; } - if (Config.Configuration().EnableAntiCheat) + if (Config.Configuration().AnticheatConfiguration.Enable) { _metaService.AddRuntimeMeta(MetaType.Information, getAnticheatInfo); } @@ -496,6 +497,6 @@ namespace IW4MAdmin.Plugins.Stats /// /// /// - private bool ShouldOverrideAnticheatSetting(Server s) => Config.Configuration().EnableAntiCheat && s.GameName == Server.Game.IW5; + private bool ShouldOverrideAnticheatSetting(Server s) => Config.Configuration().AnticheatConfiguration.Enable && s.GameName == Server.Game.IW5; } } diff --git a/SharedLibraryCore/Commands/NativeCommands.cs b/SharedLibraryCore/Commands/NativeCommands.cs index cee2adada..edd4a5c06 100644 --- a/SharedLibraryCore/Commands/NativeCommands.cs +++ b/SharedLibraryCore/Commands/NativeCommands.cs @@ -772,8 +772,6 @@ namespace SharedLibraryCore.Commands /// public class ListAdminsCommand : Command { - private readonly CommandConfiguration _config; - public ListAdminsCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup) { Name = "admins"; @@ -781,8 +779,6 @@ namespace SharedLibraryCore.Commands Alias = "a"; Permission = Permission.User; RequiresTarget = false; - - _config = config; } public static string OnlineAdmins(Server S, ITranslationLookup lookup) @@ -901,8 +897,6 @@ namespace SharedLibraryCore.Commands /// public class ListRulesCommands : Command { - private readonly CommandConfiguration _config; - public ListRulesCommands(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup) { Name = "rules"; @@ -910,8 +904,6 @@ namespace SharedLibraryCore.Commands Alias = "r"; Permission = Permission.User; RequiresTarget = false; - - _config = config; } public override Task ExecuteAsync(GameEvent E)