From 1ac7956c22422ef1ab634240659708737c20bfe6 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Sun, 18 Mar 2018 21:24:06 -0500 Subject: [PATCH] more configuration changes, fixed issue with not escaping regex for validating commands --- Plugins/SimpleStats/Cheat/Detection.cs | 306 ++++++++++++++++++ .../Cheat/DetectionPenaltyResult.cs | 17 + Plugins/SimpleStats/Cheat/Thresholds.cs | 41 +++ 3 files changed, 364 insertions(+) create mode 100644 Plugins/SimpleStats/Cheat/Detection.cs create mode 100644 Plugins/SimpleStats/Cheat/DetectionPenaltyResult.cs create mode 100644 Plugins/SimpleStats/Cheat/Thresholds.cs diff --git a/Plugins/SimpleStats/Cheat/Detection.cs b/Plugins/SimpleStats/Cheat/Detection.cs new file mode 100644 index 000000000..15c08f51f --- /dev/null +++ b/Plugins/SimpleStats/Cheat/Detection.cs @@ -0,0 +1,306 @@ +using SharedLibrary.Interfaces; +using SharedLibrary.Objects; +using StatsPlugin.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace StatsPlugin.Cheat +{ + class Detection + { + int Kills; + int AboveThresholdCount; + double AverageKillTime; + Dictionary HitLocationCount; + DateTime LastKill; + ILogger Log; + + public Detection(ILogger log) + { + Log = log; + HitLocationCount = new Dictionary(); + foreach (var loc in Enum.GetValues(typeof(IW4Info.HitLocation))) + HitLocationCount.Add((IW4Info.HitLocation)loc, 0); + LastKill = DateTime.UtcNow; + } + + /// + /// Analyze kill and see if performed by a cheater + /// + /// kill performed by the player + /// true if detection reached thresholds, false otherwise + public DetectionPenaltyResult ProcessKill(EFClientKill kill) + { + if ((kill.DeathType != IW4Info.MeansOfDeath.MOD_PISTOL_BULLET && + kill.DeathType != IW4Info.MeansOfDeath.MOD_RIFLE_BULLET) || + kill.HitLoc == IW4Info.HitLocation.none) + return new DetectionPenaltyResult() + { + ClientPenalty = Penalty.PenaltyType.Any, + RatioAmount = 0 + }; + + HitLocationCount[kill.HitLoc]++; + Kills++; + AverageKillTime = (AverageKillTime + (DateTime.UtcNow - LastKill).TotalSeconds) / Kills; + +#region SESSION_RATIOS + if (Kills >= Thresholds.LowSampleMinKills) + { + double marginOfError = Thresholds.GetMarginOfError(Kills); + // determine what the max headshot percentage can be for current number of kills + double lerpAmount = Math.Min(1.0, (Kills - Thresholds.LowSampleMinKills) / (double)(Thresholds.HighSampleMinKills - Thresholds.LowSampleMinKills)); + double maxHeadshotLerpValueForFlag = Thresholds.Lerp(Thresholds.HeadshotRatioThresholdLowSample(2.0), Thresholds.HeadshotRatioThresholdHighSample(2.0), lerpAmount) + marginOfError; + double maxHeadshotLerpValueForBan = Thresholds.Lerp(Thresholds.HeadshotRatioThresholdLowSample(3.0), Thresholds.HeadshotRatioThresholdHighSample(3.0), lerpAmount) + marginOfError; + // determine what the max bone percentage can be for current number of kills + double maxBoneRatioLerpValueForFlag = Thresholds.Lerp(Thresholds.BoneRatioThresholdLowSample(2.25), Thresholds.BoneRatioThresholdHighSample(2.25), lerpAmount) + marginOfError; + double maxBoneRatioLerpValueForBan = Thresholds.Lerp(Thresholds.BoneRatioThresholdLowSample(3.25), Thresholds.BoneRatioThresholdHighSample(3.25), lerpAmount) + marginOfError; + + // calculate headshot ratio + double currentHeadshotRatio = ((HitLocationCount[IW4Info.HitLocation.head] + HitLocationCount[IW4Info.HitLocation.helmet]) / (double)Kills); + // calculate maximum bone + double currentMaxBoneRatio = (HitLocationCount.Values.Select(v => v / (double)Kills).Max()); + + var bone = HitLocationCount.FirstOrDefault(b => b.Value == HitLocationCount.Values.Max()).Key; + #region HEADSHOT_RATIO + // flag on headshot + if (currentHeadshotRatio > maxHeadshotLerpValueForFlag) + { + // ban on headshot + if (currentHeadshotRatio > maxHeadshotLerpValueForFlag) + { + AboveThresholdCount++; + Log.WriteDebug("**Maximum Headshot Ratio Reached For Ban**"); + Log.WriteDebug($"ClientId: {kill.AttackerId}"); + Log.WriteDebug($"**Kills: {Kills}"); + Log.WriteDebug($"**Ratio {currentHeadshotRatio}"); + Log.WriteDebug($"**MaxRatio {maxHeadshotLerpValueForFlag}"); + var sb = new StringBuilder(); + foreach (var kvp in HitLocationCount) + sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n"); + Log.WriteDebug(sb.ToString()); + Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}"); + + return new DetectionPenaltyResult() + { + ClientPenalty = Penalty.PenaltyType.Ban, + RatioAmount = currentHeadshotRatio, + Bone = IW4Info.HitLocation.head, + KillCount = Kills + }; + } + else + { + AboveThresholdCount++; + Log.WriteDebug("**Maximum Headshot Ratio Reached For Flag**"); + Log.WriteDebug($"ClientId: {kill.AttackerId}"); + Log.WriteDebug($"**Kills: {Kills}"); + Log.WriteDebug($"**Ratio {currentHeadshotRatio}"); + Log.WriteDebug($"**MaxRatio {maxHeadshotLerpValueForFlag}"); + var sb = new StringBuilder(); + foreach (var kvp in HitLocationCount) + sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n"); + Log.WriteDebug(sb.ToString()); + Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}"); + + return new DetectionPenaltyResult() + { + ClientPenalty = Penalty.PenaltyType.Flag, + RatioAmount = currentHeadshotRatio, + Bone = IW4Info.HitLocation.head, + KillCount = Kills + }; + } + } + #endregion + + #region BONE_RATIO + // flag on bone ratio + else if (currentMaxBoneRatio > maxBoneRatioLerpValueForFlag) + { + // ban on bone ratio + if (currentMaxBoneRatio > maxBoneRatioLerpValueForBan) + { + Log.WriteDebug("**Maximum Bone Ratio Reached For Ban**"); + Log.WriteDebug($"ClientId: {kill.AttackerId}"); + Log.WriteDebug($"**Kills: {Kills}"); + Log.WriteDebug($"**Ratio {currentMaxBoneRatio}"); + Log.WriteDebug($"**MaxRatio {maxBoneRatioLerpValueForBan}"); + var sb = new StringBuilder(); + foreach (var kvp in HitLocationCount) + sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n"); + Log.WriteDebug(sb.ToString()); + + return new DetectionPenaltyResult() + { + ClientPenalty = Penalty.PenaltyType.Ban, + RatioAmount = currentMaxBoneRatio, + Bone = bone, + KillCount = Kills + }; + } + else + { + Log.WriteDebug("**Maximum Bone Ratio Reached For Flag**"); + Log.WriteDebug($"ClientId: {kill.AttackerId}"); + Log.WriteDebug($"**Kills: {Kills}"); + Log.WriteDebug($"**Ratio {currentMaxBoneRatio}"); + Log.WriteDebug($"**MaxRatio {maxBoneRatioLerpValueForFlag}"); + var sb = new StringBuilder(); + foreach (var kvp in HitLocationCount) + sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n"); + Log.WriteDebug(sb.ToString()); + + return new DetectionPenaltyResult() + { + ClientPenalty = Penalty.PenaltyType.Flag, + RatioAmount = currentMaxBoneRatio, + Bone = bone, + KillCount = Kills + }; + } + } + #endregion + } + + #region CHEST_ABDOMEN_RATIO_SESSION + int chestKills = HitLocationCount[IW4Info.HitLocation.torso_upper]; + + if (chestKills >= Thresholds.MediumSampleMinKills) + { + double marginOfError = Thresholds.GetMarginOfError(chestKills); + double lerpAmount = Math.Min(1.0, (chestKills - Thresholds.LowSampleMinKills) / (double)(Thresholds.HighSampleMinKills - Thresholds.LowSampleMinKills)); + // determine max acceptable ratio of chest to abdomen kills + double chestAbdomenRatioLerpValueForFlag = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdLowSample(2.25), Thresholds.ChestAbdomenRatioThresholdHighSample(2.25), lerpAmount) + marginOfError; + double chestAbdomenLerpValueForBan = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdLowSample(3.25), Thresholds.ChestAbdomenRatioThresholdHighSample(3.25), lerpAmount) + marginOfError; + + double currentChestAbdomenRatio = HitLocationCount[IW4Info.HitLocation.torso_upper] / (double)HitLocationCount[IW4Info.HitLocation.torso_lower]; + + if (currentChestAbdomenRatio > chestAbdomenRatioLerpValueForFlag) + { + + if (currentChestAbdomenRatio > chestAbdomenLerpValueForBan && chestKills >= Thresholds.MediumSampleMinKills + 30) + { + Log.WriteDebug("**Maximum Chest/Abdomen Ratio Reached For Ban**"); + Log.WriteDebug($"ClientId: {kill.AttackerId}"); + Log.WriteDebug($"**Chest Kills: {chestKills}"); + Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}"); + Log.WriteDebug($"**MaxRatio {chestAbdomenLerpValueForBan}"); + var sb = new StringBuilder(); + foreach (var kvp in HitLocationCount) + sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n"); + Log.WriteDebug(sb.ToString()); + // Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}"); + + return new DetectionPenaltyResult() + { + ClientPenalty = Penalty.PenaltyType.Ban, + RatioAmount = currentChestAbdomenRatio, + Bone = IW4Info.HitLocation.torso_upper, + KillCount = chestKills + }; + } + else + { + Log.WriteDebug("**Maximum Chest/Abdomen Ratio Reached For Flag**"); + Log.WriteDebug($"ClientId: {kill.AttackerId}"); + Log.WriteDebug($"**Chest Kills: {chestKills}"); + Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}"); + Log.WriteDebug($"**MaxRatio {chestAbdomenRatioLerpValueForFlag}"); + var sb = new StringBuilder(); + foreach (var kvp in HitLocationCount) + sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n"); + Log.WriteDebug(sb.ToString()); + // Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}"); + + return new DetectionPenaltyResult() + { + ClientPenalty = Penalty.PenaltyType.Flag, + RatioAmount = currentChestAbdomenRatio, + Bone = IW4Info.HitLocation.torso_upper, + KillCount = chestKills + }; + } + } + } + #endregion +#endregion + return new DetectionPenaltyResult() + { + ClientPenalty = Penalty.PenaltyType.Any, + RatioAmount = 0 + }; + } + + public DetectionPenaltyResult ProcessTotalRatio(EFClientStatistics stats) + { + int totalChestKills = stats.HitLocations.Single(c => c.Location == IW4Info.HitLocation.left_arm_upper).HitCount; + + if (totalChestKills >= 250) + { + double marginOfError = Thresholds.GetMarginOfError(totalChestKills); + double lerpAmount = Math.Min(1.0, (totalChestKills - Thresholds.LowSampleMinKills) / (double)(Thresholds.HighSampleMinKills - Thresholds.LowSampleMinKills)); + // determine max acceptable ratio of chest to abdomen kills + double chestAbdomenRatioLerpValueForFlag = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdLowSample(2.25), Thresholds.ChestAbdomenRatioThresholdHighSample(2.25), lerpAmount) + marginOfError; + double chestAbdomenLerpValueForBan = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdLowSample(3.0), Thresholds.ChestAbdomenRatioThresholdHighSample(3.0), lerpAmount) + marginOfError; + + double currentChestAbdomenRatio = HitLocationCount[IW4Info.HitLocation.torso_upper] / (double)HitLocationCount[IW4Info.HitLocation.torso_lower]; + + if (currentChestAbdomenRatio > chestAbdomenRatioLerpValueForFlag) + { + + if (currentChestAbdomenRatio > chestAbdomenLerpValueForBan) + { + Log.WriteDebug("**Maximum Lifetime Chest/Abdomen Ratio Reached For Ban**"); + Log.WriteDebug($"ClientId: {stats.ClientId}"); + Log.WriteDebug($"**Total Chest Kills: {totalChestKills}"); + Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}"); + Log.WriteDebug($"**MaxRatio {chestAbdomenLerpValueForBan}"); + var sb = new StringBuilder(); + foreach (var kvp in HitLocationCount) + sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n"); + Log.WriteDebug(sb.ToString()); + // Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}"); + + return new DetectionPenaltyResult() + { + ClientPenalty = Penalty.PenaltyType.Ban, + RatioAmount = currentChestAbdomenRatio, + Bone = IW4Info.HitLocation.torso_upper, + KillCount = totalChestKills + }; + } + else + { + Log.WriteDebug("**Maximum Lifetime Chest/Abdomen Ratio Reached For Flag**"); + Log.WriteDebug($"ClientId: {stats.ClientId}"); + Log.WriteDebug($"**Total Chest Kills: {totalChestKills}"); + Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}"); + Log.WriteDebug($"**MaxRatio {chestAbdomenRatioLerpValueForFlag}"); + var sb = new StringBuilder(); + foreach (var kvp in HitLocationCount) + sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n"); + Log.WriteDebug(sb.ToString()); + // Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}"); + + return new DetectionPenaltyResult() + { + ClientPenalty = Penalty.PenaltyType.Flag, + RatioAmount = currentChestAbdomenRatio, + Bone = IW4Info.HitLocation.torso_upper, + KillCount = totalChestKills + }; + } + } + } + + return new DetectionPenaltyResult() + { + Bone = IW4Info.HitLocation.none, + ClientPenalty = Penalty.PenaltyType.Any + }; + } + } +} diff --git a/Plugins/SimpleStats/Cheat/DetectionPenaltyResult.cs b/Plugins/SimpleStats/Cheat/DetectionPenaltyResult.cs new file mode 100644 index 000000000..a53e1d6c3 --- /dev/null +++ b/Plugins/SimpleStats/Cheat/DetectionPenaltyResult.cs @@ -0,0 +1,17 @@ +using SharedLibrary.Objects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StatsPlugin.Cheat +{ + class DetectionPenaltyResult + { + public Penalty.PenaltyType ClientPenalty { get; set; } + public double RatioAmount { get; set; } + public IW4Info.HitLocation Bone { get; set; } + public int KillCount { get; set; } + } +} diff --git a/Plugins/SimpleStats/Cheat/Thresholds.cs b/Plugins/SimpleStats/Cheat/Thresholds.cs new file mode 100644 index 000000000..0fa31deff --- /dev/null +++ b/Plugins/SimpleStats/Cheat/Thresholds.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StatsPlugin.Cheat +{ + class Thresholds + { + public static double HeadshotRatioThresholdLowSample(double deviations) => HeadshotRatioStandardDeviationLowSample * deviations + HeadshotRatioMean; + public static double HeadshotRatioThresholdHighSample(double deviations) => HeadshotRatioStandardDeviationHighSample * deviations + HeadshotRatioMean; + public const double HeadshotRatioStandardDeviationLowSample = 0.1769994181; + public const double HeadshotRatioStandardDeviationHighSample = 0.03924263235; + public const double HeadshotRatioMean = 0.222; + + public static double BoneRatioThresholdLowSample(double deviations) => BoneRatioStandardDeviationLowSample * deviations + BoneRatioMean; + public static double BoneRatioThresholdHighSample(double deviations) => BoneRatioStandardDeviationHighSample * deviations + BoneRatioMean; + public const double BoneRatioStandardDeviationLowSample = 0.1324612879; + public const double BoneRatioStandardDeviationHighSample = 0.0515753935; + public const double BoneRatioMean = 0.4593110238; + + public static double ChestAbdomenRatioThresholdLowSample(double deviations) => ChestAbdomenStandardDeviationLowSample * deviations + ChestAbdomenRatioMean; + public static double ChestAbdomenRatioThresholdHighSample(double deviations) => ChestAbdomenStandardDeviationHighSample * deviations + ChestAbdomenRatioMean; + public const double ChestAbdomenStandardDeviationLowSample = 0.2859234644; + public const double ChestAbdomenStandardDeviationHighSample = 0.2195212861; + public const double ChestAbdomenRatioMean = 0.4435; + + public const int LowSampleMinKills = 15; + public const int MediumSampleMinKills = 30; + public const int HighSampleMinKills = 100; + public const double KillTimeThreshold = 0.2; + + public static double GetMarginOfError(int numKills) => 0.98 / Math.Sqrt(numKills); + + public static double Lerp(double v1, double v2, double amount) + { + return v1 + (v2 - v1) * amount; + } + } +}