Skip to content

Commit

Permalink
Support Unreal 2 Protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
BattlefieldDuck committed Jan 17, 2024
1 parent 816dee0 commit bcd6078
Show file tree
Hide file tree
Showing 7 changed files with 428 additions and 0 deletions.
183 changes: 183 additions & 0 deletions OpenGSQ/Protocols/Unreal2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using OpenGSQ.Responses.Unreal2;

namespace OpenGSQ.Protocols
{
/// <summary>
/// Unreal 2 Protocol
/// </summary>
public class Unreal2 : ProtocolBase
{
/// <inheritdoc/>
public override string FullName => "Unreal 2 Protocol";

protected const byte _DETAILS = 0x00;

Check warning on line 20 in OpenGSQ/Protocols/Unreal2.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Missing XML comment for publicly visible type or member 'Unreal2._DETAILS'

Check warning on line 20 in OpenGSQ/Protocols/Unreal2.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Missing XML comment for publicly visible type or member 'Unreal2._DETAILS'
protected const byte _RULES = 0x01;

Check warning on line 21 in OpenGSQ/Protocols/Unreal2.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Missing XML comment for publicly visible type or member 'Unreal2._RULES'

Check warning on line 21 in OpenGSQ/Protocols/Unreal2.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Missing XML comment for publicly visible type or member 'Unreal2._RULES'
protected const byte _PLAYERS = 0x02;

Check warning on line 22 in OpenGSQ/Protocols/Unreal2.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Missing XML comment for publicly visible type or member 'Unreal2._PLAYERS'

Check warning on line 22 in OpenGSQ/Protocols/Unreal2.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Missing XML comment for publicly visible type or member 'Unreal2._PLAYERS'

/// <summary>
/// Initializes a new instance of the Unreal2 class.
/// </summary>
public Unreal2(string host, int port, int timeout = 5000) : base(host, port, timeout)
{
}

/// <summary>
/// Gets the details of the server.
/// </summary>
/// <returns>The details of the server.</returns>
/// <exception cref="InvalidPacketException">Thrown when the packet header does not match the expected value.</exception>
public async Task<Status> GetDetails()
{
using var udpClient = new UdpClient();
byte[] response = await udpClient.CommunicateAsync(this, new byte[] { 0x79, 0x00, 0x00, 0x00, _DETAILS });

// Remove the first 4 bytes \x80\x00\x00\x00
BinaryReader br = new BinaryReader(new MemoryStream(response.Skip(4).ToArray()));
byte header = br.ReadByte();

if (header != _DETAILS)
{
throw new InvalidPacketException($"Packet header mismatch. Received: {header}. Expected: {_DETAILS}.");
}

var details = new Status
{
ServerId = br.ReadInt32(),
ServerIP = br.ReadString(),
GamePort = br.ReadInt32(),
QueryPort = br.ReadInt32(),
ServerName = ReadString(br),
MapName = ReadString(br),
GameType = ReadString(br),
NumPlayers = br.ReadInt32(),
MaxPlayers = br.ReadInt32(),
Ping = br.ReadInt32(),
Flags = br.ReadInt32(),
Skill = ReadString(br)
};

return details;
}

/// <summary>
/// Gets the rules of the server.
/// </summary>
/// <returns>The rules of the server.</returns>
/// <exception cref="InvalidPacketException">Thrown when the packet header does not match the expected value.</exception>
public async Task<Dictionary<string, object>> GetRules()
{
using var udpClient = new UdpClient();
byte[] response = await udpClient.CommunicateAsync(this, new byte[] { 0x79, 0x00, 0x00, 0x00, _RULES });

// Remove the first 4 bytes \x80\x00\x00\x00
BinaryReader br = new BinaryReader(new MemoryStream(response.Skip(4).ToArray()));
byte header = br.ReadByte();

if (header != _RULES)
{
throw new InvalidPacketException($"Packet header mismatch. Received: {header}. Expected: {_RULES}.");
}

var rules = new Dictionary<string, object>();
var mutators = new List<string>();

while (br.BaseStream.Position != br.BaseStream.Length)
{
string key = ReadString(br);
string val = ReadString(br);

if (key.ToLower() == "mutator")
{
mutators.Add(val);
}
else
{
rules[key] = val;
}
}

rules["Mutators"] = mutators;

return rules;
}

/// <summary>
/// Gets the players of the server.
/// </summary>
/// <returns>A list of players of the server.</returns>
/// <exception cref="InvalidPacketException">Thrown when the packet header does not match the expected value.</exception>
public async Task<List<Player>> GetPlayers()
{
using var udpClient = new UdpClient();
byte[] response = await udpClient.CommunicateAsync(this, new byte[] { 0x79, 0x00, 0x00, 0x00, _PLAYERS });

// Remove the first 4 bytes \x80\x00\x00\x00
BinaryReader br = new BinaryReader(new MemoryStream(response.Skip(4).ToArray()));
byte header = br.ReadByte();

if (header != _PLAYERS)
{
throw new InvalidPacketException($"Packet header mismatch. Received: {header}. Expected: {_PLAYERS}.");
}

var players = new List<Player>();

while (br.BaseStream.Position != br.BaseStream.Length)
{
var player = new Player
{
Id = br.ReadInt32(),
Name = ReadString(br),
Ping = br.ReadInt32(),
Score = br.ReadInt32(),
StatsId = br.ReadInt32()
};

players.Add(player);
}

return players;
}

/// <summary>
/// Strips color codes from the given text.
/// </summary>
/// <param name="text">The text to strip color codes from, represented as a byte array.</param>
/// <returns>The text with color codes stripped, represented as a string.</returns>
protected string StripColors(byte[] text)
{
string str = Encoding.UTF8.GetString(text);
return Regex.Replace(str, @"\x1b...|[\x00-\x1a]", "");
}

/// <summary>
/// Reads a string from a BinaryReader, decodes it, and strips color codes.
/// </summary>
/// <param name="br">The BinaryReader to read the string from.</param>
/// <returns>The decoded string with color codes stripped.</returns>
protected string ReadString(BinaryReader br)
{
int length = br.ReadByte();
string str = br.ReadStringEx();

byte[] b;
if (length == str.Length + 1)
{
b = Encoding.UTF8.GetBytes(str);
}
else
{
b = Encoding.Unicode.GetBytes(str);
}

return StripColors(b);
}
}
}
33 changes: 33 additions & 0 deletions OpenGSQ/Responses/Unreal2/Player.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace OpenGSQ.Responses.Unreal2
{
/// <summary>
/// Represents a player in the game.
/// </summary>
public class Player
{
/// <summary>
/// Gets or sets the ID of the player.
/// </summary>
public int Id { get; set; }

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

/// <summary>
/// Gets or sets the ping of the player.
/// </summary>
public int Ping { get; set; }

/// <summary>
/// Gets or sets the score of the player.
/// </summary>
public int Score { get; set; }

/// <summary>
/// Gets or sets the stats ID of the player.
/// </summary>
public int StatsId { get; set; }
}
}
68 changes: 68 additions & 0 deletions OpenGSQ/Responses/Unreal2/Status.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
namespace OpenGSQ.Responses.Unreal2
{
/// <summary>
/// Represents the status of a server.
/// </summary>
public class Status
{
/// <summary>
/// Gets or sets the server ID.
/// </summary>
public int ServerId { get; set; }

/// <summary>
/// Gets or sets the IP address of the server.
/// </summary>
public string ServerIP { get; set; }

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

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

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

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

/// <summary>
/// Gets or sets the type of the game.
/// </summary>
public string GameType { get; set; }

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

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

/// <summary>
/// Gets or sets the ping.
/// </summary>
public int Ping { get; set; }

/// <summary>
/// Gets or sets the flags.
/// </summary>
public int Flags { get; set; }

/// <summary>
/// Gets or sets the skill level.
/// </summary>
public string Skill { get; set; }
}
}
35 changes: 35 additions & 0 deletions OpenGSQTests/Protocols/Unreal2Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenGSQTests;

namespace OpenGSQ.Protocols.Tests
{
[TestClass()]
public class Unreal2Tests : TestBase
{
public Unreal2 unreal2 = new("109.230.224.189", 6970);

public Unreal2Tests() : base(nameof(Unreal2Tests))
{
_EnableSave = !false;
}

[TestMethod()]
public async Task GetDetailsTest()
{
SaveResult(nameof(GetDetailsTest), await unreal2.GetDetails());
}

[TestMethod()]
public async Task GetRulesTest()
{
SaveResult(nameof(GetRulesTest), await unreal2.GetRules());
}

[TestMethod()]
public async Task GetPlayersTest()
{
SaveResult(nameof(GetPlayersTest), await unreal2.GetPlayers());
}
}
}
14 changes: 14 additions & 0 deletions OpenGSQTests/Results/Unreal2Tests/GetDetailsTest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"ServerId": 0,
"ServerIP": "",
"GamePort": 6969,
"QueryPort": 0,
"ServerName": "The Usual Suspects TAM server DE (#rdturd)",
"MapName": "DM-Under_LE-2009",
"GameType": "xDeathMatch",
"NumPlayers": 6,
"MaxPlayers": 16,
"Ping": 0,
"Flags": 0,
"Skill": "0"
}
Loading

0 comments on commit bcd6078

Please sign in to comment.