diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index 671f0aa4..72891253 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -1042,8 +1042,8 @@ namespace IW4MAdmin EventParser = Manager.AdditionalEventParsers .FirstOrDefault(_parser => _parser.Version == ServerConfig.EventParserVersion); - RconParser = RconParser ?? Manager.AdditionalRConParsers[0]; - EventParser = EventParser ?? Manager.AdditionalEventParsers[0]; + RconParser ??= Manager.AdditionalRConParsers[0]; + EventParser ??= Manager.AdditionalEventParsers[0]; RemoteConnection = RConConnectionFactory.CreateConnection(IP, Port, Password, RconParser.RConEngine); RemoteConnection.SetConfiguration(RconParser); diff --git a/IW4MAdmin.sln b/IW4MAdmin.sln index 9f2f3205..06e2edea 100644 --- a/IW4MAdmin.sln +++ b/IW4MAdmin.sln @@ -378,8 +378,6 @@ Global {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|x64.Build.0 = Debug|Any CPU {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|x86.ActiveCfg = Debug|Any CPU {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|x86.Build.0 = Debug|Any CPU - {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU - {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Any CPU.Build.0 = Debug|Any CPU {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|x64.ActiveCfg = Debug|Any CPU @@ -394,6 +392,8 @@ Global {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Release|x64.Build.0 = Release|Any CPU {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Release|x86.ActiveCfg = Release|Any CPU {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Release|x86.Build.0 = Release|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|Any CPU.Build.0 = Debug|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -402,8 +402,6 @@ Global {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|x64.Build.0 = Debug|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|x86.ActiveCfg = Debug|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|x86.Build.0 = Debug|Any CPU - {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU - {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.Build.0 = Debug|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|x64.ActiveCfg = Debug|Any CPU @@ -418,6 +416,8 @@ Global {9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|x64.Build.0 = Release|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|x86.ActiveCfg = Release|Any CPU {9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|x86.Build.0 = Release|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Integrations/Cod/Integrations.Cod.csproj b/Integrations/Cod/Integrations.Cod.csproj index 26f66bae..fa9b57d7 100644 --- a/Integrations/Cod/Integrations.Cod.csproj +++ b/Integrations/Cod/Integrations.Cod.csproj @@ -4,6 +4,12 @@ netcoreapp3.1 Integrations.Cod Integrations.Cod + Debug;Release;Prerelease + AnyCPU + + + + true diff --git a/Integrations/Source/Extensions/SourceExtensions.cs b/Integrations/Source/Extensions/SourceExtensions.cs new file mode 100644 index 00000000..c373e286 --- /dev/null +++ b/Integrations/Source/Extensions/SourceExtensions.cs @@ -0,0 +1,48 @@ +using System.Text; + +namespace Integrations.Source.Extensions +{ + public static class SourceExtensions + { + public static string ReplaceUnfriendlyCharacters(this string source) + { + var result = new StringBuilder(); + var quoteStart = false; + var quoteIndex = 0; + var index = 0; + + foreach (var character in source) + { + if (character == '%') + { + result.Append('‰'); + } + + else if ((character == '"' || character == '\'') && index + 1 != source.Length) + { + if (quoteIndex > 0) + { + result.Append(!quoteStart ? "«" : "»"); + quoteStart = !quoteStart; + } + + else + { + result.Append('"'); + } + + quoteIndex++; + } + + else + { + result.Append(character); + } + + index++; + } + + return result.ToString(); + } + } +} \ No newline at end of file diff --git a/Integrations/Source/Integrations.Source.csproj b/Integrations/Source/Integrations.Source.csproj index 9835107f..da504ccd 100644 --- a/Integrations/Source/Integrations.Source.csproj +++ b/Integrations/Source/Integrations.Source.csproj @@ -4,6 +4,12 @@ netcoreapp3.1 Integrations.Source Integrations.Source + Debug;Release;Prerelease + AnyCPU + + + + true diff --git a/Integrations/Source/SourceRConConnection.cs b/Integrations/Source/SourceRConConnection.cs index 5b7fe232..cfab5682 100644 --- a/Integrations/Source/SourceRConConnection.cs +++ b/Integrations/Source/SourceRConConnection.cs @@ -1,6 +1,9 @@ -using System.Linq; +using System; +using System.Linq; using System.Net.Sockets; +using System.Threading; using System.Threading.Tasks; +using Integrations.Source.Extensions; using Integrations.Source.Interfaces; using Microsoft.Extensions.Logging; using RconSharp; @@ -20,82 +23,145 @@ namespace Integrations.Source private readonly string _hostname; private readonly int _port; private readonly IRConClientFactory _rconClientFactory; + private readonly SemaphoreSlim _activeQuery; + private static readonly TimeSpan FloodDelay = TimeSpan.FromMilliseconds(250); + + private DateTime _lastQuery = DateTime.Now; private RconClient _rconClient; public SourceRConConnection(ILogger logger, IRConClientFactory rconClientFactory, string hostname, int port, string password) { - _rconClient = rconClientFactory.CreateClient(hostname, port); _rconClientFactory = rconClientFactory; _password = password; _hostname = hostname; _port = port; _logger = logger; + _rconClient = _rconClientFactory.CreateClient(_hostname, _port); + _activeQuery = new SemaphoreSlim(1, 1); + } + + ~SourceRConConnection() + { + _activeQuery.Dispose(); } public async Task SendQueryAsync(StaticHelpers.QueryType type, string parameters = "") { - await _rconClient.ConnectAsync(); - - bool authenticated; - try { - 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) + await _activeQuery.WaitAsync(); + var diff = DateTime.Now - _lastQuery; + if (diff < FloodDelay) + { + await Task.Delay(FloodDelay - diff); + } + + using (LogContext.PushProperty("Server", $"{_hostname}:{_port}")) + { + _logger.LogDebug("Connecting to RCon socket"); + } + + await _rconClient.ConnectAsync(); + + bool authenticated; + + try { using (LogContext.PushProperty("Server", $"{_hostname}:{_port}")) { - _logger.LogWarning(ex, - "Server appears to resumed from hibernation, so we are using a new socket"); + _logger.LogDebug("Authenticating to RCon socket"); } - _rconClient = _rconClientFactory.CreateClient(_hostname, _port); + 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"); + } + + if (type == StaticHelpers.QueryType.COMMAND_STATUS) + { + parameters = "status"; + } + + parameters = parameters.ReplaceUnfriendlyCharacters(); + parameters = parameters.StripColors(); using (LogContext.PushProperty("Server", $"{_hostname}:{_port}")) { - _logger.LogError("Could not login to server"); + _logger.LogDebug("Sending query {Type} with parameters \"{Parameters}\"", type, parameters); } - throw new NetworkException("Could not authenticate with server"); + var response = await _rconClient.ExecuteCommandAsync(parameters, true); + + using (LogContext.PushProperty("Server", $"{_rconClient.Host}:{_rconClient.Port}")) + { + _logger.LogDebug("Received RCon response {Response}", response); + } + + var split = response.TrimEnd('\n').Split('\n'); + return split.Take(split.Length - 1).ToArray(); } - if (!authenticated) + catch (Exception ex) when (ex.GetType() != typeof(NetworkException) && + ex.GetType() != typeof(ServerException)) { using (LogContext.PushProperty("Server", $"{_hostname}:{_port}")) { - _logger.LogError("Could not login to server"); + _logger.LogError(ex, "Could not execute RCon query {Parameters}", parameters); } - throw new ServerException("Could not authenticate to server with provided password"); + throw new NetworkException("Unable to communicate with server"); } - if (type == StaticHelpers.QueryType.COMMAND_STATUS) + finally { - parameters = "status"; + if (_activeQuery.CurrentCount == 0) + { + _activeQuery.Release(); + } + + _lastQuery = DateTime.Now; } - - using (LogContext.PushProperty("Server", $"{_hostname}:{_port}")) - { - _logger.LogDebug("Sending query {Type} with parameters {Parameters}", type, parameters); - } - - var response = await _rconClient.ExecuteCommandAsync(parameters.StripColors(), true); - - using (LogContext.PushProperty("Server", $"{_rconClient.Host}:{_rconClient.Port}")) - { - _logger.LogDebug("Received RCon response {Response}", response); - } - - var split = response.TrimEnd('\n').Split('\n'); - return split.Take(split.Length - 1).ToArray(); } public void SetConfiguration(IRConParser config) diff --git a/Plugins/ScriptPlugins/ParserCSGO.js b/Plugins/ScriptPlugins/ParserCSGO.js index 53f96b85..37e0f4fa 100644 --- a/Plugins/ScriptPlugins/ParserCSGO.js +++ b/Plugins/ScriptPlugins/ParserCSGO.js @@ -3,7 +3,7 @@ let eventParser; const plugin = { author: 'RaidMax', - version: 0.1, + version: 0.2, name: 'CS:GO Parser', engine: 'Source', isParser: true, @@ -12,8 +12,8 @@ const plugin = { }, onLoadAsync: function (manager) { - rconParser = manager.GenerateDynamicRConParser(this.engine); - eventParser = manager.GenerateDynamicEventParser(this.engine); + rconParser = manager.GenerateDynamicRConParser(this.name); + eventParser = manager.GenerateDynamicEventParser(this.name); rconParser.RConEngine = this.engine; rconParser.Configuration.StatusHeader.Pattern = 'userid +name +uniqueid +connected +ping +loss +state +rate +adr'; @@ -24,16 +24,16 @@ const plugin = { rconParser.Configuration.HostnameStatus.Pattern = '^hostname: +(.+)$'; rconParser.Configuration.MapStatus.AddMapping(113, 1); - rconParser.Configuration.MaxPlayersStatus.Pattern = '^players *: +\\d humans, \\d bots \\((\\d+).+'; + rconParser.Configuration.MaxPlayersStatus.Pattern = '^players *: +\\d+ humans, \\d+ bots \\((\\d+).+'; rconParser.Configuration.MapStatus.AddMapping(114, 1); - rconParser.Configuration.Dvar.Pattern = '^"(.+)" = (?:"(.+)" (?:\\( def\\. "(.*)" \\))|"(.+)" +(.+)) +- (.*)$'; + rconParser.Configuration.Dvar.Pattern = '^"(.+)" = "(.+)" (?:\\( def. "(.*)" \\))?(?: |\\w)+- (.+)$'; rconParser.Configuration.Dvar.AddMapping(106, 1); rconParser.Configuration.Dvar.AddMapping(107, 2); rconParser.Configuration.Dvar.AddMapping(108, 3); rconParser.Configuration.Dvar.AddMapping(109, 3); - rconParser.Configuration.Status.Pattern = '^#\\s*(\\d+) (\\d+) "(.+)" (\\S+) (\\d+:\\d+) (\\d+) (\\S+) (\\S+) (\\d+) (\\d+\\.\\d+\\.\\d+\\.\\d+:\\d+)$'; + 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(102, 6); diff --git a/Plugins/ScriptPlugins/ParserCSGOSM.js b/Plugins/ScriptPlugins/ParserCSGOSM.js index 2a97bd2c..01e00410 100644 --- a/Plugins/ScriptPlugins/ParserCSGOSM.js +++ b/Plugins/ScriptPlugins/ParserCSGOSM.js @@ -3,7 +3,7 @@ let eventParser; const plugin = { author: 'RaidMax', - version: 0.1, + version: 0.2, name: 'CS:GO (SourceMod) Parser', engine: 'Source', isParser: true, @@ -12,8 +12,8 @@ const plugin = { }, onLoadAsync: function (manager) { - rconParser = manager.GenerateDynamicRConParser(this.engine); - eventParser = manager.GenerateDynamicEventParser(this.engine); + rconParser = manager.GenerateDynamicRConParser(this.name); + eventParser = manager.GenerateDynamicEventParser(this.name); rconParser.RConEngine = this.engine; rconParser.Configuration.StatusHeader.Pattern = 'userid +name +uniqueid +connected +ping +loss +state +rate +adr'; @@ -24,16 +24,16 @@ const plugin = { rconParser.Configuration.HostnameStatus.Pattern = '^hostname: +(.+)$'; rconParser.Configuration.MapStatus.AddMapping(113, 1); - rconParser.Configuration.MaxPlayersStatus.Pattern = '^players *: +\\d humans, \\d bots \\((\\d+).+'; + rconParser.Configuration.MaxPlayersStatus.Pattern = '^players *: +\\d+ humans, \\d+ bots \\((\\d+).+'; rconParser.Configuration.MapStatus.AddMapping(114, 1); - rconParser.Configuration.Dvar.Pattern = '^"(.+)" = (?:"(.+)" (?:\\( def\\. "(.*)" \\))|"(.+)" +(.+)) +- (.*)$'; + rconParser.Configuration.Dvar.Pattern = '^"(.+)" = "(.+)" (?:\\( def. "(.*)" \\))?(?: |\\w)+- (.+)$'; rconParser.Configuration.Dvar.AddMapping(106, 1); rconParser.Configuration.Dvar.AddMapping(107, 2); rconParser.Configuration.Dvar.AddMapping(108, 3); rconParser.Configuration.Dvar.AddMapping(109, 3); - rconParser.Configuration.Status.Pattern = '^#\\s*(\\d+) (\\d+) "(.+)" (\\S+) (\\d+:\\d+) (\\d+) (\\S+) (\\S+) (\\d+) (\\d+\\.\\d+\\.\\d+\\.\\d+:\\d+)$'; + 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(102, 6); @@ -64,7 +64,7 @@ const plugin = { rconParser.Configuration.CommandPrefixes.Ban = 'sm_kick #{0} {1}'; rconParser.Configuration.CommandPrefixes.TempBan = 'sm_kick #{0} {1}'; rconParser.Configuration.CommandPrefixes.Say = 'sm_say {0}'; - rconParser.Configuration.CommandPrefixes.Tell = 'sm_psay #{0} {1}'; + rconParser.Configuration.CommandPrefixes.Tell = 'sm_psay #{0} "{1}"'; eventParser.Configuration.Say.Pattern = '^"(.+)<(\\d+)><(.+)><(.*?)>" say "(.*)"$'; eventParser.Configuration.Say.AddMapping(5, 1);