kick clients with default name or an inuse name

fixed regular expression not being escaped when matching names
fixed reset stats
fixed duplicate kills
This commit is contained in:
RaidMax 2018-02-26 22:24:19 -06:00
parent 442569b339
commit 52cc5a30e6
14 changed files with 190 additions and 76 deletions

View File

@ -405,7 +405,6 @@ copy /Y "$(SolutionDir)_customcallbacks.gsc" "$(SolutionDir)BUILD\userraw\script
copy /Y "$(TargetDir)$(TargetName).exe" "$(SolutionDir)BUILD" copy /Y "$(TargetDir)$(TargetName).exe" "$(SolutionDir)BUILD"
copy /Y "$(TargetDir)IW4MAdmin.exe.config" "$(SolutionDir)BUILD" copy /Y "$(TargetDir)IW4MAdmin.exe.config" "$(SolutionDir)BUILD"
copy /Y "$(ProjectDir)lib\Kayak.dll" "$(SolutionDir)BUILD\lib"
xcopy /Y /I /E "$(SolutionDir)SharedLibrary\$(OutDir)*" "$(SolutionDir)Admin\Lib" xcopy /Y /I /E "$(SolutionDir)SharedLibrary\$(OutDir)*" "$(SolutionDir)Admin\Lib"
xcopy /Y /I /E "$(ProjectDir)webfront\*" "$(SolutionDir)BUILD\Webfront" xcopy /Y /I /E "$(ProjectDir)webfront\*" "$(SolutionDir)BUILD\Webfront"
@ -418,7 +417,8 @@ if $(ConfigurationName) == Release-Stable powershell.exe -file "$(SolutionDir)DE
</PostBuildEvent> </PostBuildEvent>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<PreBuildEvent>xcopy /Y "$(SolutionDir)BUILD\Plugins" "$(TargetDir)Plugins\"</PreBuildEvent> <PreBuildEvent>
</PreBuildEvent>
</PropertyGroup> </PropertyGroup>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.

View File

@ -47,6 +47,19 @@ namespace IW4MAdmin
return false; return false;
} }
if (Players.FirstOrDefault(p => p != null && p.Name == polledPlayer.Name) != null)
{
await this.ExecuteCommandAsync($"clientkick {polledPlayer.ClientNumber} \"Your name is being used by someone else.\"");
return false;
}
if (polledPlayer.Name == "Unknown Soldier" ||
polledPlayer.Name == "UnknownSoldier")
{
await this.ExecuteCommandAsync($"clientkick {polledPlayer.ClientNumber} \"Please change your name using /name.\"");
return false;
}
Logger.WriteDebug($"Client slot #{polledPlayer.ClientNumber} now reserved"); Logger.WriteDebug($"Client slot #{polledPlayer.ClientNumber} now reserved");
try try
@ -65,10 +78,10 @@ namespace IW4MAdmin
else else
{ {
client.Connections += 1; client.Connections += 1;
bool aliasExists = client.AliasLink.Children var existingAlias = client.AliasLink.Children
.FirstOrDefault(a => a.Name == polledPlayer.Name && a.IPAddress == polledPlayer.IPAddress) != null; .FirstOrDefault(a => a.Name == polledPlayer.Name && a.IPAddress == polledPlayer.IPAddress);
if (!aliasExists) if (existingAlias == null)
{ {
Logger.WriteDebug($"Client {polledPlayer} has connected previously under a different ip/name"); Logger.WriteDebug($"Client {polledPlayer} has connected previously under a different ip/name");
client.CurrentAlias = new SharedLibrary.Database.Models.EFAlias() client.CurrentAlias = new SharedLibrary.Database.Models.EFAlias()
@ -82,6 +95,12 @@ namespace IW4MAdmin
await Manager.GetClientService().Update(client); await Manager.GetClientService().Update(client);
} }
else if (existingAlias.Name == polledPlayer.Name)
{
client.CurrentAlias = existingAlias;
await Manager.GetClientService().Update(client);
}
player = client.AsPlayer(); player = client.AsPlayer();
} }
@ -252,7 +271,7 @@ namespace IW4MAdmin
else if (matchingPlayers.Count == 1) else if (matchingPlayers.Count == 1)
{ {
E.Target = matchingPlayers.First(); E.Target = matchingPlayers.First();
E.Data = Regex.Replace(E.Data, $"\"{E.Target.Name}\"", "", RegexOptions.IgnoreCase).Trim(); E.Data = Regex.Replace(E.Data, Regex.Escape($"\"{E.Target.Name}\""), "", RegexOptions.IgnoreCase).Trim();
if ((E.Data.ToLower().Trim() == E.Target.Name.ToLower().Trim() || if ((E.Data.ToLower().Trim() == E.Target.Name.ToLower().Trim() ||
E.Data == String.Empty) && E.Data == String.Empty) &&

View File

@ -9,7 +9,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder> <AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>EventAPI</RootNamespace> <RootNamespace>EventAPI</RootNamespace>
<AssemblyName>EventAPI</AssemblyName> <AssemblyName>EventAPI</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment> <FileAlignment>512</FileAlignment>
<TargetFrameworkProfile /> <TargetFrameworkProfile />
</PropertyGroup> </PropertyGroup>

View File

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using SharedLibrary; using SharedLibrary;
@ -9,61 +8,6 @@ using SharedLibrary.Objects;
namespace EventAPI namespace EventAPI
{ {
class EventsJSON : IPage
{
private struct EventResponse
{
public int eventCount;
public RestEvent Event;
}
public string GetName()
{
return "Events";
}
public string GetPath()
{
return "/api/events";
}
public async Task<HttpResponse> GetPage(System.Collections.Specialized.NameValueCollection querySet, IDictionary<string, string> headers)
{
bool shouldQuery = querySet.Get("status") != null;
EventResponse requestedEvent = new EventResponse();
HttpResponse resp = new HttpResponse();
if (shouldQuery)
{
StringBuilder s = new StringBuilder();
foreach (var S in Events.ActiveServers)
s.Append(String.Format("{0} has {1}/{4} players playing {2} on {3}\n", S.Hostname, S.GetPlayersAsList().Count, Utilities.GetLocalizedGametype(S.Gametype), S.CurrentMap.Name, S.MaxClients));
requestedEvent.Event = new RestEvent(RestEvent.EventType.STATUS, RestEvent.EventVersion.IW4MAdmin, s.ToString(), "Status", "", "");
requestedEvent.eventCount = 1;
}
else if (Events.APIEvents.Count > 0)
{
requestedEvent.Event = Events.APIEvents.Dequeue();
requestedEvent.eventCount = 1;
}
else
{
requestedEvent.eventCount = 0;
}
resp.content = Newtonsoft.Json.JsonConvert.SerializeObject(requestedEvent);
resp.contentType = GetContentType();
resp.additionalHeaders = new Dictionary<string, string>();
return resp;
}
public string GetContentType() => "application/json";
public bool Visible() => false;
}
class Events : IPlugin class Events : IPlugin
{ {
public static Queue<RestEvent> APIEvents { get; private set; } public static Queue<RestEvent> APIEvents { get; private set; }
@ -84,7 +28,6 @@ namespace EventAPI
APIEvents = new Queue<RestEvent>(); APIEvents = new Queue<RestEvent>();
flaggedMessagesText = new List<string>(); flaggedMessagesText = new List<string>();
ActiveServers = new List<Server>(); ActiveServers = new List<Server>();
WebService.PageList.Add(new EventsJSON());
} }
public async Task OnUnloadAsync() public async Task OnUnloadAsync()
@ -107,8 +50,7 @@ namespace EventAPI
if (E.Type == Event.GType.Stop) if (E.Type == Event.GType.Stop)
{ {
// fixme: this will be bad once FTP is working and there can be multiple servers on the same port. ActiveServers.RemoveAll(s => s.GetHashCode() == S.GetHashCode());
ActiveServers.RemoveAll(s => s.GetPort() == S.GetPort());
} }
if (E.Type == Event.GType.Connect) if (E.Type == Event.GType.Connect)

View File

@ -0,0 +1,91 @@
using SharedLibrary.Interfaces;
using StatsPlugin.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace StatsPlugin.Cheat
{
class Detection
{
int Kills;
int AboveThresholdCount;
double AverageKillTime;
Dictionary<IW4Info.HitLocation, int> HitLocationCount;
DateTime LastKill;
ILogger Log;
public Detection(ILogger log)
{
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;
}
/// <summary>
/// Analyze kill and see if performed by a cheater
/// </summary>
/// <param name="kill">kill performed by the player</param>
/// <returns>true if detection reached thresholds, false otherwise</returns>
public bool ProcessKill(EFClientKill kill)
{
if (kill.DeathType != IW4Info.MeansOfDeath.MOD_PISTOL_BULLET && kill.DeathType != IW4Info.MeansOfDeath.MOD_RIFLE_BULLET)
return false;
bool thresholdReached = false;
HitLocationCount[kill.HitLoc]++;
Kills++;
AverageKillTime = (AverageKillTime + (DateTime.UtcNow - LastKill).TotalSeconds) / Kills;
if (Kills > Thresholds.LowSampleMinKills)
{
double marginOfError = Thresholds.GetMarginOfError(Kills);
// determine what the max headshot percentage can be for current number of kills
double lerpAmount = Math.Min(1.0, (Kills - Thresholds.LowSampleMinKills) / (double)(Thresholds.HighSampleMinKills - Thresholds.LowSampleMinKills));
double maxHeadshotLerpValue = Thresholds.Lerp( Thresholds.HeadshotRatioThresholdLowSample, Thresholds.HeadshotRatioThresholdHighSample, lerpAmount);
// determine what the max bone percentage can be for current number of kills
double maxBoneRatioLerpValue = Thresholds.Lerp(Thresholds.BoneRatioThresholdLowSample, Thresholds.BoneRatioThresholdHighSample, lerpAmount);
// calculate headshot ratio
double headshotRatio = ((HitLocationCount[IW4Info.HitLocation.head] + HitLocationCount[IW4Info.HitLocation.helmet]) / (double)Kills) - marginOfError;
// calculate maximum bone
double maximumBoneRatio = (HitLocationCount.Values.Select(v => v / (double)Kills).Max()) - marginOfError;
if (headshotRatio > maxHeadshotLerpValue)
{
AboveThresholdCount++;
Log.WriteDebug("**Maximum Headshot Ratio Reached**");
Log.WriteDebug($"ClientId: {kill.AttackerId}");
Log.WriteDebug($"**Kills: {Kills}");
Log.WriteDebug($"**Ratio {headshotRatio}");
Log.WriteDebug($"**MaxRatio {maxHeadshotLerpValue}");
var sb = new StringBuilder();
foreach (var kvp in HitLocationCount)
sb.Append($"HitLocation: {kvp.Key} Count: {kvp.Value}");
Log.WriteDebug(sb.ToString());
Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}");
thresholdReached = true;
}
else if (maximumBoneRatio > maxBoneRatioLerpValue)
{
Log.WriteDebug("**Maximum Bone Ratio Reached**");
Log.WriteDebug($"ClientId: {kill.AttackerId}");
Log.WriteDebug($"**Kills: {Kills}");
Log.WriteDebug($"**Ratio {maximumBoneRatio}");
Log.WriteDebug($"**MaxRatio {maxBoneRatioLerpValue}");
var sb = new StringBuilder();
foreach (var kvp in HitLocationCount)
sb.Append($"HitLocation: {kvp.Key} Count: {kvp.Value}");
Log.WriteDebug(sb.ToString());
thresholdReached = true;
}
}
return thresholdReached;
}
}
}

View File

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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 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 const double BoneRatioStandardDeviationLowSample = 0.1324612879;
public const double BoneRatioStandardDeviationHighSample = 0.0515753935;
public const double BoneRatioMean = 0.3982907516;
public const int LowSampleMinKills = 15;
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 Lerp(double v1, double v2, double amount)
{
return v1 + (v2 - v1) * amount;
}
}
}

View File

@ -19,7 +19,7 @@ namespace StatsPlugin.Commands
if (E.Origin.ClientNumber >= 0) if (E.Origin.ClientNumber >= 0)
{ {
var svc = new SharedLibrary.Services.GenericRepository<EFClientStatistics>(); var svc = new SharedLibrary.Services.GenericRepository<EFClientStatistics>();
int serverId = E.Origin.GetHashCode(); int serverId = E.Owner.GetHashCode();
var stats = svc.Find(s => s.ClientId == E.Origin.ClientId && s.ServerId == serverId).First(); var stats = svc.Find(s => s.ClientId == E.Origin.ClientId && s.ServerId == serverId).First();
stats.Deaths = 0; stats.Deaths = 0;

View File

@ -1,4 +1,5 @@
using SharedLibrary; using SharedLibrary;
using StatsPlugin.Cheat;
using StatsPlugin.Models; using StatsPlugin.Models;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -8,15 +9,16 @@ using System.Threading.Tasks;
namespace StatsPlugin.Helpers namespace StatsPlugin.Helpers
{ {
public class ServerStats class ServerStats {
{
public Dictionary<int, EFClientStatistics> PlayerStats { get; set; } public Dictionary<int, EFClientStatistics> PlayerStats { get; set; }
public Dictionary<int, Detection> PlayerDetections { get; set; }
public EFServerStatistics ServerStatistics { get; private set; } public EFServerStatistics ServerStatistics { get; private set; }
public EFServer Server { get; private set; } public EFServer Server { get; private set; }
public ServerStats(EFServer sv, EFServerStatistics st) public ServerStats(EFServer sv, EFServerStatistics st)
{ {
PlayerStats = new Dictionary<int, EFClientStatistics>(); PlayerStats = new Dictionary<int, EFClientStatistics>();
PlayerDetections = new Dictionary<int, Detection>();
ServerStatistics = st; ServerStatistics = st;
Server = sv; Server = sv;
} }

View File

@ -123,6 +123,16 @@ namespace StatsPlugin.Helpers
} }
playerStats.Add(pl.ClientNumber, clientStats); playerStats.Add(pl.ClientNumber, 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));
}
return clientStats; return clientStats;
} }
@ -135,6 +145,7 @@ namespace StatsPlugin.Helpers
{ {
int serverId = pl.CurrentServer.GetHashCode(); int serverId = pl.CurrentServer.GetHashCode();
var playerStats = Servers[serverId].PlayerStats; var playerStats = Servers[serverId].PlayerStats;
var detectionStats = Servers[serverId].PlayerDetections;
var serverStats = Servers[serverId].ServerStatistics; var serverStats = Servers[serverId].ServerStatistics;
var statsSvc = ContextThreads[serverId]; var statsSvc = ContextThreads[serverId];
@ -143,6 +154,8 @@ namespace StatsPlugin.Helpers
// remove the client from the stats dictionary as they're leaving // remove the client from the stats dictionary as they're leaving
lock (playerStats) lock (playerStats)
playerStats.Remove(pl.ClientNumber); playerStats.Remove(pl.ClientNumber);
lock (detectionStats)
detectionStats.Remove(pl.ClientNumber);
// sync their stats before they leave // sync their stats before they leave
UpdateStats(clientStats); UpdateStats(clientStats);
@ -163,8 +176,9 @@ namespace StatsPlugin.Helpers
{ {
await AddStandardKill(attacker, victim); await AddStandardKill(attacker, victim);
return;
var statsSvc = ContextThreads[serverId]; var statsSvc = ContextThreads[serverId];
var playerDetection = Servers[serverId].PlayerDetections[attacker.ClientNumber];
var kill = new EFClientKill() var kill = new EFClientKill()
{ {
Active = true, Active = true,
@ -180,6 +194,10 @@ namespace StatsPlugin.Helpers
Weapon = ParseEnum<IW4Info.WeaponName>.Get(weapon, typeof(IW4Info.WeaponName)) Weapon = ParseEnum<IW4Info.WeaponName>.Get(weapon, typeof(IW4Info.WeaponName))
}; };
playerDetection.ProcessKill(kill);
return;
statsSvc.KillStatsSvc.Insert(kill); statsSvc.KillStatsSvc.Insert(kill);
await statsSvc.KillStatsSvc.SaveChangesAsync(); await statsSvc.KillStatsSvc.SaveChangesAsync();
} }

View File

@ -41,7 +41,7 @@ namespace StatsPlugin
await Manager.RemovePlayer(E.Origin); await Manager.RemovePlayer(E.Origin);
break; break;
case Event.GType.Say: case Event.GType.Say:
if (E.Data != string.Empty && E.Data.Trim().Length > 0 && E.Data[0] != '!') if (E.Data != string.Empty && E.Data.Trim().Length > 0 && E.Data.Trim()[0] != '!')
await Manager.AddMessageAsync(E.Origin.ClientId, E.Owner.GetHashCode(), E.Data); await Manager.AddMessageAsync(E.Origin.ClientId, E.Owner.GetHashCode(), E.Data);
break; break;
case Event.GType.MapChange: case Event.GType.MapChange:
@ -69,9 +69,9 @@ namespace StatsPlugin
break; break;
case Event.GType.Kill: case Event.GType.Kill:
string[] killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0]; string[] killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0];
if (killInfo.Length >= 9 && killInfo[0].Contains("ScriptKill")) 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]); await Manager.AddScriptKill(E.Origin, E.Target, S.GetHashCode(), S.CurrentMap.Name, killInfo[7], killInfo[8], killInfo[5], killInfo[6], killInfo[3], killInfo[4]);
else else if (!E.Owner.CustomCallback)
await Manager.AddStandardKill(E.Origin, E.Target); await Manager.AddStandardKill(E.Origin, E.Target);
break; break;
case Event.GType.Death: case Event.GType.Death:

View File

@ -118,6 +118,8 @@
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Cheat\Detection.cs" />
<Compile Include="Cheat\Thresholds.cs" />
<Compile Include="Commands\ResetStats.cs" /> <Compile Include="Commands\ResetStats.cs" />
<Compile Include="Commands\TopStats.cs" /> <Compile Include="Commands\TopStats.cs" />
<Compile Include="Commands\ViewStats.cs" /> <Compile Include="Commands\ViewStats.cs" />

View File

@ -740,7 +740,6 @@ namespace SharedLibrary.Commands
return; return;
} }
E.Data = E.Data.RemoveWords(1);
E.Owner.Reports.Add(new Report(E.Target, E.Origin, E.Data)); E.Owner.Reports.Add(new Report(E.Target, E.Origin, E.Data));
await E.Origin.Tell($"Thank you for your report, an administrator has been notified"); await E.Origin.Tell($"Thank you for your report, an administrator has been notified");

View File

@ -8,12 +8,12 @@ by editing this MSBuild file. In order to learn more about this please visit htt
<WebPublishMethod>FileSystem</WebPublishMethod> <WebPublishMethod>FileSystem</WebPublishMethod>
<PublishProvider>FileSystem</PublishProvider> <PublishProvider>FileSystem</PublishProvider>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration> <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform> <LastUsedPlatform>x86</LastUsedPlatform>
<SiteUrlToLaunchAfterPublish /> <SiteUrlToLaunchAfterPublish />
<LaunchSiteAfterPublish>True</LaunchSiteAfterPublish> <LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
<ExcludeApp_Data>False</ExcludeApp_Data> <ExcludeApp_Data>False</ExcludeApp_Data>
<ProjectGuid>65340d7d-5831-406c-acad-b13ba634bde2</ProjectGuid> <ProjectGuid>65340d7d-5831-406c-acad-b13ba634bde2</ProjectGuid>
<publishUrl>bin\Release\PublishOutput</publishUrl> <publishUrl>C:\Users\User\Desktop\stuff\IW4M-Admin\IW4M-Admin\WebfrontCore\bin\x86\Release\PublishOutput</publishUrl>
<DeleteExistingFiles>True</DeleteExistingFiles> <DeleteExistingFiles>True</DeleteExistingFiles>
<TargetFramework>net452</TargetFramework> <TargetFramework>net452</TargetFramework>
<RuntimeIdentifier>win7-x86</RuntimeIdentifier> <RuntimeIdentifier>win7-x86</RuntimeIdentifier>

View File

@ -63,4 +63,8 @@
</Content> </Content>
</ItemGroup> </ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="xcopy /Y &quot;$(SolutionDir)BUILD\Plugins&quot; &quot;$(TargetDir)Plugins\&quot;" />
</Target>
</Project> </Project>