Skip to content

Commit

Permalink
Support Scum Protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
BattlefieldDuck committed Jan 17, 2024
1 parent cfc283f commit 816dee0
Show file tree
Hide file tree
Showing 11 changed files with 222 additions and 14 deletions.
4 changes: 2 additions & 2 deletions OpenGSQ/Protocols/GameSpy1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ public class GameSpy1 : ProtocolBase
/// <summary>
/// Initializes a new instance of the GameSpy1 class.
/// </summary>
/// <param name="address">The IP address of the server.</param>
/// <param name="host">The IP address of the server.</param>
/// <param name="port">The port number of the server.</param>
/// <param name="timeout">The timeout for the connection in milliseconds.</param>
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)
{

}
Expand Down
4 changes: 2 additions & 2 deletions OpenGSQ/Protocols/GameSpy2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ public class GameSpy2 : ProtocolBase
/// <summary>
/// Initializes a new instance of the GameSpy2 class.
/// </summary>
/// <param name="address">The IP address of the server.</param>
/// <param name="host">The IP address of the server.</param>
/// <param name="port">The port number of the server.</param>
/// <param name="timeout">The timeout for the connection in milliseconds.</param>
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)
{

}
Expand Down
4 changes: 2 additions & 2 deletions OpenGSQ/Protocols/GameSpy3.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ public class GameSpy3 : ProtocolBase
/// <summary>
/// Initializes a new instance of the GameSpy3 class.
/// </summary>
/// <param name="address">The IP address of the server.</param>
/// <param name="host">The IP address of the server.</param>
/// <param name="port">The port number of the server.</param>
/// <param name="timeout">The timeout for the connection in milliseconds.</param>
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)
{

}
Expand Down
4 changes: 2 additions & 2 deletions OpenGSQ/Protocols/GameSpy4.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ public class GameSpy4 : GameSpy3
/// <summary>
/// Initializes a new instance of the GameSpy4 class.
/// </summary>
/// <param name="address">The IP address of the server.</param>
/// <param name="host">The IP address of the server.</param>
/// <param name="port">The port number of the server.</param>
/// <param name="timeout">The timeout for the connection in milliseconds.</param>
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;
}
Expand Down
4 changes: 2 additions & 2 deletions OpenGSQ/Protocols/Quake1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ public class Quake1 : ProtocolBase
/// <summary>
/// Initializes a new instance of the Quake1 class.
/// </summary>
/// <param name="address">The IP address of the server.</param>
/// <param name="host">The IP address of the server.</param>
/// <param name="port">The port number of the server.</param>
/// <param name="timeout">The timeout for the connection in milliseconds.</param>
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";
Expand Down
4 changes: 2 additions & 2 deletions OpenGSQ/Protocols/Quake2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ public class Quake2 : Quake1
/// <summary>
/// Initializes a new instance of the Quake2 class.
/// </summary>
/// <param name="address">The IP address of the server.</param>
/// <param name="host">The IP address of the server.</param>
/// <param name="port">The port number of the server.</param>
/// <param name="timeout">The timeout for the connection in milliseconds.</param>
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";
Expand Down
4 changes: 2 additions & 2 deletions OpenGSQ/Protocols/Quake3.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ public class Quake3 : Quake2
/// <summary>
/// Initializes a new instance of the Quake3 class.
/// </summary>
/// <param name="address">The IP address of the server.</param>
/// <param name="host">The IP address of the server.</param>
/// <param name="port">The port number of the server.</param>
/// <param name="timeout">The timeout for the connection in milliseconds.</param>
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";
Expand Down
127 changes: 127 additions & 0 deletions OpenGSQ/Protocols/Scum.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Scum Protocol
/// </summary>
public class Scum : ProtocolBase
{
/// <inheritdoc/>
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)
};

/// <summary>
/// Initializes a new instance of the Scum class.
/// </summary>
/// <param name="host">The host of the server.</param>
/// <param name="port">The port of the server.</param>
/// <param name="timeout">The timeout for server requests.</param>
public Scum(string host, int port, int timeout = 5000) : base(host, port, timeout)
{
}

/// <summary>
/// Gets the status of the server.
/// </summary>
/// <param name="masterServers">The list of master servers to query.</param>
/// <returns>The status of the server.</returns>
/// <exception cref="ServerNotFoundException">Thrown when the server is not found in the list of master servers.</exception>
public async Task<StatusResponse> GetStatus(List<StatusResponse> 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.");
}

/// <summary>
/// Queries the master servers for a list of servers.
/// </summary>
/// <returns>A list of servers from the master servers.</returns>
/// <exception cref="Exception">Thrown when failed to connect to any of the master servers.</exception>
public static async Task<List<StatusResponse>> 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<StatusResponse>();

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.");
}
}
}
48 changes: 48 additions & 0 deletions OpenGSQ/Responses/Scum/StatusResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
namespace OpenGSQ.Responses.Scum
{
/// <summary>
/// Represents the response status of a server.
/// </summary>
public class StatusResponse
{
/// <summary>
/// Gets or sets the IP address of the server.
/// </summary>
public string Ip { get; set; }

/// <summary>
/// Gets or sets the port number of the server.
/// </summary>
public int Port { get; set; }

/// <summary>
/// Gets or sets the name of the server.
/// </summary>
public string Name { get; set; }

/// <summary>
/// Gets or sets the number of players currently connected to the server.
/// </summary>
public int NumPlayers { get; set; }

/// <summary>
/// Gets or sets the maximum number of players that can connect to the server.
/// </summary>
public int MaxPlayers { get; set; }

/// <summary>
/// Gets or sets the server time.
/// </summary>
public int Time { get; set; }

/// <summary>
/// Gets or sets a value indicating whether a password is required to connect to the server.
/// </summary>
public bool Password { get; set; }

/// <summary>
/// Gets or sets the version of the server.
/// </summary>
public string Version { get; set; }
}
}
23 changes: 23 additions & 0 deletions OpenGSQTests/Protocols/ScumTests.cs
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
10 changes: 10 additions & 0 deletions OpenGSQTests/Results/ScumTests/GetStatusTest.json
Original file line number Diff line number Diff line change
@@ -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"
}

0 comments on commit 816dee0

Please sign in to comment.