From f442f251f67787ef12cfc1dc9b092682d7c2bfc9 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Thu, 3 May 2018 00:25:49 -0500 Subject: [PATCH] more stat SPM fixes prevent null say event from executing when exiting adjusted rcon and socket timeout fixed bug with login/setpassword not working after claiming ownership --- Application/Application.csproj | 4 + Application/EventParsers/IW4EventParser.cs | 12 + Application/GameEventHandler.cs | 3 +- Application/Main.cs | 9 +- Application/Misc/VPNCheck.cs | 4 +- Application/RconParsers/IW4RConParser.cs | 19 + Application/Server.cs | 38 +- Plugins/Stats/Cheat/Detection.cs | 97 +++- Plugins/Stats/Cheat/Strain.cs | 47 ++ Plugins/Stats/Helpers/Extensions.cs | 8 + Plugins/Stats/Helpers/StatManager.cs | 81 +++- Plugins/Stats/Models/EFClientKill.cs | 3 + Plugins/Stats/Models/EFClientStatistics.cs | 2 + Plugins/Stats/Models/EFHitLocationCount.cs | 3 + Plugins/Stats/Plugin.cs | 31 +- SharedLibraryCore/Commands/NativeCommands.cs | 3 +- SharedLibraryCore/Event.cs | 13 +- SharedLibraryCore/Helpers/Vector3.cs | 13 + .../20180502195450_Update.Designer.cs | 434 ++++++++++++++++++ .../Migrations/20180502195450_Update.cs | 35 ++ .../DatabaseContextModelSnapshot.cs | 433 +++++++++++++++++ SharedLibraryCore/RCon/Connection.cs | 246 +++++----- SharedLibraryCore/RCon/StaticHelpers.cs | 2 +- SharedLibraryCore/SharedLibraryCore.csproj | 4 + WebfrontCore/Controllers/ConsoleController.cs | 3 +- WebfrontCore/WebfrontCore.csproj | 4 + 26 files changed, 1382 insertions(+), 169 deletions(-) create mode 100644 Plugins/Stats/Cheat/Strain.cs create mode 100644 SharedLibraryCore/Migrations/20180502195450_Update.Designer.cs create mode 100644 SharedLibraryCore/Migrations/20180502195450_Update.cs create mode 100644 SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs diff --git a/Application/Application.csproj b/Application/Application.csproj index 718eb33f2..4b5d83804 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -27,6 +27,10 @@ + + true + + true diff --git a/Application/EventParsers/IW4EventParser.cs b/Application/EventParsers/IW4EventParser.cs index 63c3c97e4..1a206a3ef 100644 --- a/Application/EventParsers/IW4EventParser.cs +++ b/Application/EventParsers/IW4EventParser.cs @@ -54,6 +54,18 @@ namespace IW4MAdmin.Application.EventParsers }; } + if (cleanedEventLine[0] == 'D') + { + return new GameEvent() + { + Type = GameEvent.EventType.Damage, + Data = Regex.Replace(logLine, @"[0-9]+:[0-9]+\ ", "").Trim(), + Origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[5].ConvertLong()), + Target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong()), + Owner = server + }; + } + if (cleanedEventLine.Contains("ExitLevel")) { return new GameEvent() diff --git a/Application/GameEventHandler.cs b/Application/GameEventHandler.cs index 86842bceb..4717bf47d 100644 --- a/Application/GameEventHandler.cs +++ b/Application/GameEventHandler.cs @@ -27,7 +27,8 @@ namespace IW4MAdmin.Application #endif // we need this to keep accurate track of the score if (gameEvent.Type == GameEvent.EventType.Script || - gameEvent.Type == GameEvent.EventType.Kill) + gameEvent.Type == GameEvent.EventType.Kill || + gameEvent.Type == GameEvent.EventType.MapChange) { #if DEBUG Manager.GetLogger().WriteDebug($"Added sensitive event to queue"); diff --git a/Application/Main.cs b/Application/Main.cs index 4c216580e..adc554673 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -117,9 +117,12 @@ namespace IW4MAdmin.Application continue; } - Origin.CurrentServer = ServerManager.Servers[0]; - GameEvent E = new GameEvent(GameEvent.EventType.Say, userInput, Origin, null, ServerManager.Servers[0]); - ServerManager.GetEventHandler().AddEvent(E); + if (userInput?.Length > 0) + { + Origin.CurrentServer = ServerManager.Servers[0]; + GameEvent E = new GameEvent(GameEvent.EventType.Say, userInput, Origin, null, ServerManager.Servers[0]); + ServerManager.GetEventHandler().AddEvent(E); + } Console.Write('>'); } while (ServerManager.Running); diff --git a/Application/Misc/VPNCheck.cs b/Application/Misc/VPNCheck.cs index b01b82a1c..12a5e1bdb 100644 --- a/Application/Misc/VPNCheck.cs +++ b/Application/Misc/VPNCheck.cs @@ -23,11 +23,11 @@ namespace Application.Misc string response = await RequestClient.GetStringAsync($"http://v2.api.iphub.info/ip/{ip}"); var responseJson = JsonConvert.DeserializeObject(response); int blockType = Convert.ToInt32(responseJson["block"]); - if (responseJson.ContainsKey("isp")) + /*if (responseJson.ContainsKey("isp")) { if (responseJson["isp"].ToString() == "TSF-IP-CORE") return true; - } + }*/ return blockType == 1; } } diff --git a/Application/RconParsers/IW4RConParser.cs b/Application/RconParsers/IW4RConParser.cs index b0a6c2cae..904a111cb 100644 --- a/Application/RconParsers/IW4RConParser.cs +++ b/Application/RconParsers/IW4RConParser.cs @@ -80,13 +80,16 @@ namespace Application.RconParsers if (Status.Length < 4) throw new ServerException("Unexpected status response received"); + int validMatches = 0; foreach (String S in Status) { String responseLine = S.Trim(); var regex = Regex.Match(responseLine, StatusRegex, RegexOptions.IgnoreCase); + if (regex.Success) { + validMatches++; int clientNumber = int.Parse(regex.Groups[1].Value); int score = int.Parse(regex.Groups[2].Value); @@ -113,10 +116,26 @@ namespace Application.RconParsers IsBot = ip == 0 }; + if (P.IsBot) + { + P.NetworkId = -(P.ClientNumber + 1); + P.IPAddress = P.ClientNumber + 1; + } + StatusPlayers.Add(P); } } + + if (Status.Length > 5 && validMatches == 0) + { + IW4MAdmin.Application.Program.ServerManager.Logger.WriteError("BAD STATUS!"); + foreach (var s in Status) + { + IW4MAdmin.Application.Program.ServerManager.Logger.WriteDebug(s); + } + } + return StatusPlayers; } } diff --git a/Application/Server.cs b/Application/Server.cs index 5cbe9ec1b..f0509d6a4 100644 --- a/Application/Server.cs +++ b/Application/Server.cs @@ -161,6 +161,15 @@ namespace IW4MAdmin var activePenalties = await Manager.GetPenaltyService().GetActivePenaltiesAsync(player.AliasLinkId, player.IPAddress); var currentBan = activePenalties.FirstOrDefault(b => b.Expires > DateTime.UtcNow); + var currentAutoFlag = activePenalties.Where(p => p.Type == Penalty.PenaltyType.Flag && p.PunisherId == 1) + .OrderByDescending(p => p.When) + .FirstOrDefault(); + + // remove their auto flag status after a week + if (currentAutoFlag != null && (DateTime.Now - currentAutoFlag.When).TotalDays > 7) + { + player.Level = Player.Permission.User; + } if (currentBan != null) { @@ -189,7 +198,7 @@ namespace IW4MAdmin var e = new GameEvent(GameEvent.EventType.Connect, "", player, null, this); Manager.GetEventHandler().AddEvent(e); - e.OnProcessed.Wait(); + e.OnProcessed.WaitHandle.WaitOne(5000); if (!Manager.GetApplicationSettings().Configuration().EnableClientVPNs && await VPNCheck.UsingVPN(player.IPAddressString, Manager.GetApplicationSettings().Configuration().IPHubAPIKey)) @@ -218,7 +227,7 @@ namespace IW4MAdmin var e = new GameEvent(GameEvent.EventType.Disconnect, "", Leaving, null, this); Manager.GetEventHandler().AddEvent(e); - e.OnProcessed.Wait(); + e.OnProcessed.WaitHandle.WaitOne(5000); Leaving.TotalConnectionTime += (int)(DateTime.UtcNow - Leaving.ConnectionTime).TotalSeconds; Leaving.LastConnection = DateTime.UtcNow; @@ -459,10 +468,10 @@ namespace IW4MAdmin else if (E.Type == GameEvent.EventType.Script) { - Manager.GetEventHandler().AddEvent(GameEvent.TranferWaiter(GameEvent.EventType.Kill, E)); + Manager.GetEventHandler().AddEvent(GameEvent.TransferWaiter(GameEvent.EventType.Kill, E)); } - if (E.Type == GameEvent.EventType.Say && E.Data.Length >= 2) + if (E.Type == GameEvent.EventType.Say && E.Data?.Length >= 2) { if (E.Data.Substring(0, 1) == "!" || E.Data.Substring(0, 1) == "@" || @@ -492,7 +501,7 @@ namespace IW4MAdmin // reprocess event as a command - Manager.GetEventHandler().AddEvent(GameEvent.TranferWaiter(GameEvent.EventType.Command, E)); + Manager.GetEventHandler().AddEvent(GameEvent.TransferWaiter(GameEvent.EventType.Command, E)); } } @@ -518,11 +527,20 @@ namespace IW4MAdmin { var dict = await this.GetInfoAsync(); - Gametype = dict["gametype"].StripColors(); - Hostname = dict["hostname"].StripColors(); + if (dict == null) + { + Logger.WriteWarning("Map change event response doesn't have any data"); + } - string mapname = dict["mapname"].StripColors(); - CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map() { Alias = mapname, Name = mapname }; + else + { + + Gametype = dict["gametype"].StripColors(); + Hostname = dict["hostname"]?.StripColors(); + + string mapname = dict["mapname"]?.StripColors() ?? CurrentMap.Name; + CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map() { Alias = mapname, Name = mapname }; + } } else @@ -827,8 +845,6 @@ namespace IW4MAdmin Logger.WriteInfo($"Log file is {logPath}"); #if DEBUG - // LogFile = new RemoteFile("https://raidmax.org/IW4MAdmin/getlog.php"); -#else await Broadcast(loc["BROADCAST_ONLINE"]); #endif } diff --git a/Plugins/Stats/Cheat/Detection.cs b/Plugins/Stats/Cheat/Detection.cs index 77d9feab6..425bdf117 100644 --- a/Plugins/Stats/Cheat/Detection.cs +++ b/Plugins/Stats/Cheat/Detection.cs @@ -1,12 +1,12 @@ 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; +using System.Text.RegularExpressions; namespace IW4MAdmin.Plugins.Stats.Cheat { @@ -19,6 +19,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat EFClientStatistics ClientStats; DateTime LastKill; ILogger Log; + Strain Strain; public Detection(ILogger log, EFClientStatistics clientStats) { @@ -26,8 +27,28 @@ namespace IW4MAdmin.Plugins.Stats.Cheat HitLocationCount = new Dictionary(); foreach (var loc in Enum.GetValues(typeof(IW4Info.HitLocation))) HitLocationCount.Add((IW4Info.HitLocation)loc, 0); - LastKill = DateTime.UtcNow; ClientStats = clientStats; + Strain = new Strain(); + } + + public void ProcessDamage(string damageLine) + { + string regex = @"^(D);((?:bot[0-9]+)|(?:[A-Z]|[0-9])+);([0-9]+);(axis|allies);(.+);((?:[A-Z]|[0-9])+);([0-9]+);(axis|allies);(.+);((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$"; + + var match = Regex.Match(damageLine, regex, RegexOptions.IgnoreCase); + + if (match.Success) + { + var meansOfDeath = ParseEnum.Get(match.Groups[12].Value, typeof(IW4Info.MeansOfDeath)); + var hitLocation = ParseEnum.Get(match.Groups[13].Value, typeof(IW4Info.HitLocation)); + + if (meansOfDeath == IW4Info.MeansOfDeath.MOD_PISTOL_BULLET || + meansOfDeath == IW4Info.MeansOfDeath.MOD_RIFLE_BULLET || + meansOfDeath == IW4Info.MeansOfDeath.MOD_HEAD_SHOT) + { + ClientStats.HitLocations.First(hl => hl.Location == hitLocation).HitCount += 1; + } + } } /// @@ -47,20 +68,82 @@ namespace IW4MAdmin.Plugins.Stats.Cheat RatioAmount = 0 }; + if (LastKill == DateTime.MinValue) + LastKill = DateTime.UtcNow; + HitLocationCount[kill.HitLoc]++; Kills++; AverageKillTime = (AverageKillTime + (DateTime.UtcNow - LastKill).TotalSeconds) / Kills; + #region SNAPSHOTS + + + + #endregion + #region VIEWANGLES - double distance = Vector3.Distance(kill.KillOrigin, kill.DeathOrigin); + /*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); + double angle = trueVector.AngleBetween(calculatedVector);*/ - if (kill.AdsPercent > 0.5 && kill.Distance > 3) + + // make sure it's divisible by 2 + if (kill.AnglesList.Count % 2 == 0) + { + double maxDistance = 0; + for (int i = 0; i < kill.AnglesList.Count - 1; i += 1) + { + // Log.WriteDebug($"Fixed 1 {kill.AnglesList[i]}"); + // Log.WriteDebug($"Fixed 2 {kill.AnglesList[i + 1]}"); + + // fix max distance + double currDistance = Vector3.AbsoluteDistance(kill.AnglesList[i], kill.AnglesList[i + 1]); + //Log.WriteDebug($"Distance {currDistance}"); + if (currDistance > maxDistance) + { + maxDistance = currDistance; + } + } + + double realAgainstPredict = Vector3.AbsoluteDistance(kill.ViewAngles, kill.AnglesList[10]); + + var hitLoc = ClientStats.HitLocations + .First(hl => hl.Location == kill.HitLoc); + float previousAverage = hitLoc.HitOffsetAverage; + double newAverage = (previousAverage * (hitLoc.HitCount - 1) + realAgainstPredict) / hitLoc.HitCount; + hitLoc.HitOffsetAverage = (float)newAverage; + + if (maxDistance > hitLoc.MaxAngleDistance) + hitLoc.MaxAngleDistance = (float)maxDistance; + + if (double.IsNaN(hitLoc.HitOffsetAverage)) + { + Log.WriteWarning("[Detection::ProcessKill] HitOffsetAvgerage NaN"); + Log.WriteDebug($"{previousAverage}-{hitLoc.HitCount}-{hitLoc}-{newAverage}"); + hitLoc.HitOffsetAverage = 0f; + } + +#if DEBUG + Log.WriteDebug($"MaxDistance={maxDistance}, PredictVsReal={realAgainstPredict}"); +#endif + } + + var currentStrain = Strain.GetStrain(kill.ViewAngles, (kill.When - LastKill).TotalMilliseconds); + + if (currentStrain > ClientStats.MaxStrain) + { + ClientStats.MaxStrain = currentStrain; + } + +#if DEBUG + Log.WriteDebug($"Current Strain: {currentStrain}"); +#endif + + /*if (kill.AdsPercent > 0.5 && kill.Distance > 3) { var hitLoc = ClientStats.HitLocations .First(hl => hl.Location == kill.HitLoc); @@ -74,7 +157,9 @@ namespace IW4MAdmin.Plugins.Stats.Cheat Log.WriteDebug($"{previousAverage}-{hitLoc.HitCount}-{hitLoc}-{newAverage}"); hitLoc.HitOffsetAverage = 0f; } - } + }*/ + + LastKill = kill.When; #endregion diff --git a/Plugins/Stats/Cheat/Strain.cs b/Plugins/Stats/Cheat/Strain.cs new file mode 100644 index 000000000..046d25a2f --- /dev/null +++ b/Plugins/Stats/Cheat/Strain.cs @@ -0,0 +1,47 @@ +using SharedLibraryCore.Helpers; +using System; +using System.Collections.Generic; +using System.Text; + +namespace IW4MAdmin.Plugins.Stats.Cheat +{ + class Strain + { + private static double StrainDecayBase = 0.15; + private double CurrentStrain; + private Vector3 LastAngle; + + public double GetStrain(Vector3 newAngle, double deltaTime) + { + if (LastAngle == null) + LastAngle = newAngle; + + double decayFactor = GetDecay(deltaTime); + CurrentStrain *= decayFactor; + + double[] distance = Helpers.Extensions.AngleStuff(newAngle, LastAngle); + + // this happens on first kill + if ((distance[0] == 0 && distance[1] == 0) || + deltaTime == 0 || + double.IsNaN(CurrentStrain)) + { + return CurrentStrain; + } + + double newStrain = Math.Pow(distance[0] + distance[1], 0.99) / deltaTime; + + if (newStrain + CurrentStrain > 0.25) + { + Console.WriteLine($"{LastAngle}-{newAngle}-{decayFactor}-{CurrentStrain}-{newStrain}-{distance[0]}-{distance[1]}-{deltaTime}"); + } + + CurrentStrain += newStrain; + LastAngle = newAngle; + + return CurrentStrain; + } + + private double GetDecay(double deltaTime) => Math.Pow(StrainDecayBase, deltaTime / 1000.0); + } +} \ No newline at end of file diff --git a/Plugins/Stats/Helpers/Extensions.cs b/Plugins/Stats/Helpers/Extensions.cs index 3caea8fcf..e771607ae 100644 --- a/Plugins/Stats/Helpers/Extensions.cs +++ b/Plugins/Stats/Helpers/Extensions.cs @@ -22,5 +22,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers 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; + + public static double[] AngleStuff(Vector3 a, Vector3 b) + { + double deltaX = 180.0 -Math.Abs(Math.Abs(a.X - b.X) - 180.0); + double deltaY = 180.0 - Math.Abs(Math.Abs(a.Y - b.Y) - 180.0); + + return new[] { deltaX, deltaY }; + } } } diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index dcd1a6150..6b530eca1 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -130,8 +130,14 @@ namespace IW4MAdmin.Plugins.Stats.Helpers await statsSvc.ClientStatSvc.SaveChangesAsync(); } + else + { + // todo: look at this more + statsSvc.ClientStatSvc.Update(clientStats); + } + // migration for previous existing stats - else if (clientStats.HitLocations.Count == 0) + if (clientStats.HitLocations.Count == 0) { clientStats.HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType().Select(hl => new EFHitLocationCount() { @@ -140,7 +146,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers Location = hl }) .ToList(); - await statsSvc.ClientStatSvc.SaveChangesAsync(); + //await statsSvc.ClientStatSvc.SaveChangesAsync(); } // set these on connecting @@ -156,10 +162,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers if (!detectionStats.TryAdd(pl.ClientId, new Cheat.Detection(Log, clientStats))) Log.WriteDebug("Could not add client to detection"); - - // todo: look at this more - statsSvc.ClientStatSvc.Update(clientStats); - await statsSvc.ClientStatSvc.SaveChangesAsync(); + /* + await statsSvc.ClientStatSvc.SaveChangesAsync();*/ return clientStats; } @@ -197,23 +201,32 @@ namespace IW4MAdmin.Plugins.Stats.Helpers playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue3); detectionStats.TryRemove(pl.ClientId, out Cheat.Detection removedValue4); - /* // sync their stats before they leave - clientStats = UpdateStats(clientStats);*/ + /* // sync their stats before they leave + clientStats = UpdateStats(clientStats);*/ // todo: should this be saved every disconnect? statsSvc.ClientStatSvc.Update(clientStats); await statsSvc.ClientStatSvc.SaveChangesAsync(); // increment the total play time serverStats.TotalPlayTime += (int)(DateTime.UtcNow - pl.LastConnection).TotalSeconds; - await statsSvc.ServerStatsSvc.SaveChangesAsync(); + //await statsSvc.ServerStatsSvc.SaveChangesAsync(); + } + + public void AddDamageEvent(string eventLine, int clientId, int serverId) + { + if (Plugin.Config.Configuration().EnableAntiCheat) + { + var clientDetection = Servers[serverId].PlayerDetections[clientId]; + clientDetection.ProcessDamage(eventLine); + } } /// /// Process stats for kill event /// /// - 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) + public async Task AddScriptKill(DateTime time, 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, string snapAngles) { var statsSvc = ContextThreads[serverId]; Vector3 vDeathOrigin = null; @@ -235,6 +248,22 @@ namespace IW4MAdmin.Plugins.Stats.Helpers return; } + var snapshotAngles = new List(); + + try + { + foreach(string angle in snapAngles.Split(':', StringSplitOptions.RemoveEmptyEntries)) + { + snapshotAngles.Add(Vector3.Parse(angle).FixIW4Angles()); + } + } + + catch (FormatException) + { + Log.WriteWarning("Could not parse snapshot angles"); + return; + } + var kill = new EFClientKill() { Active = true, @@ -250,9 +279,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers Weapon = ParseEnum.Get(weapon, typeof(IW4Info.WeaponName)), ViewAngles = vViewAngles, TimeOffset = Int64.Parse(offset), - When = DateTime.UtcNow, + When = time, IsKillstreakKill = isKillstreakKill[0] != '0', - AdsPercent = float.Parse(Ads) + AdsPercent = float.Parse(Ads), + AnglesList = snapshotAngles }; if (kill.DeathType == IW4Info.MeansOfDeath.MOD_SUICIDE && @@ -280,7 +310,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers clientStats.HitLocations.Single(hl => hl.Location == kill.HitLoc).HitCount += 1; statsSvc.ClientStatSvc.Update(clientStats); - await statsSvc.ClientStatSvc.SaveChangesAsync(); + // await statsSvc.ClientStatSvc.SaveChangesAsync(); } //statsSvc.KillStatsSvc.Insert(kill); @@ -290,6 +320,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers { async Task executePenalty(Cheat.DetectionPenaltyResult penalty) { + // prevent multiple bans from occuring + if (attacker.Level == Player.Permission.Banned) + { + return; + } + switch (penalty.ClientPenalty) { case Penalty.PenaltyType.Ban: @@ -312,6 +348,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers await executePenalty(clientDetection.ProcessKill(kill)); await executePenalty(clientDetection.ProcessTotalRatio(clientStats)); + +#if DEBUG + statsSvc.ClientStatSvc.Update(clientStats); + await statsSvc.ClientStatSvc.SaveChangesAsync(); +#endif } } @@ -326,7 +367,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers catch (KeyNotFoundException) { - Log.WriteError($"[Stats::AddStandardKill] kill attacker ClientId is invalid {attacker.ClientId}-{attacker}"); + // happens when the client has disconnected before the last status update + Log.WriteWarning($"[Stats::AddStandardKill] kill attacker ClientId is invalid {attacker.ClientId}-{attacker}"); return; } @@ -338,7 +380,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers catch (KeyNotFoundException) { - Log.WriteError($"[Stats::AddStandardKill] kill victim ClientId is invalid {victim.ClientId}-{victim}"); + Log.WriteWarning($"[Stats::AddStandardKill] kill victim ClientId is invalid {victim.ClientId}-{victim}"); return; } @@ -393,7 +435,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers var statsSvc = ContextThreads[serverId]; statsSvc.ClientStatSvc.Update(attackerStats); statsSvc.ClientStatSvc.Update(victimStats); - await statsSvc.ClientStatSvc.SaveChangesAsync(); + //await statsSvc.ClientStatSvc.SaveChangesAsync(); } /// @@ -451,6 +493,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers // calculate the players Score Per Minute for the current session int scoreDifference = clientStats.RoundScore - clientStats.LastScore; + + // todo: fix the SPM for TEAMDAMAGE + if (scoreDifference < 0) + scoreDifference = clientStats.RoundScore; + double killSPM = scoreDifference / timeSinceLastCalc; // calculate how much the KDR should weigh diff --git a/Plugins/Stats/Models/EFClientKill.cs b/Plugins/Stats/Models/EFClientKill.cs index c483c0c10..688c73b47 100644 --- a/Plugins/Stats/Models/EFClientKill.cs +++ b/Plugins/Stats/Models/EFClientKill.cs @@ -4,6 +4,7 @@ using SharedLibraryCore.Database.Models; using System.ComponentModel.DataAnnotations.Schema; using SharedLibraryCore.Helpers; using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; namespace IW4MAdmin.Plugins.Stats.Models { @@ -38,5 +39,7 @@ namespace IW4MAdmin.Plugins.Stats.Models public bool IsKillstreakKill { get; set; } [NotMapped] public float AdsPercent { get; set; } + [NotMapped] + public List AnglesList { get; set; } } } diff --git a/Plugins/Stats/Models/EFClientStatistics.cs b/Plugins/Stats/Models/EFClientStatistics.cs index 610f4ab9b..a462b872c 100644 --- a/Plugins/Stats/Models/EFClientStatistics.cs +++ b/Plugins/Stats/Models/EFClientStatistics.cs @@ -36,6 +36,8 @@ namespace IW4MAdmin.Plugins.Stats.Models public double Skill { get; set; } [Required] public int TimePlayed { get; set; } + [Required] + public double MaxStrain { get; set; } [NotMapped] public float AverageHitOffset diff --git a/Plugins/Stats/Models/EFHitLocationCount.cs b/Plugins/Stats/Models/EFHitLocationCount.cs index 13495ea01..4d7fa192b 100644 --- a/Plugins/Stats/Models/EFHitLocationCount.cs +++ b/Plugins/Stats/Models/EFHitLocationCount.cs @@ -14,6 +14,9 @@ namespace IW4MAdmin.Plugins.Stats.Models public int HitCount { get; set; } [Required] public float HitOffsetAverage { get; set; } + [Required] + public float MaxAngleDistance { get; set; } + [Required] public int ClientId { get; set; } [ForeignKey("ClientId"), Column(Order = 0 )] public EFClient Client { get; set; } diff --git a/Plugins/Stats/Plugin.cs b/Plugins/Stats/Plugin.cs index 27dd74c4e..17f489554 100644 --- a/Plugins/Stats/Plugin.cs +++ b/Plugins/Stats/Plugin.cs @@ -74,13 +74,16 @@ namespace IW4MAdmin.Plugins.Stats case GameEvent.EventType.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]); + await Manager.AddScriptKill(E.Time, 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], killInfo[13]); else if (!E.Owner.CustomCallback) await Manager.AddStandardKill(E.Origin, E.Target); break; case GameEvent.EventType.Death: break; + case GameEvent.EventType.Damage: + Manager.AddDamageEvent(E.Data, E.Origin.ClientId, E.Owner.GetHashCode()); + break; } } @@ -146,6 +149,8 @@ namespace IW4MAdmin.Plugins.Stats double abdomenRatio = 0; double chestAbdomenRatio = 0; double hitOffsetAverage = 0; + double maxStrain = clientStats.Count(c=> c.MaxStrain > 0) == 0 ? 0 : clientStats.Max(cs => cs.MaxStrain); + //double maxAngle = clientStats.Max(cs => cs.HitLocations.Max(hl => hl.MaxAngleDistance)); if (clientStats.Where(cs => cs.HitLocations.Count > 0).FirstOrDefault() != null) { @@ -172,9 +177,9 @@ namespace IW4MAdmin.Plugins.Stats { new ProfileMeta() { - Key = "Chest Ratio", - Value = chestRatio, - Sensitive = true + Key = "Chest Ratio", + Value = chestRatio, + Sensitive = true }, new ProfileMeta() { @@ -197,9 +202,21 @@ namespace IW4MAdmin.Plugins.Stats new ProfileMeta() { Key = "Hit Offset Average", - Value = $"{Math.Round(((float)hitOffsetAverage).ToDegrees(), 4)}°", + Value = $"{Math.Round(((float)hitOffsetAverage), 4)}°", Sensitive = true - } + }, + new ProfileMeta() + { + Key = "Max Strain", + Value = Math.Round(maxStrain, 3), + Sensitive = true + }, + /*new ProfileMeta() + { + Key = "Max Angle Distance", + Value = Math.Round(maxAngle, 1), + Sensitive = true + }*/ }; } diff --git a/SharedLibraryCore/Commands/NativeCommands.cs b/SharedLibraryCore/Commands/NativeCommands.cs index 0fdc34071..f49eb96a5 100644 --- a/SharedLibraryCore/Commands/NativeCommands.cs +++ b/SharedLibraryCore/Commands/NativeCommands.cs @@ -1,7 +1,6 @@ using Microsoft.EntityFrameworkCore; using SharedLibraryCore.Database; using SharedLibraryCore.Database.Models; -using SharedLibraryCore.Exceptions; using SharedLibraryCore.Objects; using SharedLibraryCore.Services; using System; @@ -36,6 +35,8 @@ namespace SharedLibraryCore.Commands { E.Origin.Level = Player.Permission.Owner; await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationSet["COMMANDS_OWNER_SUCCESS"]); + // so setpassword/login works + E.Owner.Manager.GetPrivilegedClients().Add(E.Origin.ClientId, E.Origin); await E.Owner.Manager.GetClientService().Update(E.Origin); } else diff --git a/SharedLibraryCore/Event.cs b/SharedLibraryCore/Event.cs index 3d7a36aad..29c013928 100644 --- a/SharedLibraryCore/Event.cs +++ b/SharedLibraryCore/Event.cs @@ -1,7 +1,4 @@ using System; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; using System.Threading; using SharedLibraryCore.Objects; @@ -50,14 +47,16 @@ namespace SharedLibraryCore Target = T; Owner = S; OnProcessed = new ManualResetEventSlim(); + Time = DateTime.UtcNow; } public GameEvent() { - OnProcessed = new ManualResetEventSlim(); + OnProcessed = new ManualResetEventSlim(); + Time = DateTime.UtcNow; } - public static GameEvent TranferWaiter(EventType newType, GameEvent e) + public static GameEvent TransferWaiter(EventType newType, GameEvent e) { var newEvent = new GameEvent() { @@ -69,11 +68,12 @@ namespace SharedLibraryCore Owner = e.Owner, Remote = e.Remote, Target = e.Target, - Type = newType + Type = newType, }; // hack: prevent the previous event from completing until this one is done e.OnProcessed = new ManualResetEventSlim(); + newEvent.Time = e.Time; return newEvent; } @@ -87,5 +87,6 @@ namespace SharedLibraryCore public Boolean Remote = false; public object Extra { get; set; } public ManualResetEventSlim OnProcessed { get; set; } + public DateTime Time { get; private set; } } } diff --git a/SharedLibraryCore/Helpers/Vector3.cs b/SharedLibraryCore/Helpers/Vector3.cs index 6619ed8bf..00cb0fbf4 100644 --- a/SharedLibraryCore/Helpers/Vector3.cs +++ b/SharedLibraryCore/Helpers/Vector3.cs @@ -44,6 +44,19 @@ namespace SharedLibraryCore.Helpers return Math.Sqrt(Math.Pow(b.X - a.X, 2) + Math.Pow(b.Y - a.Y, 2) + Math.Pow(b.Z - a.Z, 2)); } + public static double AbsoluteDistance(Vector3 a, Vector3 b) + { + double deltaX = Math.Abs(b.X -a.X); + double deltaY = Math.Abs(b.Y - a.Y); + double deltaZ = Math.Abs(b.Z - a.Z); + + double dx = deltaX < 360.0 / 2 ? deltaX : 360.0 - deltaX; + double dy = deltaY < 360.0 / 2 ? deltaY : 360.0 - deltaY; + double dz = deltaZ < 360.0 / 2 ? deltaZ : 360.0 - deltaZ; + + return Math.Sqrt((dx * dx) + (dy * dy) + (dz * dx)); + } + public static Vector3 Subtract(Vector3 a, Vector3 b) => new Vector3(b.X - a.X, b.Y - a.Y, b.Z - a.Z); public double DotProduct(Vector3 a) => (a.X * this.X) + (a.Y * this.Y) + (a.Z * this.Z); diff --git a/SharedLibraryCore/Migrations/20180502195450_Update.Designer.cs b/SharedLibraryCore/Migrations/20180502195450_Update.Designer.cs new file mode 100644 index 000000000..9e2be79da --- /dev/null +++ b/SharedLibraryCore/Migrations/20180502195450_Update.Designer.cs @@ -0,0 +1,434 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using SharedLibraryCore.Database; +using SharedLibraryCore.Objects; +using System; + +namespace SharedLibraryCore.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20180502195450_Update")] + partial class Update + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011"); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AttackerId"); + + b.Property("Damage"); + + b.Property("DeathOriginVector3Id"); + + b.Property("DeathType"); + + b.Property("HitLoc"); + + b.Property("KillOriginVector3Id"); + + b.Property("Map"); + + b.Property("ServerId"); + + b.Property("VictimId"); + + b.Property("ViewAnglesVector3Id"); + + b.Property("Weapon"); + + b.Property("When"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("TimeSent"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientMessages"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b => + { + b.Property("ClientId"); + + b.Property("ServerId"); + + b.Property("Active"); + + b.Property("Deaths"); + + b.Property("Kills"); + + b.Property("MaxStrain"); + + b.Property("SPM"); + + b.Property("Skill"); + + b.Property("TimePlayed"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientStatistics"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId") + .HasColumnName("EFClientStatistics_ClientId"); + + b.Property("HitCount"); + + b.Property("HitOffsetAverage"); + + b.Property("Location"); + + b.Property("MaxAngleDistance"); + + b.Property("ServerId") + .HasColumnName("EFClientStatistics_ServerId"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFHitLocationCounts"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServer", b => + { + b.Property("ServerId"); + + b.Property("Active"); + + b.Property("Port"); + + b.HasKey("ServerId"); + + b.ToTable("EFServers"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ServerId"); + + b.Property("TotalKills"); + + b.Property("TotalPlayTime"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("DateAdded"); + + b.Property("IPAddress"); + + b.Property("LinkId"); + + b.Property("Name") + .IsRequired(); + + b.HasKey("AliasId"); + + b.HasIndex("LinkId"); + + b.ToTable("EFAlias"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AliasLinkId"); + + b.Property("Connections"); + + b.Property("CurrentAliasId"); + + b.Property("FirstConnection"); + + b.Property("LastConnection"); + + b.Property("Level"); + + b.Property("Masked"); + + b.Property("NetworkId"); + + b.Property("Password"); + + b.Property("PasswordSalt"); + + b.Property("TotalConnectionTime"); + + b.HasKey("ClientId"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("NetworkId") + .IsUnique(); + + b.ToTable("EFClients"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("Expires"); + + b.Property("LinkId"); + + b.Property("OffenderId"); + + b.Property("Offense") + .IsRequired(); + + b.Property("PunisherId"); + + b.Property("Type"); + + b.Property("When"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties"); + }); + + modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd(); + + b.Property("X"); + + b.Property("Y"); + + b.Property("Z"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics") + .WithMany("HitLocations") + .HasForeignKey("ClientId", "ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Database.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SharedLibraryCore/Migrations/20180502195450_Update.cs b/SharedLibraryCore/Migrations/20180502195450_Update.cs new file mode 100644 index 000000000..e446ed36f --- /dev/null +++ b/SharedLibraryCore/Migrations/20180502195450_Update.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace SharedLibraryCore.Migrations +{ + public partial class Update : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MaxAngleDistance", + table: "EFHitLocationCounts", + nullable: false, + defaultValue: 0f); + + migrationBuilder.AddColumn( + name: "MaxStrain", + table: "EFClientStatistics", + nullable: false, + defaultValue: 0.0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MaxAngleDistance", + table: "EFHitLocationCounts"); + + migrationBuilder.DropColumn( + name: "MaxStrain", + table: "EFClientStatistics"); + } + } +} diff --git a/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs b/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs new file mode 100644 index 000000000..e3c6c51e9 --- /dev/null +++ b/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs @@ -0,0 +1,433 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using SharedLibraryCore.Database; +using SharedLibraryCore.Objects; +using System; + +namespace SharedLibraryCore.Migrations +{ + [DbContext(typeof(DatabaseContext))] + partial class DatabaseContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.2-rtm-10011"); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AttackerId"); + + b.Property("Damage"); + + b.Property("DeathOriginVector3Id"); + + b.Property("DeathType"); + + b.Property("HitLoc"); + + b.Property("KillOriginVector3Id"); + + b.Property("Map"); + + b.Property("ServerId"); + + b.Property("VictimId"); + + b.Property("ViewAnglesVector3Id"); + + b.Property("Weapon"); + + b.Property("When"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId"); + + b.Property("Message"); + + b.Property("ServerId"); + + b.Property("TimeSent"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientMessages"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b => + { + b.Property("ClientId"); + + b.Property("ServerId"); + + b.Property("Active"); + + b.Property("Deaths"); + + b.Property("Kills"); + + b.Property("MaxStrain"); + + b.Property("SPM"); + + b.Property("Skill"); + + b.Property("TimePlayed"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientStatistics"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ClientId") + .HasColumnName("EFClientStatistics_ClientId"); + + b.Property("HitCount"); + + b.Property("HitOffsetAverage"); + + b.Property("Location"); + + b.Property("MaxAngleDistance"); + + b.Property("ServerId") + .HasColumnName("EFClientStatistics_ServerId"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFHitLocationCounts"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServer", b => + { + b.Property("ServerId"); + + b.Property("Active"); + + b.Property("Port"); + + b.HasKey("ServerId"); + + b.ToTable("EFServers"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ServerId"); + + b.Property("TotalKills"); + + b.Property("TotalPlayTime"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("DateAdded"); + + b.Property("IPAddress"); + + b.Property("LinkId"); + + b.Property("Name") + .IsRequired(); + + b.HasKey("AliasId"); + + b.HasIndex("LinkId"); + + b.ToTable("EFAlias"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AliasLinkId"); + + b.Property("Connections"); + + b.Property("CurrentAliasId"); + + b.Property("FirstConnection"); + + b.Property("LastConnection"); + + b.Property("Level"); + + b.Property("Masked"); + + b.Property("NetworkId"); + + b.Property("Password"); + + b.Property("PasswordSalt"); + + b.Property("TotalConnectionTime"); + + b.HasKey("ClientId"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("NetworkId") + .IsUnique(); + + b.ToTable("EFClients"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("Expires"); + + b.Property("LinkId"); + + b.Property("OffenderId"); + + b.Property("Offense") + .IsRequired(); + + b.Property("PunisherId"); + + b.Property("Type"); + + b.Property("When"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties"); + }); + + modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd(); + + b.Property("X"); + + b.Property("Y"); + + b.Property("Z"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Helpers.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics") + .WithMany("HitLocations") + .HasForeignKey("ClientId", "ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b => + { + b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Database.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b => + { + b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SharedLibraryCore/RCon/Connection.cs b/SharedLibraryCore/RCon/Connection.cs index ebb3be17c..04cf6561a 100644 --- a/SharedLibraryCore/RCon/Connection.cs +++ b/SharedLibraryCore/RCon/Connection.cs @@ -1,6 +1,7 @@ using SharedLibraryCore.Exceptions; using SharedLibraryCore.Interfaces; using System; +using System.Collections.Concurrent; using System.Linq; using System.Net; using System.Net.Sockets; @@ -32,11 +33,31 @@ namespace SharedLibraryCore.RCon } } + class ResponseEvent + { + public int Id { get; set; } + public string[] Response { get; set; } + public Task Awaiter + { + get + { + return Task.Run(() => FinishedEvent.Wait()); + } + } + private ManualResetEventSlim FinishedEvent; + + public ResponseEvent() + { + FinishedEvent = new ManualResetEventSlim(); + } + } + public class Connection { public IPEndPoint Endpoint { get; private set; } public string RConPassword { get; private set; } - Socket ServerConnection; + public ConcurrentQueue ResponseQueue; + //Socket ServerConnection; ILogger Log; int FailedSends; int FailedReceives; @@ -56,27 +77,13 @@ namespace SharedLibraryCore.RCon OnConnected = new ManualResetEvent(false); OnSent = new ManualResetEvent(false); OnReceived = new ManualResetEvent(false); - - try - { - ServerConnection = new Socket(Endpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); - ServerConnection.BeginConnect(Endpoint, new AsyncCallback(OnConnectedCallback), ServerConnection); - if (!OnConnected.WaitOne(StaticHelpers.SocketTimeout)) - throw new SocketException((int)SocketError.TimedOut); - FailedSends = 0; - } - - catch (SocketException e) - { - throw new NetworkException(e.Message); - } } ~Connection() { - ServerConnection.Shutdown(SocketShutdown.Both); + /*ServerConnection.Shutdown(SocketShutdown.Both); ServerConnection.Close(); - ServerConnection.Dispose(); + ServerConnection.Dispose();*/ } private void OnConnectedCallback(IAsyncResult ar) @@ -106,8 +113,9 @@ namespace SharedLibraryCore.RCon { int sentByteNum = serverConnection.EndSend(ar); #if DEBUG - Log.WriteDebug($"Sent {sentByteNum} bytes to {ServerConnection.RemoteEndPoint}"); + Log.WriteDebug($"Sent {sentByteNum} bytes to {serverConnection.RemoteEndPoint}"); #endif + // this is where we override our await to make it OnSent.Set(); } @@ -128,7 +136,7 @@ namespace SharedLibraryCore.RCon if (bytesRead > 0) { #if DEBUG - Log.WriteDebug($"Received {bytesRead} bytes from {ServerConnection.RemoteEndPoint}"); + Log.WriteDebug($"Received {bytesRead} bytes from {serverConnection.RemoteEndPoint}"); #endif FailedReceives = 0; connectionState.ResponseString.Append(Utilities.EncodingType.GetString(connectionState.Buffer, 0, bytesRead).TrimEnd('\0') + '\n'); @@ -138,7 +146,7 @@ namespace SharedLibraryCore.RCon if (serverConnection.Available > 0) { - ServerConnection.BeginReceive(connectionState.Buffer, 0, connectionState.Buffer.Length, 0, + serverConnection.BeginReceive(connectionState.Buffer, 0, connectionState.Buffer.Length, 0, new AsyncCallback(OnReceivedCallback), connectionState); } else @@ -158,14 +166,19 @@ namespace SharedLibraryCore.RCon { } + + catch (ObjectDisposedException) + { + Log.WriteWarning($"Tried to check for more available bytes for disposed socket on {Endpoint}"); + } } public async Task SendQueryAsync(StaticHelpers.QueryType type, string parameters = "", bool waitForResponse = true) { // will this really prevent flooding? - if ((DateTime.Now - LastQuery).TotalMilliseconds < 150) + if ((DateTime.Now - LastQuery).TotalMilliseconds < 250) { - await Task.Delay(150); + await Task.Delay(250); } LastQuery = DateTime.Now; @@ -191,119 +204,128 @@ namespace SharedLibraryCore.RCon break; } - retrySend: - try + using (var socketConnection = new Socket(Endpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp)) { - ServerConnection.BeginSend(payload, 0, payload.Length, 0, new AsyncCallback(OnSentCallback), ServerConnection); - bool success = await Task.FromResult(OnSent.WaitOne(StaticHelpers.SocketTimeout)); + socketConnection.BeginConnect(Endpoint, new AsyncCallback(OnConnectedCallback), socketConnection); - if (!success) + retrySend: + try { - FailedSends++; -#if DEBUG - Log.WriteDebug($"{FailedSends} failed sends to {ServerConnection.RemoteEndPoint.ToString()}"); -#endif - if (FailedSends < 4) - goto retrySend; - else if (FailedSends == 4) - Log.WriteError($"Failed to send data to {ServerConnection.RemoteEndPoint}"); - } + + if (!OnConnected.WaitOne(StaticHelpers.SocketTimeout)) + throw new SocketException((int)SocketError.TimedOut); - else - { - if (FailedSends >= 4) + socketConnection.BeginSend(payload, 0, payload.Length, 0, new AsyncCallback(OnSentCallback), socketConnection); + bool success = await Task.FromResult(OnSent.WaitOne(StaticHelpers.SocketTimeout)); + + if (!success) { - Log.WriteVerbose($"Resumed send RCon connection with {ServerConnection.RemoteEndPoint}"); - FailedSends = 0; + FailedSends++; +#if DEBUG + Log.WriteDebug($"{FailedSends} failed sends to {socketConnection.RemoteEndPoint.ToString()}"); +#endif + if (FailedSends < 4) + goto retrySend; + else if (FailedSends == 4) + Log.WriteError($"Failed to send data to {socketConnection.RemoteEndPoint}"); + } + + else + { + if (FailedSends >= 4) + { + Log.WriteVerbose($"Resumed send RCon connection with {socketConnection.RemoteEndPoint}"); + FailedSends = 0; + } } } - } - catch (SocketException e) - { - // this result is normal if the server is not listening - if (e.NativeErrorCode != (int)SocketError.ConnectionReset && - e.NativeErrorCode != (int)SocketError.TimedOut) - throw new NetworkException($"Unexpected error while sending data to server - {e.Message}"); - } - - if (!waitForResponse) - return await Task.FromResult(new string[] { "" }); - - var connectionState = new ConnectionState(ServerConnection); - - retryReceive: - try - { - ServerConnection.BeginReceive(connectionState.Buffer, 0, connectionState.Buffer.Length, 0, - new AsyncCallback(OnReceivedCallback), connectionState); - bool success = await Task.FromResult(OnReceived.WaitOne(StaticHelpers.SocketTimeout)); - - if (!success) + catch (SocketException e) { + // this result is normal if the server is not listening + if (e.NativeErrorCode != (int)SocketError.ConnectionReset && + e.NativeErrorCode != (int)SocketError.TimedOut) + throw new NetworkException($"Unexpected error while sending data to server - {e.Message}"); + } - FailedReceives++; + if (!waitForResponse) + return await Task.FromResult(new string[] { "" }); + + var connectionState = new ConnectionState(socketConnection); + + retryReceive: + try + { + socketConnection.BeginReceive(connectionState.Buffer, 0, connectionState.Buffer.Length, 0, + new AsyncCallback(OnReceivedCallback), connectionState); + bool success = await Task.FromResult(OnReceived.WaitOne(StaticHelpers.SocketTimeout)); + + if (!success) + { + + FailedReceives++; #if DEBUG - Log.WriteDebug($"{FailedReceives} failed receives from {ServerConnection.RemoteEndPoint.ToString()}"); + Log.WriteDebug($"{FailedReceives} failed receives from {socketConnection.RemoteEndPoint.ToString()}"); #endif - if (FailedReceives < 4) - goto retrySend; + if (FailedReceives < 4) + goto retrySend; + else if (FailedReceives == 4) + { + Log.WriteError($"Failed to receive data from {socketConnection.RemoteEndPoint} after {FailedReceives} tries"); + } + + if (FailedReceives >= 4) + { + throw new NetworkException($"Could not receive data from {socketConnection.RemoteEndPoint}"); + } + } + + else + { + if (FailedReceives >= 4) + { + Log.WriteVerbose($"Resumed receive RCon connection from {socketConnection.RemoteEndPoint}"); + FailedReceives = 0; + } + } + } + + catch (SocketException e) + { + // this result is normal if the server is not listening + if (e.NativeErrorCode != (int)SocketError.ConnectionReset && + e.NativeErrorCode != (int)SocketError.TimedOut) + throw new NetworkException($"Unexpected error while receiving data from server - {e.Message}"); + else if (FailedReceives < 4) + { + goto retryReceive; + } + else if (FailedReceives == 4) { - Log.WriteError($"Failed to receive data from {ServerConnection.RemoteEndPoint} after {FailedReceives} tries"); + Log.WriteError($"Failed to receive data from {socketConnection.RemoteEndPoint} after {FailedReceives} tries"); } if (FailedReceives >= 4) { - throw new NetworkException($"Could not receive data from {ServerConnection.RemoteEndPoint}"); + throw new NetworkException(e.Message); } } - else + string queryResponse = response; + + if (queryResponse.Contains("Invalid password")) + throw new NetworkException("RCON password is invalid"); + if (queryResponse.ToString().Contains("rcon_password")) + throw new NetworkException("RCON password has not been set"); + + string[] splitResponse = queryResponse.Split(new char[] { - if (FailedReceives >= 4) - { - Log.WriteVerbose($"Resumed receive RCon connection from {ServerConnection.RemoteEndPoint}"); - FailedReceives = 0; - } - } - } - - catch (SocketException e) - { - // this result is normal if the server is not listening - if (e.NativeErrorCode != (int)SocketError.ConnectionReset && - e.NativeErrorCode != (int)SocketError.TimedOut) - throw new NetworkException($"Unexpected error while receiving data from server - {e.Message}"); - else if (FailedReceives < 4) - { - goto retryReceive; - } - - else if (FailedReceives == 4) - { - Log.WriteError($"Failed to receive data from {ServerConnection.RemoteEndPoint} after {FailedReceives} tries"); - } - - if (FailedReceives >= 4) - { - throw new NetworkException(e.Message); - } - } - - string queryResponse = response; - - if (queryResponse.Contains("Invalid password")) - throw new NetworkException("RCON password is invalid"); - if (queryResponse.ToString().Contains("rcon_password")) - throw new NetworkException("RCON password has not been set"); - - string[] splitResponse = queryResponse.Split(new char[] - { '\n' - }, StringSplitOptions.RemoveEmptyEntries) - .Select(line => line.Trim()).ToArray(); - return splitResponse; + }, StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()).ToArray(); + return splitResponse; + } } } } diff --git a/SharedLibraryCore/RCon/StaticHelpers.cs b/SharedLibraryCore/RCon/StaticHelpers.cs index 04d10d06d..0782d0391 100644 --- a/SharedLibraryCore/RCon/StaticHelpers.cs +++ b/SharedLibraryCore/RCon/StaticHelpers.cs @@ -13,6 +13,6 @@ namespace SharedLibraryCore.RCon } public static char SeperatorChar = (char)int.Parse("0a", System.Globalization.NumberStyles.AllowHexSpecifier); - public static readonly TimeSpan SocketTimeout = new TimeSpan(0, 0, 2); + public static readonly TimeSpan SocketTimeout = new TimeSpan(0, 0, 10); } } diff --git a/SharedLibraryCore/SharedLibraryCore.csproj b/SharedLibraryCore/SharedLibraryCore.csproj index 109637529..13c869646 100644 --- a/SharedLibraryCore/SharedLibraryCore.csproj +++ b/SharedLibraryCore/SharedLibraryCore.csproj @@ -12,6 +12,10 @@ Debug;Release;Prerelease + + + + diff --git a/WebfrontCore/Controllers/ConsoleController.cs b/WebfrontCore/Controllers/ConsoleController.cs index 8014a2ec6..a9c604dad 100644 --- a/WebfrontCore/Controllers/ConsoleController.cs +++ b/WebfrontCore/Controllers/ConsoleController.cs @@ -28,7 +28,6 @@ namespace WebfrontCore.Controllers public async Task ExecuteAsync(int serverId, string command) { - var server = Manager.GetServers().First(s => s.GetHashCode() == serverId); var client = new Player() { @@ -50,7 +49,7 @@ namespace WebfrontCore.Controllers Manager.GetEventHandler().AddEvent(remoteEvent); // wait for the event to process - await Task.Run(() => remoteEvent.OnProcessed.Wait()); + await Task.Run(() => remoteEvent.OnProcessed.WaitHandle.WaitOne(5000)); var response = server.CommandResult.Where(c => c.ClientId == client.ClientId).ToList(); // remove the added command response diff --git a/WebfrontCore/WebfrontCore.csproj b/WebfrontCore/WebfrontCore.csproj index 15ce95ab1..c0077afaf 100644 --- a/WebfrontCore/WebfrontCore.csproj +++ b/WebfrontCore/WebfrontCore.csproj @@ -22,6 +22,10 @@ Debug;Release;Prerelease + + true + +