From af4630ecb9dd7881c0ac3c71a1695ca642338c6f Mon Sep 17 00:00:00 2001 From: RaidMax Date: Wed, 16 Jun 2021 08:53:50 -0500 Subject: [PATCH] Additional CSGO compatibility improvements --- Application/RConParsers/BaseRConParser.cs | 13 +- Integrations/Source/SourceRConConnection.cs | 136 +++++++++++--------- Plugins/ScriptPlugins/ParserCSGO.js | 3 +- Plugins/ScriptPlugins/ParserCSGOSM.js | 3 +- Plugins/Stats/Client/HitCalculator.cs | 7 +- Plugins/Stats/Helpers/StatManager.cs | 71 ++++------ Plugins/Stats/Plugin.cs | 1 + SharedLibraryCore/Utilities.cs | 12 ++ 8 files changed, 129 insertions(+), 117 deletions(-) diff --git a/Application/RConParsers/BaseRConParser.cs b/Application/RConParsers/BaseRConParser.cs index 5df6cf728..fcf128215 100644 --- a/Application/RConParsers/BaseRConParser.cs +++ b/Application/RConParsers/BaseRConParser.cs @@ -217,10 +217,15 @@ namespace IW4MAdmin.Application.RConParsers continue; } - int clientNumber = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]]); - int score = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]]); + var clientNumber = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]]); + var score = 0; + + if (Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore] > 0) + { + score = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]]); + } - int ping = 999; + var ping = 999; // their state can be CNCT, ZMBI etc if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Length <= 3) @@ -229,7 +234,7 @@ namespace IW4MAdmin.Application.RConParsers } long networkId; - string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine(); + var name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine(); string networkIdString; var ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP(); diff --git a/Integrations/Source/SourceRConConnection.cs b/Integrations/Source/SourceRConConnection.cs index cfab5682d..c8f3661ae 100644 --- a/Integrations/Source/SourceRConConnection.cs +++ b/Integrations/Source/SourceRConConnection.cs @@ -26,9 +26,12 @@ namespace Integrations.Source private readonly SemaphoreSlim _activeQuery; private static readonly TimeSpan FloodDelay = TimeSpan.FromMilliseconds(250); - + private static readonly TimeSpan ConnectionTimeout = TimeSpan.FromSeconds(30); + private DateTime _lastQuery = DateTime.Now; private RconClient _rconClient; + private bool _authenticated; + private bool _needNewSocket = true; public SourceRConConnection(ILogger logger, IRConClientFactory rconClientFactory, string hostname, int port, string password) @@ -38,7 +41,6 @@ namespace Integrations.Source _hostname = hostname; _port = port; _logger = logger; - _rconClient = _rconClientFactory.CreateClient(_hostname, _port); _activeQuery = new SemaphoreSlim(1, 1); } @@ -52,10 +54,22 @@ namespace Integrations.Source try { await _activeQuery.WaitAsync(); - var diff = DateTime.Now - _lastQuery; - if (diff < FloodDelay) + await WaitForAvailable(); + + if (_needNewSocket) { - await Task.Delay(FloodDelay - diff); + try + { + _rconClient?.Disconnect(); + } + catch + { + // ignored + } + + _rconClient = _rconClientFactory.CreateClient(_hostname, _port); + _authenticated = false; + _needNewSocket = false; } using (LogContext.PushProperty("Server", $"{_hostname}:{_port}")) @@ -63,64 +77,14 @@ namespace Integrations.Source _logger.LogDebug("Connecting to RCon socket"); } - await _rconClient.ConnectAsync(); + await TryConnectAndAuthenticate().WithTimeout(ConnectionTimeout); - bool authenticated; - - try - { - using (LogContext.PushProperty("Server", $"{_hostname}:{_port}")) - { - _logger.LogDebug("Authenticating to RCon socket"); - } - - authenticated = await _rconClient.AuthenticateAsync(_password); - } - catch (SocketException ex) - { - // occurs when the server comes back from hibernation - // this is probably a bug in the library - if (ex.ErrorCode == 10053 || ex.ErrorCode == 10054) - { - using (LogContext.PushProperty("Server", $"{_hostname}:{_port}")) - { - _logger.LogWarning(ex, - "Server appears to resumed from hibernation, so we are using a new socket"); - } - - try - { - _rconClient.Disconnect(); - } - catch - { - // ignored - } - - _rconClient = _rconClientFactory.CreateClient(_hostname, _port); - } - - using (LogContext.PushProperty("Server", $"{_hostname}:{_port}")) - { - _logger.LogError(ex, "Error occurred authenticating with server"); - } - - throw new NetworkException("Error occurred authenticating with server"); - } - - if (!authenticated) - { - using (LogContext.PushProperty("Server", $"{_hostname}:{_port}")) - { - _logger.LogError("Could not login to server"); - } - - throw new ServerException("Could not authenticate to server with provided password"); - } + var multiPacket = false; if (type == StaticHelpers.QueryType.COMMAND_STATUS) { parameters = "status"; + multiPacket = true; } parameters = parameters.ReplaceUnfriendlyCharacters(); @@ -131,9 +95,10 @@ namespace Integrations.Source _logger.LogDebug("Sending query {Type} with parameters \"{Parameters}\"", type, parameters); } - var response = await _rconClient.ExecuteCommandAsync(parameters, true); + var response = await _rconClient.ExecuteCommandAsync(parameters, multiPacket) + .WithTimeout(ConnectionTimeout); - using (LogContext.PushProperty("Server", $"{_rconClient.Host}:{_rconClient.Port}")) + using (LogContext.PushProperty("Server", $"{_hostname}:{_port}")) { _logger.LogDebug("Received RCon response {Response}", response); } @@ -142,6 +107,24 @@ namespace Integrations.Source return split.Take(split.Length - 1).ToArray(); } + catch (TaskCanceledException) + { + _needNewSocket = true; + throw new NetworkException("Timeout while attempting to communicate with server"); + } + + catch (SocketException ex) + { + using (LogContext.PushProperty("Server", $"{_hostname}:{_port}")) + { + _logger.LogError(ex, "Socket exception encountered while attempting to communicate with server"); + } + + _needNewSocket = true; + + throw new NetworkException("Socket exception encountered while attempting to communicate with server"); + } + catch (Exception ex) when (ex.GetType() != typeof(NetworkException) && ex.GetType() != typeof(ServerException)) { @@ -164,6 +147,39 @@ namespace Integrations.Source } } + private async Task WaitForAvailable() + { + var diff = DateTime.Now - _lastQuery; + if (diff < FloodDelay) + { + await Task.Delay(FloodDelay - diff); + } + } + + private async Task TryConnectAndAuthenticate() + { + if (!_authenticated) + { + using (LogContext.PushProperty("Server", $"{_hostname}:{_port}")) + { + _logger.LogDebug("Authenticating to RCon socket"); + } + + await _rconClient.ConnectAsync().WithTimeout(ConnectionTimeout); + _authenticated = await _rconClient.AuthenticateAsync(_password).WithTimeout(ConnectionTimeout); + + if (!_authenticated) + { + using (LogContext.PushProperty("Server", $"{_hostname}:{_port}")) + { + _logger.LogError("Could not login to server"); + } + + throw new ServerException("Could not authenticate to server with provided password"); + } + } + } + public void SetConfiguration(IRConParser config) { } diff --git a/Plugins/ScriptPlugins/ParserCSGO.js b/Plugins/ScriptPlugins/ParserCSGO.js index 37e0f4fa8..7ede1e714 100644 --- a/Plugins/ScriptPlugins/ParserCSGO.js +++ b/Plugins/ScriptPlugins/ParserCSGO.js @@ -35,7 +35,7 @@ const plugin = { rconParser.Configuration.Status.Pattern = '^#\\s*(\\d+) (\\d+) "(.+)" (\\S+) +(\\d+:\\d+(?::\\d+)?) (\\d+) (\\S+) (\\S+) (\\d+) (\\d+\\.\\d+\\.\\d+\\.\\d+:\\d+)$'; rconParser.Configuration.Status.AddMapping(100, 2); - rconParser.Configuration.Status.AddMapping(101, 7); + rconParser.Configuration.Status.AddMapping(101, -1); rconParser.Configuration.Status.AddMapping(102, 6); rconParser.Configuration.Status.AddMapping(103, 4) rconParser.Configuration.Status.AddMapping(104, 3); @@ -90,6 +90,7 @@ const plugin = { rconParser.GameName = 10; // CSGO eventParser.Version = 'CSGO'; eventParser.GameName = 10; // CSGO + eventParser.URLProtocolFormat = 'steam://connect/{{ip}}:{{port}}'; }, onUnloadAsync: function () { diff --git a/Plugins/ScriptPlugins/ParserCSGOSM.js b/Plugins/ScriptPlugins/ParserCSGOSM.js index 01e004103..cfeb5bdff 100644 --- a/Plugins/ScriptPlugins/ParserCSGOSM.js +++ b/Plugins/ScriptPlugins/ParserCSGOSM.js @@ -35,7 +35,7 @@ const plugin = { rconParser.Configuration.Status.Pattern = '^#\\s*(\\d+) (\\d+) "(.+)" (\\S+) +(\\d+:\\d+(?::\\d+)?) (\\d+) (\\S+) (\\S+) (\\d+) (\\d+\\.\\d+\\.\\d+\\.\\d+:\\d+)$'; rconParser.Configuration.Status.AddMapping(100, 2); - rconParser.Configuration.Status.AddMapping(101, 7); + rconParser.Configuration.Status.AddMapping(101, -1); rconParser.Configuration.Status.AddMapping(102, 6); rconParser.Configuration.Status.AddMapping(103, 4) rconParser.Configuration.Status.AddMapping(104, 3); @@ -90,6 +90,7 @@ const plugin = { rconParser.GameName = 10; // CSGO eventParser.Version = 'CSGOSM'; eventParser.GameName = 10; // CSGO + eventParser.URLProtocolFormat = 'steam://connect/{{ip}}:{{port}}'; }, onUnloadAsync: function () { diff --git a/Plugins/Stats/Client/HitCalculator.cs b/Plugins/Stats/Client/HitCalculator.cs index 02bf1c12e..b48b7f2f2 100644 --- a/Plugins/Stats/Client/HitCalculator.cs +++ b/Plugins/Stats/Client/HitCalculator.cs @@ -11,6 +11,7 @@ using Data.Models.Client.Stats.Reference; using Data.Models.Server; using IW4MAdmin.Plugins.Stats.Client.Abstractions; using IW4MAdmin.Plugins.Stats.Client.Game; +using IW4MAdmin.Plugins.Stats.Helpers; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SharedLibraryCore; @@ -147,7 +148,7 @@ namespace IW4MAdmin.Plugins.Stats.Client foreach (var client in gameEvent.Owner.GetClientsAsList()) { var scores = client.GetAdditionalProperty>(SessionScores); - scores?.Add((client.Score, DateTime.Now)); + scores?.Add((client.GetAdditionalProperty(StatManager.ESTIMATED_SCORE) ?? client.Score, DateTime.Now)); } } @@ -590,7 +591,7 @@ namespace IW4MAdmin.Plugins.Stats.Client if (sessionScores == null) { - _logger.LogWarning($"No session scores available for {client}"); + _logger.LogWarning("No session scores available for {Client}", client.ToString()); return; } @@ -600,7 +601,7 @@ namespace IW4MAdmin.Plugins.Stats.Client if (sessionScores.Count == 0) { - stat.Score += client.Score; + stat.Score += client.Score > 0 ? client.Score : client.GetAdditionalProperty(Helpers.StatManager.ESTIMATED_SCORE) ?? 0 * 50; } else diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index 312d145e9..4f9a651ea 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -38,6 +38,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers private static List serverModels; public static string CLIENT_STATS_KEY = "ClientStats"; public static string CLIENT_DETECTIONS_KEY = "ClientDetections"; + public static string ESTIMATED_SCORE = "EstimatedScore"; private readonly SemaphoreSlim _addPlayerWaiter = new SemaphoreSlim(1, 1); private readonly IServerDistributionCalculator _serverDistributionCalculator; @@ -859,7 +860,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers // update the total stats _servers[serverId].ServerStatistics.TotalKills += 1; - + // this happens when the round has changed if (attackerStats.SessionScore == 0) { @@ -871,18 +872,24 @@ namespace IW4MAdmin.Plugins.Stats.Helpers victimStats.LastScore = 0; } - attackerStats.SessionScore = attacker.Score; - victimStats.SessionScore = victim.Score; + var estimatedAttackerScore = attacker.Score > 0 ? attacker.Score : attackerStats.SessionKills * 50; + var estimatedVictimScore = victim.Score > 0 ? victim.Score : victimStats.SessionKills * 50; + + attackerStats.SessionScore = estimatedAttackerScore; + victimStats.SessionScore = estimatedVictimScore; + + attacker.SetAdditionalProperty(ESTIMATED_SCORE, estimatedAttackerScore); + victim.SetAdditionalProperty(ESTIMATED_SCORE, estimatedVictimScore); // calculate for the clients CalculateKill(attackerStats, victimStats); // this should fix the negative SPM // updates their last score after being calculated - attackerStats.LastScore = attacker.Score; - victimStats.LastScore = victim.Score; + attackerStats.LastScore = estimatedAttackerScore; + victimStats.LastScore = estimatedVictimScore; // show encouragement/discouragement - string streakMessage = (attackerStats.ClientId != victimStats.ClientId) + var streakMessage = (attackerStats.ClientId != victimStats.ClientId) ? StreakMessage.MessageOnStreak(attackerStats.KillStreak, attackerStats.DeathStreak) : StreakMessage.MessageOnStreak(-1, -1); @@ -1248,41 +1255,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers // process the attacker's stats after the kills attackerStats = UpdateStats(attackerStats); - #region DEPRECATED - - /* var validAttackerLobbyRatings = Servers[attackerStats.ServerId].PlayerStats - .Where(cs => cs.Value.ClientId != attackerStats.ClientId) - .Where(cs => - Servers[attackerStats.ServerId].IsTeamBased ? - cs.Value.Team != attackerStats.Team : - cs.Value.Team != IW4Info.Team.Spectator) - .Where(cs => cs.Value.Team != IW4Info.Team.Spectator); - - double attackerLobbyRating = validAttackerLobbyRatings.Count() > 0 ? - validAttackerLobbyRatings.Average(cs => cs.Value.EloRating) : - attackerStats.EloRating; - - var validVictimLobbyRatings = Servers[victimStats.ServerId].PlayerStats - .Where(cs => cs.Value.ClientId != victimStats.ClientId) - .Where(cs => - Servers[attackerStats.ServerId].IsTeamBased ? - cs.Value.Team != victimStats.Team : - cs.Value.Team != IW4Info.Team.Spectator) - .Where(cs => cs.Value.Team != IW4Info.Team.Spectator); - - double victimLobbyRating = validVictimLobbyRatings.Count() > 0 ? - validVictimLobbyRatings.Average(cs => cs.Value.EloRating) : - victimStats.EloRating;*/ - - #endregion - // calculate elo - double attackerEloDifference = Math.Log(Math.Max(1, victimStats.EloRating)) - + var attackerEloDifference = Math.Log(Math.Max(1, victimStats.EloRating)) - Math.Log(Math.Max(1, attackerStats.EloRating)); - double winPercentage = 1.0 / (1 + Math.Pow(10, attackerEloDifference / Math.E)); - - // double victimEloDifference = Math.Log(Math.Max(1, attackerStats.EloRating)) - Math.Log(Math.Max(1, victimStats.EloRating)); - // double lossPercentage = 1.0 / (1 + Math.Pow(10, victimEloDifference/ Math.E)); + var winPercentage = 1.0 / (1 + Math.Pow(10, attackerEloDifference / Math.E)); attackerStats.EloRating += 6.0 * (1 - winPercentage); victimStats.EloRating -= 6.0 * (1 - winPercentage); @@ -1314,10 +1290,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers return clientStats; } - double timeSinceLastCalc = (DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0; - double timeSinceLastActive = (DateTime.UtcNow - clientStats.LastActive).TotalSeconds / 60.0; + var timeSinceLastCalc = (DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0; - int scoreDifference = 0; + var scoreDifference = 0; // this means they've been tking or suicide and is the only time they can have a negative SPM if (clientStats.RoundScore < 0) { @@ -1329,17 +1304,17 @@ namespace IW4MAdmin.Plugins.Stats.Helpers scoreDifference = clientStats.RoundScore - clientStats.LastScore; } - double killSPM = scoreDifference / timeSinceLastCalc; - double spmMultiplier = 2.934 * + var killSpm = scoreDifference / timeSinceLastCalc; + var spmMultiplier = 2.934 * Math.Pow( _servers[clientStats.ServerId] .TeamCount((IW4Info.Team) clientStats.Team == IW4Info.Team.Allies ? IW4Info.Team.Axis : IW4Info.Team.Allies), -0.454); - killSPM *= Math.Max(1, spmMultiplier); + killSpm *= Math.Max(1, spmMultiplier); // update this for ac tracking - clientStats.SessionSPM = killSPM; + clientStats.SessionSPM = killSpm; // calculate how much the KDR should weigh // 1.637 is a Eddie-Generated number that weights the KDR nicely @@ -1358,7 +1333,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers 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)); + clientStats.SPM = (killSpm * SPMAgainstPlayWeight) + (clientStats.SPM * (1 - SPMAgainstPlayWeight)); if (clientStats.SPM < 0) { @@ -1373,7 +1348,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill)) { _log.LogWarning("clientStats SPM/Skill NaN {@killInfo}", - new {killSPM, KDRWeight, totalPlayTime, SPMAgainstPlayWeight, clientStats, scoreDifference}); + new {killSPM = killSpm, KDRWeight, totalPlayTime, SPMAgainstPlayWeight, clientStats, scoreDifference}); clientStats.SPM = 0; clientStats.Skill = 0; } diff --git a/Plugins/Stats/Plugin.cs b/Plugins/Stats/Plugin.cs index fe6456f45..3959be7e4 100644 --- a/Plugins/Stats/Plugin.cs +++ b/Plugins/Stats/Plugin.cs @@ -83,6 +83,7 @@ namespace IW4MAdmin.Plugins.Stats await Manager.Sync(S); break; case GameEvent.EventType.MapEnd: + Manager.ResetKillstreaks(S); await Manager.Sync(S); break; case GameEvent.EventType.Command: diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index 901cd125d..210550c1f 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -917,6 +917,18 @@ namespace SharedLibraryCore } } + public static async Task WithTimeout(this Task task, TimeSpan timeout) + { + await Task.WhenAny(task, Task.Delay(timeout)); + return await task; + } + + public static async Task WithTimeout(this Task task, TimeSpan timeout) + { + await Task.WhenAny(task, Task.Delay(timeout)); + } + + public static bool ShouldHideLevel(this Permission perm) => perm == Permission.Flagged; ///