mirror of
https://github.com/RaidMax/IW4M-Admin.git
synced 2025-06-11 15:52:25 -05:00
the meats
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
using SharedLibrary.Interfaces;
|
||||
using SharedLibrary.Objects;
|
||||
using StatsPlugin.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -30,62 +31,276 @@ namespace StatsPlugin.Cheat
|
||||
/// </summary>
|
||||
/// <param name="kill">kill performed by the player</param>
|
||||
/// <returns>true if detection reached thresholds, false otherwise</returns>
|
||||
public bool ProcessKill(EFClientKill kill)
|
||||
public DetectionPenaltyResult ProcessKill(EFClientKill kill)
|
||||
{
|
||||
if (kill.DeathType != IW4Info.MeansOfDeath.MOD_PISTOL_BULLET && kill.DeathType != IW4Info.MeansOfDeath.MOD_RIFLE_BULLET)
|
||||
return false;
|
||||
|
||||
bool thresholdReached = false;
|
||||
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;
|
||||
|
||||
if (Kills > Thresholds.LowSampleMinKills)
|
||||
#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 maxHeadshotLerpValue = Thresholds.Lerp( Thresholds.HeadshotRatioThresholdLowSample, Thresholds.HeadshotRatioThresholdHighSample, lerpAmount);
|
||||
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 maxBoneRatioLerpValue = Thresholds.Lerp(Thresholds.BoneRatioThresholdLowSample, Thresholds.BoneRatioThresholdHighSample, lerpAmount);
|
||||
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 headshotRatio = ((HitLocationCount[IW4Info.HitLocation.head] + HitLocationCount[IW4Info.HitLocation.helmet]) / (double)Kills) - marginOfError;
|
||||
double currentHeadshotRatio = ((HitLocationCount[IW4Info.HitLocation.head] + HitLocationCount[IW4Info.HitLocation.helmet]) / (double)Kills);
|
||||
// calculate maximum bone
|
||||
double maximumBoneRatio = (HitLocationCount.Values.Select(v => v / (double)Kills).Max()) - marginOfError;
|
||||
double currentMaxBoneRatio = (HitLocationCount.Values.Select(v => v / (double)Kills).Max());
|
||||
|
||||
if (headshotRatio > maxHeadshotLerpValue)
|
||||
var bone = HitLocationCount.FirstOrDefault(b => b.Value == HitLocationCount.Values.Max()).Key;
|
||||
#region HEADSHOT_RATIO
|
||||
// flag on headshot
|
||||
if (currentHeadshotRatio > maxHeadshotLerpValueForFlag)
|
||||
{
|
||||
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;
|
||||
// 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
|
||||
|
||||
else if (maximumBoneRatio > maxBoneRatioLerpValue)
|
||||
#region BONE_RATIO
|
||||
// flag on bone ratio
|
||||
else if (currentMaxBoneRatio > maxBoneRatioLerpValueForFlag)
|
||||
{
|
||||
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;
|
||||
// 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 thresholdReached;
|
||||
return new DetectionPenaltyResult()
|
||||
{
|
||||
Bone = IW4Info.HitLocation.none,
|
||||
ClientPenalty = Penalty.PenaltyType.Any
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,26 +8,30 @@ namespace StatsPlugin.Cheat
|
||||
{
|
||||
class Thresholds
|
||||
{
|
||||
private const double Deviations = 3.33;
|
||||
|
||||
public const double HeadshotRatioThresholdLowSample = HeadshotRatioStandardDeviationLowSample * Deviations + HeadshotRatioMean;
|
||||
public const double HeadshotRatioThresholdHighSample = HeadshotRatioStandardDeviationHighSample * Deviations + HeadshotRatioMean;
|
||||
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.09587712258;
|
||||
public const double HeadshotRatioMean = 0.222;
|
||||
|
||||
public const double BoneRatioThresholdLowSample = BoneRatioStandardDeviationLowSample * Deviations + BoneRatioMean;
|
||||
public const double BoneRatioThresholdHighSample = BoneRatioStandardDeviationHighSample * Deviations + BoneRatioMean;
|
||||
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.3982907516;
|
||||
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.3925617500;
|
||||
|
||||
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) => 1.645 /(2 * Math.Sqrt(numKills));
|
||||
public static double GetMarginOfError(int numKills) => 0.98 / Math.Sqrt(numKills);
|
||||
|
||||
public static double Lerp(double v1, double v2, double amount)
|
||||
{
|
||||
|
@ -27,6 +27,9 @@ namespace StatsPlugin.Commands
|
||||
stats.SPM = 0;
|
||||
stats.Skill = 0;
|
||||
|
||||
// reset the cached version
|
||||
Plugin.Manager.ResetStats(E.Origin.ClientId, E.Owner.GetHashCode());
|
||||
|
||||
// fixme: this doesn't work properly when another context exists
|
||||
await svc.SaveChangesAsync();
|
||||
await E.Origin.Tell("Your stats have been reset");
|
||||
|
@ -2,6 +2,7 @@
|
||||
using StatsPlugin.Cheat;
|
||||
using StatsPlugin.Models;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@ -10,15 +11,15 @@ using System.Threading.Tasks;
|
||||
namespace StatsPlugin.Helpers
|
||||
{
|
||||
class ServerStats {
|
||||
public Dictionary<int, EFClientStatistics> PlayerStats { get; set; }
|
||||
public Dictionary<int, Detection> PlayerDetections { get; set; }
|
||||
public ConcurrentDictionary<int, EFClientStatistics> PlayerStats { get; set; }
|
||||
public ConcurrentDictionary<int, Detection> PlayerDetections { get; set; }
|
||||
public EFServerStatistics ServerStatistics { get; private set; }
|
||||
public EFServer Server { get; private set; }
|
||||
|
||||
public ServerStats(EFServer sv, EFServerStatistics st)
|
||||
{
|
||||
PlayerStats = new Dictionary<int, EFClientStatistics>();
|
||||
PlayerDetections = new Dictionary<int, Detection>();
|
||||
PlayerStats = new ConcurrentDictionary<int, EFClientStatistics>();
|
||||
PlayerDetections = new ConcurrentDictionary<int, Detection>();
|
||||
ServerStatistics = st;
|
||||
Server = sv;
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ using SharedLibrary.Interfaces;
|
||||
using SharedLibrary.Objects;
|
||||
using SharedLibrary.Services;
|
||||
using StatsPlugin.Models;
|
||||
using SharedLibrary.Commands;
|
||||
|
||||
namespace StatsPlugin.Helpers
|
||||
{
|
||||
@ -75,7 +76,7 @@ namespace StatsPlugin.Helpers
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.WriteWarning($"Could not add server to ServerStats - {e.Message}");
|
||||
Log.WriteError($"Could not add server to ServerStats - {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,9 +85,17 @@ namespace StatsPlugin.Helpers
|
||||
/// </summary>
|
||||
/// <param name="pl">Player to add/retrieve stats for</param>
|
||||
/// <returns>EFClientStatistic of specified player</returns>
|
||||
public EFClientStatistics AddPlayer(Player pl)
|
||||
public async Task<EFClientStatistics> AddPlayer(Player pl)
|
||||
{
|
||||
Log.WriteInfo($"Adding {pl} to stats");
|
||||
int serverId = pl.CurrentServer.GetHashCode();
|
||||
|
||||
if (!Servers.ContainsKey(serverId))
|
||||
{
|
||||
Log.WriteError($"[Stats::AddPlayer] Server with id {serverId} could not be found");
|
||||
return null;
|
||||
}
|
||||
|
||||
var playerStats = Servers[serverId].PlayerStats;
|
||||
var statsSvc = ContextThreads[serverId];
|
||||
|
||||
@ -105,33 +114,50 @@ namespace StatsPlugin.Helpers
|
||||
ServerId = serverId,
|
||||
Skill = 0.0,
|
||||
SPM = 0.0,
|
||||
HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType<IW4Info.HitLocation>().Select(hl => new EFHitLocationCount()
|
||||
{
|
||||
Active = true,
|
||||
HitCount = 0,
|
||||
Location = hl
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
|
||||
clientStats = statsSvc.ClientStatSvc.Insert(clientStats);
|
||||
await statsSvc.ClientStatSvc.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// migration for previous existing stats
|
||||
else if (clientStats.HitLocations.Count == 0)
|
||||
{
|
||||
clientStats.HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType<IW4Info.HitLocation>().Select(hl => new EFHitLocationCount()
|
||||
{
|
||||
Active = true,
|
||||
HitCount = 0,
|
||||
Location = hl
|
||||
})
|
||||
.ToList();
|
||||
await statsSvc.ClientStatSvc.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// set these on connecting
|
||||
clientStats.LastActive = DateTime.UtcNow;
|
||||
clientStats.LastStatCalculation = DateTime.UtcNow;
|
||||
clientStats.SessionScore = pl.Score;
|
||||
|
||||
lock (playerStats)
|
||||
if (playerStats.ContainsKey(pl.ClientId))
|
||||
{
|
||||
if (playerStats.ContainsKey(pl.ClientNumber))
|
||||
{
|
||||
Log.WriteWarning($"Duplicate clientnumber in stats {pl.ClientId} vs {playerStats[pl.ClientNumber].ClientId}");
|
||||
playerStats.Remove(pl.ClientNumber);
|
||||
}
|
||||
playerStats.Add(pl.ClientNumber, clientStats);
|
||||
Log.WriteWarning($"Duplicate ClientId in stats {pl.ClientId} vs {playerStats[pl.ClientId].ClientId}");
|
||||
playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue);
|
||||
}
|
||||
playerStats.TryAdd(pl.ClientId, clientStats);
|
||||
|
||||
var detectionStats = Servers[serverId].PlayerDetections;
|
||||
lock (detectionStats)
|
||||
{
|
||||
if (detectionStats.ContainsKey(pl.ClientNumber))
|
||||
detectionStats.Remove(pl.ClientNumber);
|
||||
|
||||
detectionStats.Add(pl.ClientNumber, new Cheat.Detection(Log));
|
||||
}
|
||||
if (detectionStats.ContainsKey(pl.ClientId))
|
||||
detectionStats.TryRemove(pl.ClientId, out Cheat.Detection removedValue);
|
||||
|
||||
detectionStats.TryAdd(pl.ClientId, new Cheat.Detection(Log));
|
||||
|
||||
return clientStats;
|
||||
}
|
||||
@ -143,19 +169,27 @@ namespace StatsPlugin.Helpers
|
||||
/// <returns></returns>
|
||||
public async Task RemovePlayer(Player pl)
|
||||
{
|
||||
Log.WriteInfo($"Removing {pl} from stats");
|
||||
|
||||
int serverId = pl.CurrentServer.GetHashCode();
|
||||
var playerStats = Servers[serverId].PlayerStats;
|
||||
var detectionStats = Servers[serverId].PlayerDetections;
|
||||
var serverStats = Servers[serverId].ServerStatistics;
|
||||
var statsSvc = ContextThreads[serverId];
|
||||
|
||||
if (!playerStats.ContainsKey(pl.ClientId))
|
||||
{
|
||||
Log.WriteWarning($"Client disconnecting not in stats {pl}");
|
||||
return;
|
||||
}
|
||||
|
||||
// get individual client's stats
|
||||
var clientStats = playerStats[pl.ClientNumber];
|
||||
var clientStats = playerStats[pl.ClientId];
|
||||
// sync their score
|
||||
clientStats.SessionScore = pl.Score;
|
||||
// remove the client from the stats dictionary as they're leaving
|
||||
lock (playerStats)
|
||||
playerStats.Remove(pl.ClientNumber);
|
||||
lock (detectionStats)
|
||||
detectionStats.Remove(pl.ClientNumber);
|
||||
playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue);
|
||||
detectionStats.TryRemove(pl.ClientId, out Cheat.Detection removedValue2);
|
||||
|
||||
// sync their stats before they leave
|
||||
UpdateStats(clientStats);
|
||||
@ -174,16 +208,7 @@ namespace StatsPlugin.Helpers
|
||||
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()
|
||||
{
|
||||
@ -200,30 +225,88 @@ namespace StatsPlugin.Helpers
|
||||
Weapon = ParseEnum<IW4Info.WeaponName>.Get(weapon, typeof(IW4Info.WeaponName))
|
||||
};
|
||||
|
||||
playerDetection.ProcessKill(kill);
|
||||
if (kill.DeathType == IW4Info.MeansOfDeath.MOD_SUICIDE &&
|
||||
kill.Damage == 10000)
|
||||
{
|
||||
// suicide by switching teams so let's not count it against them
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
await AddStandardKill(attacker, victim);
|
||||
|
||||
statsSvc.KillStatsSvc.Insert(kill);
|
||||
await statsSvc.KillStatsSvc.SaveChangesAsync();
|
||||
var playerDetection = Servers[serverId].PlayerDetections[attacker.ClientId];
|
||||
var playerStats = Servers[serverId].PlayerStats[attacker.ClientId];
|
||||
|
||||
// increment their hit count
|
||||
if (kill.DeathType == IW4Info.MeansOfDeath.MOD_PISTOL_BULLET ||
|
||||
kill.DeathType == IW4Info.MeansOfDeath.MOD_RIFLE_BULLET)
|
||||
{
|
||||
playerStats.HitLocations.Single(hl => hl.Location == kill.HitLoc).HitCount += 1;
|
||||
await statsSvc.ClientStatSvc.SaveChangesAsync();
|
||||
}
|
||||
|
||||
//statsSvc.KillStatsSvc.Insert(kill);
|
||||
//await statsSvc.KillStatsSvc.SaveChangesAsync();
|
||||
|
||||
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 Event(Event.GType.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(playerDetection.ProcessKill(kill));
|
||||
await executePenalty(playerDetection.ProcessTotalRatio(playerStats));
|
||||
}
|
||||
|
||||
public async Task AddStandardKill(Player attacker, Player victim)
|
||||
{
|
||||
int serverId = attacker.CurrentServer.GetHashCode();
|
||||
var attackerStats = Servers[serverId].PlayerStats[attacker.ClientNumber];
|
||||
|
||||
if (victim == null)
|
||||
EFClientStatistics attackerStats = null;
|
||||
try
|
||||
{
|
||||
Log.WriteError($"[AddStandardKill] Victim is null");
|
||||
attackerStats = Servers[serverId].PlayerStats[attacker.ClientId];
|
||||
}
|
||||
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
Log.WriteError($"[Stats::AddStandardKill] kill attacker ClientId is invalid {attacker.ClientId}-{attacker}");
|
||||
return;
|
||||
}
|
||||
|
||||
var victimStats = Servers[serverId].PlayerStats[victim.ClientNumber];
|
||||
EFClientStatistics victimStats = null;
|
||||
try
|
||||
{
|
||||
victimStats = Servers[serverId].PlayerStats[victim.ClientId];
|
||||
}
|
||||
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
Log.WriteError($"[Stats::AddStandardKill] kill victim ClientId is invalid {victim.ClientId}-{victim}");
|
||||
return;
|
||||
}
|
||||
|
||||
// update the total stats
|
||||
Servers[serverId].ServerStatistics.TotalKills += 1;
|
||||
|
||||
attackerStats.SessionScore = attacker.Score;
|
||||
victimStats.SessionScore = victim.Score;
|
||||
|
||||
// calculate for the clients
|
||||
CalculateKill(attackerStats, victimStats);
|
||||
|
||||
@ -292,9 +375,7 @@ namespace StatsPlugin.Helpers
|
||||
return clientStats;
|
||||
|
||||
// calculate the players Score Per Minute for the current session
|
||||
int currentScore = Manager.GetActiveClients()
|
||||
.First(c => c.ClientId == clientStats.ClientId)
|
||||
.Score;
|
||||
int currentScore = clientStats.SessionScore;
|
||||
double killSPM = currentScore / (timeSinceLastCalc * 60.0);
|
||||
|
||||
// calculate how much the KDR should weigh
|
||||
@ -350,6 +431,25 @@ namespace StatsPlugin.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetKillstreaks(int serverId)
|
||||
{
|
||||
var serverStats = Servers[serverId];
|
||||
foreach (var stat in serverStats.PlayerStats.Values)
|
||||
{
|
||||
stat.KillStreak = 0;
|
||||
stat.DeathStreak = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetStats(int clientId, int serverId)
|
||||
{
|
||||
var stats = Servers[serverId].PlayerStats[clientId];
|
||||
stats.Kills = 0;
|
||||
stats.Deaths = 0;
|
||||
stats.SPM = 0;
|
||||
stats.Skill = 0;
|
||||
}
|
||||
|
||||
public async Task AddMessageAsync(int clientId, int serverId, string message)
|
||||
{
|
||||
// the web users can have no account
|
||||
|
@ -10,7 +10,6 @@ namespace StatsPlugin.Helpers
|
||||
{
|
||||
public class ThreadSafeStatsService
|
||||
{
|
||||
|
||||
public GenericRepository<EFClientStatistics> ClientStatSvc { get; private set; }
|
||||
public GenericRepository<EFServer> ServerSvc { get; private set; }
|
||||
public GenericRepository<EFClientKill> KillStatsSvc { get; private set; }
|
||||
|
@ -1358,7 +1358,9 @@ namespace StatsPlugin
|
||||
m40a3_mp,
|
||||
peacekeeper_mp,
|
||||
dragunov_mp,
|
||||
cobra_player_minigun_mp
|
||||
cobra_player_minigun_mp,
|
||||
destructible_car,
|
||||
sentry_minigun_mp
|
||||
}
|
||||
|
||||
public enum MapName
|
||||
|
@ -24,7 +24,9 @@ namespace StatsPlugin.Models
|
||||
public int Kills { get; set; }
|
||||
[Required]
|
||||
public int Deaths { get; set; }
|
||||
[Required]
|
||||
|
||||
public virtual ICollection<EFHitLocationCount> HitLocations { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public double KDR
|
||||
{
|
||||
@ -51,5 +53,7 @@ namespace StatsPlugin.Models
|
||||
public int LastScore { get; set; }
|
||||
[NotMapped]
|
||||
public DateTime LastActive { get; set; }
|
||||
[NotMapped]
|
||||
public int SessionScore { get; set; }
|
||||
}
|
||||
}
|
||||
|
21
Plugins/SimpleStats/Models/EFHitLocationCount.cs
Normal file
21
Plugins/SimpleStats/Models/EFHitLocationCount.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using SharedLibrary.Database.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StatsPlugin.Models
|
||||
{
|
||||
public class EFHitLocationCount : SharedEntity
|
||||
{
|
||||
[Key]
|
||||
public int HitLocationCountId { get; set; }
|
||||
[Required]
|
||||
public IW4Info.HitLocation Location { get; set; }
|
||||
[Required]
|
||||
public int HitCount { get; set; }
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ namespace StatsPlugin
|
||||
|
||||
public string Author => "RaidMax";
|
||||
|
||||
private StatManager Manager;
|
||||
public static StatManager Manager { get; private set; }
|
||||
private IManager ServerManager;
|
||||
|
||||
public async Task OnEventAsync(Event E, Server S)
|
||||
@ -35,19 +35,20 @@ namespace StatsPlugin
|
||||
case Event.GType.Stop:
|
||||
break;
|
||||
case Event.GType.Connect:
|
||||
Manager.AddPlayer(E.Origin);
|
||||
await Manager.AddPlayer(E.Origin);
|
||||
break;
|
||||
case Event.GType.Disconnect:
|
||||
await Manager.RemovePlayer(E.Origin);
|
||||
break;
|
||||
case Event.GType.Say:
|
||||
if (E.Data != string.Empty && E.Data.Trim().Length > 0 && E.Data.Trim()[0] != '!')
|
||||
if (E.Data != string.Empty && E.Data.Trim().Length > 0 && E.Message.Trim()[0] != '!' && E.Origin.ClientId > 1)
|
||||
await Manager.AddMessageAsync(E.Origin.ClientId, E.Owner.GetHashCode(), E.Data);
|
||||
break;
|
||||
case Event.GType.MapChange:
|
||||
Manager.ResetKillstreaks(S.GetHashCode());
|
||||
await Manager.Sync(S);
|
||||
break;
|
||||
case Event.GType.MapEnd:
|
||||
await Manager.Sync(S);
|
||||
break;
|
||||
case Event.GType.Broadcast:
|
||||
break;
|
||||
@ -92,6 +93,25 @@ namespace StatsPlugin
|
||||
double kdr = Math.Round(kills / (double)deaths, 2);
|
||||
double skill = Math.Round(clientStats.Sum(c => c.Skill) / clientStats.Count, 2);
|
||||
|
||||
double chestRatio = 0;
|
||||
double abdomenRatio = 0;
|
||||
double chestAbdomenRatio = 0;
|
||||
|
||||
if (clientStats.FirstOrDefault()?.HitLocations.Count > 0)
|
||||
{
|
||||
chestRatio = Math.Round(clientStats.Where(c => c.HitLocations.Count > 0).Sum(c =>
|
||||
c.HitLocations.First(hl => hl.Location == IW4Info.HitLocation.torso_upper).HitCount) /
|
||||
(double)clientStats.Where(c => c.HitLocations.Count > 0)
|
||||
.Sum(c => c.HitLocations.Where(hl => hl.Location != IW4Info.HitLocation.none).Sum(f => f.HitCount)), 2);
|
||||
|
||||
abdomenRatio = Math.Round(clientStats.Where(c => c.HitLocations.Count > 0).Sum(c =>
|
||||
c.HitLocations.First(hl => hl.Location == IW4Info.HitLocation.torso_lower).HitCount) /
|
||||
(double)clientStats.Where(c => c.HitLocations.Count > 0).Sum(c => c.HitLocations.Where(hl => hl.Location != IW4Info.HitLocation.none).Sum(f => f.HitCount)), 2);
|
||||
|
||||
chestAbdomenRatio = Math.Round(clientStats.Where(c => c.HitLocations.Count > 0).Sum(cs => cs.HitLocations.First(hl => hl.Location == IW4Info.HitLocation.torso_upper).HitCount) /
|
||||
(double)clientStats.Where(c => c.HitLocations.Count > 0).Sum(cs => cs.HitLocations.First(hl => hl.Location == IW4Info.HitLocation.torso_lower).HitCount), 2);
|
||||
}
|
||||
|
||||
return new List<ProfileMeta>()
|
||||
{
|
||||
new ProfileMeta()
|
||||
@ -113,6 +133,24 @@ namespace StatsPlugin
|
||||
{
|
||||
Key = "Skill",
|
||||
Value = skill
|
||||
},
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = "Chest Ratio",
|
||||
Value = chestRatio,
|
||||
Sensitive = true
|
||||
},
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = "Abdomen Ratio",
|
||||
Value = abdomenRatio,
|
||||
Sensitive = true
|
||||
},
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = "Chest To Abdomen Ratio",
|
||||
Value = chestAbdomenRatio,
|
||||
Sensitive = true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -119,6 +119,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Cheat\Detection.cs" />
|
||||
<Compile Include="Cheat\DetectionPenaltyResult.cs" />
|
||||
<Compile Include="Cheat\Thresholds.cs" />
|
||||
<Compile Include="Commands\ResetStats.cs" />
|
||||
<Compile Include="Commands\TopStats.cs" />
|
||||
@ -131,6 +132,7 @@
|
||||
<Compile Include="MinimapConfig.cs" />
|
||||
<Compile Include="Models\EFClientKill.cs" />
|
||||
<Compile Include="Models\EFClientMessage.cs" />
|
||||
<Compile Include="Models\EFHitLocationCount.cs" />
|
||||
<Compile Include="Models\EFServer.cs" />
|
||||
<Compile Include="Models\EFClientStatistics.cs" />
|
||||
<Compile Include="Models\EFServerStatistics.cs" />
|
||||
@ -146,7 +148,6 @@
|
||||
<ProjectReference Include="..\..\SharedLibrary\SharedLibrary.csproj">
|
||||
<Project>{d51eeceb-438a-47da-870f-7d7b41bc24d6}</Project>
|
||||
<Name>SharedLibrary</Name>
|
||||
<Private>False</Private>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
|
Reference in New Issue
Block a user