IW4M-Admin/Plugins/Stats/Cheat/Detection.cs

460 lines
22 KiB
C#
Raw Normal View History

using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
using IW4MAdmin.Plugins.Stats.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace IW4MAdmin.Plugins.Stats.Cheat
{
class Detection
{
public enum DetectionType
{
Bone,
Chest,
Offset,
Strain
};
public ChangeTracking<EFACSnapshot> Tracker { get; private set; }
int Kills;
2018-05-10 01:34:29 -04:00
int HitCount;
Dictionary<IW4Info.HitLocation, int> HitLocationCount;
double AngleDifferenceAverage;
EFClientStatistics ClientStats;
long LastOffset;
IW4Info.WeaponName LastWeapon;
ILogger Log;
Strain Strain;
readonly DateTime ConnectionTime = DateTime.UtcNow;
public Detection(ILogger log, EFClientStatistics clientStats)
{
Log = log;
HitLocationCount = new Dictionary<IW4Info.HitLocation, int>();
foreach (var loc in Enum.GetValues(typeof(IW4Info.HitLocation)))
HitLocationCount.Add((IW4Info.HitLocation)loc, 0);
ClientStats = clientStats;
Strain = new Strain();
Tracker = new ChangeTracking<EFACSnapshot>();
}
/// <summary>
/// Analyze kill and see if performed by a cheater
/// </summary>
2018-10-28 21:47:56 -04:00
/// <param name="hit">kill performed by the player</param>
/// <returns>true if detection reached thresholds, false otherwise</returns>
2018-10-28 21:47:56 -04:00
public DetectionPenaltyResult ProcessHit(EFClientKill hit, bool isDamage)
{
2018-10-28 21:47:56 -04:00
if ((hit.DeathType != IW4Info.MeansOfDeath.MOD_PISTOL_BULLET &&
hit.DeathType != IW4Info.MeansOfDeath.MOD_RIFLE_BULLET &&
hit.DeathType != IW4Info.MeansOfDeath.MOD_HEAD_SHOT) ||
hit.HitLoc == IW4Info.HitLocation.none || hit.TimeOffset - LastOffset < 0 ||
// hack: prevents false positives
2018-10-28 21:47:56 -04:00
(LastWeapon != hit.Weapon && (hit.TimeOffset - LastOffset) == 50))
return new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Any,
};
DetectionPenaltyResult result = null;
2018-10-28 21:47:56 -04:00
LastWeapon = hit.Weapon;
HitLocationCount[hit.HitLoc]++;
HitCount++;
if (!isDamage)
{
Kills++;
}
#region VIEWANGLES
2018-10-28 21:47:56 -04:00
if (hit.AnglesList.Count >= 2)
{
2018-10-28 21:47:56 -04:00
double realAgainstPredict = Vector3.ViewAngleDistance(hit.AnglesList[0], hit.AnglesList[1], hit.ViewAngles);
// LIFETIME
var hitLoc = ClientStats.HitLocations
2018-10-28 21:47:56 -04:00
.First(hl => hl.Location == hit.HitLoc);
float previousAverage = hitLoc.HitOffsetAverage;
double newAverage = (previousAverage * (hitLoc.HitCount - 1) + realAgainstPredict) / hitLoc.HitCount;
hitLoc.HitOffsetAverage = (float)newAverage;
if (hitLoc.HitOffsetAverage > Thresholds.MaxOffset(hitLoc.HitCount) &&
hitLoc.HitCount > 100)
{
2018-10-28 21:47:56 -04:00
//Log.WriteDebug("*** Reached Max Lifetime Average for Angle Difference ***");
//Log.WriteDebug($"Lifetime Average = {newAverage}");
//Log.WriteDebug($"Bone = {hitLoc.Location}");
//Log.WriteDebug($"HitCount = {hitLoc.HitCount}");
//Log.WriteDebug($"ID = {hit.AttackerId}");
2018-05-10 01:34:29 -04:00
result = new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Ban,
Value = hitLoc.HitOffsetAverage,
HitCount = hitLoc.HitCount,
Type = DetectionType.Offset
};
}
// SESSION
2018-05-10 01:34:29 -04:00
double sessAverage = (AngleDifferenceAverage * (HitCount - 1) + realAgainstPredict) / HitCount;
AngleDifferenceAverage = sessAverage;
if (sessAverage > Thresholds.MaxOffset(HitCount) &&
HitCount > 30)
{
2018-10-28 21:47:56 -04:00
//Log.WriteDebug("*** Reached Max Session Average for Angle Difference ***");
//Log.WriteDebug($"Session Average = {sessAverage}");
//Log.WriteDebug($"HitCount = {HitCount}");
//Log.WriteDebug($"ID = {hit.AttackerId}");
2018-05-10 01:34:29 -04:00
result = new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Ban,
Value = sessAverage,
HitCount = HitCount,
Type = DetectionType.Offset,
Location = hitLoc.Location
};
}
#if DEBUG
Log.WriteDebug($"PredictVsReal={realAgainstPredict}");
#endif
}
2018-10-28 21:47:56 -04:00
double currentStrain = Strain.GetStrain(isDamage, hit.Damage, hit.Distance / 0.0254, hit.ViewAngles, Math.Max(50, hit.TimeOffset - LastOffset));
#if DEBUG == true
Log.WriteDebug($"Current Strain: {currentStrain}");
#endif
LastOffset = hit.TimeOffset;
if (currentStrain > ClientStats.MaxStrain)
{
ClientStats.MaxStrain = currentStrain;
}
2018-10-28 21:47:56 -04:00
// flag
2018-10-28 21:47:56 -04:00
if (currentStrain > Thresholds.MaxStrainFlag)
{
result = new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Flag,
Value = currentStrain,
HitCount = HitCount,
Type = DetectionType.Strain
};
}
// ban
if (currentStrain > Thresholds.MaxStrainBan &&
2018-10-28 21:47:56 -04:00
HitCount >= 5)
{
result = new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Ban,
Value = currentStrain,
HitCount = HitCount,
Type = DetectionType.Strain
};
}
#endregion
#region SESSION_RATIOS
if (Kills >= Thresholds.LowSampleMinKills)
{
2018-05-10 01:34:29 -04:00
double marginOfError = Thresholds.GetMarginOfError(HitCount);
// determine what the max headshot percentage can be for current number of kills
2018-05-10 01:34:29 -04:00
double lerpAmount = Math.Min(1.0, (HitCount - Thresholds.LowSampleMinKills) / (double)(/*Thresholds.HighSampleMinKills*/ 60 - Thresholds.LowSampleMinKills));
double maxHeadshotLerpValueForFlag = Thresholds.Lerp(Thresholds.HeadshotRatioThresholdLowSample(2.0), Thresholds.HeadshotRatioThresholdHighSample(2.0), lerpAmount) + marginOfError;
double maxHeadshotLerpValueForBan = Thresholds.Lerp(Thresholds.HeadshotRatioThresholdLowSample(3.5), Thresholds.HeadshotRatioThresholdHighSample(3.5), 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
2018-05-10 01:34:29 -04:00
double currentHeadshotRatio = ((HitLocationCount[IW4Info.HitLocation.head] + HitLocationCount[IW4Info.HitLocation.helmet] + HitLocationCount[IW4Info.HitLocation.neck]) / (double)HitCount);
// calculate maximum bone
2018-05-10 01:34:29 -04:00
double currentMaxBoneRatio = (HitLocationCount.Values.Select(v => v / (double)HitCount).Max());
var bone = HitLocationCount.FirstOrDefault(b => b.Value == HitLocationCount.Values.Max()).Key;
#region HEADSHOT_RATIO
// flag on headshot
if (currentHeadshotRatio > maxHeadshotLerpValueForFlag)
{
// ban on headshot
2018-09-02 23:09:25 -04:00
if (currentHeadshotRatio > maxHeadshotLerpValueForBan)
{
Log.WriteDebug("**Maximum Headshot Ratio Reached For Ban**");
2018-10-28 21:47:56 -04:00
Log.WriteDebug($"ClientId: {hit.AttackerId}");
Log.WriteDebug($"**HitCount: {HitCount}");
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());
result = new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Ban,
Value = currentHeadshotRatio,
Location = IW4Info.HitLocation.head,
HitCount = HitCount,
Type = DetectionType.Bone
};
}
else
{
Log.WriteDebug("**Maximum Headshot Ratio Reached For Flag**");
2018-10-28 21:47:56 -04:00
Log.WriteDebug($"ClientId: {hit.AttackerId}");
Log.WriteDebug($"**HitCount: {HitCount}");
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());
result = new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Flag,
Value = currentHeadshotRatio,
Location = IW4Info.HitLocation.head,
HitCount = HitCount,
Type = DetectionType.Bone
};
}
}
#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**");
2018-10-28 21:47:56 -04:00
Log.WriteDebug($"ClientId: {hit.AttackerId}");
Log.WriteDebug($"**HitCount: {HitCount}");
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());
result = new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Ban,
Value = currentMaxBoneRatio,
Location = bone,
HitCount = HitCount,
Type = DetectionType.Bone
};
}
else
{
Log.WriteDebug("**Maximum Bone Ratio Reached For Flag**");
2018-10-28 21:47:56 -04:00
Log.WriteDebug($"ClientId: {hit.AttackerId}");
Log.WriteDebug($"**HitCount: {HitCount}");
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());
result = new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Flag,
Value = currentMaxBoneRatio,
Location = bone,
HitCount = HitCount,
Type = DetectionType.Bone
};
}
}
#endregion
}
#region CHEST_ABDOMEN_RATIO_SESSION
int chestHits = HitLocationCount[IW4Info.HitLocation.torso_upper];
if (chestHits >= Thresholds.MediumSampleMinKills)
{
double marginOfError = Thresholds.GetMarginOfError(chestHits);
double lerpAmount = Math.Min(1.0, (chestHits - Thresholds.MediumSampleMinKills) / (double)(Thresholds.HighSampleMinKills - Thresholds.LowSampleMinKills));
// determine max acceptable ratio of chest to abdomen kills
double chestAbdomenRatioLerpValueForFlag = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdLowSample(3), Thresholds.ChestAbdomenRatioThresholdHighSample(3), lerpAmount) + marginOfError;
double chestAbdomenLerpValueForBan = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdLowSample(4), Thresholds.ChestAbdomenRatioThresholdHighSample(4), lerpAmount) + marginOfError;
double currentChestAbdomenRatio = HitLocationCount[IW4Info.HitLocation.torso_upper] / (double)HitLocationCount[IW4Info.HitLocation.torso_lower];
if (currentChestAbdomenRatio > chestAbdomenRatioLerpValueForFlag)
{
if (currentChestAbdomenRatio > chestAbdomenLerpValueForBan && chestHits >= Thresholds.MediumSampleMinKills + 30)
{
Log.WriteDebug("**Maximum Chest/Abdomen Ratio Reached For Ban**");
2018-10-28 21:47:56 -04:00
Log.WriteDebug($"ClientId: {hit.AttackerId}");
Log.WriteDebug($"**Chest Hits: {chestHits}");
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());
result = new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Ban,
Value = currentChestAbdomenRatio,
Location = IW4Info.HitLocation.torso_upper,
Type = DetectionType.Chest,
HitCount = chestHits
};
}
else
{
Log.WriteDebug("**Maximum Chest/Abdomen Ratio Reached For Flag**");
2018-10-28 21:47:56 -04:00
Log.WriteDebug($"ClientId: {hit.AttackerId}");
Log.WriteDebug($"**Chest Hits: {chestHits}");
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}");
result = new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Flag,
Value = currentChestAbdomenRatio,
Location = IW4Info.HitLocation.torso_upper,
Type = DetectionType.Chest,
HitCount = chestHits
};
}
}
}
#endregion
#endregion
Tracker.OnChange(new EFACSnapshot()
{
2018-10-28 21:47:56 -04:00
When = hit.When,
ClientId = ClientStats.ClientId,
SessionAngleOffset = AngleDifferenceAverage,
CurrentSessionLength = (int)(DateTime.UtcNow - ConnectionTime).TotalSeconds,
CurrentStrain = currentStrain,
2018-10-28 21:47:56 -04:00
CurrentViewAngle = hit.ViewAngles,
Hits = HitCount,
Kills = Kills,
Deaths = ClientStats.SessionDeaths,
2018-10-28 21:47:56 -04:00
HitDestinationId = hit.DeathOrigin.Vector3Id,
HitDestination = hit.DeathOrigin,
HitOriginId = hit.KillOrigin.Vector3Id,
HitOrigin = hit.KillOrigin,
EloRating = ClientStats.EloRating,
2018-10-28 21:47:56 -04:00
HitLocation = hit.HitLoc,
LastStrainAngle = Strain.LastAngle,
2018-10-28 21:47:56 -04:00
PredictedViewAngles = hit.AnglesList,
// this is in "meters"
2018-10-28 21:47:56 -04:00
Distance = hit.Distance,
SessionScore = ClientStats.SessionScore,
2018-10-28 21:47:56 -04:00
HitType = hit.DeathType,
SessionSPM = ClientStats.SessionSPM,
StrainAngleBetween = Strain.LastDistance,
TimeSinceLastEvent = (int)Strain.LastDeltaTime,
2018-10-28 21:47:56 -04:00
WeaponId = hit.Weapon
});
return result ?? new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Any,
};
}
public DetectionPenaltyResult ProcessTotalRatio(EFClientStatistics stats)
{
int totalChestHits = stats.HitLocations.Single(c => c.Location == IW4Info.HitLocation.torso_upper).HitCount;
if (totalChestHits >= 60)
{
double marginOfError = Thresholds.GetMarginOfError(totalChestHits);
double lerpAmount = Math.Min(1.0, (totalChestHits - 60) / 250.0);
// determine max acceptable ratio of chest to abdomen kills
double chestAbdomenRatioLerpValueForFlag = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdHighSample(3.0), Thresholds.ChestAbdomenRatioThresholdHighSample(2.0), lerpAmount) + marginOfError;
double chestAbdomenLerpValueForBan = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdHighSample(4.0), Thresholds.ChestAbdomenRatioThresholdHighSample(3.0), lerpAmount) + marginOfError;
double currentChestAbdomenRatio = totalChestHits /
stats.HitLocations.Single(hl => hl.Location == IW4Info.HitLocation.torso_lower).HitCount;
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 Hits: {totalChestHits}");
Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
Log.WriteDebug($"**MaxRatio {chestAbdomenLerpValueForBan}");
var sb = new StringBuilder();
foreach (var location in stats.HitLocations)
sb.Append($"HitLocation: {location.Location} -> {location.HitCount}\r\n");
Log.WriteDebug(sb.ToString());
return new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Ban,
Value = currentChestAbdomenRatio,
Location = IW4Info.HitLocation.torso_upper,
HitCount = totalChestHits,
Type = DetectionType.Chest
};
}
else
{
Log.WriteDebug("**Maximum Lifetime Chest/Abdomen Ratio Reached For Flag**");
Log.WriteDebug($"ClientId: {stats.ClientId}");
Log.WriteDebug($"**Total Chest Hits: {totalChestHits}");
Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
Log.WriteDebug($"**MaxRatio {chestAbdomenRatioLerpValueForFlag}");
var sb = new StringBuilder();
foreach (var location in stats.HitLocations)
sb.Append($"HitLocation: {location.Location} -> {location.HitCount}\r\n");
Log.WriteDebug(sb.ToString());
return new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Flag,
Value = currentChestAbdomenRatio,
Location = IW4Info.HitLocation.torso_upper,
HitCount = totalChestHits,
Type = DetectionType.Chest
};
}
}
}
return new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Any
};
}
}
}