/// <summary> /// Process stats for kill event /// </summary> /// <returns></returns> public async Task AddScriptKill(Player attacker, Player victim, int serverId, string map, string hitLoc, string type, string damage, string weapon, string killOrigin, string deathOrigin) { await AddStandardKill(attacker, victim); if (victim == null) { Log.WriteError($"[AddScriptKill] Victim is null"); return; } var statsSvc = ContextThreads[serverId]; var playerDetection = Servers[serverId].PlayerDetections[attacker.ClientNumber]; var kill = new EFClientKill() { Active = true, AttackerId = attacker.ClientId, VictimId = victim.ClientId, ServerId = serverId, Map = ParseEnum <IW4Info.MapName> .Get(map, typeof(IW4Info.MapName)), DeathOrigin = Vector3.Parse(deathOrigin), KillOrigin = Vector3.Parse(killOrigin), DeathType = ParseEnum <IW4Info.MeansOfDeath> .Get(type, typeof(IW4Info.MeansOfDeath)), Damage = Int32.Parse(damage), HitLoc = ParseEnum <IW4Info.HitLocation> .Get(hitLoc, typeof(IW4Info.HitLocation)), Weapon = ParseEnum <IW4Info.WeaponName> .Get(weapon, typeof(IW4Info.WeaponName)) }; playerDetection.ProcessKill(kill); return; statsSvc.KillStatsSvc.Insert(kill); await statsSvc.KillStatsSvc.SaveChangesAsync(); }
/// <summary> /// Process stats for kill event /// </summary> /// <returns></returns> public async Task AddScriptKill(Player attacker, Player victim, int serverId, string map, string hitLoc, string type, string damage, string weapon, string killOrigin, string deathOrigin, string viewAngles, string offset, string isKillstreakKill, string Ads) { var statsSvc = ContextThreads[serverId]; Vector3 vDeathOrigin = null; Vector3 vKillOrigin = null; try { vDeathOrigin = Vector3.Parse(deathOrigin); vKillOrigin = Vector3.Parse(killOrigin); } catch (FormatException) { Log.WriteWarning("Could not parse kill or death origin vector"); Log.WriteDebug($"Kill - {killOrigin} Death - {deathOrigin}"); await AddStandardKill(attacker, victim); return; } var kill = new EFClientKill() { Active = true, AttackerId = attacker.ClientId, VictimId = victim.ClientId, ServerId = serverId, Map = ParseEnum <IW4Info.MapName> .Get(map, typeof(IW4Info.MapName)), DeathOrigin = vDeathOrigin, KillOrigin = vKillOrigin, DeathType = ParseEnum <IW4Info.MeansOfDeath> .Get(type, typeof(IW4Info.MeansOfDeath)), Damage = Int32.Parse(damage), HitLoc = ParseEnum <IW4Info.HitLocation> .Get(hitLoc, typeof(IW4Info.HitLocation)), Weapon = ParseEnum <IW4Info.WeaponName> .Get(weapon, typeof(IW4Info.WeaponName)), ViewAngles = Vector3.Parse(viewAngles).FixIW4Angles(), TimeOffset = Int64.Parse(offset), When = DateTime.UtcNow, IsKillstreakKill = isKillstreakKill[0] != '0', AdsPercent = float.Parse(Ads) }; if (kill.DeathType == IW4Info.MeansOfDeath.MOD_SUICIDE && kill.Damage == 100000) { // suicide by switching teams so let's not count it against them return; } await AddStandardKill(attacker, victim); if (kill.IsKillstreakKill) { return; } var clientDetection = Servers[serverId].PlayerDetections[attacker.ClientId]; var clientStats = Servers[serverId].PlayerStats[attacker.ClientId]; // increment their hit count if (kill.DeathType == IW4Info.MeansOfDeath.MOD_PISTOL_BULLET || kill.DeathType == IW4Info.MeansOfDeath.MOD_RIFLE_BULLET || kill.DeathType == IW4Info.MeansOfDeath.MOD_HEAD_SHOT) { clientStats.HitLocations.Single(hl => hl.Location == kill.HitLoc).HitCount += 1; statsSvc.ClientStatSvc.Update(clientStats); await statsSvc.ClientStatSvc.SaveChangesAsync(); } //statsSvc.KillStatsSvc.Insert(kill); //await statsSvc.KillStatsSvc.SaveChangesAsync(); if (Plugin.Config.Configuration().EnableAntiCheat) { async Task executePenalty(Cheat.DetectionPenaltyResult penalty) { switch (penalty.ClientPenalty) { case Penalty.PenaltyType.Ban: await attacker.Ban("You appear to be cheating", new Player() { ClientId = 1 }); break; case Penalty.PenaltyType.Flag: if (attacker.Level != Player.Permission.User) { break; } var flagCmd = new CFlag(); await flagCmd.ExecuteAsync(new GameEvent(GameEvent.EventType.Flag, $"{(int)penalty.Bone}-{Math.Round(penalty.RatioAmount, 2).ToString()}@{penalty.KillCount}", new Player() { ClientId = 1, Level = Player.Permission.Console, ClientNumber = -1, CurrentServer = attacker.CurrentServer }, attacker, attacker.CurrentServer)); break; } } await executePenalty(clientDetection.ProcessKill(kill)); await executePenalty(clientDetection.ProcessTotalRatio(clientStats)); } }
/// <summary> /// Analyze kill and see if performed by a cheater /// </summary> /// <param name="kill">kill performed by the player</param> /// <returns>true if detection reached thresholds, false otherwise</returns> public bool ProcessKill(EFClientKill kill) { if (kill.DeathType != IW4Info.MeansOfDeath.MOD_PISTOL_BULLET && kill.DeathType != IW4Info.MeansOfDeath.MOD_RIFLE_BULLET) { return(false); } bool thresholdReached = false; HitLocationCount[kill.HitLoc]++; Kills++; AverageKillTime = (AverageKillTime + (DateTime.UtcNow - LastKill).TotalSeconds) / Kills; 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 maxHeadshotLerpValue = Thresholds.Lerp(Thresholds.HeadshotRatioThresholdLowSample, Thresholds.HeadshotRatioThresholdHighSample, lerpAmount); // determine what the max bone percentage can be for current number of kills double maxBoneRatioLerpValue = Thresholds.Lerp(Thresholds.BoneRatioThresholdLowSample, Thresholds.BoneRatioThresholdHighSample, lerpAmount); // calculate headshot ratio double headshotRatio = ((HitLocationCount[IW4Info.HitLocation.head] + HitLocationCount[IW4Info.HitLocation.helmet]) / (double)Kills) - marginOfError; // calculate maximum bone double maximumBoneRatio = (HitLocationCount.Values.Select(v => v / (double)Kills).Max()) - marginOfError; if (headshotRatio > maxHeadshotLerpValue) { AboveThresholdCount++; Log.WriteDebug("**Maximum Headshot Ratio Reached**"); Log.WriteDebug($"ClientId: {kill.AttackerId}"); Log.WriteDebug($"**Kills: {Kills}"); Log.WriteDebug($"**Ratio {headshotRatio}"); Log.WriteDebug($"**MaxRatio {maxHeadshotLerpValue}"); var sb = new StringBuilder(); foreach (var kvp in HitLocationCount) { sb.Append($"HitLocation: {kvp.Key} Count: {kvp.Value}"); } Log.WriteDebug(sb.ToString()); Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}"); thresholdReached = true; } else if (maximumBoneRatio > maxBoneRatioLerpValue) { Log.WriteDebug("**Maximum Bone Ratio Reached**"); Log.WriteDebug($"ClientId: {kill.AttackerId}"); Log.WriteDebug($"**Kills: {Kills}"); Log.WriteDebug($"**Ratio {maximumBoneRatio}"); Log.WriteDebug($"**MaxRatio {maxBoneRatioLerpValue}"); var sb = new StringBuilder(); foreach (var kvp in HitLocationCount) { sb.Append($"HitLocation: {kvp.Key} Count: {kvp.Value}"); } Log.WriteDebug(sb.ToString()); thresholdReached = true; } } return(thresholdReached); }
/// <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 IEnumerable <DetectionPenaltyResult> ProcessHit(EFClientKill hit) { var results = new List <DetectionPenaltyResult>(); 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[] { new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Any, } }); } LastWeapon = hit.Weapon; HitLocationCount[hit.HitLoc].Count++; HitCount++; if (hit.IsKill) { Kills++; } #region SNAP if (hit.AnglesList.Count == MIN_ANGLE_COUNT) { if (lastHit == null) { lastHit = hit; } bool areAnglesInvalid = hit.AnglesList[0].Equals(hit.AnglesList[1]) && hit.AnglesList[3].Equals(hit.AnglesList[4]); if ((lastHit == hit || lastHit.VictimId != hit.VictimId || (hit.TimeOffset - lastHit.TimeOffset) >= 1000) && !areAnglesInvalid) { ClientStats.SnapHitCount++; sessionSnapHits++; var currentSnapDistance = Vector3.SnapDistance(hit.AnglesList[0], hit.AnglesList[1], hit.ViewAngles); double previousAverage = ClientStats.AverageSnapValue; ClientStats.AverageSnapValue = (previousAverage * (ClientStats.SnapHitCount - 1) + currentSnapDistance) / ClientStats.SnapHitCount; double previousSessionAverage = sessionAverageSnapAmount; sessionAverageSnapAmount = (previousSessionAverage * (sessionSnapHits - 1) + currentSnapDistance) / sessionSnapHits; lastHit = hit; //var marginOfError = Thresholds.GetMarginOfError(sessionSnapHits); //var marginOfErrorLifetime = Thresholds.GetMarginOfError(ClientStats.SnapHitCount); if (sessionSnapHits >= Thresholds.MediumSampleMinKills && sessionAverageSnapAmount >= Thresholds.SnapFlagValue /* + marginOfError*/) { results.Add(new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Flag, Value = sessionAverageSnapAmount, HitCount = sessionSnapHits, Type = DetectionType.Snap }); } if (sessionSnapHits >= Thresholds.MediumSampleMinKills && sessionAverageSnapAmount >= Thresholds.SnapBanValue /* + marginOfError*/) { results.Add(new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Ban, Value = sessionAverageSnapAmount, HitCount = sessionSnapHits, Type = DetectionType.Snap }); } // lifetime if (ClientStats.SnapHitCount >= Thresholds.MediumSampleMinKills * 2 && ClientStats.AverageSnapValue >= Thresholds.SnapFlagValue /* + marginOfErrorLifetime*/) { results.Add(new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Flag, Value = sessionAverageSnapAmount, HitCount = ClientStats.SnapHitCount, Type = DetectionType.Snap }); } if (ClientStats.SnapHitCount >= Thresholds.MediumSampleMinKills * 2 && ClientStats.AverageSnapValue >= Thresholds.SnapBanValue /* + marginOfErrorLifetime*/) { results.Add(new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Ban, Value = sessionAverageSnapAmount, HitCount = ClientStats.SnapHitCount, Type = DetectionType.Snap }); } } } #endregion #region VIEWANGLES int totalUsableAngleCount = hit.AnglesList.Count - 1; int angleOffsetIndex = totalUsableAngleCount / 2; if (hit.AnglesList.Count == 5) { double realAgainstPredict = Vector3.ViewAngleDistance(hit.AnglesList[angleOffsetIndex - 1], hit.AnglesList[angleOffsetIndex + 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; int totalHits = ClientStats.HitLocations.Sum(_hit => _hit.HitCount); var weightedLifetimeAverage = ClientStats.HitLocations.Where(_hit => _hit.HitCount > 0) .Sum(_hit => _hit.HitOffsetAverage * _hit.HitCount) / totalHits; if (weightedLifetimeAverage > Thresholds.MaxOffset(totalHits) && hitLoc.HitCount > 100) { results.Add(new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Ban, Value = hitLoc.HitOffsetAverage, HitCount = hitLoc.HitCount, Type = DetectionType.Offset }); } // SESSION var sessionHitLoc = HitLocationCount[hit.HitLoc]; sessionHitLoc.Offset = (sessionHitLoc.Offset * (sessionHitLoc.Count - 1) + realAgainstPredict) / sessionHitLoc.Count; int totalSessionHits = HitLocationCount.Sum(_hit => _hit.Value.Count); var weightedSessionAverage = HitLocationCount.Where(_hit => _hit.Value.Count > 0) .Sum(_hit => _hit.Value.Offset * _hit.Value.Count) / totalSessionHits; AngleDifferenceAverage = weightedSessionAverage; if (weightedSessionAverage > Thresholds.MaxOffset(totalSessionHits) && totalSessionHits >= (Thresholds.MediumSampleMinKills * 2)) { Log.WriteDebug("*** Reached Max Session Average for Angle Difference ***"); Log.WriteDebug($"Session Average = {weightedSessionAverage}"); Log.WriteDebug($"HitCount = {HitCount}"); Log.WriteDebug($"ID = {hit.AttackerId}"); results.Add(new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Ban, Value = weightedSessionAverage, HitCount = HitCount, Type = DetectionType.Offset, Location = hitLoc.Location }); } #if DEBUG Log.WriteDebug($"PredictVsReal={realAgainstPredict}"); #endif } #endregion #region STRAIN double currentStrain = Strain.GetStrain(hit.Distance / 0.0254, hit.ViewAngles, Math.Max(50, LastOffset == 0 ? 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) { results.Add(new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Flag, Value = currentStrain, HitCount = HitCount, Type = DetectionType.Strain }); } // ban if (currentStrain > Thresholds.MaxStrainBan && HitCount >= 5) { results.Add(new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Ban, Value = currentStrain, HitCount = HitCount, Type = DetectionType.Strain }); } #endregion #region RECOIL float hitRecoilAverage = 0; 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); mapAverageRecoilAmount = (mapAverageRecoilAmount * (validRecoilHitCount - 1) + hitRecoilAverage) / validRecoilHitCount; if (validRecoilHitCount >= Thresholds.LowSampleMinKills && Kills > Thresholds.LowSampleMinKillsRecoil && mapAverageRecoilAmount == 0) { results.Add(new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Ban, Value = mapAverageRecoilAmount, HitCount = HitCount, Type = DetectionType.Recoil }); } } #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) { 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].Count + HitLocationCount[IW4Info.HitLocation.helmet].Count + HitLocationCount[IW4Info.HitLocation.neck].Count) / (double)HitCount); // calculate maximum bone double currentMaxBoneRatio = (HitLocationCount.Values.Select(v => v.Count / (double)HitCount).Max()); var bone = HitLocationCount.FirstOrDefault(b => b.Value.Count == HitLocationCount.Values.Max(_hit => _hit.Count)).Key; #region HEADSHOT_RATIO // flag on headshot if (currentHeadshotRatio > maxHeadshotLerpValueForFlag) { // ban on headshot if (currentHeadshotRatio > maxHeadshotLerpValueForBan) { results.Add(new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Ban, Value = currentHeadshotRatio, Location = IW4Info.HitLocation.head, HitCount = HitCount, Type = DetectionType.Bone }); } else { results.Add(new DetectionPenaltyResult() { ClientPenalty = EFPenalty.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) { results.Add(new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Ban, Value = currentMaxBoneRatio, Location = bone, HitCount = HitCount, Type = DetectionType.Bone }); } else { results.Add(new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Flag, Value = currentMaxBoneRatio, Location = bone, HitCount = HitCount, Type = DetectionType.Bone }); } } #endregion } #region CHEST_ABDOMEN_RATIO_SESSION int chestHits = HitLocationCount[IW4Info.HitLocation.torso_upper].Count; 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)); // 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].Count / (double)HitLocationCount[IW4Info.HitLocation.torso_lower].Count; if (currentChestAbdomenRatio > chestAbdomenRatioLerpValueForFlag) { if (currentChestAbdomenRatio > chestAbdomenLerpValueForBan && chestHits >= Thresholds.MediumSampleMinKills * 2) { results.Add(new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Ban, Value = currentChestAbdomenRatio, Location = IW4Info.HitLocation.torso_upper, Type = DetectionType.Chest, HitCount = chestHits }); } else { results.Add(new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Flag, Value = currentChestAbdomenRatio, Location = IW4Info.HitLocation.torso_upper, Type = DetectionType.Chest, HitCount = chestHits }); } } } #endregion #endregion var snapshot = new EFACSnapshot() { When = hit.When, ClientId = ClientStats.ClientId, SessionAngleOffset = AngleDifferenceAverage, RecoilOffset = hitRecoilAverage, CurrentSessionLength = (int)(DateTime.UtcNow - ConnectionTime).TotalMinutes, CurrentStrain = currentStrain, CurrentViewAngle = new Vector3(hit.ViewAngles.X, hit.ViewAngles.Y, hit.ViewAngles.Z), Hits = HitCount, Kills = Kills, Deaths = ClientStats.SessionDeaths, //todo[9.1.19]: why does this cause unique failure? HitDestination = new Vector3(hit.DeathOrigin.X, hit.DeathOrigin.Y, hit.DeathOrigin.Z), HitOrigin = new Vector3(hit.KillOrigin.X, hit.KillOrigin.Y, hit.KillOrigin.Z), EloRating = ClientStats.EloRating, HitLocation = hit.HitLoc, LastStrainAngle = new Vector3(Strain.LastAngle.X, Strain.LastAngle.Y, Strain.LastAngle.Z), // this is in "meters" Distance = hit.Distance, SessionScore = ClientStats.SessionScore, HitType = hit.DeathType, SessionSPM = Math.Round(ClientStats.SessionSPM, 0), StrainAngleBetween = Strain.LastDistance, TimeSinceLastEvent = (int)Strain.LastDeltaTime, WeaponId = hit.Weapon, SessionSnapHits = sessionSnapHits, SessionAverageSnapValue = sessionAverageSnapAmount }; snapshot.PredictedViewAngles = hit.AnglesList .Select(_angle => new EFACSnapshotVector3() { Vector = _angle, Snapshot = snapshot }) .ToList(); Tracker.OnChange(snapshot); return(results); }
/// <summary> /// Analyze kill and see if performed by a cheater /// </summary> /// <param name="kill">kill performed by the player</param> /// <returns>true if detection reached thresholds, false otherwise</returns> public DetectionPenaltyResult ProcessKill(EFClientKill kill) { if ((kill.DeathType != IW4Info.MeansOfDeath.MOD_PISTOL_BULLET && kill.DeathType != IW4Info.MeansOfDeath.MOD_RIFLE_BULLET && kill.DeathType != IW4Info.MeansOfDeath.MOD_HEAD_SHOT) || 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 VIEWANGLES double distance = Vector3.Distance(kill.KillOrigin, kill.DeathOrigin); double x = kill.KillOrigin.X + distance * Math.Cos(kill.ViewAngles.X.ToRadians()) * Math.Cos(kill.ViewAngles.Y.ToRadians()); double y = kill.KillOrigin.Y + (distance * Math.Sin(kill.ViewAngles.X.ToRadians()) * Math.Cos(kill.ViewAngles.Y.ToRadians())); double z = kill.KillOrigin.Z + distance * Math.Sin((360.0f - kill.ViewAngles.Y).ToRadians()); var trueVector = Vector3.Subtract(kill.KillOrigin, kill.DeathOrigin); var calculatedVector = Vector3.Subtract(kill.KillOrigin, new Vector3((float)x, (float)y, (float)z)); double angle = trueVector.AngleBetween(calculatedVector); if (kill.AdsPercent > 0.5 && kill.Distance > 3) { var hitLoc = ClientStats.HitLocations .First(hl => hl.Location == kill.HitLoc); float previousAverage = hitLoc.HitOffsetAverage; double newAverage = (previousAverage * (hitLoc.HitCount - 1) + angle) / hitLoc.HitCount; hitLoc.HitOffsetAverage = (float)newAverage; if (double.IsNaN(hitLoc.HitOffsetAverage)) { Log.WriteWarning("[Detection::ProcessKill] HitOffsetAvgerage NaN"); Log.WriteDebug($"{previousAverage}-{hitLoc.HitCount}-{hitLoc}-{newAverage}"); hitLoc.HitOffsetAverage = 0f; } } #endregion #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.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 && 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 = 0, 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 = 0, KillCount = chestKills }); } } } #endregion #endregion return(new DetectionPenaltyResult() { ClientPenalty = Penalty.PenaltyType.Any, RatioAmount = 0 }); }
/// <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, }); }