diff --git a/Facepunch.Steamworks.Test/Client/ServerlistTest.cs b/Facepunch.Steamworks.Test/Client/ServerlistTest.cs
index 6fa37a0..9f5b413 100644
--- a/Facepunch.Steamworks.Test/Client/ServerlistTest.cs
+++ b/Facepunch.Steamworks.Test/Client/ServerlistTest.cs
@@ -45,7 +45,7 @@ public void InternetList()
for ( int i = 0; i < 1000; i++ )
{
client.Update();
- System.Threading.Thread.Sleep( 5 );
+ System.Threading.Thread.Sleep( 100 );
foreach ( var s in query.Responded )
{
@@ -425,20 +425,21 @@ public void Rules()
query.Dispose();
- foreach ( var server in query.Responded.Take( 20 ) )
- {
- GC.Collect();
- server.FetchRules();
- GC.Collect();
+ var servers = query.Responded.Take( 10 );
+ foreach ( var server in servers )
+ {
+ server.FetchRules();
+ }
+
+ foreach ( var server in servers )
+ {
int i = 0;
while ( !server.HasRules )
{
i++;
- GC.Collect();
client.Update();
- GC.Collect();
- System.Threading.Thread.Sleep( 2 );
+ System.Threading.Thread.Sleep( 10 );
if ( i > 100 )
break;
@@ -446,11 +447,17 @@ public void Rules()
if ( server.HasRules )
{
+ Console.WriteLine( "SERVER HAS RULES :D" );
+
foreach ( var rule in server.Rules )
{
Console.WriteLine( rule.Key + " = " + rule.Value );
}
}
+ else
+ {
+ Console.WriteLine( "SERVER HAS NO RULES :(" );
+ }
}
diff --git a/Facepunch.Steamworks/BaseSteamworks.cs b/Facepunch.Steamworks/BaseSteamworks.cs
index 7d19b7b..e32744c 100644
--- a/Facepunch.Steamworks/BaseSteamworks.cs
+++ b/Facepunch.Steamworks/BaseSteamworks.cs
@@ -145,6 +145,13 @@ public void RunUpdateCallbacks()
{
CallResults[i].Try();
}
+
+ //
+ // The SourceServerQuery's happen in another thread, so we
+ // query them to see if they're finished, and if so post a callback
+ // in our main thread. This will all suck less once we have async.
+ //
+ Facepunch.Steamworks.SourceServerQuery.Cycle();
}
///
diff --git a/Facepunch.Steamworks/Client/ServerList.Server.cs b/Facepunch.Steamworks/Client/ServerList.Server.cs
index a750f2d..fbe10b8 100644
--- a/Facepunch.Steamworks/Client/ServerList.Server.cs
+++ b/Facepunch.Steamworks/Client/ServerList.Server.cs
@@ -86,7 +86,7 @@ internal static Server FromSteam( Client client, SteamNative.gameserveritem_t it
///
public bool HasRules { get { return Rules != null && Rules.Count > 0; } }
- internal Interop.ServerRules RulesRequest;
+ internal SourceServerQuery RulesRequest;
///
/// Populates Rules for this server
@@ -96,18 +96,23 @@ public void FetchRules()
if ( RulesRequest != null )
return;
- Rules = new Dictionary();
-
- RulesRequest = new Interop.ServerRules( this, Address, QueryPort );
+ Rules = null;
+ RulesRequest = new SourceServerQuery( this, Address, QueryPort );
}
- internal void OnServerRulesReceiveFinished( bool Success )
+ internal void OnServerRulesReceiveFinished( Dictionary rules, bool Success )
{
- RulesRequest.Dispose();
RulesRequest = null;
+ if ( Success )
+ {
+ Rules = rules;
+ }
+
if ( OnReceivedRules != null )
+ {
OnReceivedRules( Success );
+ }
}
internal const uint k_unFavoriteFlagNone = 0x00;
diff --git a/Facepunch.Steamworks/Client/ServerQuery.cs b/Facepunch.Steamworks/Client/ServerQuery.cs
deleted file mode 100644
index b1abaac..0000000
--- a/Facepunch.Steamworks/Client/ServerQuery.cs
+++ /dev/null
@@ -1,513 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Net;
-using System.Net.Sockets;
-using System.IO;
-
-#if !NET_CORE
-
-internal class SourceServerQuery :IDisposable
-{
- public class PlayersResponse
- {
- public short player_count;
- public List players = new List();
-
- public class Player
- {
- public String name { get; set; }
- public int score { get; set; }
- public float playtime { get; set; }
- }
- }
-
- private IPEndPoint endPoint;
-
- private Socket socket;
- private UdpClient client;
-
- // send & receive timeouts
- private int send_timeout = 2500;
- private int receive_timeout = 2500;
-
- // raw response returned from the server
- private byte[] raw_data;
-
- private int offset = 0;
-
- // constants
- private readonly byte[] FFFFFFFF = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF };
-
- public SourceServerQuery( String ip, int port )
- {
- this.endPoint = new IPEndPoint( IPAddress.Parse( ip ), port );
- }
-
- ///
- /// Get a list of currently in-game clients on the specified gameserver.
- /// Please note: the playtime is stored as a float in seconds, you might want to convert it.
- ///
- /// See https://developer.valvesoftware.com/wiki/Server_queries#A2S_PLAYER for more Information
- ///
- /// A PLayersResponse Object containing the name, score and playtime of each player
- public PlayersResponse GetPlayerList()
- {
- // open socket if not already open
- this.GetSocket();
- // we don't need the header, so set pointer to where the payload begins
- this.offset = 5;
-
- try
- {
- PlayersResponse pr = new PlayersResponse();
-
- // since A2S_PLAYER requests require a valid challenge, get it first
- byte[] challenge = this.GetChallenge(0x55, true);
-
- byte[] request = new byte[challenge.Length + this.FFFFFFFF.Length + 1];
- Array.Copy( this.FFFFFFFF, 0, request, 0, this.FFFFFFFF.Length );
- request[this.FFFFFFFF.Length] = 0x55;
- Array.Copy( challenge, 0, request, this.FFFFFFFF.Length + 1, challenge.Length );
-
- this.socket.Send( request );
-
- // MODIFIED BY ZACKBOE
- // Increased byte size from 1024 in order to receive more player data
- // Previously returned a socket exception at >~ 51 players.
- this.raw_data = new byte[2048];
- // END MODIFICATION
- this.socket.Receive( this.raw_data );
-
- byte player_count = this.ReadByte();
-
- // fill up the list of players
- for ( int i = 0; i < player_count; i++ )
- {
- this.ReadByte();
-
- PlayersResponse.Player p = new PlayersResponse.Player();
-
- p.name = this.ReadString();
- p.score = this.ReadInt32();
- p.playtime = this.ReadFloat();
-
- pr.players.Add( p );
- }
-
- pr.player_count = player_count;
-
- return pr;
- }
- catch ( SocketException )
- {
- return null;
- }
- }
-
- ///
- /// Get a list of all publically available CVars ("rules") from the server.
- /// Note: Due to a bug in the Source Engine, it might happen that some CVars/values are cut off.
- ///
- /// Example: mp_idlemaxtime = [nothing]
- /// Only Valve can fix that.
- ///
- /// A RulesResponse Object containing a Name-Value pair of each CVar
- public Dictionary GetRules()
- {
- // open udpclient if not already open
- this.GetClient();
-
- try
- {
- var d = new Dictionary();
-
- // similar to A2S_PLAYER requests, A2S_RULES require a valid challenge
- byte[] challenge = this.GetChallenge(0x56, false);
-
- byte[] request = new byte[challenge.Length + this.FFFFFFFF.Length + 1];
- Array.Copy( this.FFFFFFFF, 0, request, 0, this.FFFFFFFF.Length );
- request[this.FFFFFFFF.Length] = 0x56;
- Array.Copy( challenge, 0, request, this.FFFFFFFF.Length + 1, challenge.Length );
-
- this.client.Send( request, request.Length );
-
- //
- // Since A2S_RULES responses might be split up into several packages/compressed, we have to do a special handling of them
- //
- int bytesRead;
-
- // this will keep our assembled message
- byte[] buffer = new byte[4096];
-
- // send first request
-
- this.raw_data = this.client.Receive( ref this.endPoint );
-
- bytesRead = this.raw_data.Length;
-
- // reset pointer
- this.offset = 0;
-
- int is_split = this.ReadInt32();
- int requestid = this.ReadInt32();
-
- this.offset = 4;
-
- // response is split up into several packets
- if ( this.PacketIsSplit( is_split ) )
- {
- bool isCompressed = false;
- byte[] splitData;
- int packetCount, packetNumber, requestId;
- int packetsReceived = 1;
- int packetChecksum = 0;
- int packetSplit = 0;
- short splitSize;
- int uncompressedSize = 0;
- List splitPackets = new List();
-
- do
- {
- // unique request id
- requestId = this.ReverseBytes( this.ReadInt32() );
- isCompressed = this.PacketIsCompressed( requestId );
-
- packetCount = this.ReadByte();
- packetNumber = this.ReadByte() + 1;
- // so we know how big our byte arrays have to be
- splitSize = this.ReadInt16();
- splitSize -= 4; // fix
-
- if ( packetsReceived == 1 )
- {
- for ( int i = 0; i < packetCount; i++ )
- {
- splitPackets.Add( new byte[] { } );
- }
- }
-
- // if the packets are compressed, get some data to decompress them
- if ( isCompressed )
- {
- uncompressedSize = ReverseBytes( this.ReadInt32() );
- packetChecksum = ReverseBytes( this.ReadInt32() );
- }
-
- // ommit header in first packet
- if ( packetNumber == 1 ) this.ReadInt32();
-
- splitData = new byte[splitSize];
- splitPackets[packetNumber - 1] = this.ReadBytes();
-
- // fixes a case where the returned package might still contain a character after the last \0 terminator (truncated name => value)
- // please note: this therefore also removes the value of said variable, but atleast the program won't crash
- if ( splitPackets[packetNumber - 1].Length - 1 > 0 && splitPackets[packetNumber - 1][splitPackets[packetNumber - 1].Length - 1] != 0x00 )
- {
- splitPackets[packetNumber - 1][splitPackets[packetNumber - 1].Length - 1] = 0x00;
- }
-
- // reset pointer again, so we can copy over the contents
- this.offset = 0;
-
- if ( packetsReceived < packetCount )
- {
-
- this.raw_data = this.client.Receive( ref this.endPoint );
- bytesRead = this.raw_data.Length;
-
- // continue with the next packets
- packetSplit = this.ReadInt32();
- packetsReceived++;
- }
- else
- {
- // all packets received
- bytesRead = 0;
- }
- }
- while ( packetsReceived <= packetCount && bytesRead > 0 && packetSplit == -2 );
-
- // decompress
- if ( isCompressed )
- {
- buffer = ReassemblePacket( splitPackets, true, uncompressedSize, packetChecksum );
- }
- else
- {
- buffer = ReassemblePacket( splitPackets, false, 0, 0 );
- }
- }
- else
- {
- buffer = this.raw_data;
- }
-
- // move our final result over to handle it
- this.raw_data = buffer;
-
- // omitting header
- this.offset += 1;
- var count = this.ReadInt16();
-
- for ( int i = 0; i < count; i++ )
- {
- var k = this.ReadString();
- var v = this.ReadString();
-
- if ( !d.ContainsKey( k ) )
- d.Add( k, v );
- }
-
- return d;
- }
- catch ( SocketException e )
- {
- Console.WriteLine( e );
- return null;
- }
- }
-
- ///
- /// Close all currently open socket/UdpClient connections
- ///
- public void Dispose()
- {
- if ( this.socket != null ) this.socket.Close();
- if ( this.client != null ) this.client.Close();
- }
-
- ///
- /// Open up a new Socket-based connection to a server, if not already open.
- ///
- private void GetSocket()
- {
- if ( this.socket == null )
- {
- this.socket = new Socket(
- AddressFamily.InterNetwork,
- SocketType.Dgram,
- ProtocolType.Udp );
-
- this.socket.SendTimeout = this.send_timeout;
- this.socket.ReceiveTimeout = this.receive_timeout;
-
- this.socket.Connect( this.endPoint );
- }
- }
-
- ///
- /// Create a new UdpClient connection to a server (mostly used for multi-packet answers)
- ///
- private void GetClient()
- {
- if ( this.client == null )
- {
- this.client = new UdpClient();
- this.client.Connect( this.endPoint );
- this.client.DontFragment = true;
-
- this.client.Client.SendTimeout = this.send_timeout;
- this.client.Client.ReceiveTimeout = this.receive_timeout;
- }
- }
-
- ///
- /// Reassmble a multi-packet response.
- ///
- /// The packets to assemble
- /// true: packets are compressed; false: not
- /// The size of the message after decompression (for comparison)
- /// Validation of the result
- /// A byte-array containing all packets assembled together/decompressed.
- private byte[] ReassemblePacket( List splitPackets, bool isCompressed, int uncompressedSize, int packetChecksum )
- {
- byte[] packetData, tmpData;
- packetData = new byte[0];
-
- foreach ( byte[] splitPacket in splitPackets )
- {
- if ( splitPacket == null )
- {
- throw new Exception();
- }
-
- tmpData = packetData;
- packetData = new byte[tmpData.Length + splitPacket.Length];
-
- MemoryStream memStream = new MemoryStream(packetData);
- memStream.Write( tmpData, 0, tmpData.Length );
- memStream.Write( splitPacket, 0, splitPacket.Length );
- }
-
- if ( isCompressed )
- {
- throw new System.NotImplementedException();
- }
-
- return packetData;
- }
-
- ///
- /// Invert the Byte-order Mark of an value, used for compatibility between Little Large BOM
- ///
- /// The value to invert
- /// BOM-inversed value (if needed), otherwise the original value
- private int ReverseBytes( int value )
- {
- byte[] bytes = BitConverter.GetBytes(value);
- if ( BitConverter.IsLittleEndian )
- {
- Array.Reverse( bytes );
- }
- return BitConverter.ToInt32( bytes, 0 );
- }
-
- ///
- /// Determine whetever or not a message is compressed.
- /// Simply detects if the most significant bit is 1.
- ///
- /// The value to check
- /// true, if message is compressed, false otherwise
- private bool PacketIsCompressed( int value )
- {
- return ( value & 0x8000 ) != 0;
- }
-
- ///
- /// Determine whetever or not a message is split up.
- ///
- /// The value to check
- /// true, if message is split up, false otherwise
- private bool PacketIsSplit( int paket )
- {
- return ( paket == -2 );
- }
-
- ///
- /// Request the 4-byte challenge id from the server, required for A2S_RULES and A2S_PLAYER.
- ///
- /// The type of message to request the challenge for (see constants)
- /// Request method to use (performance reasons)
- /// A Byte Array (4-bytes) containing the challenge
- private Byte[] GetChallenge( byte type, bool socket = true )
- {
- byte[] request = new byte[this.FFFFFFFF.Length + this.FFFFFFFF.Length + 1];
- Array.Copy( this.FFFFFFFF, 0, request, 0, this.FFFFFFFF.Length );
- request[FFFFFFFF.Length] = type;
- Array.Copy( this.FFFFFFFF, 0, request, this.FFFFFFFF.Length + 1, this.FFFFFFFF.Length );
-
- byte[] raw_response = new byte[24];
- byte[] challenge = new byte[4];
-
- // using sockets
- if ( socket )
- {
- this.socket.Send( request );
- this.socket.Receive( raw_response );
- }
- else
- {
- this.client.Send( request, request.Length );
- raw_response = this.client.Receive( ref this.endPoint );
- }
-
- Array.Copy( raw_response, 5, challenge, 0, 4 ); // change this valve modifies the protocol!
-
- return challenge;
- }
-
- ///
- /// Read a single byte value from our raw data.
- ///
- /// A single Byte at the next Offset Address
- private Byte ReadByte()
- {
- byte[] b = new byte[1];
- Array.Copy( this.raw_data, this.offset, b, 0, 1 );
-
- this.offset++;
- return b[0];
- }
-
- ///
- /// Read all remaining Bytes from our raw data.
- /// Used for multi-packet responses.
- ///
- /// All remaining data
- private Byte[] ReadBytes()
- {
- int size = (this.raw_data.Length - this.offset - 4);
- if ( size < 1 ) return new Byte[] { };
-
- byte[] b = new byte[size];
- Array.Copy( this.raw_data, this.offset, b, 0, this.raw_data.Length - this.offset - 4 );
-
- this.offset += ( this.raw_data.Length - this.offset - 4 );
- return b;
- }
-
- ///
- /// Read a 32-Bit Integer value from the next offset address.
- ///
- /// The Int32 Value found at the offset address
- private Int32 ReadInt32()
- {
- byte[] b = new byte[4];
- Array.Copy( this.raw_data, this.offset, b, 0, 4 );
-
- this.offset += 4;
- return BitConverter.ToInt32( b, 0 );
- }
-
- ///
- /// Read a 16-Bit Integer (also called "short") value from the next offset address.
- ///
- /// The Int16 Value found at the offset address
- private Int16 ReadInt16()
- {
- byte[] b = new byte[2];
- Array.Copy( this.raw_data, this.offset, b, 0, 2 );
-
- this.offset += 2;
- return BitConverter.ToInt16( b, 0 );
- }
-
- ///
- /// Read a Float value from the next offset address.
- ///
- /// The Float Value found at the offset address
- private float ReadFloat()
- {
- byte[] b = new byte[4];
- Array.Copy( this.raw_data, this.offset, b, 0, 4 );
-
- this.offset += 4;
- return BitConverter.ToSingle( b, 0 );
- }
-
- ///
- /// Read a String until its end starting from the next offset address.
- /// Reading stops once the method detects a 0x00 Character at the next position (\0 terminator)
- ///
- /// The String read
- private String ReadString()
- {
- byte[] cache = new byte[1] { 0x01 };
- String output = "";
-
- while ( cache[0] != 0x00 )
- {
- if ( this.offset == this.raw_data.Length ) break; // fixes Valve's inability to code a proper query protocol
- Array.Copy( this.raw_data, this.offset, cache, 0, 1 );
- this.offset++;
-
- if ( cache[0] != 0x00)
- output += Encoding.UTF8.GetString( cache );
- }
-
- return output;
- }
-}
-
-#endif
\ No newline at end of file
diff --git a/Facepunch.Steamworks/Interop/ServerRules.cs b/Facepunch.Steamworks/Interop/ServerRules.cs
deleted file mode 100644
index 2cdb16a..0000000
--- a/Facepunch.Steamworks/Interop/ServerRules.cs
+++ /dev/null
@@ -1,171 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using System.Runtime.InteropServices;
-using System.Text;
-
-namespace Facepunch.Steamworks.Interop
-{
- class ServerRules : IDisposable
- {
- // Pins and pointers for the created vtable
- private GCHandle vTablePin;
- private IntPtr vTablePtr;
-
- // Pins for the functions
- private GCHandle RulesRespondPin;
- private GCHandle FailedRespondPin;
- private GCHandle CompletePin;
-
- // The server that called us
- private ServerList.Server Server;
-
- public ServerRules( ServerList.Server server, IPAddress address, int queryPort )
- {
- Server = server;
-
- //
- // Create a fake VTable to pass to c++
- //
- InstallVTable();
-
- //
- // Ask Steam to get the server rules, respond to our fake vtable
- //
- Server.Client.native.servers.ServerRules( Utility.IpToInt32( address ), (ushort)queryPort, GetPtr() );
- }
-
- public void Dispose()
- {
- if ( vTablePtr != IntPtr.Zero )
- {
- Marshal.FreeHGlobal( vTablePtr );
- vTablePtr = IntPtr.Zero;
- }
-
- if ( vTablePin.IsAllocated )
- vTablePin.Free();
-
- if ( RulesRespondPin.IsAllocated )
- RulesRespondPin.Free();
-
- if ( FailedRespondPin.IsAllocated )
- FailedRespondPin.Free();
-
- if ( CompletePin.IsAllocated )
- CompletePin.Free();
- }
-
- void InstallVTable()
- {
-
- //
- // Depending on platform, we either use ThisCall or stdcall.
- // This is a bit of a fuckabout but you need to define Client.UseThisCall
- //
-
- if ( Config.UseThisCall )
- {
- ThisVTable.InternalRulesResponded da = ( _, k, v ) => InternalOnRulesResponded( k, v );
- ThisVTable.InternalRulesFailedToRespond db = ( _ ) => InternalOnRulesFailedToRespond();
- ThisVTable.InternalRulesRefreshComplete dc = ( _ ) => InternalOnRulesRefreshComplete();
-
- RulesRespondPin = GCHandle.Alloc( da );
- FailedRespondPin = GCHandle.Alloc( db );
- CompletePin = GCHandle.Alloc( dc );
-
- var t = new ThisVTable()
- {
- m_VTRulesResponded = da,
- m_VTRulesFailedToRespond = db,
- m_VTRulesRefreshComplete = dc,
- };
- vTablePtr = Marshal.AllocHGlobal( Marshal.SizeOf( typeof( ThisVTable ) ) );
- Marshal.StructureToPtr( t, vTablePtr, false );
-
- vTablePin = GCHandle.Alloc( vTablePtr, GCHandleType.Pinned );
-
- }
- else
- {
- StdVTable.InternalRulesResponded da = InternalOnRulesResponded;
- StdVTable.InternalRulesFailedToRespond db = InternalOnRulesFailedToRespond;
- StdVTable.InternalRulesRefreshComplete dc = InternalOnRulesRefreshComplete;
-
- RulesRespondPin = GCHandle.Alloc( da );
- FailedRespondPin = GCHandle.Alloc( db );
- CompletePin = GCHandle.Alloc( dc );
-
- var t = new StdVTable()
- {
- m_VTRulesResponded = da,
- m_VTRulesFailedToRespond = db,
- m_VTRulesRefreshComplete = dc
- };
- vTablePtr = Marshal.AllocHGlobal( Marshal.SizeOf( typeof( StdVTable ) ) );
- Marshal.StructureToPtr( t, vTablePtr, false );
-
- vTablePin = GCHandle.Alloc( vTablePtr, GCHandleType.Pinned );
- }
- }
-
- private void InternalOnRulesResponded( string k, string v )
- {
- Server.Rules.Add( k, v );
- }
- private void InternalOnRulesFailedToRespond()
- {
- Server.OnServerRulesReceiveFinished( false );
- }
- private void InternalOnRulesRefreshComplete()
- {
- Server.OnServerRulesReceiveFinished( true );
- }
-
- [StructLayout( LayoutKind.Sequential )]
- private class StdVTable
- {
- [MarshalAs(UnmanagedType.FunctionPtr)]
- public InternalRulesResponded m_VTRulesResponded;
-
- [MarshalAs(UnmanagedType.FunctionPtr)]
- public InternalRulesFailedToRespond m_VTRulesFailedToRespond;
-
- [MarshalAs(UnmanagedType.FunctionPtr)]
- public InternalRulesRefreshComplete m_VTRulesRefreshComplete;
-
- [UnmanagedFunctionPointer( CallingConvention.StdCall )]
- public delegate void InternalRulesResponded( string pchRule, string pchValue );
- [UnmanagedFunctionPointer( CallingConvention.StdCall )]
- public delegate void InternalRulesFailedToRespond();
- [UnmanagedFunctionPointer( CallingConvention.StdCall )]
- public delegate void InternalRulesRefreshComplete();
- }
-
- [StructLayout( LayoutKind.Sequential )]
- private class ThisVTable
- {
- [MarshalAs(UnmanagedType.FunctionPtr)]
- public InternalRulesResponded m_VTRulesResponded;
-
- [MarshalAs(UnmanagedType.FunctionPtr)]
- public InternalRulesFailedToRespond m_VTRulesFailedToRespond;
-
- [MarshalAs(UnmanagedType.FunctionPtr)]
- public InternalRulesRefreshComplete m_VTRulesRefreshComplete;
-
- [UnmanagedFunctionPointer( CallingConvention.ThisCall )]
- public delegate void InternalRulesResponded( IntPtr thisptr, string pchRule, string pchValue );
- [UnmanagedFunctionPointer( CallingConvention.ThisCall )]
- public delegate void InternalRulesFailedToRespond( IntPtr thisptr );
- [UnmanagedFunctionPointer( CallingConvention.ThisCall )]
- public delegate void InternalRulesRefreshComplete( IntPtr thisptr );
- }
-
- public System.IntPtr GetPtr()
- {
- return vTablePin.AddrOfPinnedObject();
- }
- };
-}
diff --git a/Facepunch.Steamworks/Utility.cs b/Facepunch.Steamworks/Utility.cs
index bbd5764..1b86696 100644
--- a/Facepunch.Steamworks/Utility.cs
+++ b/Facepunch.Steamworks/Utility.cs
@@ -1,12 +1,13 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Net;
using System.Text;
namespace Facepunch.Steamworks
{
- public static class Utility
+ public static partial class Utility
{
static internal uint Swap( uint x )
{
@@ -76,8 +77,23 @@ internal static string FormatPrice(string currency, ulong price)
default: return $"{decimaled}{currency}";
}
-
-
}
+
+ public static string ReadAnsiString( 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.ASCII.GetString( buffer, 0, i );
+ }
+
}
}
diff --git a/Facepunch.Steamworks/Utility/SourceServerQuery.cs b/Facepunch.Steamworks/Utility/SourceServerQuery.cs
new file mode 100644
index 0000000..e831ac8
--- /dev/null
+++ b/Facepunch.Steamworks/Utility/SourceServerQuery.cs
@@ -0,0 +1,206 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+
+namespace Facepunch.Steamworks
+{
+
+ internal class SourceServerQuery : IDisposable
+ {
+ public static List Current = new List();
+
+ public static void Cycle()
+ {
+ if ( Current.Count == 0 )
+ return;
+
+ for( int i = Current.Count; i>0; i-- )
+ {
+ Current[i-1].Update();
+ }
+ }
+
+ 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;
+
+ public volatile bool IsRunning;
+ public volatile bool IsSuccessful;
+
+ private ServerList.Server Server;
+ private UdpClient udpClient;
+ private IPEndPoint endPoint;
+ private System.Threading.Thread thread;
+ private byte[] _challengeBytes;
+
+ private Dictionary rules = new Dictionary();
+
+ public SourceServerQuery( ServerList.Server server, IPAddress address, int queryPort )
+ {
+ Server = server;
+ endPoint = new IPEndPoint( address, queryPort );
+
+ Current.Add( this );
+
+ IsRunning = true;
+ IsSuccessful = false;
+ thread = new System.Threading.Thread( ThreadedStart );
+ thread.Start();
+ }
+
+ void Update()
+ {
+ if ( !IsRunning )
+ {
+ Current.Remove( this );
+ Server.OnServerRulesReceiveFinished( rules, IsSuccessful );
+ }
+ }
+
+ private void ThreadedStart( object obj )
+ {
+ try
+ {
+ using ( udpClient = new UdpClient() )
+ {
+ udpClient.Client.SendTimeout = 3000;
+ udpClient.Client.ReceiveTimeout = 3000;
+ udpClient.Connect( endPoint );
+
+ GetRules();
+
+ IsSuccessful = true;
+ }
+ }
+ catch ( System.Exception )
+ {
+ IsSuccessful = false;
+ }
+
+ udpClient = null;
+ IsRunning = false;
+ }
+
+ void GetRules()
+ {
+ GetChallengeData();
+
+ _challengeBytes[0] = A2S_RULES;
+ Send( _challengeBytes );
+ var ruleData = Receive();
+
+ 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.ReadAnsiString( readBuffer ), br.ReadAnsiString( readBuffer ) );
+ }
+ }
+
+ }
+
+ byte[] readBuffer = new byte[1024 * 4];
+
+ private byte[] Receive()
+ {
+ byte[][] packets = null;
+ byte packetNumber = 0, packetCount = 1;
+
+ do
+ {
+ var result = udpClient.Receive( ref endPoint );
+
+ using ( var br = new BinaryReader( new MemoryStream( result ) ) )
+ {
+ var header = br.ReadInt32();
+
+ if ( header == -1 )
+ {
+ var unsplitdata = new byte[result.Length - br.BaseStream.Position];
+ Buffer.BlockCopy( result, (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[result.Length - br.BaseStream.Position];
+ Buffer.BlockCopy( result, (int)br.BaseStream.Position, data, 0, data.Length );
+ packets[packetNumber] = data;
+ }
+ }
+ while ( packets.Any( p => p == null ) );
+
+ var combinedData = Combine( packets );
+ return combinedData;
+ }
+
+ private void GetChallengeData()
+ {
+ if ( _challengeBytes != null ) return;
+
+ Send( A2S_SERVERQUERY_GETCHALLENGE );
+
+ var challengeData = Receive();
+
+ if ( challengeData[0] != 0x41 )
+ throw new Exception( "Invalid Challenge" );
+
+ _challengeBytes = challengeData;
+ }
+
+ byte[] sendBuffer = new byte[1024];
+
+ private void Send( byte[] message )
+ {
+ sendBuffer[0] = 0xFF;
+ sendBuffer[1] = 0xFF;
+ sendBuffer[2] = 0xFF;
+ sendBuffer[3] = 0xFF;
+
+ Buffer.BlockCopy( message, 0, sendBuffer, 4, message.Length );
+
+ udpClient.Send( sendBuffer, message.Length + 4 );
+ }
+
+ private 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;
+ }
+
+ public void Dispose()
+ {
+ if ( thread != null && thread.IsAlive )
+ {
+ thread.Abort();
+ }
+
+ thread = null;
+ }
+ };
+
+}