ed83c4c011
fix bug with unbanned players still showing as banned via lock icon move player based stuff into client class finally renamed Player to EFClient via partial class don't try to run this build because it's in between stages
478 lines
22 KiB
C#
478 lines
22 KiB
C#
using IW4MAdmin.Plugins.Stats.Models;
|
|
using SharedLibraryCore.Helpers;
|
|
using SharedLibraryCore.Interfaces;
|
|
using SharedLibraryCore.Objects;
|
|
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; }
|
|
public const int QUEUE_COUNT = 10;
|
|
|
|
public List<EFClientKill> QueuedHits { get; set; }
|
|
int Kills;
|
|
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>();
|
|
QueuedHits = new List<EFClientKill>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Analyze kill and see if performed by a cheater
|
|
/// </summary>
|
|
/// <param name="hit">kill performed by the player</param>
|
|
/// <returns>true if detection reached thresholds, false otherwise</returns>
|
|
public DetectionPenaltyResult ProcessHit(EFClientKill hit, bool isDamage)
|
|
{
|
|
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
|
|
(LastWeapon != hit.Weapon && (hit.TimeOffset - LastOffset) == 50))
|
|
{
|
|
return new DetectionPenaltyResult()
|
|
{
|
|
ClientPenalty = Penalty.PenaltyType.Any,
|
|
};
|
|
}
|
|
|
|
DetectionPenaltyResult result = null;
|
|
LastWeapon = hit.Weapon;
|
|
|
|
HitLocationCount[hit.HitLoc]++;
|
|
HitCount++;
|
|
|
|
if (!isDamage)
|
|
{
|
|
Kills++;
|
|
}
|
|
|
|
#region VIEWANGLES
|
|
if (hit.AnglesList.Count >= 2)
|
|
{
|
|
double realAgainstPredict = Vector3.ViewAngleDistance(hit.AnglesList[0], hit.AnglesList[1], hit.ViewAngles);
|
|
|
|
// LIFETIME
|
|
var hitLoc = ClientStats.HitLocations
|
|
.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)
|
|
{
|
|
//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}");
|
|
|
|
result = new DetectionPenaltyResult()
|
|
{
|
|
ClientPenalty = Penalty.PenaltyType.Ban,
|
|
Value = hitLoc.HitOffsetAverage,
|
|
HitCount = hitLoc.HitCount,
|
|
Type = DetectionType.Offset
|
|
};
|
|
}
|
|
|
|
// SESSION
|
|
double sessAverage = (AngleDifferenceAverage * (HitCount - 1) + realAgainstPredict) / HitCount;
|
|
AngleDifferenceAverage = sessAverage;
|
|
|
|
if (sessAverage > Thresholds.MaxOffset(HitCount) &&
|
|
HitCount > 30)
|
|
{
|
|
//Log.WriteDebug("*** Reached Max Session Average for Angle Difference ***");
|
|
//Log.WriteDebug($"Session Average = {sessAverage}");
|
|
//Log.WriteDebug($"HitCount = {HitCount}");
|
|
//Log.WriteDebug($"ID = {hit.AttackerId}");
|
|
|
|
result = new DetectionPenaltyResult()
|
|
{
|
|
ClientPenalty = Penalty.PenaltyType.Ban,
|
|
Value = sessAverage,
|
|
HitCount = HitCount,
|
|
Type = DetectionType.Offset,
|
|
Location = hitLoc.Location
|
|
};
|
|
}
|
|
|
|
#if DEBUG
|
|
Log.WriteDebug($"PredictVsReal={realAgainstPredict}");
|
|
#endif
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// flag
|
|
if (currentStrain > Thresholds.MaxStrainFlag)
|
|
{
|
|
result = new DetectionPenaltyResult()
|
|
{
|
|
ClientPenalty = Penalty.PenaltyType.Flag,
|
|
Value = currentStrain,
|
|
HitCount = HitCount,
|
|
Type = DetectionType.Strain
|
|
};
|
|
}
|
|
|
|
// ban
|
|
if (currentStrain > Thresholds.MaxStrainBan &&
|
|
HitCount >= 5)
|
|
{
|
|
result = new DetectionPenaltyResult()
|
|
{
|
|
ClientPenalty = Penalty.PenaltyType.Ban,
|
|
Value = currentStrain,
|
|
HitCount = HitCount,
|
|
Type = DetectionType.Strain
|
|
};
|
|
}
|
|
#endregion
|
|
|
|
#region SESSION_RATIOS
|
|
if (Kills >= Thresholds.LowSampleMinKills)
|
|
{
|
|
double marginOfError = Thresholds.GetMarginOfError(HitCount);
|
|
// determine what the max headshot percentage can be for current number of kills
|
|
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
|
|
double currentHeadshotRatio = ((HitLocationCount[IW4Info.HitLocation.head] + HitLocationCount[IW4Info.HitLocation.helmet] + HitLocationCount[IW4Info.HitLocation.neck]) / (double)HitCount);
|
|
|
|
// calculate maximum bone
|
|
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
|
|
if (currentHeadshotRatio > maxHeadshotLerpValueForBan)
|
|
{
|
|
Log.WriteDebug("**Maximum Headshot Ratio Reached For Ban**");
|
|
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**");
|
|
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**");
|
|
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**");
|
|
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**");
|
|
//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**");
|
|
//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());
|
|
|
|
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()
|
|
{
|
|
When = hit.When,
|
|
ClientId = ClientStats.ClientId,
|
|
SessionAngleOffset = AngleDifferenceAverage,
|
|
CurrentSessionLength = (int)(DateTime.UtcNow - ConnectionTime).TotalSeconds,
|
|
CurrentStrain = currentStrain,
|
|
CurrentViewAngle = hit.ViewAngles,
|
|
Hits = HitCount,
|
|
Kills = Kills,
|
|
Deaths = ClientStats.SessionDeaths,
|
|
HitDestinationId = hit.DeathOrigin.Vector3Id,
|
|
HitDestination = hit.DeathOrigin,
|
|
HitOriginId = hit.KillOrigin.Vector3Id,
|
|
HitOrigin = hit.KillOrigin,
|
|
EloRating = ClientStats.EloRating,
|
|
HitLocation = hit.HitLoc,
|
|
LastStrainAngle = Strain.LastAngle,
|
|
PredictedViewAngles = hit.AnglesList,
|
|
// this is in "meters"
|
|
Distance = hit.Distance,
|
|
SessionScore = ClientStats.SessionScore,
|
|
HitType = hit.DeathType,
|
|
SessionSPM = ClientStats.SessionSPM,
|
|
StrainAngleBetween = Strain.LastDistance,
|
|
TimeSinceLastEvent = (int)Strain.LastDeltaTime,
|
|
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
|
|
};
|
|
}
|
|
}
|
|
}
|