diff --git a/OpenGSQ/Protocols/GameSpy1.cs b/OpenGSQ/Protocols/GameSpy1.cs index 200b637..717b1f2 100644 --- a/OpenGSQ/Protocols/GameSpy1.cs +++ b/OpenGSQ/Protocols/GameSpy1.cs @@ -21,10 +21,10 @@ public class GameSpy1 : ProtocolBase /// /// Initializes a new instance of the GameSpy1 class. /// - /// The IP address of the server. + /// The IP address of the server. /// The port number of the server. /// The timeout for the connection in milliseconds. - public GameSpy1(string address, int port, int timeout = 5000) : base(address, port, timeout) + public GameSpy1(string host, int port, int timeout = 5000) : base(host, port, timeout) { } diff --git a/OpenGSQ/Protocols/GameSpy2.cs b/OpenGSQ/Protocols/GameSpy2.cs index 7cc86b6..635cdaf 100644 --- a/OpenGSQ/Protocols/GameSpy2.cs +++ b/OpenGSQ/Protocols/GameSpy2.cs @@ -20,10 +20,10 @@ public class GameSpy2 : ProtocolBase /// /// Initializes a new instance of the GameSpy2 class. /// - /// The IP address of the server. + /// The IP address of the server. /// The port number of the server. /// The timeout for the connection in milliseconds. - public GameSpy2(string address, int port, int timeout = 5000) : base(address, port, timeout) + public GameSpy2(string host, int port, int timeout = 5000) : base(host, port, timeout) { } diff --git a/OpenGSQ/Protocols/GameSpy3.cs b/OpenGSQ/Protocols/GameSpy3.cs index db014c8..0e737c4 100644 --- a/OpenGSQ/Protocols/GameSpy3.cs +++ b/OpenGSQ/Protocols/GameSpy3.cs @@ -25,10 +25,10 @@ public class GameSpy3 : ProtocolBase /// /// Initializes a new instance of the GameSpy3 class. /// - /// The IP address of the server. + /// The IP address of the server. /// The port number of the server. /// The timeout for the connection in milliseconds. - public GameSpy3(string address, int port, int timeout = 5000) : base(address, port, timeout) + public GameSpy3(string host, int port, int timeout = 5000) : base(host, port, timeout) { } diff --git a/OpenGSQ/Protocols/GameSpy4.cs b/OpenGSQ/Protocols/GameSpy4.cs index 6c3285f..446fac1 100644 --- a/OpenGSQ/Protocols/GameSpy4.cs +++ b/OpenGSQ/Protocols/GameSpy4.cs @@ -11,10 +11,10 @@ public class GameSpy4 : GameSpy3 /// /// Initializes a new instance of the GameSpy4 class. /// - /// The IP address of the server. + /// The IP address of the server. /// The port number of the server. /// The timeout for the connection in milliseconds. - public GameSpy4(string address, int port, int timeout = 5000) : base(address, port, timeout) + public GameSpy4(string host, int port, int timeout = 5000) : base(host, port, timeout) { _Challenge = true; } diff --git a/OpenGSQ/Protocols/Quake1.cs b/OpenGSQ/Protocols/Quake1.cs index 791dda4..0dab745 100644 --- a/OpenGSQ/Protocols/Quake1.cs +++ b/OpenGSQ/Protocols/Quake1.cs @@ -41,10 +41,10 @@ public class Quake1 : ProtocolBase /// /// Initializes a new instance of the Quake1 class. /// - /// The IP address of the server. + /// The IP address of the server. /// The port number of the server. /// The timeout for the connection in milliseconds. - public Quake1(string address, int port, int timeout = 5000) : base(address, port, timeout) + public Quake1(string host, int port, int timeout = 5000) : base(host, port, timeout) { _RequestHeader = "status"; _ResponseHeader = "n"; diff --git a/OpenGSQ/Protocols/Quake2.cs b/OpenGSQ/Protocols/Quake2.cs index 6e66887..6785e2e 100644 --- a/OpenGSQ/Protocols/Quake2.cs +++ b/OpenGSQ/Protocols/Quake2.cs @@ -17,10 +17,10 @@ public class Quake2 : Quake1 /// /// Initializes a new instance of the Quake2 class. /// - /// The IP address of the server. + /// The IP address of the server. /// The port number of the server. /// The timeout for the connection in milliseconds. - public Quake2(string address, int port, int timeout = 5000) : base(address, port, timeout) + public Quake2(string host, int port, int timeout = 5000) : base(host, port, timeout) { _RequestHeader = "status"; _ResponseHeader = "print\n"; diff --git a/OpenGSQ/Protocols/Quake3.cs b/OpenGSQ/Protocols/Quake3.cs index 45977ca..03d5713 100644 --- a/OpenGSQ/Protocols/Quake3.cs +++ b/OpenGSQ/Protocols/Quake3.cs @@ -20,10 +20,10 @@ public class Quake3 : Quake2 /// /// Initializes a new instance of the Quake3 class. /// - /// The IP address of the server. + /// The IP address of the server. /// The port number of the server. /// The timeout for the connection in milliseconds. - public Quake3(string address, int port, int timeout = 5000) : base(address, port, timeout) + public Quake3(string host, int port, int timeout = 5000) : base(host, port, timeout) { _RequestHeader = "getstatus"; _ResponseHeader = "statusResponse\n"; diff --git a/OpenGSQ/Protocols/Scum.cs b/OpenGSQ/Protocols/Scum.cs new file mode 100644 index 0000000..8fde9b2 --- /dev/null +++ b/OpenGSQ/Protocols/Scum.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.Linq; +using System.IO; +using System.Net.Sockets; +using OpenGSQ.Responses.Scum; + +namespace OpenGSQ.Protocols +{ + /// + /// Scum Protocol + /// + public class Scum : ProtocolBase + { + /// + public override string FullName => "Scum Protocol"; + + private static readonly List<(string, int)> _masterServers = new List<(string, int)> + { + ("176.57.138.2", 1040), + ("172.107.16.215", 1040), + ("206.189.248.133", 1040) + }; + + /// + /// Initializes a new instance of the Scum class. + /// + /// The host of the server. + /// The port of the server. + /// The timeout for server requests. + public Scum(string host, int port, int timeout = 5000) : base(host, port, timeout) + { + } + + /// + /// Gets the status of the server. + /// + /// The list of master servers to query. + /// The status of the server. + /// Thrown when the server is not found in the list of master servers. + public async Task GetStatus(List masterServers = null) + { + var ip = (await GetIPEndPoint()).Address.ToString(); + + masterServers ??= await QueryMasterServers(); + + foreach (var server in masterServers) + { + if (server.Ip == ip && server.Port == Port) + { + return server; + } + } + + throw new ServerNotFoundException($"The server with IP address {ip} and port {Port} was not found in the list of master servers."); + } + + /// + /// Queries the master servers for a list of servers. + /// + /// A list of servers from the master servers. + /// Thrown when failed to connect to any of the master servers. + public static async Task> QueryMasterServers() + { + foreach (var (host, port) in _masterServers) + { + try + { + using var tcpClient = new TcpClient(); + tcpClient.ReceiveTimeout = 5000; + await tcpClient.ConnectAsync(host, port); + await tcpClient.SendAsync(new byte[] { 0x04, 0x03, 0x00, 0x00 }); + + var total = -1; + var response = new byte[0]; + var servers = new List(); + + while (total == -1 || servers.Count < total) + { + response = response.Concat(await tcpClient.ReceiveAsync()).ToArray(); + using var br = new BinaryReader(new MemoryStream(response)); + + // first packet return the total number of servers + if (total == -1) + { + total = br.ReadInt16(); + } + + // server bytes length always 127 + while (br.BaseStream.Length - br.BaseStream.Position >= 127) + { + var statusResponse = new StatusResponse + { + Ip = string.Join(".", br.ReadBytes(4).Reverse().Select(b => b.ToString())), + Port = br.ReadInt16(), + Name = Encoding.UTF8.GetString(br.ReadBytes(100).TakeWhile(b => b != 0).ToArray()) + }; + br.ReadByte(); // skip + statusResponse.NumPlayers = br.ReadByte(); + statusResponse.MaxPlayers = br.ReadByte(); + statusResponse.Time = br.ReadByte(); + br.ReadByte(); // skip + statusResponse.Password = ((br.ReadByte() >> 1) & 1) == 1; + br.ReadBytes(7); // skip + var v = br.ReadBytes(8).Reverse().Select(b => Convert.ToInt32(b).ToString("X").PadLeft(2, '0')).ToList(); + statusResponse.Version = $"{Convert.ToInt32(v[0], 16)}.{Convert.ToInt32(v[1], 16)}.{Convert.ToInt32(v[2] + v[3], 16)}.{Convert.ToInt32(v[4] + v[5] + v[6] + v[7], 16)}"; + servers.Add(statusResponse); + } + + // if the length is less than 127, save the unused bytes for next loop + response = br.ReadBytes((int)(br.BaseStream.Length - br.BaseStream.Position)); + } + + return servers; + } + catch (SocketException) + { + // Ignore exceptions + } + } + + throw new Exception("Failed to connect to any of the master servers."); + } + } +} diff --git a/OpenGSQ/Responses/Scum/StatusResponse.cs b/OpenGSQ/Responses/Scum/StatusResponse.cs new file mode 100644 index 0000000..076c1cd --- /dev/null +++ b/OpenGSQ/Responses/Scum/StatusResponse.cs @@ -0,0 +1,48 @@ +namespace OpenGSQ.Responses.Scum +{ + /// + /// Represents the response status of a server. + /// + public class StatusResponse + { + /// + /// Gets or sets the IP address of the server. + /// + public string Ip { get; set; } + + /// + /// Gets or sets the port number of the server. + /// + public int Port { get; set; } + + /// + /// Gets or sets the name of the server. + /// + public string Name { get; set; } + + /// + /// Gets or sets the number of players currently connected to the server. + /// + public int NumPlayers { get; set; } + + /// + /// Gets or sets the maximum number of players that can connect to the server. + /// + public int MaxPlayers { get; set; } + + /// + /// Gets or sets the server time. + /// + public int Time { get; set; } + + /// + /// Gets or sets a value indicating whether a password is required to connect to the server. + /// + public bool Password { get; set; } + + /// + /// Gets or sets the version of the server. + /// + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/OpenGSQTests/Protocols/ScumTests.cs b/OpenGSQTests/Protocols/ScumTests.cs new file mode 100644 index 0000000..588ffaf --- /dev/null +++ b/OpenGSQTests/Protocols/ScumTests.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenGSQTests; + +namespace OpenGSQ.Protocols.Tests +{ + [TestClass()] + public class ScumTests : TestBase + { + public Scum scum = new("15.235.181.19", 7042); + + public ScumTests() : base(nameof(ScumTests)) + { + _EnableSave = false; + } + + [TestMethod()] + public async Task GetStatusTest() + { + SaveResult(nameof(GetStatusTest), await scum.GetStatus()); + } + } +} \ No newline at end of file diff --git a/OpenGSQTests/Results/ScumTests/GetStatusTest.json b/OpenGSQTests/Results/ScumTests/GetStatusTest.json new file mode 100644 index 0000000..e73f9e5 --- /dev/null +++ b/OpenGSQTests/Results/ScumTests/GetStatusTest.json @@ -0,0 +1,10 @@ +{ + "Ip": "15.235.181.19", + "Port": 7042, + "Name": "★6868★PVP打架爽服/小队扶持/满人送V10/10倍魔改/高性能服务器", + "NumPlayers": 2, + "MaxPlayers": 100, + "Time": 21, + "Password": false, + "Version": "0.9.511.80646" +} \ No newline at end of file