diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index 1b5f02a6..f3f8de0c 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -972,7 +972,7 @@ namespace IW4MAdmin if (!string.IsNullOrEmpty(svRunning.Value) && svRunning.Value != "1") { - throw new ServerException(loc["SERVER_ERROR_NOT_RUNNING"]); + throw new ServerException(loc["SERVER_ERROR_NOT_RUNNING"].FormatExt(this.ToString())); } var infoResponse = RconParser.Configuration.CommandPrefixes.RConGetInfo != null ? await this.GetInfoAsync() : null; @@ -1042,9 +1042,10 @@ namespace IW4MAdmin } if (needsRestart) - { - Logger.WriteWarning("Game log file not properly initialized, restarting map..."); - await this.ExecuteCommandAsync("map_restart"); + { + // disabling this for the time being + /*Logger.WriteWarning("Game log file not properly initialized, restarting map..."); + await this.ExecuteCommandAsync("map_restart");*/ } // this DVAR isn't set until the a map is loaded diff --git a/Application/RCon/RConConnection.cs b/Application/RCon/RConConnection.cs index ca131f4c..8dbadbb3 100644 --- a/Application/RCon/RConConnection.cs +++ b/Application/RCon/RConConnection.cs @@ -49,9 +49,11 @@ namespace IW4MAdmin.Application.RCon var connectionState = ActiveQueries[this.Endpoint]; -#if DEBUG == true - _log.WriteDebug($"Waiting for semaphore to be released [{this.Endpoint}]"); -#endif + if (Utilities.IsDevelopment) + { + _log.WriteDebug($"Waiting for semaphore to be released [{this.Endpoint}]"); + } + // enter the semaphore so only one query is sent at a time per server. await connectionState.OnComplete.WaitAsync(); @@ -64,10 +66,11 @@ namespace IW4MAdmin.Application.RCon connectionState.LastQuery = DateTime.Now; -#if DEBUG == true - _log.WriteDebug($"Semaphore has been released [{this.Endpoint}]"); - _log.WriteDebug($"Query [{this.Endpoint},{type.ToString()},{parameters}]"); -#endif + if (Utilities.IsDevelopment) + { + _log.WriteDebug($"Semaphore has been released [{Endpoint}]"); + _log.WriteDebug($"Query [{Endpoint},{type},{parameters}]"); + } byte[] payload = null; bool waitForResponse = config.WaitForResponse; @@ -133,6 +136,7 @@ namespace IW4MAdmin.Application.RCon connectionState.OnReceivedData.Reset(); connectionState.ConnectionAttempts++; connectionState.BytesReadPerSegment.Clear(); + bool exceptionCaught = false; #if DEBUG == true _log.WriteDebug($"Sending {payload.Length} bytes to [{this.Endpoint}] ({connectionState.ConnectionAttempts}/{StaticHelpers.AllowedConnectionFails})"); #endif @@ -150,9 +154,11 @@ namespace IW4MAdmin.Application.RCon catch { + // we want to retry with a delay if (connectionState.ConnectionAttempts < StaticHelpers.AllowedConnectionFails) { - await Task.Delay(StaticHelpers.FloodProtectionInterval); + exceptionCaught = true; + await Task.Delay(StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts)); goto retrySend; } @@ -161,7 +167,8 @@ namespace IW4MAdmin.Application.RCon finally { - if (connectionState.OnComplete.CurrentCount == 0) + // we don't want to release if we're going to retry the query + if (connectionState.OnComplete.CurrentCount == 0 && !exceptionCaught) { connectionState.OnComplete.Release(1); } @@ -170,13 +177,12 @@ namespace IW4MAdmin.Application.RCon if (response.Length == 0) { - _log.WriteWarning($"Received empty response for request [{type.ToString()}, {parameters}, {Endpoint.ToString()}]"); + _log.WriteWarning($"Received empty response for request [{type}, {parameters}, {Endpoint}]"); return new string[0]; } string responseString = type == StaticHelpers.QueryType.COMMAND_STATUS ? - ReassembleSegmentedStatus(response) : - _gameEncoding.GetString(response[0]) + '\n'; + ReassembleSegmentedStatus(response) : RecombineMessages(response); // note: not all games respond if the pasword is wrong or not set if (responseString.Contains("Invalid password") || responseString.Contains("rconpassword")) @@ -234,6 +240,35 @@ namespace IW4MAdmin.Application.RCon return string.Join("", splitStatusStrings); } + /// + /// Recombines multiple game messages into one + /// + /// + /// + private string RecombineMessages(byte[][] payload) + { + if (payload.Length == 1) + { + return _gameEncoding.GetString(payload[0]).TrimEnd('\n') + '\n'; + } + + else + { + var builder = new StringBuilder(); + for (int i = 0; i < payload.Length; i++) + { + string message = _gameEncoding.GetString(payload[i]).TrimEnd('\n') + '\n'; + if (i > 0) + { + message = message.Replace(config.CommandPrefixes.RConResponse, ""); + } + builder.Append(message); + } + builder.Append('\n'); + return builder.ToString(); + } + } + private async Task SendPayloadAsync(byte[] payload, bool waitForResponse) { var connectionState = ActiveQueries[this.Endpoint]; @@ -259,7 +294,8 @@ namespace IW4MAdmin.Application.RCon if (sendDataPending) { // the send has not been completed asyncronously - if (!await Task.Run(() => connectionState.OnSentData.Wait(StaticHelpers.SocketTimeout))) + // this really shouldn't ever happen because it's UDP + if (!await Task.Run(() => connectionState.OnSentData.Wait(StaticHelpers.SocketTimeout(1)))) { rconSocket.Close(); throw new NetworkException("Timed out sending data", rconSocket); @@ -278,7 +314,11 @@ namespace IW4MAdmin.Application.RCon if (receiveDataPending) { - if (!await Task.Run(() => connectionState.OnReceivedData.Wait(10000))) + if (Utilities.IsDevelopment) + { + _log.WriteDebug($"Waiting to asynchrously receive data on attempt #{connectionState.ConnectionAttempts}"); + } + if (!await Task.Run(() => connectionState.OnReceivedData.Wait(StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts)))) { rconSocket.Close(); throw new NetworkException("Timed out waiting for response", rconSocket); @@ -287,6 +327,11 @@ namespace IW4MAdmin.Application.RCon rconSocket.Close(); + return GetResponseData(connectionState); + } + + private byte[][] GetResponseData(ConnectionState connectionState) + { var responseList = new List(); int totalBytesRead = 0; @@ -305,9 +350,10 @@ namespace IW4MAdmin.Application.RCon private void OnDataReceived(object sender, SocketAsyncEventArgs e) { -#if DEBUG == true - _log.WriteDebug($"Read {e.BytesTransferred} bytes from {e.RemoteEndPoint.ToString()}"); -#endif + if (Utilities.IsDevelopment) + { + _log.WriteDebug($"Read {e.BytesTransferred} bytes from {e.RemoteEndPoint}"); + } // this occurs when we close the socket if (e.BytesTransferred == 0) @@ -330,9 +376,10 @@ namespace IW4MAdmin.Application.RCon if (!sock.ReceiveAsync(state.ReceiveEventArgs)) { -#if DEBUG == true - _log.WriteDebug($"Read {state.ReceiveEventArgs.BytesTransferred} synchronous bytes from {e.RemoteEndPoint.ToString()}"); -#endif + if (Utilities.IsDevelopment) + { + _log.WriteDebug($"Read {state.ReceiveEventArgs.BytesTransferred} synchronous bytes from {e.RemoteEndPoint}"); + } // we need to increment this here because the callback isn't executed if there's no pending IO state.BytesReadPerSegment.Add(state.ReceiveEventArgs.BytesTransferred); ActiveQueries[this.Endpoint].OnReceivedData.Set(); @@ -354,9 +401,10 @@ namespace IW4MAdmin.Application.RCon private void OnDataSent(object sender, SocketAsyncEventArgs e) { -#if DEBUG == true - _log.WriteDebug($"Sent {e.Buffer?.Length} bytes to {e.ConnectSocket?.RemoteEndPoint?.ToString()}"); -#endif + if (Utilities.IsDevelopment) + { + _log.WriteDebug($"Sent {e.Buffer?.Length} bytes to {e.ConnectSocket?.RemoteEndPoint?.ToString()}"); + } ActiveQueries[this.Endpoint].OnSentData.Set(); } } diff --git a/Application/RconParsers/BaseRConParser.cs b/Application/RconParsers/BaseRConParser.cs index d1af19c5..d797111c 100644 --- a/Application/RconParsers/BaseRConParser.cs +++ b/Application/RconParsers/BaseRConParser.cs @@ -76,7 +76,22 @@ namespace IW4MAdmin.Application.RconParsers public async Task> GetDvarAsync(IRConConnection connection, string dvarName, T fallbackValue = default) { - string[] lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName); + string[] lineSplit; + + try + { + lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName); + } + catch + { + if (fallbackValue == null) + { + throw; + } + + lineSplit = new string[0]; + } + string response = string.Join('\n', lineSplit).TrimEnd('\0'); var match = Regex.Match(response, Configuration.Dvar.Pattern); diff --git a/IW4MAdmin.sln b/IW4MAdmin.sln index 6190d1e4..c2dfee15 100644 --- a/IW4MAdmin.sln +++ b/IW4MAdmin.sln @@ -35,6 +35,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlug Plugins\ScriptPlugins\ActionOnReport.js = Plugins\ScriptPlugins\ActionOnReport.js Plugins\ScriptPlugins\ParserCoD4x.js = Plugins\ScriptPlugins\ParserCoD4x.js Plugins\ScriptPlugins\ParserIW4x.js = Plugins\ScriptPlugins\ParserIW4x.js + Plugins\ScriptPlugins\ParserIW6x.js = Plugins\ScriptPlugins\ParserIW6x.js Plugins\ScriptPlugins\ParserPIW5.js = Plugins\ScriptPlugins\ParserPIW5.js Plugins\ScriptPlugins\ParserPT6.js = Plugins\ScriptPlugins\ParserPT6.js Plugins\ScriptPlugins\ParserRektT5M.js = Plugins\ScriptPlugins\ParserRektT5M.js diff --git a/Plugins/ScriptPlugins/ParserIW6x.js b/Plugins/ScriptPlugins/ParserIW6x.js new file mode 100644 index 00000000..facfedab --- /dev/null +++ b/Plugins/ScriptPlugins/ParserIW6x.js @@ -0,0 +1,43 @@ +var rconParser; +var eventParser; + +var plugin = { + author: 'Xerxes, RaidMax', + version: 0.1, + name: 'IW6x Parser', + isParser: true, + + onEventAsync: function (gameEvent, server) { + }, + + onLoadAsync: function (manager) { + rconParser = manager.GenerateDynamicRConParser(this.name); + eventParser = manager.GenerateDynamicEventParser(this.name); + + rconParser.Configuration.CommandPrefixes.Tell = 'tell {0} {1}'; + rconParser.Configuration.CommandPrefixes.Say = 'say {0}'; + rconParser.Configuration.CommandPrefixes.Kick = 'clientkick {0} "{1}"'; + rconParser.Configuration.CommandPrefixes.Ban = 'clientkick {0} "{1}"'; + rconParser.Configuration.CommandPrefixes.TempBan = 'clientkick {0} "{1}"'; + rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n'; + rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n(?:latched: \\"(.+)?\\"\\n)? *(.+)$'; + rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +(Yes|No) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +(\d+\.\d+\.\d+.\d+\:-*\d{1,5}|0+.0+:-*\d{1,5}|loopback|unknown|bot) +(-*[0-9]+) *$'; + rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +address +qport *'; + rconParser.Configuration.WaitForResponse = false; + rconParser.Configuration.Status.AddMapping(102, 4); + rconParser.Configuration.Status.AddMapping(103, 5); + rconParser.Configuration.Status.AddMapping(104, 6); + + rconParser.Version = 'IW6 MP 3.15 build 2 Sat Sep 14 2013 03:58:30PM win64'; + rconParser.GameName = 4; // IW6 + eventParser.Version = 'IW6 MP 3.15 build 2 Sat Sep 14 2013 03:58:30PM win64'; + eventParser.GameName = 4; // IW6 + eventParser.Configuration.GameDirectory = 'iw6x'; + }, + + onUnloadAsync: function () { + }, + + onTickAsync: function (server) { + } +}; diff --git a/SharedLibraryCore/RCon/StaticHelpers.cs b/SharedLibraryCore/RCon/StaticHelpers.cs index eb1e7436..68625a5a 100644 --- a/SharedLibraryCore/RCon/StaticHelpers.cs +++ b/SharedLibraryCore/RCon/StaticHelpers.cs @@ -49,14 +49,23 @@ namespace SharedLibraryCore.RCon /// /// timeout in seconds to wait for a socket send or receive before giving up /// - public static readonly int SocketTimeout = 10000; + public static TimeSpan SocketTimeout(int retryAttempt) + { + return retryAttempt switch + { + 1 => TimeSpan.FromMilliseconds(550), + 2 => TimeSpan.FromMilliseconds(1000), + 3 => TimeSpan.FromMilliseconds(2000), + _ => TimeSpan.FromMilliseconds(5000), + }; + } /// /// interval in milliseconds to wait before sending the next RCon request /// - public static readonly int FloodProtectionInterval = 650; + public static readonly int FloodProtectionInterval = 750; /// /// how many failed connection attempts before aborting connection /// - public static readonly int AllowedConnectionFails = 3; + public static readonly int AllowedConnectionFails = 4; } } diff --git a/SharedLibraryCore/Server.cs b/SharedLibraryCore/Server.cs index 58332bb9..2428ce30 100644 --- a/SharedLibraryCore/Server.cs +++ b/SharedLibraryCore/Server.cs @@ -269,7 +269,7 @@ namespace SharedLibraryCore { try { - return (await this.GetDvarAsync("sv_customcallbacks")).Value == "1"; + return (await this.GetDvarAsync("sv_customcallbacks", "0")).Value == "1"; } catch (Exceptions.DvarException)