From 8c29027b3fbb8ed78831babe5f8ddc65591de6c1 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Fri, 17 Apr 2020 15:05:16 -0500 Subject: [PATCH] partial T7 (BO3) support. includes rcon communication improvements and a small fix for displaying live radar tab --- Application/IW4MServer.cs | 20 +-- Application/RCon/ConnectionState.cs | 2 + Application/RCon/RConConnection.cs | 115 +++++++++++++++--- Application/RconParsers/BaseRConParser.cs | 57 +++++---- .../DynamicRConParserConfiguration.cs | 2 + IW4MAdmin.sln | 1 + Plugins/LiveRadar/Plugin.cs | 14 ++- Plugins/ScriptPlugins/ParserCoD4x.js | 3 +- Plugins/ScriptPlugins/ParserPIW5.js | 7 +- Plugins/ScriptPlugins/ParserPT6.js | 3 +- Plugins/ScriptPlugins/ParserT7.js | 45 +++++++ SharedLibraryCore/Interfaces/IRConParser.cs | 3 +- .../Interfaces/IRConParserConfiguration.cs | 5 + SharedLibraryCore/Utilities.cs | 9 +- 14 files changed, 226 insertions(+), 60 deletions(-) create mode 100644 Plugins/ScriptPlugins/ParserT7.js diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index 7d4f88ed0..63306b672 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -911,7 +911,8 @@ namespace IW4MAdmin Version = RconParser.Version; } - var svRunning = await this.GetDvarAsync("sv_running"); + // these T7 specific things aren't ideal , but it's a quick fix + var svRunning = await this.GetDvarAsync("sv_running", GameName == Game.T7 ? "1" : null); if (!string.IsNullOrEmpty(svRunning.Value) && svRunning.Value != "1") { @@ -924,7 +925,7 @@ namespace IW4MAdmin (await this.GetDvarAsync("sv_hostname")).Value : infoResponse.Where(kvp => kvp.Key.Contains("hostname")).Select(kvp => kvp.Value).First(); var mapname = infoResponse == null ? - (await this.GetDvarAsync("mapname")).Value : + (await this.GetDvarAsync("mapname", "Unknown")).Value : infoResponse["mapname"]; int maxplayers = (GameName == Game.IW4) ? // gotta love IW4 idiosyncrasies (await this.GetDvarAsync("party_maxplayers")).Value : @@ -932,12 +933,12 @@ namespace IW4MAdmin (await this.GetDvarAsync("sv_maxclients")).Value : Convert.ToInt32(infoResponse["sv_maxclients"]); var gametype = infoResponse == null ? - (await this.GetDvarAsync("g_gametype")).Value : + (await this.GetDvarAsync("g_gametype", GameName == Game.T7 ? "" : null)).Value : infoResponse.Where(kvp => kvp.Key.Contains("gametype")).Select(kvp => kvp.Value).First(); - var basepath = await this.GetDvarAsync("fs_basepath"); - var basegame = await this.GetDvarAsync("fs_basegame"); + var basepath = await this.GetDvarAsync("fs_basepath", GameName == Game.T7 ? "" : null); + var basegame = await this.GetDvarAsync("fs_basegame", GameName == Game.T7 ? "" : null); var game = infoResponse == null || !infoResponse.ContainsKey("fs_game") ? - (await this.GetDvarAsync("fs_game")).Value : + (await this.GetDvarAsync("fs_game", GameName == Game.T7 ? "" : null)).Value : infoResponse["fs_game"]; var logfile = await this.GetDvarAsync("g_log"); var logsync = await this.GetDvarAsync("g_logsync"); @@ -1002,9 +1003,14 @@ namespace IW4MAdmin CustomCallback = await ScriptLoaded(); // they've manually specified the log path - if (!string.IsNullOrEmpty(ServerConfig.ManualLogPath)) + if (!string.IsNullOrEmpty(ServerConfig.ManualLogPath) || !RconParser.CanGenerateLogPath) { LogPath = ServerConfig.ManualLogPath; + + if (string.IsNullOrEmpty(LogPath) && !RconParser.CanGenerateLogPath) + { + throw new ServerException(loc["SERVER_ERROR_REQUIRES_PATH"].FormatExt(GameName.ToString())); + } } else diff --git a/Application/RCon/ConnectionState.cs b/Application/RCon/ConnectionState.cs index 0dcfcc660..00595be23 100644 --- a/Application/RCon/ConnectionState.cs +++ b/Application/RCon/ConnectionState.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net.Sockets; using System.Threading; @@ -22,6 +23,7 @@ namespace IW4MAdmin.Application.RCon public readonly SemaphoreSlim OnComplete = new SemaphoreSlim(1, 1); public readonly ManualResetEventSlim OnSentData = new ManualResetEventSlim(false); public readonly ManualResetEventSlim OnReceivedData = new ManualResetEventSlim(false); + public List BytesReadPerSegment { get; set; } = new List(); public SocketAsyncEventArgs SendEventArgs { get; set; } = new SocketAsyncEventArgs(); public SocketAsyncEventArgs ReceiveEventArgs { get; set; } = new SocketAsyncEventArgs(); public DateTime LastQuery { get; set; } = DateTime.Now; diff --git a/Application/RCon/RConConnection.cs b/Application/RCon/RConConnection.cs index 63616a812..3520d79c1 100644 --- a/Application/RCon/RConConnection.cs +++ b/Application/RCon/RConConnection.cs @@ -4,6 +4,7 @@ using SharedLibraryCore.Interfaces; using SharedLibraryCore.RCon; using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; @@ -116,7 +117,7 @@ namespace IW4MAdmin.Application.RCon throw new NetworkException($"Invalid character encountered when converting encodings - {parameters}"); } - byte[] response = null; + byte[][] response = null; retrySend: using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp) @@ -130,6 +131,7 @@ namespace IW4MAdmin.Application.RCon connectionState.OnSentData.Reset(); connectionState.OnReceivedData.Reset(); connectionState.ConnectionAttempts++; + connectionState.BytesReadPerSegment.Clear(); #if DEBUG == true _log.WriteDebug($"Sending {payload.Length} bytes to [{this.Endpoint}] ({connectionState.ConnectionAttempts}/{StaticHelpers.AllowedConnectionFails})"); #endif @@ -137,7 +139,7 @@ namespace IW4MAdmin.Application.RCon { response = await SendPayloadAsync(payload, waitForResponse); - if (response.Length == 0 && waitForResponse) + if ((response.Length == 0 || response[0].Length == 0) && waitForResponse) { throw new NetworkException("Expected response but got 0 bytes back"); } @@ -165,7 +167,9 @@ namespace IW4MAdmin.Application.RCon } } - string responseString = _gameEncoding.GetString(response, 0, response.Length) + '\n'; + string responseString = type == StaticHelpers.QueryType.COMMAND_STATUS ? + ReassembleSegmentedStatus(response) : + _gameEncoding.GetString(response[0]) + '\n'; // note: not all games respond if the pasword is wrong or not set if (responseString.Contains("Invalid password") || responseString.Contains("rconpassword")) @@ -183,13 +187,46 @@ namespace IW4MAdmin.Application.RCon throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_NOT_RUNNING"].FormatExt(Endpoint.ToString())); } - string[] splitResponse = responseString.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries) - .Select(line => line.Trim()) - .ToArray(); + string[] headerSplit = responseString.Split(config.CommandPrefixes.RConResponse); + + if (headerSplit.Length != 2 && type != StaticHelpers.QueryType.GET_INFO) + { + throw new NetworkException("Unexpected response header from server"); + } + + string[] splitResponse = headerSplit.Last().Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); return splitResponse; } - private async Task SendPayloadAsync(byte[] payload, bool waitForResponse) + /// + /// reassembles broken status segments into the 'correct' ordering + /// this is primarily for T7, and is really only reliable for 2 segments + /// + /// array of segmented byte arrays + /// + public string ReassembleSegmentedStatus(byte[][] segments) + { + var splitStatusStrings = new List(); + + foreach (byte[] segment in segments) + { + string responseString = _gameEncoding.GetString(segment, 0, segment.Length); + var statusHeaderMatch = config.StatusHeader.PatternMatcher.Match(responseString); + if (statusHeaderMatch.Success) + { + splitStatusStrings.Insert(0, responseString); + } + + else + { + splitStatusStrings.Add(responseString.Replace(config.CommandPrefixes.RConResponse, "")); + } + } + + return string.Join("", splitStatusStrings); + } + + private async Task SendPayloadAsync(byte[] payload, bool waitForResponse) { var connectionState = ActiveQueries[this.Endpoint]; var rconSocket = (Socket)connectionState.SendEventArgs.UserToken; @@ -223,7 +260,7 @@ namespace IW4MAdmin.Application.RCon if (!waitForResponse) { - return new byte[0]; + return new byte[0][]; } connectionState.ReceiveEventArgs.SetBuffer(connectionState.ReceiveBuffer); @@ -233,7 +270,7 @@ namespace IW4MAdmin.Application.RCon if (receiveDataPending) { - if (!await Task.Run(() => connectionState.OnReceivedData.Wait(StaticHelpers.SocketTimeout))) + if (!await Task.Run(() => connectionState.OnReceivedData.Wait(10000))) { rconSocket.Close(); throw new NetworkException("Timed out waiting for response", rconSocket); @@ -242,11 +279,20 @@ namespace IW4MAdmin.Application.RCon rconSocket.Close(); - byte[] response = connectionState.ReceiveBuffer - .Take(connectionState.ReceiveEventArgs.BytesTransferred) - .ToArray(); + var responseList = new List(); + int totalBytesRead = 0; - return response; + foreach (int bytesRead in connectionState.BytesReadPerSegment) + { + responseList.Add(connectionState.ReceiveBuffer + .Skip(totalBytesRead) + .Take(bytesRead) + .ToArray()); + + totalBytesRead += bytesRead; + } + + return responseList.ToArray(); } private void OnDataReceived(object sender, SocketAsyncEventArgs e) @@ -254,7 +300,48 @@ namespace IW4MAdmin.Application.RCon #if DEBUG == true _log.WriteDebug($"Read {e.BytesTransferred} bytes from {e.RemoteEndPoint.ToString()}"); #endif - ActiveQueries[this.Endpoint].OnReceivedData.Set(); + + // this occurs when we close the socket + if (e.BytesTransferred == 0) + { + ActiveQueries[this.Endpoint].OnReceivedData.Set(); + return; + } + + if (sender is Socket sock) + { + var state = ActiveQueries[this.Endpoint]; + state.BytesReadPerSegment.Add(e.BytesTransferred); + + try + { + // we still have available data so the payload was segmented + if (sock.Available > 0) + { + state.ReceiveEventArgs.SetBuffer(state.ReceiveBuffer, e.BytesTransferred, state.ReceiveBuffer.Length - e.BytesTransferred); + + if (!sock.ReceiveAsync(state.ReceiveEventArgs)) + { +#if DEBUG == true + _log.WriteDebug($"Read {state.ReceiveEventArgs.BytesTransferred} synchronous bytes from {e.RemoteEndPoint.ToString()}"); +#endif + // 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(); + } + } + + else + { + ActiveQueries[this.Endpoint].OnReceivedData.Set(); + } + } + + catch (ObjectDisposedException) + { + ActiveQueries[this.Endpoint].OnReceivedData.Set(); + } + } } private void OnDataSent(object sender, SocketAsyncEventArgs e) diff --git a/Application/RconParsers/BaseRConParser.cs b/Application/RconParsers/BaseRConParser.cs index 0b3ecf3bb..e559e95e1 100644 --- a/Application/RconParsers/BaseRConParser.cs +++ b/Application/RconParsers/BaseRConParser.cs @@ -52,6 +52,7 @@ namespace IW4MAdmin.Application.RconParsers Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarLatchedValue, 4); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDomain, 5); + Configuration.StatusHeader.Pattern = "num +score +ping +guid +name +lastmsg +address +qport +rate *"; Configuration.MapStatus.Pattern = @"map: (([a-z]|_|\d)+)"; Configuration.MapStatus.AddMapping(ParserRegex.GroupType.RConStatusMap, 1); } @@ -69,16 +70,24 @@ namespace IW4MAdmin.Application.RconParsers return response.Skip(1).ToArray(); } - public async Task> GetDvarAsync(IRConConnection connection, string dvarName) + public async Task> GetDvarAsync(IRConConnection connection, string dvarName, T fallbackValue = default) { string[] lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName); - string response = string.Join('\n', lineSplit.Skip(1)); + string response = string.Join('\n', lineSplit).TrimEnd('\0'); var match = Regex.Match(response, Configuration.Dvar.Pattern); - if (!lineSplit[0].Contains(Configuration.CommandPrefixes.RConResponse) || - response.Contains("Unknown command") || + if (response.Contains("Unknown command") || !match.Success) { + if (fallbackValue != null) + { + return new Dvar() + { + Name = dvarName, + Value = fallbackValue + }; + } + throw new DvarException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt(dvarName)); } @@ -142,36 +151,36 @@ namespace IW4MAdmin.Application.RconParsers { List StatusPlayers = new List(); - if (Status.Length < 4) - { - throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNEXPECTED_STATUS"]); - } - - int validMatches = 0; + bool parsedHeader = false; foreach (string statusLine in Status) { string responseLine = statusLine.Trim(); - var regex = Regex.Match(responseLine, Configuration.Status.Pattern, RegexOptions.IgnoreCase); - - if (regex.Success) + if (Configuration.StatusHeader.PatternMatcher.Match(responseLine).Success) { - validMatches++; - int clientNumber = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]].Value); - int score = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]].Value); + parsedHeader = true; + continue; + } + + var match = Configuration.Status.PatternMatcher.Match(responseLine); + + if (match.Success) + { + int clientNumber = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]]); + int score = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]]); int ping = 999; // their state can be CNCT, ZMBI etc - if (regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Value.Length <= 3) + if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Length <= 3) { - ping = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Value); + ping = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]]); } long networkId; try { - networkId = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]].Value.ConvertGuidToLong(Configuration.GuidNumberStyle); + networkId = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]].ConvertGuidToLong(Configuration.GuidNumberStyle); } catch (FormatException) @@ -179,8 +188,8 @@ namespace IW4MAdmin.Application.RconParsers continue; } - string name = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].Value.TrimNewLine(); - int? ip = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Value.Split(':')[0].ConvertToIP(); + string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine(); + int? ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP(); var client = new EFClient() { @@ -208,10 +217,10 @@ namespace IW4MAdmin.Application.RconParsers } } - // this happens if status is requested while map is rotating - if (Status.Length > MAX_FAULTY_STATUS_LINES && validMatches == 0) + // this can happen if status is requested while map is rotating and we get a log dump back + if (!parsedHeader) { - throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_ROTATING_MAP"]); + throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNEXPECTED_STATUS"]); } return StatusPlayers; diff --git a/Application/RconParsers/DynamicRConParserConfiguration.cs b/Application/RconParsers/DynamicRConParserConfiguration.cs index 16aa03b5f..12309f458 100644 --- a/Application/RconParsers/DynamicRConParserConfiguration.cs +++ b/Application/RconParsers/DynamicRConParserConfiguration.cs @@ -15,6 +15,7 @@ namespace IW4MAdmin.Application.RconParsers public ParserRegex Status { get; set; } public ParserRegex MapStatus { get; set; } public ParserRegex Dvar { get; set; } + public ParserRegex StatusHeader { get; set; } public string ServerNotRunningResponse { get; set; } public bool WaitForResponse { get; set; } = true; public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber; @@ -24,6 +25,7 @@ namespace IW4MAdmin.Application.RconParsers Status = parserRegexFactory.CreateParserRegex(); MapStatus = parserRegexFactory.CreateParserRegex(); Dvar = parserRegexFactory.CreateParserRegex(); + StatusHeader = parserRegexFactory.CreateParserRegex(); } } } diff --git a/IW4MAdmin.sln b/IW4MAdmin.sln index a71beb5f3..be8e6badb 100644 --- a/IW4MAdmin.sln +++ b/IW4MAdmin.sln @@ -43,6 +43,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlug Plugins\ScriptPlugins\ParserPIW5.js = Plugins\ScriptPlugins\ParserPIW5.js Plugins\ScriptPlugins\ParserPT6.js = Plugins\ScriptPlugins\ParserPT6.js Plugins\ScriptPlugins\ParserRektT5M.js = Plugins\ScriptPlugins\ParserRektT5M.js + Plugins\ScriptPlugins\ParserT7.js = Plugins\ScriptPlugins\ParserT7.js Plugins\ScriptPlugins\ParserTeknoMW3.js = Plugins\ScriptPlugins\ParserTeknoMW3.js Plugins\ScriptPlugins\SharedGUIDKick.js = Plugins\ScriptPlugins\SharedGUIDKick.js Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js diff --git a/Plugins/LiveRadar/Plugin.cs b/Plugins/LiveRadar/Plugin.cs index 872ee587b..8558713fe 100644 --- a/Plugins/LiveRadar/Plugin.cs +++ b/Plugins/LiveRadar/Plugin.cs @@ -25,6 +25,15 @@ namespace LiveRadar public Task OnEventAsync(GameEvent E, Server S) { + // if it's an IW4 game, with custom callbacks, we want to + // enable the live radar page + if (E.Type == GameEvent.EventType.Start && + S.GameName == Server.Game.IW4 && + S.CustomCallback) + { + E.Owner.Manager.GetPageList().Pages.Add(Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_RADAR_TITLE"], "/Radar/All"); + } + if (E.Type == GameEvent.EventType.Unknown) { if (E.Data?.StartsWith("LiveRadar") ?? false) @@ -59,11 +68,6 @@ namespace LiveRadar _configurationHandler.Set((LiveRadarConfiguration)new LiveRadarConfiguration().Generate()); await _configurationHandler.Save(); } - - if (manager.GetServers().Any(_server => _server.GameName == Server.Game.IW4)) - { - manager.GetPageList().Pages.Add(Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_RADAR_TITLE"], "/Radar/All"); - } } public Task OnTickAsync(Server S) diff --git a/Plugins/ScriptPlugins/ParserCoD4x.js b/Plugins/ScriptPlugins/ParserCoD4x.js index 9b12a6aca..813962c72 100644 --- a/Plugins/ScriptPlugins/ParserCoD4x.js +++ b/Plugins/ScriptPlugins/ParserCoD4x.js @@ -3,7 +3,7 @@ var eventParser; var plugin = { author: 'FrenchFry, RaidMax', - version: 0.6, + version: 0.7, name: 'CoD4x Parser', isParser: true, @@ -14,6 +14,7 @@ var plugin = { rconParser = manager.GenerateDynamicRConParser(this.name); eventParser = manager.GenerateDynamicEventParser(this.name); + rconParser.Configuration.StatusHeader.Pattern = 'num +score +ping +playerid +steamid +name +lastmsg +address +qport +rate *'; rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){16,32}|(?:[a-z]|[0-9]){32}|bot[0-9]+) ([0-9+]) *(.{0,32}) +([0-9]+) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback) +(-*[0-9]+) +([0-9]+) *$' rconParser.Configuration.Status.AddMapping(104, 6); // RConName rconParser.Configuration.Status.AddMapping(105, 8); // RConIPAddress diff --git a/Plugins/ScriptPlugins/ParserPIW5.js b/Plugins/ScriptPlugins/ParserPIW5.js index 4129f84e1..45e699648 100644 --- a/Plugins/ScriptPlugins/ParserPIW5.js +++ b/Plugins/ScriptPlugins/ParserPIW5.js @@ -3,7 +3,7 @@ var eventParser; var plugin = { author: 'RaidMax', - version: 0.1, + version: 0.2, name: 'Plutonium IW5 Parser', isParser: true, @@ -14,7 +14,7 @@ var plugin = { rconParser = manager.GenerateDynamicRConParser(this.name); eventParser = manager.GenerateDynamicEventParser(this.name); - rconParser.Configuration.CommandPrefixes.Tell = 'tell {0} {1}'; + 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}"'; @@ -24,9 +24,10 @@ var plugin = { rconParser.Configuration.Dvar.Pattern = '^(.+) is "(.+)?"'; rconParser.Configuration.Dvar.AddMapping(106, 1); rconParser.Configuration.Dvar.AddMapping(107, 2); - rconParser.Configuration.WaitForResponse = false; + rconParser.Configuration.WaitForResponse = true; rconParser.Configuration.CanGenerateLogPath = true; + rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +lastmsg +address +qport +rate *'; rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +(?:[0-1]{1}) +([0-9]+) +([A-F0-9]+) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback) +(?:-?[0-9]+) +(?:[0-9]+) *$'; rconParser.Configuration.Status.AddMapping(100, 1); rconParser.Configuration.Status.AddMapping(101, 2); diff --git a/Plugins/ScriptPlugins/ParserPT6.js b/Plugins/ScriptPlugins/ParserPT6.js index 2f3c8670b..c3ac8b7c2 100644 --- a/Plugins/ScriptPlugins/ParserPT6.js +++ b/Plugins/ScriptPlugins/ParserPT6.js @@ -3,7 +3,7 @@ var eventParser; var plugin = { author: 'RaidMax, Xerxes', - version: 0.6, + version: 0.7, name: 'Plutonium T6 Parser', isParser: true, @@ -26,6 +26,7 @@ var plugin = { rconParser.Configuration.Dvar.AddMapping(107, 2); rconParser.Configuration.WaitForResponse = false; + rconParser.Configuration.StatusHeader.Patter = 'num +score +bot +ping +guid +name +lastmsg +address +qport +rate *'; rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +(?:[0-1]{1}) +([0-9]+) +([A-F0-9]+) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback) +(?:-?[0-9]+) +(?:[0-9]+) *$'; rconParser.Configuration.Status.AddMapping(100, 1); rconParser.Configuration.Status.AddMapping(101, 2); diff --git a/Plugins/ScriptPlugins/ParserT7.js b/Plugins/ScriptPlugins/ParserT7.js new file mode 100644 index 000000000..2bdfa0c0c --- /dev/null +++ b/Plugins/ScriptPlugins/ParserT7.js @@ -0,0 +1,45 @@ +var rconParser; +var eventParser; + +var plugin = { + author: 'RaidMax', + version: 0.1, + name: 'Black Ops 3 Parser', + isParser: true, + + onEventAsync: function (gameEvent, server) { + }, + + onLoadAsync: function (manager) { + rconParser = manager.GenerateDynamicRConParser(this.name); + eventParser = manager.GenerateDynamicEventParser(this.name); + + rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +((?:[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)(?:\\([0-9]+\\)) +(-*[0-9]+) *$' + rconParser.Configuration.StatusHeader.Pattern = 'num +score +ping +xuid +name +address +qport'; + rconParser.Configuration.CommandPrefixes.Kick = 'clientkick {0}'; + rconParser.Configuration.CommandPrefixes.Ban = 'clientkick {0}'; + rconParser.Configuration.CommandPrefixes.TempBan = 'tempbanclient {0}'; + rconParser.Configuration.CommandPrefixes.RConCommand = '\xff\xff\xff\xff\x00{0} {1}'; + rconParser.Configuration.CommandPrefixes.RConGetDvar = '\xff\xff\xff\xff\x00{0} {1}'; + rconParser.Configuration.CommandPrefixes.RConSetDvar = '\xff\xff\xff\xff\x00{0} set {1}'; + rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xff\x01'; + rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined; // disables this, because it's useless on T7 + + rconParser.Configuration.Status.AddMapping(105, 6); // ip address + rconParser.Version = '[local] ship win64 CODBUILD8-764 (3421987) Mon Dec 16 10:44:20 2019 10d27bef'; + rconParser.GameName = 8; // BO3 + rconParser.CanGenerateLogPath = false; + + eventParser.Version = '[local] ship win64 CODBUILD8-764 (3421987) Mon Dec 16 10:44:20 2019 10d27bef'; + eventParser.GameName = 8; // BO3 + eventParser.Configuration.GameDirectory = 'usermaps'; + eventParser.Configuration.Say.Pattern = '^(chat|chatteam);(?:[0-9]+);([0-9]+);([0-9]+);(.+);(.*)$'; + + }, + + onUnloadAsync: function () { + }, + + onTickAsync: function (server) { + } +}; \ No newline at end of file diff --git a/SharedLibraryCore/Interfaces/IRConParser.cs b/SharedLibraryCore/Interfaces/IRConParser.cs index 62227a67b..f7768e252 100644 --- a/SharedLibraryCore/Interfaces/IRConParser.cs +++ b/SharedLibraryCore/Interfaces/IRConParser.cs @@ -13,8 +13,9 @@ namespace SharedLibraryCore.Interfaces /// type of DVAR expected (string, int, float etc...) /// RCon connection to retrieve with /// name of DVAR + /// default value to return if dvar retrieval fails /// - Task> GetDvarAsync(IRConConnection connection, string dvarName); + Task> GetDvarAsync(IRConConnection connection, string dvarName, T fallbackValue = default); /// /// set value of DVAR by name diff --git a/SharedLibraryCore/Interfaces/IRConParserConfiguration.cs b/SharedLibraryCore/Interfaces/IRConParserConfiguration.cs index 5e1b70539..571f168d8 100644 --- a/SharedLibraryCore/Interfaces/IRConParserConfiguration.cs +++ b/SharedLibraryCore/Interfaces/IRConParserConfiguration.cs @@ -25,6 +25,11 @@ namespace SharedLibraryCore.Interfaces /// ParserRegex Dvar { get; set; } + /// + /// stores the regex info for parsing the header of a status response + /// + ParserRegex StatusHeader { get; set; } + /// /// Specifies the expected response message from rcon when the server is not running /// diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index f319a13b5..a0eb98904 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -720,9 +720,9 @@ namespace SharedLibraryCore return Convert.ToBase64String(src.Select(c => Convert.ToByte(c)).ToArray()).Replace('+', '-').Replace('/', '_'); } - public static Task> GetDvarAsync(this Server server, string dvarName) + public static Task> GetDvarAsync(this Server server, string dvarName, T fallbackValue = default) { - return server.RconParser.GetDvarAsync(server.RemoteConnection, dvarName); + return server.RconParser.GetDvarAsync(server.RemoteConnection, dvarName, fallbackValue); } public static Task SetDvarAsync(this Server server, string dvarName, object dvarValue) @@ -754,7 +754,8 @@ namespace SharedLibraryCore } var response = await server.RemoteConnection.SendQueryAsync(RCon.StaticHelpers.QueryType.GET_INFO); - return response.FirstOrDefault(r => r[0] == '\\')?.DictionaryFromKeyValue(); + string combinedResponse = string.Join('\\', response.Where(r => r.Length > 0 && r[0] == '\\')); + return combinedResponse.DictionaryFromKeyValue(); } public static double GetVersionAsDouble() @@ -883,7 +884,7 @@ namespace SharedLibraryCore { foreach (char separator in DirectorySeparatorChars) { - path = path.Replace(separator, Path.DirectorySeparatorChar); + path = (path ?? "").Replace(separator, Path.DirectorySeparatorChar); } return path;