mirror of
https://github.com/RaidMax/IW4M-Admin.git
synced 2025-06-28 08:00:14 -05:00
migrating Stats to .Net Core 2
moved buildscripts to application added publish profile
This commit is contained in:
332
Plugins/Stats/Cheat/Detection.cs
Normal file
332
Plugins/Stats/Cheat/Detection.cs
Normal file
@ -0,0 +1,332 @@
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Objects;
|
||||
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||
using IW4MAdmin.Plugins.Stats.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
{
|
||||
class Detection
|
||||
{
|
||||
int Kills;
|
||||
int AboveThresholdCount;
|
||||
double AverageKillTime;
|
||||
Dictionary<IW4Info.HitLocation, int> HitLocationCount;
|
||||
EFClientStatistics ClientStats;
|
||||
DateTime LastKill;
|
||||
ILogger Log;
|
||||
|
||||
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);
|
||||
LastKill = DateTime.UtcNow;
|
||||
ClientStats = 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 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;
|
||||
}
|
||||
|
||||
#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.LowSampleMinKills) / (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
|
||||
};
|
||||
}
|
||||
|
||||
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 = stats.HitLocations.Single(hl => hl.Location == IW4Info.HitLocation.torso_upper).HitCount /
|
||||
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 Kills: {totalChestKills}");
|
||||
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());
|
||||
// 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 location in stats.HitLocations)
|
||||
sb.Append($"HitLocation: {location.Location} -> {location.HitCount}\r\n");
|
||||
Log.WriteDebug(sb.ToString());
|
||||
// Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}");
|
||||
|
||||
return new DetectionPenaltyResult()
|
||||
{
|
||||
ClientPenalty = Penalty.PenaltyType.Flag,
|
||||
RatioAmount = currentChestAbdomenRatio,
|
||||
Bone = IW4Info.HitLocation.torso_upper,
|
||||
KillCount = totalChestKills
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new DetectionPenaltyResult()
|
||||
{
|
||||
Bone = IW4Info.HitLocation.none,
|
||||
ClientPenalty = Penalty.PenaltyType.Any
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
17
Plugins/Stats/Cheat/DetectionPenaltyResult.cs
Normal file
17
Plugins/Stats/Cheat/DetectionPenaltyResult.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using SharedLibraryCore.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
{
|
||||
class DetectionPenaltyResult
|
||||
{
|
||||
public Penalty.PenaltyType ClientPenalty { get; set; }
|
||||
public double RatioAmount { get; set; }
|
||||
public IW4Info.HitLocation Bone { get; set; }
|
||||
public int KillCount { get; set; }
|
||||
}
|
||||
}
|
41
Plugins/Stats/Cheat/Thresholds.cs
Normal file
41
Plugins/Stats/Cheat/Thresholds.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
{
|
||||
class Thresholds
|
||||
{
|
||||
public static double HeadshotRatioThresholdLowSample(double deviations) => HeadshotRatioStandardDeviationLowSample * deviations + HeadshotRatioMean;
|
||||
public static double HeadshotRatioThresholdHighSample(double deviations) => HeadshotRatioStandardDeviationHighSample * deviations + HeadshotRatioMean;
|
||||
public const double HeadshotRatioStandardDeviationLowSample = 0.1769994181;
|
||||
public const double HeadshotRatioStandardDeviationHighSample = 0.03924263235;
|
||||
public const double HeadshotRatioMean = 0.222;
|
||||
|
||||
public static double BoneRatioThresholdLowSample(double deviations) => BoneRatioStandardDeviationLowSample * deviations + BoneRatioMean;
|
||||
public static double BoneRatioThresholdHighSample(double deviations) => BoneRatioStandardDeviationHighSample * deviations + BoneRatioMean;
|
||||
public const double BoneRatioStandardDeviationLowSample = 0.1324612879;
|
||||
public const double BoneRatioStandardDeviationHighSample = 0.0515753935;
|
||||
public const double BoneRatioMean = 0.4593110238;
|
||||
|
||||
public static double ChestAbdomenRatioThresholdLowSample(double deviations) => ChestAbdomenStandardDeviationLowSample * deviations + ChestAbdomenRatioMean;
|
||||
public static double ChestAbdomenRatioThresholdHighSample(double deviations) => ChestAbdomenStandardDeviationHighSample * deviations + ChestAbdomenRatioMean;
|
||||
public const double ChestAbdomenStandardDeviationLowSample = 0.2859234644;
|
||||
public const double ChestAbdomenStandardDeviationHighSample = 0.2195212861;
|
||||
public const double ChestAbdomenRatioMean = 0.4435;
|
||||
|
||||
public const int LowSampleMinKills = 15;
|
||||
public const int MediumSampleMinKills = 30;
|
||||
public const int HighSampleMinKills = 100;
|
||||
public const double KillTimeThreshold = 0.2;
|
||||
|
||||
public static double GetMarginOfError(int numKills) => 1.6455 / Math.Sqrt(numKills);
|
||||
|
||||
public static double Lerp(double v1, double v2, double amount)
|
||||
{
|
||||
return v1 + (v2 - v1) * amount;
|
||||
}
|
||||
}
|
||||
}
|
43
Plugins/Stats/Commands/ResetStats.cs
Normal file
43
Plugins/Stats/Commands/ResetStats.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Objects;
|
||||
using IW4MAdmin.Plugins.Stats.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
{
|
||||
public class ResetStats : Command
|
||||
{
|
||||
public ResetStats() : base("resetstats", "reset your stats to factory-new", "rs", Player.Permission.User, false) { }
|
||||
|
||||
public override async Task ExecuteAsync(Event E)
|
||||
{
|
||||
if (E.Origin.ClientNumber >= 0)
|
||||
{
|
||||
var svc = new SharedLibraryCore.Services.GenericRepository<EFClientStatistics>();
|
||||
int serverId = E.Owner.GetHashCode();
|
||||
var stats = svc.Find(s => s.ClientId == E.Origin.ClientId && s.ServerId == serverId).First();
|
||||
|
||||
stats.Deaths = 0;
|
||||
stats.Kills = 0;
|
||||
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 for this server have been reset");
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
await E.Origin.Tell("You must be connected to a server to reset your stats");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
46
Plugins/Stats/Commands/TopStats.cs
Normal file
46
Plugins/Stats/Commands/TopStats.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Objects;
|
||||
using SharedLibraryCore.Services;
|
||||
using IW4MAdmin.Plugins.Stats.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
{
|
||||
class TopStats : Command
|
||||
{
|
||||
public TopStats() : base("topstats", "view the top 5 players on this server", "ts", Player.Permission.User, false) { }
|
||||
|
||||
public override async Task ExecuteAsync(Event E)
|
||||
{
|
||||
var statsSvc = new GenericRepository<EFClientStatistics>();
|
||||
int serverId = E.Owner.GetHashCode();
|
||||
var iqStats = statsSvc.GetQuery(cs => cs.ServerId == serverId);
|
||||
|
||||
var topStats = iqStats.Where(cs => cs.Skill > 100)
|
||||
.Where(cs => cs.TimePlayed >= 3600)
|
||||
.Where(cs => cs.Client.Level != Player.Permission.Banned)
|
||||
.OrderByDescending(cs => cs.Skill)
|
||||
.Take(5)
|
||||
.ToList();
|
||||
|
||||
if (!E.Message.IsBroadcastCommand())
|
||||
{
|
||||
await E.Origin.Tell("^5--Top Players--");
|
||||
|
||||
foreach (var stat in topStats)
|
||||
await E.Origin.Tell($"^3{stat.Client.Name}^7 - ^5{stat.KDR} ^7KDR | ^5{stat.Skill} ^7SKILL");
|
||||
}
|
||||
else
|
||||
{
|
||||
await E.Owner.Broadcast("^5--Top Players--");
|
||||
|
||||
foreach (var stat in topStats)
|
||||
await E.Owner.Broadcast($"^3{stat.Client.Name}^7 - ^5{stat.KDR} ^7KDR | ^5{stat.Skill} ^7SKILL");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
78
Plugins/Stats/Commands/ViewStats.cs
Normal file
78
Plugins/Stats/Commands/ViewStats.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Objects;
|
||||
using SharedLibraryCore.Services;
|
||||
using IW4MAdmin.Plugins.Stats.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
{
|
||||
public class CViewStats : Command
|
||||
{
|
||||
public CViewStats() : base("stats", "view your stats", "xlrstats", Player.Permission.User, false, new CommandArgument[]
|
||||
{
|
||||
new CommandArgument()
|
||||
{
|
||||
Name = "player",
|
||||
Required = false
|
||||
}
|
||||
})
|
||||
{ }
|
||||
|
||||
public override async Task ExecuteAsync(Event E)
|
||||
{
|
||||
if (E.Target?.ClientNumber < 0)
|
||||
{
|
||||
await E.Origin.Tell("The specified player must be ingame");
|
||||
return;
|
||||
}
|
||||
|
||||
if (E.Origin.ClientNumber < 0 && E.Target == null)
|
||||
{
|
||||
await E.Origin.Tell("You must be ingame to view your stats");
|
||||
return;
|
||||
}
|
||||
|
||||
String statLine;
|
||||
EFClientStatistics pStats;
|
||||
|
||||
if (E.Data.Length > 0 && E.Target == null)
|
||||
{
|
||||
await E.Origin.Tell("Cannot find the player you specified");
|
||||
return;
|
||||
}
|
||||
|
||||
var clientStats = new GenericRepository<EFClientStatistics>();
|
||||
int serverId = E.Owner.GetHashCode();
|
||||
|
||||
if (E.Target != null)
|
||||
{
|
||||
pStats = clientStats.Find(c => c.ServerId == serverId && c.ClientId == E.Target.ClientId).First();
|
||||
statLine = String.Format("^5{0} ^7KILLS | ^5{1} ^7DEATHS | ^5{2} ^7KDR | ^5{3} ^7SKILL", pStats.Kills, pStats.Deaths, pStats.KDR, pStats.Skill);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
pStats = pStats = clientStats.Find(c => c.ServerId == serverId && c.ClientId == E.Origin.ClientId).First();
|
||||
statLine = String.Format("^5{0} ^7KILLS | ^5{1} ^7DEATHS | ^5{2} ^7KDR | ^5{3} ^7SKILL", pStats.Kills, pStats.Deaths, pStats.KDR, pStats.Skill);
|
||||
}
|
||||
|
||||
if (E.Message.IsBroadcastCommand())
|
||||
{
|
||||
string name = E.Target == null ? E.Origin.Name : E.Target.Name;
|
||||
await E.Owner.Broadcast($"Stats for ^5{name}^7");
|
||||
await E.Owner.Broadcast(statLine);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
if (E.Target != null)
|
||||
await E.Origin.Tell($"Stats for ^5{E.Target.Name}^7");
|
||||
await E.Origin.Tell(statLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
61
Plugins/Stats/Config/StatsConfiguration.cs
Normal file
61
Plugins/Stats/Config/StatsConfiguration.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Config
|
||||
{
|
||||
class StatsConfiguration : IBaseConfiguration
|
||||
{
|
||||
public bool EnableAntiCheat { get; set; }
|
||||
public List<StreakMessageConfiguration> KillstreakMessages { get; set; }
|
||||
public List<StreakMessageConfiguration> DeathstreakMessages { get; set; }
|
||||
public string Name() => "Stats";
|
||||
public IBaseConfiguration Generate()
|
||||
{
|
||||
var config = new StatsConfiguration();
|
||||
|
||||
Console.Write("Enable server-side anti-cheat? [y/n]: ");
|
||||
config.EnableAntiCheat = (Console.ReadLine().ToLower().FirstOrDefault() as char?) == 'y';
|
||||
|
||||
config.KillstreakMessages = new List<StreakMessageConfiguration>()
|
||||
{
|
||||
new StreakMessageConfiguration(){
|
||||
Count = -1,
|
||||
Message = "Try not to kill yourself anymore"
|
||||
},
|
||||
new StreakMessageConfiguration() {
|
||||
Count = 5,
|
||||
Message = "Great job! You're on a ^55 killstreak!"
|
||||
},
|
||||
new StreakMessageConfiguration()
|
||||
{
|
||||
Count = 10,
|
||||
Message = "Amazing! ^510 kills ^7without dying!"
|
||||
},
|
||||
new StreakMessageConfiguration(){
|
||||
Count = 25,
|
||||
Message = "You better call in that nuke, ^525 killstreak^7!"
|
||||
}
|
||||
};
|
||||
|
||||
config.DeathstreakMessages = new List<StreakMessageConfiguration>()
|
||||
{
|
||||
new StreakMessageConfiguration()
|
||||
{
|
||||
Count = 5,
|
||||
Message = "Pick it up soldier, you've died ^55 times ^7in a row..."
|
||||
},
|
||||
new StreakMessageConfiguration(){
|
||||
Count = 10,
|
||||
Message = "Seriously? ^510 deaths ^7without getting a kill?"
|
||||
},
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
14
Plugins/Stats/Config/StreakMessageConfiguration.cs
Normal file
14
Plugins/Stats/Config/StreakMessageConfiguration.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Config
|
||||
{
|
||||
public class StreakMessageConfiguration
|
||||
{
|
||||
public int Count { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
26
Plugins/Stats/Helpers/Extensions.cs
Normal file
26
Plugins/Stats/Helpers/Extensions.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using SharedLibraryCore.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
static class Extensions
|
||||
{
|
||||
|
||||
public static Vector3 FixIW4Angles(this Vector3 vector)
|
||||
{
|
||||
float X = vector.X >= 0 ? vector.X : 360.0f + vector.X;
|
||||
float Y = vector.Y >= 0 ? vector.Y : 360.0f + vector.Y;
|
||||
float Z = vector.Z >= 0 ? vector.Z : 360.0f + vector.Z;
|
||||
|
||||
return new Vector3(Y, X, Z);
|
||||
}
|
||||
|
||||
public static float ToRadians(this float value) => (float)Math.PI * value / 180.0f;
|
||||
|
||||
public static float ToDegrees(this float value) => value * 180.0f / (float)Math.PI;
|
||||
}
|
||||
}
|
27
Plugins/Stats/Helpers/ServerStats.cs
Normal file
27
Plugins/Stats/Helpers/ServerStats.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using SharedLibraryCore;
|
||||
using IW4MAdmin.Plugins.Stats.Cheat;
|
||||
using IW4MAdmin.Plugins.Stats.Models;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
class ServerStats {
|
||||
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 ConcurrentDictionary<int, EFClientStatistics>();
|
||||
PlayerDetections = new ConcurrentDictionary<int, Detection>();
|
||||
ServerStatistics = st;
|
||||
Server = sv;
|
||||
}
|
||||
}
|
||||
}
|
501
Plugins/Stats/Helpers/StatManager.cs
Normal file
501
Plugins/Stats/Helpers/StatManager.cs
Normal file
@ -0,0 +1,501 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Objects;
|
||||
using SharedLibraryCore.Services;
|
||||
using IW4MAdmin.Plugins.Stats.Models;
|
||||
using SharedLibraryCore.Commands;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using IW4MAdmin.Plugins.Stats.Config;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
public class StatManager
|
||||
{
|
||||
private ConcurrentDictionary<int, ServerStats> Servers;
|
||||
private ConcurrentDictionary<int, ThreadSafeStatsService> ContextThreads;
|
||||
private ILogger Log;
|
||||
private IManager Manager;
|
||||
|
||||
public StatManager(IManager mgr)
|
||||
{
|
||||
Servers = new ConcurrentDictionary<int, ServerStats>();
|
||||
ContextThreads = new ConcurrentDictionary<int, ThreadSafeStatsService>();
|
||||
Log = mgr.GetLogger();
|
||||
Manager = mgr;
|
||||
}
|
||||
|
||||
~StatManager()
|
||||
{
|
||||
Servers.Clear();
|
||||
Log = null;
|
||||
Servers = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a server to the StatManager server pool
|
||||
/// </summary>
|
||||
/// <param name="sv"></param>
|
||||
public void AddServer(Server sv)
|
||||
{
|
||||
try
|
||||
{
|
||||
int serverId = sv.GetHashCode();
|
||||
var statsSvc = new ThreadSafeStatsService();
|
||||
ContextThreads.TryAdd(serverId, statsSvc);
|
||||
|
||||
// get the server from the database if it exists, otherwise create and insert a new one
|
||||
var server = statsSvc.ServerSvc.Find(c => c.ServerId == serverId).FirstOrDefault();
|
||||
if (server == null)
|
||||
{
|
||||
server = new EFServer()
|
||||
{
|
||||
Port = sv.GetPort(),
|
||||
Active = true,
|
||||
ServerId = serverId
|
||||
};
|
||||
|
||||
statsSvc.ServerSvc.Insert(server);
|
||||
}
|
||||
|
||||
// this doesn't need to be async as it's during initialization
|
||||
statsSvc.ServerSvc.SaveChanges();
|
||||
// check to see if the stats have ever been initialized
|
||||
InitializeServerStats(sv);
|
||||
statsSvc.ServerStatsSvc.SaveChanges();
|
||||
|
||||
var serverStats = statsSvc.ServerStatsSvc.Find(c => c.ServerId == serverId).FirstOrDefault();
|
||||
Servers.TryAdd(serverId, new ServerStats(server, serverStats));
|
||||
}
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.WriteError($"Could not add server to ServerStats - {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add Player to the player stats
|
||||
/// </summary>
|
||||
/// <param name="pl">Player to add/retrieve stats for</param>
|
||||
/// <returns>EFClientStatistic of specified player</returns>
|
||||
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];
|
||||
|
||||
// get the client's stats from the database if it exists, otherwise create and attach a new one
|
||||
// if this fails we want to throw an exception
|
||||
var clientStats = statsSvc.ClientStatSvc.Find(c => c.ClientId == pl.ClientId && c.ServerId == serverId).FirstOrDefault();
|
||||
|
||||
if (clientStats == null)
|
||||
{
|
||||
clientStats = new EFClientStatistics()
|
||||
{
|
||||
Active = true,
|
||||
ClientId = pl.ClientId,
|
||||
Deaths = 0,
|
||||
Kills = 0,
|
||||
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;
|
||||
|
||||
if (playerStats.ContainsKey(pl.ClientId))
|
||||
{
|
||||
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;
|
||||
|
||||
if (detectionStats.ContainsKey(pl.ClientId))
|
||||
detectionStats.TryRemove(pl.ClientId, out Cheat.Detection removedValue);
|
||||
|
||||
detectionStats.TryAdd(pl.ClientId, new Cheat.Detection(Log, clientStats));
|
||||
|
||||
return clientStats;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform stat updates for disconnecting client
|
||||
/// </summary>
|
||||
/// <param name="pl">Disconnecting client</param>
|
||||
/// <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.ClientId];
|
||||
// sync their score
|
||||
clientStats.SessionScore = pl.Score;
|
||||
// remove the client from the stats dictionary as they're leaving
|
||||
playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue);
|
||||
detectionStats.TryRemove(pl.ClientId, out Cheat.Detection removedValue2);
|
||||
|
||||
// sync their stats before they leave
|
||||
UpdateStats(clientStats);
|
||||
|
||||
// todo: should this be saved every disconnect?
|
||||
await statsSvc.ClientStatSvc.SaveChangesAsync();
|
||||
// increment the total play time
|
||||
serverStats.TotalPlayTime += (int)(DateTime.UtcNow - pl.LastConnection).TotalSeconds;
|
||||
await statsSvc.ServerStatsSvc.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];
|
||||
|
||||
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)),
|
||||
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 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 ||
|
||||
kill.DeathType == IW4Info.MeansOfDeath.MOD_HEAD_SHOT)
|
||||
{
|
||||
playerStats.HitLocations.Single(hl => hl.Location == kill.HitLoc).HitCount += 1;
|
||||
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 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();
|
||||
EFClientStatistics attackerStats = null;
|
||||
try
|
||||
{
|
||||
attackerStats = Servers[serverId].PlayerStats[attacker.ClientId];
|
||||
}
|
||||
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
Log.WriteError($"[Stats::AddStandardKill] kill attacker ClientId is invalid {attacker.ClientId}-{attacker}");
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// show encouragement/discouragement
|
||||
string streakMessage = (attackerStats.ClientId != victimStats.ClientId) ?
|
||||
StreakMessage.MessageOnStreak(attackerStats.KillStreak, attackerStats.DeathStreak) :
|
||||
StreakMessage.MessageOnStreak(-1, -1);
|
||||
|
||||
if (streakMessage != string.Empty)
|
||||
await attacker.Tell(streakMessage);
|
||||
|
||||
// todo: do we want to save this immediately?
|
||||
var statsSvc = ContextThreads[serverId];
|
||||
statsSvc.ClientStatSvc.SaveChanges();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs the incrementation of kills and deaths for client statistics
|
||||
/// </summary>
|
||||
/// <param name="attackerStats">Stats of the attacker</param>
|
||||
/// <param name="victimStats">Stats of the victim</param>
|
||||
public void CalculateKill(EFClientStatistics attackerStats, EFClientStatistics victimStats)
|
||||
{
|
||||
bool suicide = attackerStats.ClientId == victimStats.ClientId;
|
||||
|
||||
// only update their kills if they didn't kill themselves
|
||||
if (!suicide)
|
||||
{
|
||||
attackerStats.Kills += 1;
|
||||
attackerStats.SessionKills += 1;
|
||||
attackerStats.KillStreak += 1;
|
||||
attackerStats.DeathStreak = 0;
|
||||
}
|
||||
|
||||
victimStats.Deaths += 1;
|
||||
victimStats.SessionDeaths += 1;
|
||||
victimStats.DeathStreak += 1;
|
||||
victimStats.KillStreak = 0;
|
||||
|
||||
// process the attacker's stats after the kills
|
||||
UpdateStats(attackerStats);
|
||||
|
||||
// update after calculation
|
||||
attackerStats.TimePlayed += (int)(DateTime.UtcNow - attackerStats.LastActive).TotalSeconds;
|
||||
victimStats.TimePlayed += (int)(DateTime.UtcNow - victimStats.LastActive).TotalSeconds;
|
||||
attackerStats.LastActive = DateTime.UtcNow;
|
||||
victimStats.LastActive = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the client stats (skill etc)
|
||||
/// </summary>
|
||||
/// <param name="clientStats">Client statistics</param>
|
||||
/// <returns></returns>
|
||||
private EFClientStatistics UpdateStats(EFClientStatistics clientStats)
|
||||
{
|
||||
double timeSinceLastCalc = (DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0;
|
||||
double timeSinceLastActive = (DateTime.UtcNow - clientStats.LastActive).TotalSeconds / 60.0;
|
||||
|
||||
// prevent NaN or inactive time lowering SPM
|
||||
if (timeSinceLastCalc == 0 || timeSinceLastActive > 3 || clientStats.SPM < 1)
|
||||
return clientStats;
|
||||
|
||||
// calculate the players Score Per Minute for the current session
|
||||
int currentScore = clientStats.SessionScore;
|
||||
double killSPM = currentScore / (timeSinceLastCalc * 60.0);
|
||||
|
||||
// calculate how much the KDR should weigh
|
||||
// 1.637 is a Eddie-Generated number that weights the KDR nicely
|
||||
double KDRWeight = Math.Round(Math.Pow(clientStats.KDR, 1.637 / Math.E), 3);
|
||||
|
||||
// if no SPM, weight is 1 else the weight ishe current session's spm / lifetime average score per minute
|
||||
double SPMWeightAgainstAverage = (clientStats.SPM < 1) ? 1 : killSPM / clientStats.SPM;
|
||||
|
||||
// calculate the weight of the new play time against last 10 hours of gameplay
|
||||
int totalPlayTime = (clientStats.TimePlayed == 0) ?
|
||||
(int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds :
|
||||
clientStats.TimePlayed + (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds;
|
||||
|
||||
double SPMAgainstPlayWeight = timeSinceLastCalc / Math.Min(600, (totalPlayTime / 60.0));
|
||||
|
||||
// calculate the new weight against average times the weight against play time
|
||||
clientStats.SPM = (killSPM * SPMAgainstPlayWeight) + (clientStats.SPM * (1 - SPMAgainstPlayWeight));
|
||||
// fixme: how does this happen?
|
||||
if (clientStats.SPM == double.NaN)
|
||||
clientStats.SPM = 0;
|
||||
clientStats.SPM = Math.Round(clientStats.SPM, 3);
|
||||
clientStats.Skill = Math.Round((clientStats.SPM * KDRWeight), 3);
|
||||
|
||||
clientStats.LastStatCalculation = DateTime.UtcNow;
|
||||
clientStats.LastScore = currentScore;
|
||||
|
||||
return clientStats;
|
||||
}
|
||||
|
||||
public void InitializeServerStats(Server sv)
|
||||
{
|
||||
int serverId = sv.GetHashCode();
|
||||
var statsSvc = ContextThreads[serverId];
|
||||
|
||||
var serverStats = statsSvc.ServerStatsSvc.Find(s => s.ServerId == serverId).FirstOrDefault();
|
||||
if (serverStats == null)
|
||||
{
|
||||
Log.WriteDebug($"Initializing server stats for {sv}");
|
||||
// server stats have never been generated before
|
||||
serverStats = new EFServerStatistics()
|
||||
{
|
||||
Active = true,
|
||||
ServerId = serverId,
|
||||
TotalKills = 0,
|
||||
TotalPlayTime = 0,
|
||||
};
|
||||
|
||||
var ieClientStats = statsSvc.ClientStatSvc.Find(cs => cs.ServerId == serverId);
|
||||
|
||||
// set these incase they've we've imported settings
|
||||
serverStats.TotalKills = ieClientStats.Sum(cs => cs.Kills);
|
||||
serverStats.TotalPlayTime = Manager.GetClientService().GetTotalPlayTime().Result;
|
||||
|
||||
statsSvc.ServerStatsSvc.Insert(serverStats);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if (clientId < 1)
|
||||
return;
|
||||
|
||||
var messageSvc = ContextThreads[serverId].MessageSvc;
|
||||
messageSvc.Insert(new EFClientMessage()
|
||||
{
|
||||
Active = true,
|
||||
ClientId = clientId,
|
||||
Message = message,
|
||||
ServerId = serverId,
|
||||
TimeSent = DateTime.UtcNow
|
||||
});
|
||||
await messageSvc.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task Sync(Server sv)
|
||||
{
|
||||
int serverId = sv.GetHashCode();
|
||||
var statsSvc = ContextThreads[serverId];
|
||||
|
||||
Log.WriteDebug("Syncing server stats");
|
||||
await statsSvc.ServerStatsSvc.SaveChangesAsync();
|
||||
|
||||
Log.WriteDebug("Syncing client stats");
|
||||
await statsSvc.ClientStatSvc.SaveChangesAsync();
|
||||
|
||||
Log.WriteDebug("Syncing kill stats");
|
||||
await statsSvc.KillStatsSvc.SaveChangesAsync();
|
||||
|
||||
Log.WriteDebug("Syncing servers");
|
||||
await statsSvc.ServerSvc.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
29
Plugins/Stats/Helpers/StreakMessage.cs
Normal file
29
Plugins/Stats/Helpers/StreakMessage.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
public class StreakMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a message from the configuration encouraging or discouraging clients
|
||||
/// </summary>
|
||||
/// <param name="killStreak">how many kills the client has without dying</param>
|
||||
/// <param name="deathStreak">how many deaths the client has without getting a kill</param>
|
||||
/// <returns>message to send to the client</returns>
|
||||
public static string MessageOnStreak(int killStreak, int deathStreak)
|
||||
{
|
||||
var killstreakMessage = Plugin.Config.Configuration().KillstreakMessages;
|
||||
var deathstreakMessage = Plugin.Config.Configuration().DeathstreakMessages;
|
||||
|
||||
string message = killstreakMessage.FirstOrDefault(m => m.Count == killStreak)?.Message;
|
||||
message = message ?? deathstreakMessage.FirstOrDefault(m => m.Count == deathStreak)?.Message;
|
||||
return message ?? "";
|
||||
}
|
||||
}
|
||||
}
|
28
Plugins/Stats/Helpers/ThreadSafeStatsService.cs
Normal file
28
Plugins/Stats/Helpers/ThreadSafeStatsService.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using SharedLibraryCore.Services;
|
||||
using IW4MAdmin.Plugins.Stats.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.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; }
|
||||
public GenericRepository<EFServerStatistics> ServerStatsSvc { get; private set; }
|
||||
public GenericRepository<EFClientMessage> MessageSvc { get; private set; }
|
||||
|
||||
public ThreadSafeStatsService()
|
||||
{
|
||||
ClientStatSvc = new GenericRepository<EFClientStatistics>();
|
||||
ServerSvc = new GenericRepository<EFServer>();
|
||||
KillStatsSvc = new GenericRepository<EFClientKill>();
|
||||
ServerStatsSvc = new GenericRepository<EFServerStatistics>();
|
||||
MessageSvc = new GenericRepository<EFClientMessage>();
|
||||
}
|
||||
}
|
||||
}
|
1374
Plugins/Stats/IW4Info.cs
Normal file
1374
Plugins/Stats/IW4Info.cs
Normal file
File diff suppressed because it is too large
Load Diff
70
Plugins/Stats/MinimapConfig.cs
Normal file
70
Plugins/Stats/MinimapConfig.cs
Normal file
@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats
|
||||
{
|
||||
public class MinimapInfo
|
||||
{
|
||||
public string MapName { get; set; }
|
||||
// distance from the edge of the minimap image
|
||||
// to the "playable" area
|
||||
public int Top { get; set; }
|
||||
public int Bottom { get; set; }
|
||||
public int Left { get; set; }
|
||||
public int Right { get; set; }
|
||||
// maximum coordinate values for the map
|
||||
public int MaxTop { get; set; }
|
||||
public int MaxBottom { get; set; }
|
||||
public int MaxLeft { get; set; }
|
||||
public int MaxRight { get; set; }
|
||||
|
||||
public int Width => MaxLeft - MaxRight;
|
||||
public int Height => MaxTop - MaxBottom;
|
||||
}
|
||||
|
||||
public class MinimapConfig : Serialize<MinimapConfig>
|
||||
{
|
||||
public List<MinimapInfo> MapInfo;
|
||||
|
||||
public static MinimapConfig IW4Minimaps()
|
||||
{
|
||||
return new MinimapConfig()
|
||||
{
|
||||
MapInfo = new List<MinimapInfo>()
|
||||
{
|
||||
new MinimapInfo()
|
||||
{
|
||||
MapName = "mp_terminal",
|
||||
Top = 85,
|
||||
Bottom = 89,
|
||||
Left = 7,
|
||||
Right = 6,
|
||||
MaxTop = 2929,
|
||||
MaxBottom = -513,
|
||||
MaxLeft = 7520,
|
||||
MaxRight = 2447
|
||||
},
|
||||
|
||||
new MinimapInfo()
|
||||
{
|
||||
MapName = "mp_rust",
|
||||
Top = 122,
|
||||
Bottom = 104,
|
||||
Left = 155,
|
||||
Right = 82,
|
||||
MaxRight = -225,
|
||||
MaxLeft = 1809,
|
||||
MaxTop = 1641,
|
||||
MaxBottom = -469
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
42
Plugins/Stats/Models/EFClientKill.cs
Normal file
42
Plugins/Stats/Models/EFClientKill.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using System;
|
||||
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Models
|
||||
{
|
||||
public class EFClientKill : SharedEntity
|
||||
{
|
||||
[Key]
|
||||
public long KillId { get; set; }
|
||||
public int VictimId { get; set; }
|
||||
[ForeignKey("VictimId")]
|
||||
public virtual EFClient Victim { get; set; }
|
||||
public int AttackerId { get; set; }
|
||||
[ForeignKey("AttackerId")]
|
||||
public virtual EFClient Attacker { get; set; }
|
||||
public int ServerId { get; set; }
|
||||
[ForeignKey("ServerId")]
|
||||
public virtual EFServer Server { get; set; }
|
||||
public IW4Info.HitLocation HitLoc { get; set; }
|
||||
public IW4Info.MeansOfDeath DeathType { get; set; }
|
||||
public int Damage { get; set; }
|
||||
public IW4Info.WeaponName Weapon { get; set; }
|
||||
public Vector3 KillOrigin { get; set; }
|
||||
public Vector3 DeathOrigin { get; set; }
|
||||
public Vector3 ViewAngles { get; set; }
|
||||
public DateTime When { get; set; }
|
||||
// http://wiki.modsrepository.com/index.php?title=Call_of_Duty_5:_Gameplay_standards for conversion to meters
|
||||
[NotMapped]
|
||||
public double Distance => Vector3.Distance(KillOrigin, DeathOrigin) * 0.0254;
|
||||
public IW4Info.MapName Map { get; set; }
|
||||
[NotMapped]
|
||||
public long TimeOffset { get; set; }
|
||||
[NotMapped]
|
||||
public bool IsKillstreakKill { get; set; }
|
||||
[NotMapped]
|
||||
public float AdsPercent { get; set; }
|
||||
}
|
||||
}
|
25
Plugins/Stats/Models/EFClientMessage.cs
Normal file
25
Plugins/Stats/Models/EFClientMessage.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using SharedLibraryCore.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 IW4MAdmin.Plugins.Stats.Models
|
||||
{
|
||||
public class EFClientMessage : SharedEntity
|
||||
{
|
||||
[Key]
|
||||
public long MessageId { get; set; }
|
||||
public int ServerId { get; set; }
|
||||
[ForeignKey("ServerId")]
|
||||
public virtual EFServer Server { get; set; }
|
||||
public int ClientId { get; set; }
|
||||
[ForeignKey("ClientId")]
|
||||
public virtual EFClient Client { get; set; }
|
||||
public string Message { get; set; }
|
||||
public DateTime TimeSent { get; set; }
|
||||
}
|
||||
}
|
64
Plugins/Stats/Models/EFClientStatistics.cs
Normal file
64
Plugins/Stats/Models/EFClientStatistics.cs
Normal file
@ -0,0 +1,64 @@
|
||||
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;
|
||||
|
||||
using SharedLibraryCore.Database.Models;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Models
|
||||
{
|
||||
public class EFClientStatistics : SharedEntity
|
||||
{
|
||||
//[Key, Column(Order = 0)]
|
||||
public int ClientId { get; set; }
|
||||
[ForeignKey("ClientId")]
|
||||
public virtual EFClient Client { get; set; }
|
||||
//[Key, Column(Order = 1)]
|
||||
public int ServerId { get; set; }
|
||||
[ForeignKey("ServerId")]
|
||||
public virtual EFServer Server { get; set; }
|
||||
[Required]
|
||||
public int Kills { get; set; }
|
||||
[Required]
|
||||
public int Deaths { get; set; }
|
||||
|
||||
public virtual ICollection<EFHitLocationCount> HitLocations { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public double KDR
|
||||
{
|
||||
get => Deaths == 0 ? Kills : Math.Round(Kills / (double)Deaths, 2);
|
||||
}
|
||||
[Required]
|
||||
public double SPM { get; set; }
|
||||
[Required]
|
||||
public double Skill { get; set; }
|
||||
[Required]
|
||||
public int TimePlayed { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public float AverageHitOffset
|
||||
{
|
||||
get => (float)Math.Round(HitLocations.Sum(c => c.HitOffsetAverage) / Math.Max(1, HitLocations.Where(c => c.HitOffsetAverage > 0).Count()), 4);
|
||||
}
|
||||
[NotMapped]
|
||||
public int SessionKills { get; set; }
|
||||
[NotMapped]
|
||||
public int SessionDeaths { get; set; }
|
||||
[NotMapped]
|
||||
public int KillStreak { get; set; }
|
||||
[NotMapped]
|
||||
public int DeathStreak { get; set; }
|
||||
[NotMapped]
|
||||
public DateTime LastStatCalculation { get; set; }
|
||||
[NotMapped]
|
||||
public int LastScore { get; set; }
|
||||
[NotMapped]
|
||||
public DateTime LastActive { get; set; }
|
||||
[NotMapped]
|
||||
public int SessionScore { get; set; }
|
||||
}
|
||||
}
|
17
Plugins/Stats/Models/EFHitLocationCount.cs
Normal file
17
Plugins/Stats/Models/EFHitLocationCount.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Models
|
||||
{
|
||||
public class EFHitLocationCount : SharedEntity
|
||||
{
|
||||
[Key]
|
||||
public int HitLocationCountId { get; set; }
|
||||
[Required]
|
||||
public IW4Info.HitLocation Location { get; set; }
|
||||
[Required]
|
||||
public int HitCount { get; set; }
|
||||
[Required]
|
||||
public float HitOffsetAverage { get; set; }
|
||||
}
|
||||
}
|
16
Plugins/Stats/Models/EFServer.cs
Normal file
16
Plugins/Stats/Models/EFServer.cs
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Models
|
||||
{
|
||||
public class EFServer : SharedEntity
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.None)]
|
||||
public int ServerId { get; set; }
|
||||
[Required]
|
||||
public int Port { get; set; }
|
||||
}
|
||||
}
|
17
Plugins/Stats/Models/EFServerStatistics.cs
Normal file
17
Plugins/Stats/Models/EFServerStatistics.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Models
|
||||
{
|
||||
public class EFServerStatistics : SharedEntity
|
||||
{
|
||||
[Key]
|
||||
public int StatisticId { get; set; }
|
||||
public int ServerId { get; set; }
|
||||
[ForeignKey("ServerId")]
|
||||
public virtual EFServer Server { get; set; }
|
||||
public long TotalKills { get; set; }
|
||||
public long TotalPlayTime { get; set; }
|
||||
}
|
||||
}
|
16
Plugins/Stats/Models/ModelConfiguration.cs
Normal file
16
Plugins/Stats/Models/ModelConfiguration.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using IW4MAdmin.Plugins.Stats.Models;
|
||||
|
||||
namespace Stats.Models
|
||||
{
|
||||
public class ModelConfiguration : IModelConfiguration
|
||||
{
|
||||
public void Configure(ModelBuilder builder)
|
||||
{
|
||||
builder.Entity<EFClientStatistics>()
|
||||
.HasKey(cs => new { cs.ClientId, cs.ServerId });
|
||||
}
|
||||
}
|
||||
}
|
264
Plugins/Stats/Plugin.cs
Normal file
264
Plugins/Stats/Plugin.cs
Normal file
@ -0,0 +1,264 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Reflection;
|
||||
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Services;
|
||||
using IW4MAdmin.Plugins.Stats.Config;
|
||||
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||
using IW4MAdmin.Plugins.Stats.Models;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats
|
||||
{
|
||||
class Plugin : IPlugin
|
||||
{
|
||||
public string Name => "Simple Stats";
|
||||
|
||||
public float Version => Assembly.GetExecutingAssembly().GetName().Version.Major + Assembly.GetExecutingAssembly().GetName().Version.Minor / 10.0f;
|
||||
|
||||
public string Author => "RaidMax";
|
||||
|
||||
public static StatManager Manager { get; private set; }
|
||||
private IManager ServerManager;
|
||||
public static BaseConfigurationHandler<StatsConfiguration> Config { get; private set; }
|
||||
|
||||
public async Task OnEventAsync(Event E, Server S)
|
||||
{
|
||||
switch (E.Type)
|
||||
{
|
||||
case Event.GType.Start:
|
||||
Manager.AddServer(S);
|
||||
break;
|
||||
case Event.GType.Stop:
|
||||
break;
|
||||
case Event.GType.Connect:
|
||||
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.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:
|
||||
break;
|
||||
case Event.GType.Broadcast:
|
||||
break;
|
||||
case Event.GType.Tell:
|
||||
break;
|
||||
case Event.GType.Kick:
|
||||
break;
|
||||
case Event.GType.Ban:
|
||||
break;
|
||||
case Event.GType.Remote:
|
||||
break;
|
||||
case Event.GType.Unknown:
|
||||
break;
|
||||
case Event.GType.Report:
|
||||
break;
|
||||
case Event.GType.Flag:
|
||||
break;
|
||||
case Event.GType.Script:
|
||||
break;
|
||||
case Event.GType.Kill:
|
||||
string[] killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0];
|
||||
if (killInfo.Length >= 9 && killInfo[0].Contains("ScriptKill") && E.Owner.CustomCallback)
|
||||
await Manager.AddScriptKill(E.Origin, E.Target, S.GetHashCode(), S.CurrentMap.Name, killInfo[7], killInfo[8],
|
||||
killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12]);
|
||||
else if (!E.Owner.CustomCallback)
|
||||
await Manager.AddStandardKill(E.Origin, E.Target);
|
||||
break;
|
||||
case Event.GType.Death:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OnLoadAsync(IManager manager)
|
||||
{
|
||||
// load custom configuration
|
||||
Config = new BaseConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
|
||||
if (Config.Configuration() == null)
|
||||
{
|
||||
Config.Set((StatsConfiguration)new StatsConfiguration().Generate());
|
||||
await Config.Save();
|
||||
}
|
||||
|
||||
// meta data info
|
||||
async Task<List<ProfileMeta>> getStats(int clientId)
|
||||
{
|
||||
var statsSvc = new GenericRepository<EFClientStatistics>();
|
||||
var clientStats = await statsSvc.FindAsync(c => c.ClientId == clientId);
|
||||
|
||||
int kills = clientStats.Sum(c => c.Kills);
|
||||
int deaths = clientStats.Sum(c => c.Deaths);
|
||||
double kdr = Math.Round(kills / (double)deaths, 2);
|
||||
double skill = Math.Round(clientStats.Sum(c => c.Skill) / clientStats.Count, 2);
|
||||
double spm = Math.Round(clientStats.Sum(c => c.SPM), 1);
|
||||
|
||||
return new List<ProfileMeta>()
|
||||
{
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = "Kills",
|
||||
Value = kills
|
||||
},
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = "Deaths",
|
||||
Value = deaths
|
||||
},
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = "KDR",
|
||||
Value = kdr
|
||||
},
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = "Skill",
|
||||
Value = skill
|
||||
},
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = "Score Per Minute",
|
||||
Value = spm
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async Task<List<ProfileMeta>> getAnticheatInfo(int clientId)
|
||||
{
|
||||
var statsSvc = new GenericRepository<EFClientStatistics>();
|
||||
var clientStats = await statsSvc.FindAsync(c => c.ClientId == clientId);
|
||||
|
||||
double headRatio = 0;
|
||||
double chestRatio = 0;
|
||||
double abdomenRatio = 0;
|
||||
double chestAbdomenRatio = 0;
|
||||
double hitOffsetAverage = 0;
|
||||
|
||||
if (clientStats.Where(cs => cs.HitLocations.Count > 0).FirstOrDefault() != null)
|
||||
{
|
||||
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);
|
||||
|
||||
headRatio = Math.Round(clientStats.Where(c => c.HitLocations.Count > 0).Sum(cs => cs.HitLocations.First(hl => hl.Location == IW4Info.HitLocation.head).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);
|
||||
|
||||
hitOffsetAverage = clientStats.Sum(c => c.AverageHitOffset) / Math.Max(1, clientStats.Where(c => c.AverageHitOffset > 0).Count());
|
||||
}
|
||||
|
||||
return new List<ProfileMeta>()
|
||||
{
|
||||
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
|
||||
},
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = "Headshot Ratio",
|
||||
Value = headRatio,
|
||||
Sensitive = true
|
||||
},
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = "Hit Offset Average",
|
||||
Value = $"{Math.Round(((float)hitOffsetAverage).ToDegrees(), 4)}°",
|
||||
Sensitive = true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async Task<List<ProfileMeta>> getMessages(int clientId)
|
||||
{
|
||||
var messageSvc = new GenericRepository<EFClientMessage>();
|
||||
var messages = await messageSvc.FindAsync(m => m.ClientId == clientId);
|
||||
var messageMeta = messages.Select(m => new ProfileMeta()
|
||||
{
|
||||
Key = "EventMessage",
|
||||
Value = m.Message,
|
||||
When = m.TimeSent
|
||||
}).ToList();
|
||||
messageMeta.Add(new ProfileMeta()
|
||||
{
|
||||
Key = "Messages",
|
||||
Value = messages.Count
|
||||
});
|
||||
|
||||
return messageMeta;
|
||||
}
|
||||
|
||||
MetaService.AddMeta(getStats);
|
||||
|
||||
if (Config.Configuration().EnableAntiCheat)
|
||||
{
|
||||
MetaService.AddMeta(getAnticheatInfo);
|
||||
}
|
||||
|
||||
MetaService.AddMeta(getMessages);
|
||||
|
||||
string totalKills()
|
||||
{
|
||||
var serverStats = new GenericRepository<EFServerStatistics>();
|
||||
return serverStats.Find(s => s.Active)
|
||||
.Sum(c => c.TotalKills).ToString("#,##0");
|
||||
}
|
||||
|
||||
string totalPlayTime()
|
||||
{
|
||||
var serverStats = new GenericRepository<EFServerStatistics>();
|
||||
return Math.Ceiling((serverStats.GetQuery(s => s.Active)
|
||||
.Sum(c => c.TotalPlayTime) / 3600.0)).ToString("#,##0");
|
||||
}
|
||||
|
||||
manager.GetMessageTokens().Add(new MessageToken("TOTALKILLS", totalKills));
|
||||
manager.GetMessageTokens().Add(new MessageToken("TOTALPLAYTIME", totalPlayTime));
|
||||
|
||||
ServerManager = manager;
|
||||
|
||||
Manager = new StatManager(manager);
|
||||
}
|
||||
|
||||
public Task OnTickAsync(Server S) => Utilities.CompletedTask;
|
||||
|
||||
public async Task OnUnloadAsync()
|
||||
{
|
||||
foreach (var sv in ServerManager.GetServers())
|
||||
await Manager.Sync(sv);
|
||||
}
|
||||
}
|
||||
}
|
24
Plugins/Stats/Stats.csproj
Normal file
24
Plugins/Stats/Stats.csproj
Normal file
@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<ApplicationIcon />
|
||||
<StartupObject />
|
||||
<PackageId>RaidMax.IW4MAdmin.Plugins.Stats</PackageId>
|
||||
<Authors>RaidMax</Authors>
|
||||
<Company>Forever None</Company>
|
||||
<Product>Client Statistics</Product>
|
||||
<Description>Client Statistics Plugin for IW4MAdmin</Description>
|
||||
<Copyright>2018</Copyright>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
<Exec Command="copy "$(TargetPath)" $(SolutionDir)BUILD\Plugins"" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
Reference in New Issue
Block a user