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 );
+ }
+ }
}