diff --git a/README.md b/README.md index c28d62c..63611f9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ SteamQueryNet is a C# wrapper for [Steam Server Queries](https://developer.valve * Light * Dependency free -* Written in .net standard 2.0 so that it works with both .NET framework 4.6+ and core. +* Written in .NET 6 # How to install ? @@ -22,9 +22,8 @@ SteamQueryNet comes with a single object that gives you access to all API's of t To make use of the API's listed above, an instance of `ServerQuery` should be created. ```csharp -string serverIp = "127.0.0.1"; -int serverPort = 27015; -IServerQuery serverQuery = new ServerQuery(serverIp, serverPort); +IServerQuery serverQuery = new ServerQuery(); +serverQuery.Connect(host, port); ``` or you can use string resolvers like below: @@ -45,15 +44,6 @@ or you can use string resolvers like below: IServerQuery serverQuery = new ServerQuery(myHostAndPort); ``` -Also, it is possible to create `ServerQuery` object without connecting like below: - -```csharp -IServerQuery serverQuery = new ServerQuery(); -serverQuery.Connect(host, port); -``` - -*Note*: `Connect` function overloads are similar to `ServerQuery` non-empty constructors. - ## Providing Custom UDPClient You can provide custom UDP clients by implementing `IUdpClient` in `SteamQueryNet.Interfaces` namespace. @@ -114,13 +104,5 @@ List players = serverQuery.GetPlayers(); List rules = serverQuery.GetRules(); ``` -While **it is not encouraged**, you can chain `Connect` function or Non-empty Constructors to get information in a single line. - -```csharp -ServerInfo serverInfo = new ServerQuery() -.Connect(host, port) -.GetServerInfo(); -``` - # Todos * Enable CI diff --git a/SteamQueryNet/SteamQueryNet.Tests/ServerQueryTests.cs b/SteamQueryNet/SteamQueryNet.Tests/ServerQueryTests.cs index 8205270..c624bbd 100644 --- a/SteamQueryNet/SteamQueryNet.Tests/ServerQueryTests.cs +++ b/SteamQueryNet/SteamQueryNet.Tests/ServerQueryTests.cs @@ -17,145 +17,145 @@ namespace SteamQueryNet.Tests { - public class ServerQueryTests - { - private const string IP_ADDRESS = "127.0.0.1"; - private const string HOST_NAME = "localhost"; - private const ushort PORT = 27015; - private byte _packetCount = 0; - private readonly IPEndPoint _localIpEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 0); - - [Theory] - [InlineData(IP_ADDRESS)] - [InlineData(HOST_NAME)] - public void ShouldInitializeWithProperHost(string host) - { - using (var sq = new ServerQuery(new Mock().Object, It.IsAny())) - { - sq.Connect(host, PORT); - } - } - - [Theory] - [InlineData("127.0.0.1:27015")] - [InlineData("127.0.0.1,27015")] - [InlineData("localhost:27015")] - [InlineData("localhost,27015")] - [InlineData("steam://connect/localhost:27015")] - [InlineData("steam://connect/127.0.0.1:27015")] - public void ShouldInitializeWithProperHostAndPort(string ipAndHost) - { - using (var sq = new ServerQuery(new Mock().Object, It.IsAny())) - { - sq.Connect(ipAndHost); - } - } - - [Theory] - [InlineData("invalidHost:-1")] - [InlineData("invalidHost,-1")] - [InlineData("invalidHost:65536")] - [InlineData("invalidHost,65536")] - [InlineData("256.256.256.256:-1")] - [InlineData("256.256.256.256,-1")] - [InlineData("256.256.256.256:65536")] - [InlineData("256.256.256.256,65536")] - public void ShouldNotInitializeWithAnInvalidHostAndPort(string invalidHost) - { - Assert.Throws(() => - { - using (var sq = new ServerQuery(new Mock().Object, It.IsAny())) - { - sq.Connect(invalidHost); - } - }); - } - - [Fact] - public void GetServerInfo_ShouldPopulateCorrectServerInfo() - { - (byte[] responsePacket, object responseObject) = ResponseHelper.GetValidResponse(ResponseHelper.ServerInfo); - var expectedObject = (ServerInfo)responseObject; - - byte[][] requestPackets = new byte[][] { RequestHelpers.PrepareAS2_INFO_Request() }; - byte[][] responsePackets = new byte[][] { responsePacket }; - - Mock udpClientMock = SetupReceiveResponse(responsePackets); - SetupRequestCompare(requestPackets, udpClientMock); - - using (var sq = new ServerQuery(udpClientMock.Object, _localIpEndpoint)) - { - Assert.Equal(JsonConvert.SerializeObject(expectedObject), JsonConvert.SerializeObject(sq.GetServerInfo())); - } - } - - [Fact] - public void GetPlayers_ShouldPopulateCorrectPlayers() - { - (byte[] playersPacket, object responseObject) = ResponseHelper.GetValidResponse(ResponseHelper.GetPlayers); - var expectedObject = (List)responseObject; - - byte[] challengePacket = RequestHelpers.PrepareAS2_RENEW_CHALLENGE_Request(); - - // Both requests will be executed on AS2_PLAYER since thats how you refresh challenges. - byte[][] requestPackets = new byte[][] { challengePacket, challengePacket }; - - // First response is the Challenge renewal response and the second - byte[][] responsePackets = new byte[][] { challengePacket, playersPacket }; - - Mock udpClientMock = SetupReceiveResponse(responsePackets); - SetupRequestCompare(requestPackets, udpClientMock); - - // Ayylmao it looks ugly as hell but we will improve it later on. - using (var sq = new ServerQuery(udpClientMock.Object, _localIpEndpoint)) - { - Assert.Equal(JsonConvert.SerializeObject(expectedObject), JsonConvert.SerializeObject(sq.GetPlayers())); - } - } - - /* - * We keep this test here to be able to have us a notifier when the Rules API becomes available. - * So, this is more like an integration test than an unit test. - * If this test starts to fail, we'll know that the Rules API started to respond. - */ - [Fact] - public void GetRules_ShouldThrowTimeoutException() - { - // Surf Heaven rulez. - const string trustedServer = "steam://connect/54.37.111.217:27015"; - - using (var sq = new ServerQuery()) - { - sq.Connect(trustedServer); - - // Make sure that the server is still alive. - Assert.True(sq.IsConnected); - bool responded = Task.WaitAll(new Task[] { sq.GetRulesAsync() }, 2000); - Assert.True(!responded); - } - } - - private void SetupRequestCompare(IEnumerable requestPackets, Mock udpClientMock) - { - udpClientMock - .Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) - .Callback((request, length) => - { - Assert.True(TestValidators.CompareBytes(requestPackets.ElementAt(_packetCount), request)); - ++_packetCount; - }); - } - - private Mock SetupReceiveResponse(IEnumerable udpPackets) - { - var udpClientMock = new Mock(); - var setupSequence = udpClientMock.SetupSequence(x => x.ReceiveAsync()); - foreach (byte[] packet in udpPackets) - { - setupSequence = setupSequence.ReturnsAsync(new UdpReceiveResult(packet, _localIpEndpoint)); - } - - return udpClientMock; - } - } + public class ServerQueryTests + { + private const string IP_ADDRESS = "127.0.0.1"; + private const string HOST_NAME = "localhost"; + private const ushort PORT = 27015; + private byte _packetCount = 0; + private readonly IPEndPoint _localIpEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 0); + + [Theory] + [InlineData(IP_ADDRESS)] + [InlineData(HOST_NAME)] + public void ShouldInitializeWithProperHost(string host) + { + using (var sq = new ServerQuery(new Mock().Object, It.IsAny())) + { + sq.Connect(host, PORT); + } + } + + [Theory] + [InlineData("127.0.0.1:27015")] + [InlineData("127.0.0.1,27015")] + [InlineData("localhost:27015")] + [InlineData("localhost,27015")] + [InlineData("steam://connect/localhost:27015")] + [InlineData("steam://connect/127.0.0.1:27015")] + public void ShouldInitializeWithProperHostAndPort(string ipAndHost) + { + using (var sq = new ServerQuery(new Mock().Object, It.IsAny())) + { + sq.Connect(ipAndHost); + } + } + + [Theory] + [InlineData("invalidHost:-1")] + [InlineData("invalidHost,-1")] + [InlineData("invalidHost:65536")] + [InlineData("invalidHost,65536")] + [InlineData("256.256.256.256:-1")] + [InlineData("256.256.256.256,-1")] + [InlineData("256.256.256.256:65536")] + [InlineData("256.256.256.256,65536")] + public void ShouldNotInitializeWithAnInvalidHostAndPort(string invalidHost) + { + Assert.Throws(() => + { + using (var sq = new ServerQuery(new Mock().Object, It.IsAny())) + { + sq.Connect(invalidHost); + } + }); + } + + [Fact] + public void GetServerInfo_ShouldPopulateCorrectServerInfo() + { + (byte[] responsePacket, object responseObject) = ResponseHelper.GetValidResponse(ResponseHelper.ServerInfo); + var expectedObject = (ServerInfo)responseObject; + + byte[][] requestPackets = new byte[][] { RequestHelpers.PrepareAS2_INFO_Request(0) }; + byte[][] responsePackets = new byte[][] { responsePacket }; + + Mock udpClientMock = SetupReceiveResponse(responsePackets); + SetupRequestCompare(requestPackets, udpClientMock); + + using (var sq = new ServerQuery(udpClientMock.Object, _localIpEndpoint)) + { + Assert.Equal(JsonConvert.SerializeObject(expectedObject), JsonConvert.SerializeObject(sq.GetServerInfo())); + } + } + + [Fact] + public void GetPlayers_ShouldPopulateCorrectPlayers() + { + (byte[] playersPacket, object responseObject) = ResponseHelper.GetValidResponse(ResponseHelper.GetPlayers); + var expectedObject = (List)responseObject; + + byte[] challengePacket = RequestHelpers.PrepareAS2_RENEW_CHALLENGE_Request(); + + // Both requests will be executed on AS2_PLAYER since thats how you refresh challenges. + byte[][] requestPackets = new byte[][] { challengePacket, challengePacket }; + + // First response is the Challenge renewal response and the second + byte[][] responsePackets = new byte[][] { challengePacket, playersPacket }; + + Mock udpClientMock = SetupReceiveResponse(responsePackets); + SetupRequestCompare(requestPackets, udpClientMock); + + // Ayylmao it looks ugly as hell but we will improve it later on. + using (var sq = new ServerQuery(udpClientMock.Object, _localIpEndpoint)) + { + Assert.Equal(JsonConvert.SerializeObject(expectedObject), JsonConvert.SerializeObject(sq.GetPlayers())); + } + } + + /* + * We keep this test here to be able to have us a notifier when the Rules API becomes available. + * So, this is more like an integration test than an unit test. + * If this test starts to fail, we'll know that the Rules API started to respond. + */ + [Fact] + public void GetRules_ShouldThrowTimeoutException() + { + // Surf Heaven rulez. + const string trustedServer = "steam://connect/54.37.111.217:27015"; + + using (var sq = new ServerQuery()) + { + sq.Connect(trustedServer); + + // Make sure that the server is still alive. + Assert.True(sq.IsConnected); + bool responded = Task.WaitAll(new Task[] { sq.GetRulesAsync() }, 2000); + Assert.True(!responded); + } + } + + private void SetupRequestCompare(IEnumerable requestPackets, Mock udpClientMock) + { + udpClientMock + .Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) + .Callback((request, length) => + { + Assert.True(TestValidators.CompareBytes(requestPackets.ElementAt(_packetCount), request)); + ++_packetCount; + }); + } + + private Mock SetupReceiveResponse(IEnumerable udpPackets) + { + var udpClientMock = new Mock(); + var setupSequence = udpClientMock.SetupSequence(x => x.ReceiveAsync()); + foreach (byte[] packet in udpPackets) + { + setupSequence = setupSequence.ReturnsAsync(new UdpReceiveResult(packet, _localIpEndpoint)); + } + + return udpClientMock; + } + } } diff --git a/SteamQueryNet/SteamQueryNet.Tests/SteamQueryNet.Tests.csproj b/SteamQueryNet/SteamQueryNet.Tests/SteamQueryNet.Tests.csproj index 23f9821..de840c8 100644 --- a/SteamQueryNet/SteamQueryNet.Tests/SteamQueryNet.Tests.csproj +++ b/SteamQueryNet/SteamQueryNet.Tests/SteamQueryNet.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp2.0 + net5.0-windows false diff --git a/SteamQueryNet/SteamQueryNet.Tests/TestValidators.cs b/SteamQueryNet/SteamQueryNet.Tests/TestValidators.cs index b9e1325..0e5f6be 100644 --- a/SteamQueryNet/SteamQueryNet.Tests/TestValidators.cs +++ b/SteamQueryNet/SteamQueryNet.Tests/TestValidators.cs @@ -1,39 +1,39 @@ namespace SteamQueryNet.Tests { - internal static class TestValidators - { - public static bool CompareBytes(byte[] source1, byte[] source2) - { - // Lets check their refs first. - if (source1 == source2) - { - // yay. - return true; - } + internal static class TestValidators + { + public static bool CompareBytes(byte[] source1, byte[] source2) + { + // Lets check their refs first. + if (source1 == source2) + { + // yay. + return true; + } - // Check their operability. - if (source1 == null || source2 == null) - { - // Consider: Maybe we should throw an exception here. - return false; - } + // Check their operability. + if (source1 == null || source2 == null) + { + // Consider: Maybe we should throw an exception here. + return false; + } - // They are not even same length lul. - if (source1.Length != source1.Length) - { - return false; - } + // They are not even same length lul. + if (source1.Length != source1.Length) + { + return false; + } - // Byte by byte comparison intensifies. - for (int i = 0; i < source1.Length; ++i) - { - if (source1[i] != source2[i]) - { - return false; - } - } + // Byte by byte comparison intensifies. + for (int i = 0; i < source1.Length; ++i) + { + if (source1[i] != source2[i]) + { + return false; + } + } - return true; - } - } + return true; + } + } } diff --git a/SteamQueryNet/SteamQueryNet/Attributes/EDFAttribute.cs b/SteamQueryNet/SteamQueryNet/Attributes/EDFAttribute.cs index a263b70..b59cba6 100644 --- a/SteamQueryNet/SteamQueryNet/Attributes/EDFAttribute.cs +++ b/SteamQueryNet/SteamQueryNet/Attributes/EDFAttribute.cs @@ -2,9 +2,9 @@ namespace SteamQueryNet.Attributes { - [AttributeUsage(AttributeTargets.Property)] - internal sealed class EDFAttribute : Attribute - { - internal EDFAttribute(byte condition) { } - } + [AttributeUsage(AttributeTargets.Property)] + internal sealed class EDFAttribute : Attribute + { + internal EDFAttribute(byte condition) { } + } } diff --git a/SteamQueryNet/SteamQueryNet/Attributes/NotParsableAttribute.cs b/SteamQueryNet/SteamQueryNet/Attributes/NotParsableAttribute.cs index 6079c4d..dc34f01 100644 --- a/SteamQueryNet/SteamQueryNet/Attributes/NotParsableAttribute.cs +++ b/SteamQueryNet/SteamQueryNet/Attributes/NotParsableAttribute.cs @@ -2,6 +2,6 @@ namespace SteamQueryNet.Attributes { - [AttributeUsage(AttributeTargets.Property)] - internal sealed class NotParsableAttribute : Attribute { } + [AttributeUsage(AttributeTargets.Property)] + internal sealed class NotParsableAttribute : Attribute { } } diff --git a/SteamQueryNet/SteamQueryNet/Attributes/ParseCustomAttribute.cs b/SteamQueryNet/SteamQueryNet/Attributes/ParseCustomAttribute.cs index c1b2851..9cf789b 100644 --- a/SteamQueryNet/SteamQueryNet/Attributes/ParseCustomAttribute.cs +++ b/SteamQueryNet/SteamQueryNet/Attributes/ParseCustomAttribute.cs @@ -2,6 +2,6 @@ namespace SteamQueryNet.Attributes { - [AttributeUsage(AttributeTargets.Property)] - internal sealed class ParseCustomAttribute : Attribute { } + [AttributeUsage(AttributeTargets.Property)] + internal sealed class ParseCustomAttribute : Attribute { } } diff --git a/SteamQueryNet/SteamQueryNet/Enums/EDFFlags.cs b/SteamQueryNet/SteamQueryNet/Enums/EDFFlags.cs index 672e314..2eeb59c 100644 --- a/SteamQueryNet/SteamQueryNet/Enums/EDFFlags.cs +++ b/SteamQueryNet/SteamQueryNet/Enums/EDFFlags.cs @@ -1,12 +1,12 @@ namespace SteamQueryNet.Enums { - public enum EDFFlags : byte - { - Port = 0x80, - SteamID = 0x10, - SourceTVPort = 0x40, - SourceTVServerName = 0x40, - Keywords = 0x20, - GameID = 0x01 - } + public enum EDFFlags : byte + { + Port = 0x80, + SteamID = 0x10, + SourceTVPort = 0x40, + SourceTVServerName = 0x40, + Keywords = 0x20, + GameID = 0x01 + } } diff --git a/SteamQueryNet/SteamQueryNet/Enums/Environment.cs b/SteamQueryNet/SteamQueryNet/Enums/Environment.cs index 5073505..e2c063c 100644 --- a/SteamQueryNet/SteamQueryNet/Enums/Environment.cs +++ b/SteamQueryNet/SteamQueryNet/Enums/Environment.cs @@ -1,9 +1,9 @@ namespace SteamQueryNet.Enums { - public enum ServerEnvironment : byte - { - Linux = (byte)'l', - Windows = (byte)'w', - Mac = (byte)'m' - } + public enum ServerEnvironment : byte + { + Linux = (byte)'l', + Windows = (byte)'w', + Mac = (byte)'m' + } } diff --git a/SteamQueryNet/SteamQueryNet/Enums/ServerType.cs b/SteamQueryNet/SteamQueryNet/Enums/ServerType.cs index b732c39..8c887c1 100644 --- a/SteamQueryNet/SteamQueryNet/Enums/ServerType.cs +++ b/SteamQueryNet/SteamQueryNet/Enums/ServerType.cs @@ -1,9 +1,9 @@ namespace SteamQueryNet.Enums { - public enum ServerType : byte - { - Dedicated = (byte)'d', - NonDedicated = (byte)'l', - SourceTVRelay = (byte)'p' - } + public enum ServerType : byte + { + Dedicated = (byte)'d', + NonDedicated = (byte)'l', + SourceTVRelay = (byte)'p' + } } diff --git a/SteamQueryNet/SteamQueryNet/Enums/ShipGameMode.cs b/SteamQueryNet/SteamQueryNet/Enums/ShipGameMode.cs index 06724cd..a82ebfb 100644 --- a/SteamQueryNet/SteamQueryNet/Enums/ShipGameMode.cs +++ b/SteamQueryNet/SteamQueryNet/Enums/ShipGameMode.cs @@ -1,12 +1,12 @@ namespace SteamQueryNet.Enums { - public enum ShipGameMode : byte - { - Hunt = 0, - Elimination = 1, - Duel = 2, - Deathmatch = 3, - VIPTeam = 4, - TeamElimination = 5 - } + public enum ShipGameMode : byte + { + Hunt = 0, + Elimination = 1, + Duel = 2, + Deathmatch = 3, + VIPTeam = 4, + TeamElimination = 5 + } } diff --git a/SteamQueryNet/SteamQueryNet/Enums/VAC.cs b/SteamQueryNet/SteamQueryNet/Enums/VAC.cs index 303596c..a3357f4 100644 --- a/SteamQueryNet/SteamQueryNet/Enums/VAC.cs +++ b/SteamQueryNet/SteamQueryNet/Enums/VAC.cs @@ -1,8 +1,8 @@ namespace SteamQueryNet.Enums { - public enum VAC : byte - { - Unsecured = 0, - Secured = 1 - } + public enum VAC : byte + { + Unsecured = 0, + Secured = 1 + } } diff --git a/SteamQueryNet/SteamQueryNet/Enums/Visibility.cs b/SteamQueryNet/SteamQueryNet/Enums/Visibility.cs index c310532..1c881f9 100644 --- a/SteamQueryNet/SteamQueryNet/Enums/Visibility.cs +++ b/SteamQueryNet/SteamQueryNet/Enums/Visibility.cs @@ -1,8 +1,8 @@ namespace SteamQueryNet.Enums { - public enum Visibility : byte - { - Public = 0, - Private = 1 - } + public enum Visibility : byte + { + Public = 0, + Private = 1 + } } diff --git a/SteamQueryNet/SteamQueryNet/Interfaces/IServerQuery.cs b/SteamQueryNet/SteamQueryNet/Interfaces/IServerQuery.cs index 2745090..375023d 100644 --- a/SteamQueryNet/SteamQueryNet/Interfaces/IServerQuery.cs +++ b/SteamQueryNet/SteamQueryNet/Interfaces/IServerQuery.cs @@ -2,94 +2,95 @@ using System.Collections.Generic; using System.Net; +using System.Threading; using System.Threading.Tasks; namespace SteamQueryNet.Interfaces { - public interface IServerQuery - { - /// - /// Renews the server challenge code of the ServerQuery instance in order to be able to execute further operations. - /// - /// The new created challenge. - int RenewChallenge(); + public interface IServerQuery + { + /// + /// Renews the server challenge code of the ServerQuery instance in order to be able to execute further operations. + /// + /// The new created challenge. + int RenewChallenge(); - /// - /// Renews the server challenge code of the ServerQuery instance in order to be able to execute further operations. - /// - /// The new created challenge. - Task RenewChallengeAsync(); + /// + /// Renews the server challenge code of the ServerQuery instance in order to be able to execute further operations. + /// + /// The new created challenge. + Task RenewChallengeAsync(CancellationToken cancellationToken); - /// - /// Configures and Connects the created instance of SteamQuery UDP socket for Steam Server Query Operations. - /// - /// IPAddress or HostName of the server that queries will be sent. - /// Port of the server that queries will be sent. - /// Connected instance of ServerQuery. - IServerQuery Connect(string serverAddress, ushort port); + /// + /// Configures and Connects the created instance of SteamQuery UDP socket for Steam Server Query Operations. + /// + /// IPAddress or HostName of the server that queries will be sent. + /// Port of the server that queries will be sent. + /// Connected instance of ServerQuery. + IServerQuery Connect(string serverAddress, ushort port); - /// - /// Configures and Connects the created instance of SteamQuery UDP socket for Steam Server Query Operations. - /// - /// IPAddress or HostName of the server and port separated by a colon(:) or a comma(,). - /// Connected instance of ServerQuery. - IServerQuery Connect(string serverAddressAndPort); + /// + /// Configures and Connects the created instance of SteamQuery UDP socket for Steam Server Query Operations. + /// + /// IPAddress or HostName of the server and port separated by a colon(:) or a comma(,). + /// Connected instance of ServerQuery. + IServerQuery Connect(string serverAddressAndPort); - /// - /// Configures and Connects the created instance of SteamQuery UDP socket for Steam Server Query Operations. - /// - /// Desired local IPEndpoint to bound. - /// IPAddress or HostName of the server and port separated by a colon(:) or a comma(,). - /// Connected instance of ServerQuery. - IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddressAndPort); + /// + /// Configures and Connects the created instance of SteamQuery UDP socket for Steam Server Query Operations. + /// + /// Desired local IPEndpoint to bound. + /// IPAddress or HostName of the server and port separated by a colon(:) or a comma(,). + /// Connected instance of ServerQuery. + IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddressAndPort); - /// - /// Configures and Connects the created instance of SteamQuery UDP socket for Steam Server Query Operations. - /// - /// Desired local IPEndpoint to bound. - /// IPAddress or HostName of the server that queries will be sent. - /// Port of the server that queries will be sent. - /// Connected instance of ServerQuery. - IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddress, ushort port); + /// + /// Configures and Connects the created instance of SteamQuery UDP socket for Steam Server Query Operations. + /// + /// Desired local IPEndpoint to bound. + /// IPAddress or HostName of the server that queries will be sent. + /// Port of the server that queries will be sent. + /// Connected instance of ServerQuery. + IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddress, ushort port); - /// - /// Requests and serializes the server information. - /// - /// Serialized ServerInfo instance. - ServerInfo GetServerInfo(); + /// + /// Requests and serializes the server information. + /// + /// Serialized ServerInfo instance. + ServerInfo GetServerInfo(); - /// - /// Requests and serializes the server information. - /// - /// Serialized ServerInfo instance. - Task GetServerInfoAsync(); + /// + /// Requests and serializes the server information. + /// + /// Serialized ServerInfo instance. + Task GetServerInfoAsync(CancellationToken cancellationToken); - /// - /// Requests and serializes the list of player information. - /// - /// Serialized list of Player instances. - List GetPlayers(); + /// + /// Requests and serializes the list of player information. + /// + /// Serialized list of Player instances. + List GetPlayers(); - /// - /// Requests and serializes the list of player information. - /// - /// Serialized list of Player instances. - Task> GetPlayersAsync(); + /// + /// Requests and serializes the list of player information. + /// + /// Serialized list of Player instances. + Task> GetPlayersAsync(CancellationToken cancellationToken); - /// - /// Requests and serializes the list of rules defined by the server. - /// Warning: CS:GO Rules reply is broken since update CSGO 1.32.3.0 (Feb 21, 2014). - /// Before the update rules got truncated when exceeding MTU, after the update rules reply is not sent at all. - /// - /// Serialized list of Rule instances. - List GetRules(); + /// + /// Requests and serializes the list of rules defined by the server. + /// Warning: CS:GO Rules reply is broken since update CSGO 1.32.3.0 (Feb 21, 2014). + /// Before the update rules got truncated when exceeding MTU, after the update rules reply is not sent at all. + /// + /// Serialized list of Rule instances. + List GetRules(); - /// - /// Requests and serializes the list of rules defined by the server. - /// Warning: CS:GO Rules reply is broken since update CSGO 1.32.3.0 (Feb 21, 2014). - /// Before the update rules got truncated when exceeding MTU, after the update rules reply is not sent at all. - /// - /// Serialized list of Rule instances. - Task> GetRulesAsync(); - } + /// + /// Requests and serializes the list of rules defined by the server. + /// Warning: CS:GO Rules reply is broken since update CSGO 1.32.3.0 (Feb 21, 2014). + /// Before the update rules got truncated when exceeding MTU, after the update rules reply is not sent at all. + /// + /// Serialized list of Rule instances. + Task> GetRulesAsync(CancellationToken cancellationToken); + } } diff --git a/SteamQueryNet/SteamQueryNet/Interfaces/IUdpClient.cs b/SteamQueryNet/SteamQueryNet/Interfaces/IUdpClient.cs index 10d006f..e922f79 100644 --- a/SteamQueryNet/SteamQueryNet/Interfaces/IUdpClient.cs +++ b/SteamQueryNet/SteamQueryNet/Interfaces/IUdpClient.cs @@ -1,20 +1,21 @@ using System; using System.Net; using System.Net.Sockets; +using System.Threading; using System.Threading.Tasks; namespace SteamQueryNet.Interfaces { - public interface IUdpClient : IDisposable - { - bool IsConnected { get; } + public interface IUdpClient : IDisposable + { + bool IsConnected { get; } - void Close(); + void Close(); - void Connect(IPEndPoint remoteIpEndpoint); + void Connect(IPEndPoint remoteIpEndpoint); - Task SendAsync(byte[] datagram, int bytes); + Task SendAsync(byte[] datagram, CancellationToken cancellationToken); - Task ReceiveAsync(); - } + Task ReceiveAsync(CancellationToken cancellationToken); + } } diff --git a/SteamQueryNet/SteamQueryNet/Models/PacketHeaders.cs b/SteamQueryNet/SteamQueryNet/Models/PacketHeaders.cs index 34b895a..37a3e6c 100644 --- a/SteamQueryNet/SteamQueryNet/Models/PacketHeaders.cs +++ b/SteamQueryNet/SteamQueryNet/Models/PacketHeaders.cs @@ -1,11 +1,11 @@ namespace SteamQueryNet.Models { - internal sealed class RequestHeaders - { - public const byte A2S_INFO = 0x54; + internal sealed class RequestHeaders + { + public const byte A2S_INFO = 0x54; - public const byte A2S_PLAYER = 0x55; + public const byte A2S_PLAYER = 0x55; - public const byte A2S_RULES = 0x56; - } + public const byte A2S_RULES = 0x56; + } } diff --git a/SteamQueryNet/SteamQueryNet/Models/Player.cs b/SteamQueryNet/SteamQueryNet/Models/Player.cs index 0a94c81..c87542b 100644 --- a/SteamQueryNet/SteamQueryNet/Models/Player.cs +++ b/SteamQueryNet/SteamQueryNet/Models/Player.cs @@ -5,58 +5,58 @@ namespace SteamQueryNet.Models { - public class Player - { - /// - /// Index of player chunk starting from 0. - /// - public byte Index { get; set; } - - /// - /// Name of the player. - /// - public string Name { get; set; } - - /// - /// Player's score (usually "frags" or "kills".) - /// - public int Score { get; set; } - - /// - /// Time (in seconds) player has been connected to the server. - /// - public float Duration { get; set; } - - /// - /// Total time as Hours:Minutes:Seconds format. - /// - [NotParsable] - public string TotalDurationAsString - { - get - { - TimeSpan totalSpan = TimeSpan.FromSeconds(Duration); - string parsedHours = totalSpan.Hours >= 10 - ? totalSpan.Hours.ToString() - : $"0{totalSpan.Hours}"; - - string parsedMinutes = totalSpan.Minutes >= 10 - ? totalSpan.Minutes.ToString() - : $"0{totalSpan.Minutes}"; - - string parsedSeconds = totalSpan.Seconds >= 10 - ? totalSpan.Seconds.ToString() - : $"0{totalSpan.Seconds}"; - - return $"{parsedHours}:{parsedMinutes}:{parsedSeconds}"; - } - } - - /// - /// The Ship additional player info. - /// - /// Warning: this property information is not supported by SteamQueryNet yet. - [ParseCustom] - public ShipPlayerDetails ShipPlayerDetails { get; set; } - } + public class Player + { + /// + /// Index of player chunk starting from 0. + /// + public byte Index { get; set; } + + /// + /// Name of the player. + /// + public string Name { get; set; } + + /// + /// Player's score (usually "frags" or "kills".) + /// + public int Score { get; set; } + + /// + /// Time (in seconds) player has been connected to the server. + /// + public float Duration { get; set; } + + /// + /// Total time as Hours:Minutes:Seconds format. + /// + [NotParsable] + public string TotalDurationAsString + { + get + { + TimeSpan totalSpan = TimeSpan.FromSeconds(Duration); + string parsedHours = totalSpan.Hours >= 10 + ? totalSpan.Hours.ToString() + : $"0{totalSpan.Hours}"; + + string parsedMinutes = totalSpan.Minutes >= 10 + ? totalSpan.Minutes.ToString() + : $"0{totalSpan.Minutes}"; + + string parsedSeconds = totalSpan.Seconds >= 10 + ? totalSpan.Seconds.ToString() + : $"0{totalSpan.Seconds}"; + + return $"{parsedHours}:{parsedMinutes}:{parsedSeconds}"; + } + } + + /// + /// The Ship additional player info. + /// + /// Warning: this property information is not supported by SteamQueryNet yet. + [ParseCustom] + public ShipPlayerDetails ShipPlayerDetails { get; set; } + } } diff --git a/SteamQueryNet/SteamQueryNet/Models/ServerInfo.cs b/SteamQueryNet/SteamQueryNet/Models/ServerInfo.cs index 3b68730..0a25981 100644 --- a/SteamQueryNet/SteamQueryNet/Models/ServerInfo.cs +++ b/SteamQueryNet/SteamQueryNet/Models/ServerInfo.cs @@ -4,145 +4,139 @@ namespace SteamQueryNet.Models { - public class ServerInfo - { - /// - /// Protocol version used by the server. - /// - public byte Protocol { get; set; } - - /// - /// Name of the server. - /// - public string Name { get; set; } - - /// - /// Map the server has currently loaded. - /// - public string Map { get; set; } - - /// - /// Name of the folder containing the game files. - /// - public string Folder { get; set; } - - /// - /// Full name of the game. - /// - public string Game { get; set; } - - /// - /// Steam Application ID of game. - /// - public short ID { get; set; } - - /// - /// Number of players on the server. - /// - - private byte _players; - public byte Players - { - get - { - // Some servers send bots as players. We don't want that here. - return (byte)(this._players - this.Bots); - } - set - { - this._players = value; - } - } - - /// - /// Maximum number of players the server reports it can hold. - /// - public byte MaxPlayers { get; set; } - - /// - /// Number of bots on the server. - /// - public byte Bots { get; set; } - - /// - /// Indicates the type of server. - /// - public ServerType ServerType { get; set; } - - /// - /// Indicates the operating system of the server. - /// - public ServerEnvironment Environment { get; set; } - - /// - /// Indicates whether the server requires a password. - /// - public Visibility Visibility { get; set; } - - /// - /// Specifies whether the server uses VAC. - /// - public VAC VAC { get; set; } - - /// - /// This property only exist in a response if the server is running The Ship. - /// Warning: this property information is not supported by SteamQueryNet yet. - /// - [ParseCustom] - public ShipGameInfo ShipGameInfo { get; set; } - - /// - /// Version of the game installed on the server. - /// - public string Version { get; set; } - - /// - /// If present, this specifies which additional data fields will be included. - /// - public byte EDF { get; set; } - - /// - /// The server's game port number. - /// - [EDF((byte)EDFFlags.Port)] - public short Port { get; set; } - - /// - /// Server's SteamID. - /// - [EDF((byte)EDFFlags.SteamID)] - public long SteamID { get; set; } - - /// - /// Spectator port number for SourceTV. - /// - [EDF((byte)EDFFlags.SourceTVPort)] - public short SourceTVPort { get; set; } - - /// - /// Name of the spectator server for SourceTV. - /// - [EDF((byte)EDFFlags.SourceTVServerName)] - public string SourceTVServerName { get; set; } - - /// - /// Tags that describe the game according to the server (for future use.) - /// - [EDF((byte)EDFFlags.Keywords)] - public string Keywords { get; set; } - - /// - /// The server's 64-bit GameID. If this is present, a more accurate AppID is present in the low 24 bits. - /// The earlier AppID could have been truncated as it was forced into 16-bit storage. - /// - [EDF((byte)EDFFlags.GameID)] - public long GameID { get; set; } - - /// - /// Calculated roundtrip time of the server. - /// Warning: this value will be calculated by SteamQueryNet instead of steam itself. - /// - [NotParsable] - public long Ping { get; set; } - } + public class ServerInfo + { + /// + /// Protocol version used by the server. + /// + public byte Protocol { get; set; } + + /// + /// Name of the server. + /// + public string Name { get; set; } + + /// + /// Map the server has currently loaded. + /// + public string Map { get; set; } + + /// + /// Name of the folder containing the game files. + /// + public string Folder { get; set; } + + /// + /// Full name of the game. + /// + public string Game { get; set; } + + /// + /// Steam Application ID of game. + /// + public short ID { get; set; } + + /// + /// Number of players on the server. + /// + + private byte m_players; + public byte Players + { + // Some servers send bots as players. We don't want that here. + get => (byte)(m_players - Bots); + set => m_players = value; + } + + /// + /// Maximum number of players the server reports it can hold. + /// + public byte MaxPlayers { get; set; } + + /// + /// Number of bots on the server. + /// + public byte Bots { get; set; } + + /// + /// Indicates the type of server. + /// + public ServerType ServerType { get; set; } + + /// + /// Indicates the operating system of the server. + /// + public ServerEnvironment Environment { get; set; } + + /// + /// Indicates whether the server requires a password. + /// + public Visibility Visibility { get; set; } + + /// + /// Specifies whether the server uses VAC. + /// + public VAC VAC { get; set; } + + /// + /// This property only exist in a response if the server is running The Ship. + /// Warning: this property information is not supported by SteamQueryNet yet. + /// + [ParseCustom] + public ShipGameInfo ShipGameInfo { get; set; } + + /// + /// Version of the game installed on the server. + /// + public string Version { get; set; } + + /// + /// If present, this specifies which additional data fields will be included. + /// + public byte EDF { get; set; } + + /// + /// The server's game port number. + /// + [EDF((byte)EDFFlags.Port)] + public short Port { get; set; } + + /// + /// Server's SteamID. + /// + [EDF((byte)EDFFlags.SteamID)] + public long SteamID { get; set; } + + /// + /// Spectator port number for SourceTV. + /// + [EDF((byte)EDFFlags.SourceTVPort)] + public short SourceTVPort { get; set; } + + /// + /// Name of the spectator server for SourceTV. + /// + [EDF((byte)EDFFlags.SourceTVServerName)] + public string SourceTVServerName { get; set; } + + /// + /// Tags that describe the game according to the server (for future use.) + /// + [EDF((byte)EDFFlags.Keywords)] + public string Keywords { get; set; } + + /// + /// The server's 64-bit GameID. If this is present, a more accurate AppID is present in the low 24 bits. + /// The earlier AppID could have been truncated as it was forced into 16-bit storage. + /// + [EDF((byte)EDFFlags.GameID)] + public long GameID { get; set; } + + /// + /// Calculated roundtrip time of the server. + /// Warning: this value will be calculated by SteamQueryNet instead of steam itself. + /// + [NotParsable] + public long Ping { get; set; } + } } diff --git a/SteamQueryNet/SteamQueryNet/Models/TheShip/ShipGameInfo.cs b/SteamQueryNet/SteamQueryNet/Models/TheShip/ShipGameInfo.cs index 364253b..67fb320 100644 --- a/SteamQueryNet/SteamQueryNet/Models/TheShip/ShipGameInfo.cs +++ b/SteamQueryNet/SteamQueryNet/Models/TheShip/ShipGameInfo.cs @@ -2,24 +2,24 @@ namespace SteamQueryNet.Models.TheShip { - /// - /// These fields only exist in a response if the server is running The Ship. - /// - public class ShipGameInfo - { - /// - /// Indicates the game mode. - /// - public ShipGameMode Mode { get; set; } + /// + /// These fields only exist in a response if the server is running The Ship. + /// + public class ShipGameInfo + { + /// + /// Indicates the game mode. + /// + public ShipGameMode Mode { get; set; } - /// - /// The number of witnesses necessary to have a player arrested. - /// - public byte Witnesses { get; set; } + /// + /// The number of witnesses necessary to have a player arrested. + /// + public byte Witnesses { get; set; } - /// - /// Time (in seconds) before a player is arrested while being witnessed. - /// - public byte Duration { get; set; } - } + /// + /// Time (in seconds) before a player is arrested while being witnessed. + /// + public byte Duration { get; set; } + } } diff --git a/SteamQueryNet/SteamQueryNet/Models/TheShip/ShipPlayerDetails.cs b/SteamQueryNet/SteamQueryNet/Models/TheShip/ShipPlayerDetails.cs index 92db4e7..6625139 100644 --- a/SteamQueryNet/SteamQueryNet/Models/TheShip/ShipPlayerDetails.cs +++ b/SteamQueryNet/SteamQueryNet/Models/TheShip/ShipPlayerDetails.cs @@ -1,18 +1,18 @@ namespace SteamQueryNet.Models.TheShip { - /// - /// The Ship additional player info. - /// - public class ShipPlayerDetails - { - /// - /// Player's deaths. - /// - public long Deats { get; set; } + /// + /// The Ship additional player info. + /// + public class ShipPlayerDetails + { + /// + /// Player's deaths. + /// + public long Deats { get; set; } - /// - /// Player's money. - /// - public long Money { get; set; } - } + /// + /// Player's money. + /// + public long Money { get; set; } + } } diff --git a/SteamQueryNet/SteamQueryNet/ServerQuery.cs b/SteamQueryNet/SteamQueryNet/ServerQuery.cs index a59faa6..c0d0b38 100644 --- a/SteamQueryNet/SteamQueryNet/ServerQuery.cs +++ b/SteamQueryNet/SteamQueryNet/ServerQuery.cs @@ -9,278 +9,263 @@ using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; +using System.Threading; using System.Threading.Tasks; [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("SteamQueryNet.Tests")] namespace SteamQueryNet { - public class ServerQuery : IServerQuery, IDisposable - { - private IPEndPoint _remoteIpEndpoint; - - private ushort _port; - private int _currentChallenge; - - internal virtual IUdpClient UdpClient { get; private set; } - - /// - /// Reflects the udp client connection state. - /// - public bool IsConnected - { - get - { - return UdpClient.IsConnected; - } - } - - /// - /// Amount of time in miliseconds to terminate send operation if the server won't respond. - /// - public int SendTimeout { get; set; } - - /// - /// Amount of time in miliseconds to terminate receive operation if the server won't respond. - /// - public int ReceiveTimeout { get; set; } - - /// - /// Creates a new instance of ServerQuery with given UDPClient and remote endpoint. - /// - /// UdpClient to communicate. - /// Remote server endpoint. - public ServerQuery(IUdpClient udpClient, IPEndPoint remoteEndpoint) - { - UdpClient = udpClient; - _remoteIpEndpoint = remoteEndpoint; - } - - /// - /// Creates a new instance of ServerQuery without UDP socket connection. - /// - public ServerQuery() { } - - /// - /// Creates a new ServerQuery instance for Steam Server Query Operations. - /// - /// IPAddress or HostName of the server that queries will be sent. - /// Port of the server that queries will be sent. - public ServerQuery(string serverAddress, ushort port) - { - PrepareAndConnect(serverAddress, port); - } - - /// - /// Creates a new ServerQuery instance for Steam Server Query Operations. - /// - /// IPAddress or HostName of the server and port separated by a colon(:) or a comma(,). - public ServerQuery(string serverAddressAndPort) - { - (string serverAddress, ushort port) = Helpers.ResolveIPAndPortFromString(serverAddressAndPort); - PrepareAndConnect(serverAddress, port); - } - - /// - /// Creates a new instance of ServerQuery with the given Local IPEndpoint. - /// - /// Desired local IPEndpoint to bound. - /// IPAddress or HostName of the server and port separated by a colon(:) or a comma(,). - public ServerQuery(IPEndPoint customLocalIPEndpoint, string serverAddressAndPort) - { - UdpClient = new UdpWrapper(customLocalIPEndpoint, SendTimeout, ReceiveTimeout); - (string serverAddress, ushort port) = Helpers.ResolveIPAndPortFromString(serverAddressAndPort); - PrepareAndConnect(serverAddress, port); - } - - /// - /// Creates a new instance of ServerQuery with the given Local IPEndpoint. - /// - /// Desired local IPEndpoint to bound. - /// IPAddress or HostName of the server that queries will be sent. - /// Port of the server that queries will be sent. - public ServerQuery(IPEndPoint customLocalIPEndpoint, string serverAddress, ushort port) - { - UdpClient = new UdpWrapper(customLocalIPEndpoint, SendTimeout, ReceiveTimeout); - PrepareAndConnect(serverAddress, port); - } - - /// - public IServerQuery Connect(string serverAddress, ushort port) - { - PrepareAndConnect(serverAddress, port); - return this; - } - - /// - public IServerQuery Connect(string serverAddressAndPort) - { - (string serverAddress, ushort port) = Helpers.ResolveIPAndPortFromString(serverAddressAndPort); - PrepareAndConnect(serverAddress, port); - return this; - } - - /// - public IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddressAndPort) - { - UdpClient = new UdpWrapper(customLocalIPEndpoint, SendTimeout, ReceiveTimeout); - (string serverAddress, ushort port) = Helpers.ResolveIPAndPortFromString(serverAddressAndPort); - PrepareAndConnect(serverAddress, port); - return this; - } - - /// - public IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddress, ushort port) - { - UdpClient = new UdpWrapper(customLocalIPEndpoint, SendTimeout, ReceiveTimeout); - PrepareAndConnect(serverAddress, port); - return this; - } - - /// - public async Task GetServerInfoAsync() - { - var sInfo = new ServerInfo - { - Ping = new Ping().Send(_remoteIpEndpoint.Address).RoundtripTime - }; - - byte[] response = await SendRequestAsync(RequestHelpers.PrepareAS2_INFO_Request()); - if (response.Length > 0) - { - DataResolutionUtils.ExtractData(sInfo, response, nameof(sInfo.EDF), true); - } - - return sInfo; - } - - /// - public ServerInfo GetServerInfo() - { - return Helpers.RunSync(GetServerInfoAsync); - } - - /// - public async Task RenewChallengeAsync() - { - byte[] response = await SendRequestAsync(RequestHelpers.PrepareAS2_RENEW_CHALLENGE_Request()); - if (response.Length > 0) - { - _currentChallenge = BitConverter.ToInt32(response.Skip(DataResolutionUtils.RESPONSE_CODE_INDEX).Take(sizeof(int)).ToArray(), 0); - } - - return _currentChallenge; - } - - /// - public int RenewChallenge() - { - return Helpers.RunSync(RenewChallengeAsync); - } - - /// - public async Task> GetPlayersAsync() - { - if (_currentChallenge == 0) - { - await RenewChallengeAsync(); - } - - byte[] response = await SendRequestAsync( - RequestHelpers.PrepareAS2_GENERIC_Request(RequestHeaders.A2S_PLAYER,_currentChallenge)); - - if (response.Length > 0) - { - return DataResolutionUtils.ExtractListData(response); - } - else - { - throw new InvalidOperationException("Server did not response the query"); - } - } - - /// - public List GetPlayers() - { - return Helpers.RunSync(GetPlayersAsync); - } - - /// - public async Task> GetRulesAsync() - { - if (_currentChallenge == 0) - { - await RenewChallengeAsync(); - } - - byte[] response = await SendRequestAsync( - RequestHelpers.PrepareAS2_GENERIC_Request(RequestHeaders.A2S_RULES, _currentChallenge)); - - if (response.Length > 0) - { - return DataResolutionUtils.ExtractListData(response); - } - else - { - throw new InvalidOperationException("Server did not response the query"); - } - } - - /// - public List GetRules() - { - return Helpers.RunSync(GetRulesAsync); - } - - /// - /// Disposes the object and its disposables. - /// - public void Dispose() - { - UdpClient.Close(); - UdpClient.Dispose(); - } - - private void PrepareAndConnect(string serverAddress, ushort port) - { - _port = port; - - // Try to parse the serverAddress as IP first - if (IPAddress.TryParse(serverAddress, out IPAddress parsedIpAddress)) - { - // Yep its an IP. - _remoteIpEndpoint = new IPEndPoint(parsedIpAddress, _port); - } - else - { - // Nope it might be a hostname. - try - { - IPAddress[] addresslist = Dns.GetHostAddresses(serverAddress); - if (addresslist.Length > 0) - { - // We get the first address. - _remoteIpEndpoint = new IPEndPoint(addresslist[0], _port); - } - else - { - throw new ArgumentException($"Invalid host address {serverAddress}"); - } - } - catch (SocketException ex) - { - throw new ArgumentException("Could not reach the hostname.", ex); - } - } - - UdpClient = UdpClient ?? new UdpWrapper(new IPEndPoint(IPAddress.Any, 0), SendTimeout, ReceiveTimeout); - UdpClient.Connect(_remoteIpEndpoint); - } - - private async Task SendRequestAsync(byte[] request) - { - await UdpClient.SendAsync(request, request.Length); - UdpReceiveResult result = await UdpClient.ReceiveAsync(); - return result.Buffer; - } - } + public class ServerQuery : IServerQuery, IDisposable + { + private IPEndPoint m_remoteIpEndpoint; + + private ushort m_port; + private int m_currentChallenge; + + internal virtual IUdpClient UdpClient { get; private set; } + + /// + /// Reflects the udp client connection state. + /// + public bool IsConnected => UdpClient.IsConnected; + + /// + /// Amount of time in milliseconds to terminate send operation if the server won't respond. + /// + public int SendTimeout { get; set; } + + /// + /// Amount of time in milliseconds to terminate receive operation if the server won't respond. + /// + public int ReceiveTimeout { get; set; } + + /// + /// Creates a new instance of ServerQuery with given UDPClient and remote endpoint. + /// + /// UdpClient to communicate. + /// Remote server endpoint. + public ServerQuery(IUdpClient udpClient, IPEndPoint remoteEndpoint) + { + UdpClient = udpClient; + m_remoteIpEndpoint = remoteEndpoint; + } + + /// + /// Creates a new instance of ServerQuery without UDP socket connection. + /// + public ServerQuery() { } + + /// + /// Creates a new ServerQuery instance for Steam Server Query Operations. + /// + /// IPAddress or HostName of the server that queries will be sent. + /// Port of the server that queries will be sent. + public IServerQuery Connect(string serverAddress, ushort port) + { + PrepareAndConnect(serverAddress, port); + return this; + } + + /// + /// Creates a new ServerQuery instance for Steam Server Query Operations. + /// + /// IPAddress or HostName of the server and port separated by a colon(:) or a comma(,). + public IServerQuery Connect(string serverAddressAndPort) + { + (string serverAddress, ushort port) = Helpers.ResolveIPAndPortFromString(serverAddressAndPort); + PrepareAndConnect(serverAddress, port); + return this; + } + + /// + /// Creates a new instance of ServerQuery with the given Local IPEndpoint. + /// + /// Desired local IPEndpoint to bound. + /// IPAddress or HostName of the server and port separated by a colon(:) or a comma(,). + public IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddressAndPort) + { + UdpClient = new UdpWrapper(customLocalIPEndpoint, SendTimeout, ReceiveTimeout); + (string serverAddress, ushort port) = Helpers.ResolveIPAndPortFromString(serverAddressAndPort); + PrepareAndConnect(serverAddress, port); + return this; + } + + /// + /// Creates a new instance of ServerQuery with the given Local IPEndpoint. + /// + /// Desired local IPEndpoint to bound. + /// IPAddress or HostName of the server that queries will be sent. + /// Port of the server that queries will be sent. + public IServerQuery Connect(IPEndPoint customLocalIPEndpoint, string serverAddress, ushort port) + { + UdpClient = new UdpWrapper(customLocalIPEndpoint, SendTimeout, ReceiveTimeout); + PrepareAndConnect(serverAddress, port); + return this; + } + + /// + public async Task GetServerInfoAsync(CancellationToken cancellationToken) + { + var sInfo = new ServerInfo + { + Ping = new Ping().Send(m_remoteIpEndpoint.Address)?.RoundtripTime ?? default + }; + + if (m_currentChallenge == 0) + { + await RenewChallengeAsync(cancellationToken); + } + + byte[] response = await SendRequestAsync(RequestHelpers.PrepareAS2_INFO_Request(m_currentChallenge), cancellationToken); + if (response.Length > 0) + { + DataResolutionUtils.ExtractData(sInfo, response, nameof(sInfo.EDF), true); + } + + return sInfo; + } + + /// + public ServerInfo GetServerInfo() + { + Task task = GetServerInfoAsync(new CancellationTokenSource().Token); + task.RunSynchronously(); + return task.Result; + // return Helpers.RunSync(GetServerInfoAsync); + } + + /// + public async Task RenewChallengeAsync(CancellationToken cancellationToken) + { + byte[] response = await SendRequestAsync(RequestHelpers.PrepareAS2_RENEW_CHALLENGE_Request(), cancellationToken); + if (response.Length > 0) + { + m_currentChallenge = BitConverter.ToInt32(response.Skip(DataResolutionUtils.RESPONSE_CODE_INDEX).Take(sizeof(int)).ToArray(), 0); + } + + return m_currentChallenge; + } + + /// + public int RenewChallenge() + { + Task task = RenewChallengeAsync(new CancellationTokenSource().Token); + task.RunSynchronously(); + return task.Result; + // return Helpers.RunSync(RenewChallengeAsync); + } + + /// + public async Task> GetPlayersAsync(CancellationToken cancellationToken) + { + if (m_currentChallenge == 0) + { + await RenewChallengeAsync(cancellationToken); + } + + byte[] response = await SendRequestAsync( + RequestHelpers.PrepareAS2_GENERIC_Request(RequestHeaders.A2S_PLAYER, m_currentChallenge), + cancellationToken); + + if (response.Length > 0) + { + return DataResolutionUtils.ExtractPlayersData(response); + } + else + { + throw new InvalidOperationException("Server did not response the query"); + } + } + + /// + public List GetPlayers() + { + Task> task = GetPlayersAsync(new CancellationTokenSource().Token); + task.RunSynchronously(); + return task.Result; + // return Helpers.RunSync(GetPlayersAsync); + } + + /// + public async Task> GetRulesAsync(CancellationToken cancellationToken) + { + if (m_currentChallenge == 0) + { + await RenewChallengeAsync(cancellationToken); + } + + byte[] response = await SendRequestAsync( + RequestHelpers.PrepareAS2_GENERIC_Request(RequestHeaders.A2S_RULES, m_currentChallenge), + cancellationToken); + if (response.Length > 0) + { + return DataResolutionUtils.ExtractRulesData(response); + } + else + { + throw new InvalidOperationException("Server did not response the query"); + } + } + + /// + public List GetRules() + { + Task> task = GetRulesAsync(new CancellationTokenSource().Token); + task.RunSynchronously(); + return task.Result; + // return Helpers.RunSync(GetRulesAsync); + } + + /// + /// Disposes the object and its disposables. + /// + public void Dispose() + { + UdpClient.Close(); + UdpClient.Dispose(); + } + + private void PrepareAndConnect(string serverAddress, ushort port) + { + m_port = port; + + // Try to parse the serverAddress as IP first + if (IPAddress.TryParse(serverAddress, out IPAddress parsedIpAddress)) + { + // Yep its an IP. + m_remoteIpEndpoint = new IPEndPoint(parsedIpAddress, m_port); + } + else + { + // Nope it might be a hostname. + try + { + IPAddress[] addressList = Dns.GetHostAddresses(serverAddress); + if (addressList.Length > 0) + { + // We get the first address. + m_remoteIpEndpoint = new IPEndPoint(addressList[0], m_port); + } + else + { + throw new ArgumentException($"Invalid host address {serverAddress}"); + } + } + catch (SocketException ex) + { + throw new ArgumentException("Could not reach the hostname.", ex); + } + } + + UdpClient ??= new UdpWrapper(new IPEndPoint(IPAddress.Any, 0), SendTimeout, ReceiveTimeout); + UdpClient.Connect(m_remoteIpEndpoint); + } + + private async Task SendRequestAsync(byte[] request, CancellationToken cancellationToken) + { + await UdpClient.SendAsync(request, cancellationToken); + UdpReceiveResult result = await UdpClient.ReceiveAsync(cancellationToken); + return result.Buffer; + } + } } diff --git a/SteamQueryNet/SteamQueryNet/Services/UdpWrapper.cs b/SteamQueryNet/SteamQueryNet/Services/UdpWrapper.cs index 4f24a23..93bd174 100644 --- a/SteamQueryNet/SteamQueryNet/Services/UdpWrapper.cs +++ b/SteamQueryNet/SteamQueryNet/Services/UdpWrapper.cs @@ -1,53 +1,84 @@ using SteamQueryNet.Interfaces; - +using System; using System.Net; using System.Net.Sockets; +using System.Threading; using System.Threading.Tasks; namespace SteamQueryNet.Services { - internal sealed class UdpWrapper : IUdpClient - { - private readonly UdpClient _udpClient; - - public UdpWrapper(IPEndPoint localIpEndPoint, int sendTimeout, int receiveTimeout) - { - _udpClient = new UdpClient(localIpEndPoint); - _udpClient.Client.SendTimeout = sendTimeout; - _udpClient.Client.ReceiveTimeout = receiveTimeout; - } - - public bool IsConnected - { - get - { - return this._udpClient.Client.Connected; - } - } - - public void Close() - { - this._udpClient.Close(); - } - - public void Connect(IPEndPoint remoteIpEndpoint) - { - this._udpClient.Connect(remoteIpEndpoint); - } - - public void Dispose() - { - this._udpClient.Dispose(); - } - - public Task ReceiveAsync() - { - return this._udpClient.ReceiveAsync(); - } - - public Task SendAsync(byte[] datagram, int bytes) - { - return this._udpClient.SendAsync(datagram, bytes); - } - } + internal sealed class UdpWrapper : IUdpClient + { + private readonly UdpClient m_udpClient; + private readonly int m_sendTimeout; + private readonly int m_receiveTimeout; + + public UdpWrapper(IPEndPoint localIpEndPoint, int sendTimeout, int receiveTimeout) + { + m_udpClient = new UdpClient(localIpEndPoint); + m_sendTimeout = sendTimeout; + m_receiveTimeout = receiveTimeout; + } + + public bool IsConnected => m_udpClient.Client.Connected; + + public void Close() + { + m_udpClient.Close(); + } + + public void Connect(IPEndPoint remoteIpEndpoint) + { + m_udpClient.Connect(remoteIpEndpoint); + } + + public void Dispose() + { + m_udpClient.Dispose(); + } + + public async Task ReceiveAsync(CancellationToken cancellationToken) + { + var source = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + source.CancelAfter(m_receiveTimeout); + + try + { + return await m_udpClient.ReceiveAsync(source.Token); + } + catch (OperationCanceledException) + { + if (cancellationToken.IsCancellationRequested) + { + throw; + } + else + { + throw new TimeoutException(); + } + } + } + + public async Task SendAsync(byte[] datagram, CancellationToken cancellationToken) + { + var source = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + source.CancelAfter(m_receiveTimeout); + + try + { + return await m_udpClient.SendAsync(datagram, source.Token); + } + catch (OperationCanceledException) + { + if (cancellationToken.IsCancellationRequested) + { + throw; + } + else + { + throw new TimeoutException(); + } + } + } + } } diff --git a/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj b/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj index 96551ea..693e9c3 100644 --- a/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj +++ b/SteamQueryNet/SteamQueryNet/SteamQueryNet.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net8.0 Cem YILMAZ Cem YILMAZ 1.0.6 @@ -22,6 +22,9 @@ Written in .net standard 2.0 so that it works with both .NET framework 4.6+ and https://github.com/cyilcode/SteamQueryNet git true + + Library + diff --git a/SteamQueryNet/SteamQueryNet/Utils/DataResolutionUtils.cs b/SteamQueryNet/SteamQueryNet/Utils/DataResolutionUtils.cs index 211da19..73e9c8d 100644 --- a/SteamQueryNet/SteamQueryNet/Utils/DataResolutionUtils.cs +++ b/SteamQueryNet/SteamQueryNet/Utils/DataResolutionUtils.cs @@ -9,139 +9,169 @@ namespace SteamQueryNet.Utils { - internal sealed class DataResolutionUtils - { - internal const byte RESPONSE_HEADER_COUNT = 5; - internal const byte RESPONSE_CODE_INDEX = 5; - - internal static IEnumerable ExtractData( - TObject objectRef, - byte[] dataSource, - string edfPropName = "", - bool stripHeaders = false) - where TObject : class - { - IEnumerable takenBytes = new List(); - - // We can be a good guy and ask for any extra jobs :) - IEnumerable enumerableSource = stripHeaders - ? dataSource.Skip(RESPONSE_HEADER_COUNT) - : dataSource; - - // We get every property that does not contain ParseCustom and NotParsable attributes on them to iterate through all and parse/assign their values. - IEnumerable propsOfObject = typeof(TObject).GetProperties() - .Where(x => !x.CustomAttributes.Any(y => y.AttributeType == typeof(ParseCustomAttribute) || y.AttributeType == typeof(NotParsableAttribute))); - - foreach (PropertyInfo property in propsOfObject) - { - /* Check for EDF property name, if it was provided then it mean that we have EDF properties in the model. - * You can check here: https://developer.valvesoftware.com/wiki/Server_queries#A2S_INFO to get more info about EDF's. */ - if (!string.IsNullOrEmpty(edfPropName)) - { - // Does the property have an EDFAttribute assigned ? - CustomAttributeData edfInfo = property.CustomAttributes.FirstOrDefault(x => x.AttributeType == typeof(EDFAttribute)); - if (edfInfo != null) - { - // Get the EDF value that was returned by the server. - byte edfValue = (byte)typeof(TObject).GetProperty(edfPropName).GetValue(objectRef); - - // Get the EDF condition value that was provided in the model. - byte edfPropertyConditionValue = (byte)edfInfo.ConstructorArguments[0].Value; - - // Continue if the condition does not pass because it means that the server did not include any information about this property. - if ((edfValue & edfPropertyConditionValue) <= 0) { continue; } - } - } - - /* Basic explanation of what is going of from here; - * Get the type of the property and get amount of bytes of its size from the response array, - * Convert the parsed value to its type and assign it. - */ - - /* We have to handle strings separately since their size is unknown and they are also null terminated. - * Check here: https://developer.valvesoftware.com/wiki/String for further information about Strings in the protocol. - */ - if (property.PropertyType == typeof(string)) - { - // Clear the buffer first then take till the termination. - takenBytes = enumerableSource - .SkipWhile(x => x == 0) - .TakeWhile(x => x != 0); - - // Parse it into a string. - property.SetValue(objectRef, Encoding.UTF8.GetString(takenBytes.ToArray())); - - // Update the source by skipping the amount of bytes taken from the source and + 1 for termination byte. - enumerableSource = enumerableSource.Skip(takenBytes.Count() + 1); - } - else - { - // Is the property an Enum ? if yes we should be getting the underlying type since it might differ. - Type typeOfProperty = property.PropertyType.IsEnum - ? property.PropertyType.GetEnumUnderlyingType() - : property.PropertyType; - - // Extract the value and the size from the source. - (object result, int size) = ExtractMarshalType(enumerableSource, typeOfProperty); - - /* If the property is an enum we should parse it first then assign its value, - * if not we can just give it to SetValue since it was converted by ExtractMarshalType already.*/ - property.SetValue(objectRef, property.PropertyType.IsEnum - ? Enum.Parse(property.PropertyType, result.ToString()) - : result); - - // Update the source by skipping the amount of bytes taken from the source. - enumerableSource = enumerableSource.Skip(size); - } - } - - // We return the last state of the processed source. - return enumerableSource; - } - - internal static List ExtractListData(byte[] rawSource) - where TObject : class - { - // Create a list to contain the serialized data. - var objectList = new List(); - - // Skip the response headers. - byte itemCount = rawSource[RESPONSE_CODE_INDEX]; - - // Skip +1 for item_count. - IEnumerable dataSource = rawSource.Skip(RESPONSE_HEADER_COUNT + 1); - - for (byte i = 0; i < itemCount; i++) - { - // Activate a new instance of the object. - var objectInstance = Activator.CreateInstance(); - - // Extract the data. - dataSource = ExtractData(objectInstance, dataSource.ToArray()); - - // Add it into the list. - objectList.Add(objectInstance); - } - - return objectList; - } - - internal static (object, int) ExtractMarshalType(IEnumerable source, Type type) - { - // Get the size of the given type. - int sizeOfType = Marshal.SizeOf(type); - - // Take amount of bytes from the source array. - IEnumerable takenBytes = source.Take(sizeOfType); - - // We actually need to go into an unsafe block here since as far as i know, this is the only way to convert a byte[] source into its given type on runtime. - unsafe - { - fixed (byte* sourcePtr = takenBytes.ToArray()) - { - return (Marshal.PtrToStructure(new IntPtr(sourcePtr), type), sizeOfType); - } - } - } - } + internal sealed class DataResolutionUtils + { + internal const byte RESPONSE_HEADER_COUNT = 5; + internal const byte RESPONSE_CODE_INDEX = 5; + + internal static IEnumerable ExtractData( + TObject objectRef, + byte[] dataSource, + string edfPropName = "", + bool stripHeaders = false) + where TObject : class + { + IEnumerable takenBytes = new List(); + + // We can be a good guy and ask for any extra jobs :) + IEnumerable enumerableSource = stripHeaders + ? dataSource.Skip(RESPONSE_HEADER_COUNT) + : dataSource; + + // We get every property that does not contain ParseCustom and NotParsable attributes on them to iterate through all and parse/assign their values. + IEnumerable propsOfObject = typeof(TObject).GetProperties() + .Where(x => !x.CustomAttributes.Any(y => y.AttributeType == typeof(ParseCustomAttribute) || y.AttributeType == typeof(NotParsableAttribute))); + + foreach (PropertyInfo property in propsOfObject) + { + /* Check for EDF property name, if it was provided then it mean that we have EDF properties in the model. + * You can check here: https://developer.valvesoftware.com/wiki/Server_queries#A2S_INFO to get more info about EDF's. */ + if (!string.IsNullOrEmpty(edfPropName)) + { + // Does the property have an EDFAttribute assigned ? + CustomAttributeData edfInfo = property.CustomAttributes.FirstOrDefault(x => x.AttributeType == typeof(EDFAttribute)); + if (edfInfo != null) + { + // Get the EDF value that was returned by the server. + byte edfValue = (byte)typeof(TObject).GetProperty(edfPropName).GetValue(objectRef); + + // Get the EDF condition value that was provided in the model. + byte edfPropertyConditionValue = (byte)edfInfo.ConstructorArguments[0].Value; + + // Continue if the condition does not pass because it means that the server did not include any information about this property. + if ((edfValue & edfPropertyConditionValue) <= 0) { continue; } + } + } + + /* Basic explanation of what is going of from here; + * Get the type of the property and get amount of bytes of its size from the response array, + * Convert the parsed value to its type and assign it. + */ + + /* We have to handle strings separately since their size is unknown and they are also null terminated. + * Check here: https://developer.valvesoftware.com/wiki/String for further information about Strings in the protocol. + */ + if (property.PropertyType == typeof(string)) + { + // Clear the buffer first then take till the termination. + takenBytes = enumerableSource.TakeWhile(x => x != 0); + + // Parse it into a string. + property.SetValue(objectRef, Encoding.UTF8.GetString(takenBytes.ToArray())); + + // Update the source by skipping the amount of bytes taken from the source and + 1 for termination byte. + enumerableSource = enumerableSource.Skip(takenBytes.Count() + 1); + } + else + { + // Is the property an Enum ? if yes we should be getting the underlying type since it might differ. + Type typeOfProperty = property.PropertyType.IsEnum + ? property.PropertyType.GetEnumUnderlyingType() + : property.PropertyType; + + // Extract the value and the size from the source. + (object result, int size) = ExtractMarshalType(enumerableSource, typeOfProperty); + + /* If the property is an enum we should parse it first then assign its value, + * if not we can just give it to SetValue since it was converted by ExtractMarshalType already.*/ + property.SetValue(objectRef, property.PropertyType.IsEnum + ? Enum.Parse(property.PropertyType, result.ToString()) + : result); + + // Update the source by skipping the amount of bytes taken from the source. + enumerableSource = enumerableSource.Skip(size); + } + } + + // We return the last state of the processed source. + return enumerableSource; + } + + internal static List ExtractPlayersData(byte[] rawSource) + where TObject : class + { + // Create a list to contain the serialized data. + var objectList = new List(); + + // Skip the response headers. + byte itemCount = rawSource[RESPONSE_CODE_INDEX]; + + // Skip +1 for item_count + IEnumerable dataSource = rawSource.Skip(RESPONSE_HEADER_COUNT + sizeof(byte)); + + for (byte i = 0; i < itemCount; i++) + { + // Activate a new instance of the object. + var objectInstance = Activator.CreateInstance(); + + // Extract the data. + dataSource = ExtractData(objectInstance, dataSource.ToArray()); + + // Add it into the list. + objectList.Add(objectInstance); + } + + return objectList; + } + + internal static List ExtractRulesData(byte[] rawSource) + where TObject : class + { + // Create a list to contain the serialized data. + var objectList = new List(); + + // Skip the response headers. + Int16 itemCount = BitConverter.ToInt16( + rawSource + .Skip(RESPONSE_CODE_INDEX) + .Take(sizeof(Int16)) + .ToArray() + ); + + // Skip +2 for item_count, because its short + IEnumerable dataSource = rawSource.Skip(RESPONSE_HEADER_COUNT + sizeof(Int16)); + + for (byte i = 0; i < itemCount; i++) + { + // Activate a new instance of the object. + var objectInstance = Activator.CreateInstance(); + + // Extract the data. + dataSource = ExtractData(objectInstance, dataSource.ToArray()); + + // Add it into the list. + objectList.Add(objectInstance); + } + + return objectList; + } + + internal static (object, int) ExtractMarshalType(IEnumerable source, Type type) + { + // Get the size of the given type. + int sizeOfType = Marshal.SizeOf(type); + + // Take amount of bytes from the source array. + IEnumerable takenBytes = source.Take(sizeOfType); + + // We actually need to go into an unsafe block here since as far as i know, this is the only way to convert a byte[] source into its given type on runtime. + unsafe + { + fixed (byte* sourcePtr = takenBytes.ToArray()) + { + return (Marshal.PtrToStructure(new IntPtr(sourcePtr), type), sizeOfType); + } + } + } + } } diff --git a/SteamQueryNet/SteamQueryNet/Utils/Helpers.cs b/SteamQueryNet/SteamQueryNet/Utils/Helpers.cs index 9471be2..a146b0d 100644 --- a/SteamQueryNet/SteamQueryNet/Utils/Helpers.cs +++ b/SteamQueryNet/SteamQueryNet/Utils/Helpers.cs @@ -5,60 +5,60 @@ namespace SteamQueryNet.Utils { - internal class Helpers - { - internal static TResult RunSync(Func> func) - { - var cultureUi = CultureInfo.CurrentUICulture; - var culture = CultureInfo.CurrentCulture; - return new TaskFactory().StartNew(() => - { - Thread.CurrentThread.CurrentCulture = culture; - Thread.CurrentThread.CurrentUICulture = cultureUi; - return func(); - }).Unwrap().GetAwaiter().GetResult(); - } + internal class Helpers + { + internal static TResult RunSync(Func> func) + { + var cultureUi = CultureInfo.CurrentUICulture; + var culture = CultureInfo.CurrentCulture; + return new TaskFactory().StartNew(() => + { + Thread.CurrentThread.CurrentCulture = culture; + Thread.CurrentThread.CurrentUICulture = cultureUi; + return func(); + }).Unwrap().GetAwaiter().GetResult(); + } - internal static (string serverAddress, ushort port) ResolveIPAndPortFromString(string serverAddressAndPort) - { - const string steamUrl = "steam://connect/"; - // Check for usual suspects. - if (string.IsNullOrEmpty(serverAddressAndPort)) - { - throw new ArgumentException($"Couldn't parse hostname or port with value: {serverAddressAndPort}", nameof(serverAddressAndPort)); - } + internal static (string serverAddress, ushort port) ResolveIPAndPortFromString(string serverAddressAndPort) + { + const string steamUrl = "steam://connect/"; + // Check for usual suspects. + if (string.IsNullOrEmpty(serverAddressAndPort)) + { + throw new ArgumentException($"Couldn't parse hostname or port with value: {serverAddressAndPort}", nameof(serverAddressAndPort)); + } - // Check if its a steam url. - if (serverAddressAndPort.Contains(steamUrl)) - { - // Yep lets get rid of it since we dont need it. - serverAddressAndPort = serverAddressAndPort.Replace(steamUrl, string.Empty); - } + // Check if its a steam url. + if (serverAddressAndPort.Contains(steamUrl)) + { + // Yep lets get rid of it since we dont need it. + serverAddressAndPort = serverAddressAndPort.Replace(steamUrl, string.Empty); + } - // Lets be a nice guy and clear out all possible copy paste error whitespaces. - serverAddressAndPort = serverAddressAndPort.Replace(" ", string.Empty); + // Lets be a nice guy and clear out all possible copy paste error whitespaces. + serverAddressAndPort = serverAddressAndPort.Replace(" ", string.Empty); - // Try with a colon - string[] parts = serverAddressAndPort.Split(':'); - if (parts.Length != 2) - { + // Try with a colon + string[] parts = serverAddressAndPort.Split(':'); + if (parts.Length != 2) + { - // Not a colon. Try a comma then. - parts = serverAddressAndPort.Split(','); - if (parts.Length != 2) - { - // Y u do dis ? - throw new ArgumentException($"Couldn't parse hostname or port with value: {serverAddressAndPort}", nameof(serverAddressAndPort)); - } - } + // Not a colon. Try a comma then. + parts = serverAddressAndPort.Split(','); + if (parts.Length != 2) + { + // Y u do dis ? + throw new ArgumentException($"Couldn't parse hostname or port with value: {serverAddressAndPort}", nameof(serverAddressAndPort)); + } + } - // Parse the port see if its in range. - if (!ushort.TryParse(parts[1], out ushort parsedPort)) - { - throw new ArgumentException($"Couldn't parse the port number from the parameter with value: {serverAddressAndPort}", nameof(serverAddressAndPort)); - } + // Parse the port see if its in range. + if (!ushort.TryParse(parts[1], out ushort parsedPort)) + { + throw new ArgumentException($"Couldn't parse the port number from the parameter with value: {serverAddressAndPort}", nameof(serverAddressAndPort)); + } - return (parts[0], parsedPort); - } - } + return (parts[0], parsedPort); + } + } } diff --git a/SteamQueryNet/SteamQueryNet/Utils/RequestHelpers.cs b/SteamQueryNet/SteamQueryNet/Utils/RequestHelpers.cs index 658458e..12c3ec9 100644 --- a/SteamQueryNet/SteamQueryNet/Utils/RequestHelpers.cs +++ b/SteamQueryNet/SteamQueryNet/Utils/RequestHelpers.cs @@ -1,39 +1,40 @@ using SteamQueryNet.Models; using System; +using System.Collections.Generic; using System.Linq; using System.Text; namespace SteamQueryNet.Utils { - internal sealed class RequestHelpers - { - internal static byte[] PrepareAS2_INFO_Request() - { - const string requestPayload = "Source Engine Query\0"; - return BuildRequest(RequestHeaders.A2S_INFO, Encoding.UTF8.GetBytes(requestPayload)); - } + internal sealed class RequestHelpers + { + internal static byte[] PrepareAS2_INFO_Request(int challenge) + { + const string requestPayload = "Source Engine Query\0"; + return BuildRequest(RequestHeaders.A2S_INFO, Encoding.UTF8.GetBytes(requestPayload).Concat(BitConverter.GetBytes(challenge))); + } - internal static byte[] PrepareAS2_RENEW_CHALLENGE_Request() - { - return BuildRequest(RequestHeaders.A2S_PLAYER, BitConverter.GetBytes(-1)); - } + internal static byte[] PrepareAS2_RENEW_CHALLENGE_Request() + { + return BuildRequest(RequestHeaders.A2S_PLAYER, BitConverter.GetBytes(-1)); + } - internal static byte[] PrepareAS2_GENERIC_Request(byte challengeRequestCode, int challenge) - { - return BuildRequest(challengeRequestCode, BitConverter.GetBytes(challenge)); - } + internal static byte[] PrepareAS2_GENERIC_Request(byte challengeRequestCode, int challenge) + { + return BuildRequest(challengeRequestCode, BitConverter.GetBytes(challenge)); + } - private static byte[] BuildRequest(byte headerCode, byte[] extraParams = null) - { - /* All requests consist of 4 FF's followed by a header code to execute the request. - * Check here: https://developer.valvesoftware.com/wiki/Server_queries#Protocol for further information about the protocol. */ - var request = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, headerCode }; + private static byte[] BuildRequest(byte headerCode, IEnumerable extraParams = null) + { + /* All requests consist of 4 FF's followed by a header code to execute the request. + * Check here: https://developer.valvesoftware.com/wiki/Server_queries#Protocol for further information about the protocol. */ + var request = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, headerCode }; - // If we have any extra payload, concatenate those into our requestHeaders and return; - return extraParams != null - ? request.Concat(extraParams).ToArray() - : request; - } - } + // If we have any extra payload, concatenate those into our requestHeaders and return; + return extraParams != null + ? request.Concat(extraParams).ToArray() + : request; + } + } }