1
0
mirror of https://github.com/RaidMax/IW4M-Admin.git synced 2025-06-11 15:52:25 -05:00

partial T7 (BO3) support. includes rcon communication improvements and a small fix for displaying live radar tab

This commit is contained in:
RaidMax
2020-04-17 15:05:16 -05:00
parent 86edd8a25e
commit c7005c7ac0
14 changed files with 226 additions and 60 deletions

View File

@ -911,7 +911,8 @@ namespace IW4MAdmin
Version = RconParser.Version;
}
var svRunning = await this.GetDvarAsync<string>("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<string>("sv_hostname")).Value :
infoResponse.Where(kvp => kvp.Key.Contains("hostname")).Select(kvp => kvp.Value).First();
var mapname = infoResponse == null ?
(await this.GetDvarAsync<string>("mapname")).Value :
(await this.GetDvarAsync("mapname", "Unknown")).Value :
infoResponse["mapname"];
int maxplayers = (GameName == Game.IW4) ? // gotta love IW4 idiosyncrasies
(await this.GetDvarAsync<int>("party_maxplayers")).Value :
@ -932,12 +933,12 @@ namespace IW4MAdmin
(await this.GetDvarAsync<int>("sv_maxclients")).Value :
Convert.ToInt32(infoResponse["sv_maxclients"]);
var gametype = infoResponse == null ?
(await this.GetDvarAsync<string>("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<string>("fs_basepath");
var basegame = await this.GetDvarAsync<string>("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<string>("fs_game")).Value :
(await this.GetDvarAsync("fs_game", GameName == Game.T7 ? "" : null)).Value :
infoResponse["fs_game"];
var logfile = await this.GetDvarAsync<string>("g_log");
var logsync = await this.GetDvarAsync<int>("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

View File

@ -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<int> BytesReadPerSegment { get; set; } = new List<int>();
public SocketAsyncEventArgs SendEventArgs { get; set; } = new SocketAsyncEventArgs();
public SocketAsyncEventArgs ReceiveEventArgs { get; set; } = new SocketAsyncEventArgs();
public DateTime LastQuery { get; set; } = DateTime.Now;

View File

@ -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<byte[]> SendPayloadAsync(byte[] payload, bool waitForResponse)
/// <summary>
/// reassembles broken status segments into the 'correct' ordering
/// <remarks>this is primarily for T7, and is really only reliable for 2 segments</remarks>
/// </summary>
/// <param name="segments">array of segmented byte arrays</param>
/// <returns></returns>
public string ReassembleSegmentedStatus(byte[][] segments)
{
var splitStatusStrings = new List<string>();
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<byte[][]> 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<byte[]>();
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)

View File

@ -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<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName)
public async Task<Dvar<T>> GetDvarAsync<T>(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<T>()
{
Name = dvarName,
Value = fallbackValue
};
}
throw new DvarException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt(dvarName));
}
@ -142,36 +151,36 @@ namespace IW4MAdmin.Application.RconParsers
{
List<EFClient> StatusPlayers = new List<EFClient>();
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;

View File

@ -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();
}
}
}