diff --git a/Facepunch.Steamworks.Test/ServerlistTest.cs b/Facepunch.Steamworks.Test/ServerlistTest.cs index 940c5ed..175e1ab 100644 --- a/Facepunch.Steamworks.Test/ServerlistTest.cs +++ b/Facepunch.Steamworks.Test/ServerlistTest.cs @@ -68,6 +68,30 @@ namespace Steamworks } } + [TestMethod] + public async Task SourceQuery() + { + using ( var list = new ServerList.Internet() ) + { + var task = list.RunQueryAsync(); + await Task.Delay( 1000 ); + list.Cancel(); + + foreach ( var s in list.Responsive.Take( 10 ) ) + { + Console.WriteLine( $"{s.Name} [{s.Address}]" ); + + var rules = await s.QueryRulesAsync(); + Assert.IsNotNull( rules ); + + foreach ( var rule in rules ) + { + Console.WriteLine( $" {rule.Key} = {rule.Value}" ); + } + } + } + } + [TestMethod] public async Task ServerListLan() { diff --git a/Facepunch.Steamworks/Structs/Server.cs b/Facepunch.Steamworks/Structs/Server.cs index a9009f8..5fa87fb 100644 --- a/Facepunch.Steamworks/Structs/Server.cs +++ b/Facepunch.Steamworks/Structs/Server.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Net; using System.Runtime.InteropServices; using System.Text; - +using System.Threading.Tasks; namespace Steamworks.Data { @@ -66,6 +66,14 @@ namespace Steamworks.Data //Client.native.matchmaking.AddFavoriteGame( AppId, Utility.IpToInt32( Address ), (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory, (uint)Utility.Epoch.Current ); } + /// + /// If this server responds to source engine style queries, we'll be able to get a list of rules here + /// + public async Task> QueryRulesAsync() + { + return await SourceServerQuery.GetRules( this ); + } + /// /// Remove this server from our history list /// diff --git a/Facepunch.Steamworks/Utility/SourceServerQuery.cs b/Facepunch.Steamworks/Utility/SourceServerQuery.cs new file mode 100644 index 0000000..7080df7 --- /dev/null +++ b/Facepunch.Steamworks/Utility/SourceServerQuery.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Steamworks.Data; + +namespace Steamworks +{ + internal static class SourceServerQuery + { + private static readonly byte[] A2S_SERVERQUERY_GETCHALLENGE = { 0x55, 0xFF, 0xFF, 0xFF, 0xFF }; + // private static readonly byte A2S_PLAYER = 0x55; + private static readonly byte A2S_RULES = 0x56; + + internal static async Task> GetRules( ServerInfo server ) + { + try + { + var endpoint = new IPEndPoint( server.Address, server.QueryPort ); + + using ( var client = new UdpClient() ) + { + client.Client.SendTimeout = 3000; + client.Client.ReceiveTimeout = 3000; + client.Connect( endpoint ); + + return await GetRules( client ); + } + } + catch ( System.Exception e ) + { + Console.Error.WriteLine( e.Message ); + return null; + } + } + + static async Task> GetRules( UdpClient client ) + { + var challengeBytes = await GetChallengeData( client ); + challengeBytes[0] = A2S_RULES; + await Send( client, challengeBytes ); + var ruleData = await Receive( client ); + + var rules = new Dictionary(); + + using ( var br = new BinaryReader( new MemoryStream( ruleData ) ) ) + { + if ( br.ReadByte() != 0x45 ) + throw new Exception( "Invalid data received in response to A2S_RULES request" ); + + var numRules = br.ReadUInt16(); + for ( int index = 0; index < numRules; index++ ) + { + rules.Add( br.ReadNullTerminatedUTF8String( readBuffer ), br.ReadNullTerminatedUTF8String( readBuffer ) ); + } + } + + return rules; + } + + static byte[] readBuffer = new byte[1024 * 8]; + + static async Task Receive( UdpClient client ) + { + byte[][] packets = null; + byte packetNumber = 0, packetCount = 1; + + do + { + var result = await client.ReceiveAsync(); + var buffer = result.Buffer; + + using ( var br = new BinaryReader( new MemoryStream( buffer ) ) ) + { + var header = br.ReadInt32(); + + if ( header == -1 ) + { + var unsplitdata = new byte[buffer.Length - br.BaseStream.Position]; + Buffer.BlockCopy( buffer, (int)br.BaseStream.Position, unsplitdata, 0, unsplitdata.Length ); + return unsplitdata; + } + else if ( header == -2 ) + { + int requestId = br.ReadInt32(); + packetNumber = br.ReadByte(); + packetCount = br.ReadByte(); + int splitSize = br.ReadInt32(); + } + else + { + throw new System.Exception( "Invalid Header" ); + } + + if ( packets == null ) packets = new byte[packetCount][]; + + var data = new byte[buffer.Length - br.BaseStream.Position]; + Buffer.BlockCopy( buffer, (int)br.BaseStream.Position, data, 0, data.Length ); + packets[packetNumber] = data; + } + } + while ( packets.Any( p => p == null ) ); + + var combinedData = Combine( packets ); + return combinedData; + } + + private static async Task GetChallengeData( UdpClient client ) + { + await Send( client, A2S_SERVERQUERY_GETCHALLENGE ); + + var challengeData = await Receive( client ); + + if ( challengeData[0] != 0x41 ) + throw new Exception( "Invalid Challenge" ); + + return challengeData; + } + + static byte[] sendBuffer = new byte[1024]; + + static async Task Send( UdpClient client, byte[] message ) + { + sendBuffer[0] = 0xFF; + sendBuffer[1] = 0xFF; + sendBuffer[2] = 0xFF; + sendBuffer[3] = 0xFF; + + Buffer.BlockCopy( message, 0, sendBuffer, 4, message.Length ); + + await client.SendAsync( sendBuffer, message.Length + 4 ); + } + + static byte[] Combine( byte[][] arrays ) + { + var rv = new byte[arrays.Sum( a => a.Length )]; + int offset = 0; + foreach ( byte[] array in arrays ) + { + Buffer.BlockCopy( array, 0, rv, offset, array.Length ); + offset += array.Length; + } + return rv; + } + }; + +} diff --git a/Facepunch.Steamworks/Utility/Utility.cs b/Facepunch.Steamworks/Utility/Utility.cs index ec47daa..1d7ef01 100644 --- a/Facepunch.Steamworks/Utility/Utility.cs +++ b/Facepunch.Steamworks/Utility/Utility.cs @@ -78,5 +78,21 @@ namespace Steamworks default: return $"{decimaled} {currency}"; } } - } + + public static string ReadNullTerminatedUTF8String( this BinaryReader br, byte[] buffer = null ) + { + if ( buffer == null ) + buffer = new byte[1024]; + + byte chr; + int i = 0; + while ( (chr = br.ReadByte()) != 0 && i < buffer.Length ) + { + buffer[i] = chr; + i++; + } + + return Encoding.UTF8.GetString( buffer, 0, i ); + } + } }