From c57712f840d295d2921bebf462f88e712f3d72a2 Mon Sep 17 00:00:00 2001 From: Battlefield Duck Date: Wed, 17 Jan 2024 08:38:46 +0800 Subject: [PATCH] Support World Opponent Network (WON) Query Protocol --- OpenGSQ/Protocols/Source.cs | 52 +++++---- OpenGSQ/Protocols/WON.cs | 26 +++++ OpenGSQTests/Protocols/WONTests.cs | 35 ++++++ .../Results/WONTests/GetInfoTest.json | 22 ++++ .../Results/WONTests/GetPlayersTest.json | 32 ++++++ .../Results/WONTests/GetRulesTest.json | 105 ++++++++++++++++++ 6 files changed, 250 insertions(+), 22 deletions(-) create mode 100644 OpenGSQ/Protocols/WON.cs create mode 100644 OpenGSQTests/Protocols/WONTests.cs create mode 100644 OpenGSQTests/Results/WONTests/GetInfoTest.json create mode 100644 OpenGSQTests/Results/WONTests/GetPlayersTest.json create mode 100644 OpenGSQTests/Results/WONTests/GetRulesTest.json diff --git a/OpenGSQ/Protocols/Source.cs b/OpenGSQ/Protocols/Source.cs index c7c0314..0fb97a2 100644 --- a/OpenGSQ/Protocols/Source.cs +++ b/OpenGSQ/Protocols/Source.cs @@ -21,13 +21,28 @@ public class Source : ProtocolBase public override string FullName => "Source Engine Protocol"; /// - /// Source Engine Query Protocol
- /// See: https://developer.valvesoftware.com/wiki/Server_queries + /// The byte array representing the A2S_INFO request. ///
- /// - /// - /// - public Source(string address, int port = 27015, int timeout = 5000) : base(address, port, timeout) + protected byte[] A2S_INFO = new byte[] { 0x54 }; + + /// + /// The byte array representing the A2S_PLAYER request. + /// + protected byte[] A2S_PLAYER = new byte[] { 0x55 }; + + /// + /// The byte array representing the A2S_RULES request. + /// + protected byte[] A2S_RULES = new byte[] { 0x56 }; + + + /// + /// Initializes a new instance of the Source class. + /// + /// The host to connect to. + /// The port to connect to. Default is 27015. + /// The connection timeout in milliseconds. Default is 5 seconds. + public Source(string host, int port = 27015, int timeout = 5000) : base(host, port, timeout) { } @@ -40,7 +55,7 @@ public Source(string address, int port = 27015, int timeout = 5000) : base(addre /// public async Task GetInfo() { - var responseData = await ConnectAndSendChallenge(QueryRequest.A2S_INFO); + var responseData = await ConnectAndSendChallenge(A2S_INFO); using var br = new BinaryReader(new MemoryStream(responseData), Encoding.UTF8); var header = br.ReadByte(); @@ -136,8 +151,8 @@ public async Task GetInfo() goldSource.Link = br.ReadStringEx(); goldSource.DownloadLink = br.ReadStringEx(); br.ReadByte(); - goldSource.Version = br.ReadInt64(); - goldSource.Size = br.ReadInt64(); + goldSource.Version = br.ReadInt32(); + goldSource.Size = br.ReadInt32(); goldSource.Type = br.ReadByte(); goldSource.DLL = br.ReadByte(); } @@ -157,7 +172,7 @@ public async Task GetInfo() /// public async Task> GetPlayers() { - var responseData = await ConnectAndSendChallenge(QueryRequest.A2S_PLAYER); + var responseData = await ConnectAndSendChallenge(A2S_PLAYER); using var br = new BinaryReader(new MemoryStream(responseData), Encoding.UTF8); var header = br.ReadByte(); @@ -204,7 +219,7 @@ public async Task> GetPlayers() /// public async Task> GetRules() { - var responseData = await ConnectAndSendChallenge(QueryRequest.A2S_RULES); + var responseData = await ConnectAndSendChallenge(A2S_RULES); using var br = new BinaryReader(new MemoryStream(responseData), Encoding.UTF8); var header = br.ReadByte(); @@ -227,7 +242,7 @@ public async Task> GetRules() return rules; } - private async Task ConnectAndSendChallenge(QueryRequest queryRequest) + private async Task ConnectAndSendChallenge(byte[] header) { using var udpClient = new UdpClient(); @@ -237,9 +252,9 @@ private async Task ConnectAndSendChallenge(QueryRequest queryRequest) udpClient.Client.ReceiveTimeout = Timeout; // Set up request base - byte[] requestBase = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, (byte)queryRequest }; + byte[] requestBase = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }.Concat(header).ToArray(); - if (queryRequest == QueryRequest.A2S_INFO) + if (header.SequenceEqual(A2S_INFO)) { requestBase = requestBase.Concat(Encoding.Default.GetBytes("Source Engine Query\0")).ToArray(); } @@ -247,7 +262,7 @@ private async Task ConnectAndSendChallenge(QueryRequest queryRequest) // Set up request data byte[] requestData = requestBase; - if (queryRequest != QueryRequest.A2S_INFO) + if (!header.SequenceEqual(A2S_INFO)) { requestData = requestData.Concat(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }).ToArray(); } @@ -406,13 +421,6 @@ private Environment GetEnvironment(byte environmentByte) }; } - private enum QueryRequest : byte - { - A2S_INFO = 0x54, - A2S_PLAYER = 0x55, - A2S_RULES = 0x56, - } - private enum QueryResponse : byte { S2C_CHALLENGE = 0x41, diff --git a/OpenGSQ/Protocols/WON.cs b/OpenGSQ/Protocols/WON.cs new file mode 100644 index 0000000..dc860e5 --- /dev/null +++ b/OpenGSQ/Protocols/WON.cs @@ -0,0 +1,26 @@ +using System.Text; + +namespace OpenGSQ.Protocols +{ + /// + /// World Opponent Network (WON) Query Protocol + /// + public class WON : Source + { + /// + public override string FullName => "World Opponent Network (WON) Query Protocol"; + + /// + /// Initializes a new instance of the WON class. + /// + /// The host address of the server. + /// The port to connect to. Default is 27015. + /// The connection timeout in milliseconds. Default is 5 seconds. + public WON(string host, int port = 27015, int timeout = 5000) : base(host, port, timeout) + { + A2S_INFO = Encoding.ASCII.GetBytes("details\0"); + A2S_PLAYER = Encoding.ASCII.GetBytes("players"); + A2S_RULES = Encoding.ASCII.GetBytes("rules"); + } + } +} diff --git a/OpenGSQTests/Protocols/WONTests.cs b/OpenGSQTests/Protocols/WONTests.cs new file mode 100644 index 0000000..95f4a48 --- /dev/null +++ b/OpenGSQTests/Protocols/WONTests.cs @@ -0,0 +1,35 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenGSQTests; +using System.Threading.Tasks; + +namespace OpenGSQ.Protocols.Tests +{ + [TestClass()] + public class WONTests : TestBase + { + public WON won = new("212.227.190.150", 27020); + + public WONTests() : base(nameof(WONTests)) + { + _EnableSave = false; + } + + [TestMethod()] + public async Task GetInfoTest() + { + SaveResult(nameof(GetInfoTest), await won.GetInfo()); + } + + [TestMethod()] + public async Task GetPlayersTest() + { + SaveResult(nameof(GetPlayersTest), await won.GetPlayers()); + } + + [TestMethod()] + public async Task GetRulesTest() + { + SaveResult(nameof(GetRulesTest), await won.GetRules()); + } + } +} \ No newline at end of file diff --git a/OpenGSQTests/Results/WONTests/GetInfoTest.json b/OpenGSQTests/Results/WONTests/GetInfoTest.json new file mode 100644 index 0000000..aa88d44 --- /dev/null +++ b/OpenGSQTests/Results/WONTests/GetInfoTest.json @@ -0,0 +1,22 @@ +{ + "Address": "212.227.190.150:27020", + "Name": "[Murka] NEW CS1.5 DeathMatch s4ke murka-terroristka.de", + "Map": "de_inferno", + "Folder": "cstrike", + "Game": "CounterStrike", + "Players": 6, + "MaxPlayers": 32, + "Protocol": 46, + "ServerType": 100, + "Environment": 108, + "Visibility": 0, + "Mod": 1, + "Link": "murka-terroristka.de", + "DownloadLink": "", + "Version": 1, + "Size": 184000000, + "Type": 0, + "DLL": 1, + "VAC": 0, + "Bots": 6 +} \ No newline at end of file diff --git a/OpenGSQTests/Results/WONTests/GetPlayersTest.json b/OpenGSQTests/Results/WONTests/GetPlayersTest.json new file mode 100644 index 0000000..0e14bd7 --- /dev/null +++ b/OpenGSQTests/Results/WONTests/GetPlayersTest.json @@ -0,0 +1,32 @@ +[ + { + "Name": "[P*D]Sean_Connery (21)", + "Score": 8, + "Duration": 86674.44 + }, + { + "Name": "[P0D]Jim_Carrey (41)", + "Score": 12, + "Duration": 86674.44 + }, + { + "Name": "[POD]George_Clooney (61)", + "Score": 10, + "Duration": 86674.44 + }, + { + "Name": "[P*D]Pseudolukian (21)", + "Score": 14, + "Duration": 86674.44 + }, + { + "Name": "[P0D]Jack_Nicholson (41)", + "Score": 9, + "Duration": 86674.44 + }, + { + "Name": "[POD]Jet_Li (61)", + "Score": 15, + "Duration": 86674.44 + } +] \ No newline at end of file diff --git a/OpenGSQTests/Results/WONTests/GetRulesTest.json b/OpenGSQTests/Results/WONTests/GetRulesTest.json new file mode 100644 index 0000000..9c070de --- /dev/null +++ b/OpenGSQTests/Results/WONTests/GetRulesTest.json @@ -0,0 +1,105 @@ +{ + "allow_spectators": "0", + "amx_client_languages": "0", + "amx_nextmap": "de_nuke", + "amx_timeleft": "02:16", + "amxmodx_version": "1.8.1.3746", + "coop": "0", + "csdmsake_version": "1.1e", + "deathmatch": "1", + "decalfrequency": "30", + "max_queries_sec": "1", + "max_queries_sec_global": "1", + "max_queries_window": "1", + "metamod_version": "1.19", + "mp_allowmonsters": "0", + "mp_autokick": "0", + "mp_autoteambalance": "1", + "mp_buytime": "0.50", + "mp_c4timer": "45", + "mp_chattime": "10", + "mp_consistency": "1", + "mp_fadetoblack": "0", + "mp_footsteps": "1", + "mp_forcecamera": "0", + "mp_forcechasecam": "0", + "mp_freezetime": "6", + "mp_friendlyfire": "1", + "mp_ghostfrequency": "0.1", + "mp_hostagepenalty": "13", + "mp_kickpercent": "0.66", + "mp_limitteams": "1", + "mp_logdetail": "0", + "mp_logfile": "1", + "mp_logmessages": "0", + "mp_mapvoteratio": "0.67", + "mp_maxrounds": "0", + "mp_mirrordamage": "0", + "mp_playerid": "0", + "mp_roundtime": "3", + "mp_startmoney": "800", + "mp_timeleft": "0", + "mp_timelimit": "15", + "mp_tkpunish": "0", + "mp_winlimit": "0", + "nsv_list": "", + "pb_aim_damper_coefficient_x": "0.22", + "pb_aim_damper_coefficient_y": "0.22", + "pb_aim_deviation_x": "2.0", + "pb_aim_deviation_y": "1.0", + "pb_aim_influence_x_on_y": "0.25", + "pb_aim_influence_y_on_x": "0.17", + "pb_aim_notarget_slowdown_ratio": "0.5", + "pb_aim_offset_delay": "1.2", + "pb_aim_spring_stiffness_x": "13.0", + "pb_aim_spring_stiffness_y": "13.0", + "pb_aim_target_anticipation_ratio": "2.2", + "pb_aim_type": "4", + "pb_autokill": "0", + "pb_autokilldelay": "5", + "pb_bot_join_team": "ANY", + "pb_bot_quota_match": "0", + "pb_chat": "0", + "pb_dangerfactor": "800", + "pb_detailnames": "1", + "pb_ffa": "0", + "pb_jasonmode": "0", + "pb_latencybot": "1", + "pb_mapstartbotdelay": "2", + "pb_maxbots": "0", + "pb_maxbotskill": "100", + "pb_maxcamptime": "30", + "pb_maxweaponpickup": "10", + "pb_minbots": "0", + "pb_minbotskill": "1", + "pb_numfollowuser": "5", + "pb_radio": "0", + "pb_restrequipammo": "000000000", + "pb_restrweapons": "00000000000000000000000000", + "pb_shootthruwalls": "1", + "pb_skin": "1", + "pb_spray": "1", + "pb_timer_grenade": "0.5", + "pb_timer_pickup": "0.3", + "pb_timer_sound": "0.5", + "pb_usespeech": "0", + "pb_version": "V3B20q", + "pb_welcomemsgs": "0", + "pb_wptfolder": "wptdefault", + "sv_aim": "0", + "sv_cheats": "0", + "sv_contact": "www.murka-terroristka.de", + "sv_friction": "4", + "sv_godmodetime": "1.5", + "sv_gravity": "800", + "sv_logblocks": "1", + "sv_maxrate": "0", + "sv_maxspeed": "320", + "sv_minrate": "0", + "sv_password": "0", + "sv_proxies": "0", + "sv_restart": "0", + "sv_restartround": "0", + "sv_voiceenable": "1", + "sv_weapons": "4194303" +} \ No newline at end of file