mirror of
https://github.com/Facepunch/Facepunch.Steamworks.git
synced 2024-12-24 13:45:37 +03:00
Server Rules Query uses a custom UdpClient instead of doing it through Steam, avoiding all the nasty vtable building junk
This commit is contained in:
parent
476321c026
commit
9270eaef07
@ -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 :(" );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -86,7 +86,7 @@ internal static Server FromSteam( Client client, SteamNative.gameserveritem_t it
|
||||
/// </summary>
|
||||
public bool HasRules { get { return Rules != null && Rules.Count > 0; } }
|
||||
|
||||
internal Interop.ServerRules RulesRequest;
|
||||
internal SourceServerQuery RulesRequest;
|
||||
|
||||
/// <summary>
|
||||
/// Populates Rules for this server
|
||||
@ -96,19 +96,24 @@ public void FetchRules()
|
||||
if ( RulesRequest != null )
|
||||
return;
|
||||
|
||||
Rules = new Dictionary<string, string>();
|
||||
|
||||
RulesRequest = new Interop.ServerRules( this, Address, QueryPort );
|
||||
Rules = null;
|
||||
RulesRequest = new SourceServerQuery( this, Address, QueryPort );
|
||||
}
|
||||
|
||||
internal void OnServerRulesReceiveFinished( bool Success )
|
||||
internal void OnServerRulesReceiveFinished( Dictionary<string, string> rules, bool Success )
|
||||
{
|
||||
RulesRequest.Dispose();
|
||||
RulesRequest = null;
|
||||
|
||||
if ( Success )
|
||||
{
|
||||
Rules = rules;
|
||||
}
|
||||
|
||||
if ( OnReceivedRules != null )
|
||||
{
|
||||
OnReceivedRules( Success );
|
||||
}
|
||||
}
|
||||
|
||||
internal const uint k_unFavoriteFlagNone = 0x00;
|
||||
internal const uint k_unFavoriteFlagFavorite = 0x01; // this game favorite entry is for the favorites list
|
||||
|
@ -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<Player> players = new List<Player>();
|
||||
|
||||
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 );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of currently in-game clients on the specified gameserver.
|
||||
/// <b>Please note:</b> the playtime is stored as a float in <i>seconds</i>, you might want to convert it.
|
||||
///
|
||||
/// See https://developer.valvesoftware.com/wiki/Server_queries#A2S_PLAYER for more Information
|
||||
/// </summary>
|
||||
/// <returns>A PLayersResponse Object containing the name, score and playtime of each player</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of all publically available CVars ("rules") from the server.
|
||||
/// <b>Note:</b> 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.
|
||||
/// </summary>
|
||||
/// <returns>A RulesResponse Object containing a Name-Value pair of each CVar</returns>
|
||||
public Dictionary<string, string> GetRules()
|
||||
{
|
||||
// open udpclient if not already open
|
||||
this.GetClient();
|
||||
|
||||
try
|
||||
{
|
||||
var d = new Dictionary<string, string>();
|
||||
|
||||
// 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<byte[]> splitPackets = new List<byte[]>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close all currently open socket/UdpClient connections
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if ( this.socket != null ) this.socket.Close();
|
||||
if ( this.client != null ) this.client.Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open up a new Socket-based connection to a server, if not already open.
|
||||
/// </summary>
|
||||
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 );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new UdpClient connection to a server (mostly used for multi-packet answers)
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reassmble a multi-packet response.
|
||||
/// </summary>
|
||||
/// <param name="splitPackets">The packets to assemble</param>
|
||||
/// <param name="isCompressed">true: packets are compressed; false: not</param>
|
||||
/// <param name="uncompressedSize">The size of the message after decompression (for comparison)</param>
|
||||
/// <param name="packetChecksum">Validation of the result</param>
|
||||
/// <returns>A byte-array containing all packets assembled together/decompressed.</returns>
|
||||
private byte[] ReassemblePacket( List<byte[]> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invert the Byte-order Mark of an value, used for compatibility between Little Large BOM
|
||||
/// </summary>
|
||||
/// <param name="value">The value to invert</param>
|
||||
/// <returns>BOM-inversed value (if needed), otherwise the original value</returns>
|
||||
private int ReverseBytes( int value )
|
||||
{
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
if ( BitConverter.IsLittleEndian )
|
||||
{
|
||||
Array.Reverse( bytes );
|
||||
}
|
||||
return BitConverter.ToInt32( bytes, 0 );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine whetever or not a message is compressed.
|
||||
/// Simply detects if the most significant bit is 1.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to check</param>
|
||||
/// <returns>true, if message is compressed, false otherwise</returns>
|
||||
private bool PacketIsCompressed( int value )
|
||||
{
|
||||
return ( value & 0x8000 ) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine whetever or not a message is split up.
|
||||
/// </summary>
|
||||
/// <param name="paket">The value to check</param>
|
||||
/// <returns>true, if message is split up, false otherwise</returns>
|
||||
private bool PacketIsSplit( int paket )
|
||||
{
|
||||
return ( paket == -2 );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request the 4-byte challenge id from the server, required for A2S_RULES and A2S_PLAYER.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of message to request the challenge for (see constants)</param>
|
||||
/// <param name="socket">Request method to use (performance reasons)</param>
|
||||
/// <returns>A Byte Array (4-bytes) containing the challenge</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a single byte value from our raw data.
|
||||
/// </summary>
|
||||
/// <returns>A single Byte at the next Offset Address</returns>
|
||||
private Byte ReadByte()
|
||||
{
|
||||
byte[] b = new byte[1];
|
||||
Array.Copy( this.raw_data, this.offset, b, 0, 1 );
|
||||
|
||||
this.offset++;
|
||||
return b[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read all remaining Bytes from our raw data.
|
||||
/// Used for multi-packet responses.
|
||||
/// </summary>
|
||||
/// <returns>All remaining data</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a 32-Bit Integer value from the next offset address.
|
||||
/// </summary>
|
||||
/// <returns>The Int32 Value found at the offset address</returns>
|
||||
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 );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a 16-Bit Integer (also called "short") value from the next offset address.
|
||||
/// </summary>
|
||||
/// <returns>The Int16 Value found at the offset address</returns>
|
||||
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 );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a Float value from the next offset address.
|
||||
/// </summary>
|
||||
/// <returns>The Float Value found at the offset address</returns>
|
||||
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 );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
/// </summary>
|
||||
/// <returns>The String read</returns>
|
||||
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
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
206
Facepunch.Steamworks/Utility/SourceServerQuery.cs
Normal file
206
Facepunch.Steamworks/Utility/SourceServerQuery.cs
Normal file
@ -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<SourceServerQuery> Current = new List<SourceServerQuery>();
|
||||
|
||||
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<string, string> rules = new Dictionary<string, string>();
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user