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