mirror of
https://github.com/Facepunch/Facepunch.Steamworks.git
synced 2024-12-25 14:15:47 +03:00
Server query (sidestepping using steamworks itself)
This commit is contained in:
parent
b11a657000
commit
f72e3432ae
@ -285,5 +285,44 @@ public void CustomList()
|
|||||||
query.Dispose();
|
query.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void PlayerList()
|
||||||
|
{
|
||||||
|
using ( var client = new Facepunch.Steamworks.Client( 252490 ) )
|
||||||
|
{
|
||||||
|
var filter = new Facepunch.Steamworks.ServerList.Filter();
|
||||||
|
filter.Add( "appid", client.AppId.ToString() );
|
||||||
|
filter.Add( "gamedir", "rust" );
|
||||||
|
filter.Add( "secure", "1" );
|
||||||
|
|
||||||
|
using ( var query = client.ServerList.Internet( filter ) )
|
||||||
|
{
|
||||||
|
|
||||||
|
for ( int i = 0; i < 1000; i++ )
|
||||||
|
{
|
||||||
|
client.Update();
|
||||||
|
System.Threading.Thread.Sleep( 10 );
|
||||||
|
|
||||||
|
if ( query.Responded.Count > 0 && query.Responded.Any( x => x.Players > 5 ) )
|
||||||
|
break;
|
||||||
|
|
||||||
|
if ( query.Finished )
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Dispose();
|
||||||
|
|
||||||
|
var server = query.Responded.First( x => x.Players > 5 );
|
||||||
|
server.UpdateRules();
|
||||||
|
|
||||||
|
foreach ( var rule in server.Rules )
|
||||||
|
{
|
||||||
|
Console.WriteLine( rule.Key + " = " + rule.Value );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,71 +16,6 @@ public class Request : IDisposable
|
|||||||
|
|
||||||
public Action OnUpdate;
|
public Action OnUpdate;
|
||||||
|
|
||||||
public struct Server
|
|
||||||
{
|
|
||||||
public string Name { get; set; }
|
|
||||||
public int Ping { get; set; }
|
|
||||||
public string GameDir { get; set; }
|
|
||||||
public string Map { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
public uint AppId { get; set; }
|
|
||||||
public int Players { get; set; }
|
|
||||||
public int MaxPlayers { get; set; }
|
|
||||||
public int BotPlayers { get; set; }
|
|
||||||
public bool Passworded { get; set; }
|
|
||||||
public bool Secure { get; set; }
|
|
||||||
public uint LastTimePlayed { get; set; }
|
|
||||||
public int Version { get; set; }
|
|
||||||
public string[] Tags { get; set; }
|
|
||||||
public ulong SteamId { get; set; }
|
|
||||||
|
|
||||||
public uint Address { get; set; }
|
|
||||||
|
|
||||||
public int ConnectionPort { get; set; }
|
|
||||||
|
|
||||||
public int QueryPort { get; set; }
|
|
||||||
|
|
||||||
public string AddressString
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return string.Format( "{0}.{1}.{2}.{3}", ( Address >> 24 ) & 0xFFul, ( Address >> 16 ) & 0xFFul, ( Address >> 8 ) & 0xFFul, Address & 0xFFul );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public string ConnectionAddress
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return string.Format( "{0}.{1}.{2}.{3}:{4}", ( Address >> 24 ) & 0xFFul, ( Address >> 16 ) & 0xFFul, ( Address >> 8 ) & 0xFFul, Address & 0xFFul, ConnectionPort );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Server FromSteam( gameserveritem_t item )
|
|
||||||
{
|
|
||||||
return new Server()
|
|
||||||
{
|
|
||||||
Address = item.m_NetAdr.m_unIP,
|
|
||||||
ConnectionPort = item.m_NetAdr.m_usConnectionPort,
|
|
||||||
QueryPort = item.m_NetAdr.m_usQueryPort,
|
|
||||||
Name = item.m_szServerName,
|
|
||||||
Ping = item.m_nPing,
|
|
||||||
GameDir = item.m_szGameDir,
|
|
||||||
Map = item.m_szMap,
|
|
||||||
Description = item.m_szGameDescription,
|
|
||||||
AppId = item.m_nAppID,
|
|
||||||
Players = item.m_nPlayers,
|
|
||||||
MaxPlayers = item.m_nMaxPlayers,
|
|
||||||
BotPlayers = item.m_nBotPlayers,
|
|
||||||
Passworded = item.m_bPassword,
|
|
||||||
Secure = item.m_bSecure,
|
|
||||||
LastTimePlayed = item.m_ulTimeLastPlayed,
|
|
||||||
Version = item.m_nServerVersion,
|
|
||||||
Tags = item.m_szGameTags == null ? null : item.m_szGameTags.Split( ',' ),
|
|
||||||
SteamId = item.m_steamID
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of servers that responded. If you're only interested in servers that responded since you
|
/// A list of servers that responded. If you're only interested in servers that responded since you
|
||||||
/// last updated, then simply clear this list.
|
/// last updated, then simply clear this list.
|
||||||
@ -145,7 +80,6 @@ void UpdateCustomQuery()
|
|||||||
filter.Add( "gameaddr", server );
|
filter.Add( "gameaddr", server );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
filter.Start();
|
filter.Start();
|
||||||
Id = client.native.servers.RequestInternetServerList( client.AppId, filter.NativeArray, filter.Count, IntPtr.Zero );
|
Id = client.native.servers.RequestInternetServerList( client.AppId, filter.NativeArray, filter.Count, IntPtr.Zero );
|
||||||
filter.Free();
|
filter.Free();
|
||||||
@ -227,11 +161,11 @@ private void OnServer( gameserveritem_t info )
|
|||||||
{
|
{
|
||||||
if ( info.m_bHadSuccessfulResponse )
|
if ( info.m_bHadSuccessfulResponse )
|
||||||
{
|
{
|
||||||
Responded.Add( Server.FromSteam( info ) );
|
Responded.Add( Server.FromSteam( client, info ) );
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Unresponsive.Add( Server.FromSteam( info ) );
|
Unresponsive.Add( Server.FromSteam( client, info ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -164,27 +164,27 @@ public Request Favourites()
|
|||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddToHistory( Request.Server server )
|
public void AddToHistory( Server server )
|
||||||
{
|
{
|
||||||
// client.native.matchmaking
|
// client.native.matchmaking
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveFromHistory( Request.Server server )
|
public void RemoveFromHistory( Server server )
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddToFavourite( Request.Server server )
|
public void AddToFavourite( Server server )
|
||||||
{
|
{
|
||||||
// client.native.matchmaking
|
// client.native.matchmaking
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveFromFavourite( Request.Server server )
|
public void RemoveFromFavourite( Server server )
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsFavourite( Request.Server server )
|
public bool IsFavourite( Server server )
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
105
Facepunch.Steamworks/Client/Server.cs
Normal file
105
Facepunch.Steamworks/Client/Server.cs
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using Valve.Steamworks;
|
||||||
|
|
||||||
|
namespace Facepunch.Steamworks
|
||||||
|
{
|
||||||
|
public class Server
|
||||||
|
{
|
||||||
|
internal Client client;
|
||||||
|
|
||||||
|
internal Server( Client c )
|
||||||
|
{
|
||||||
|
client = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int Ping { get; set; }
|
||||||
|
public string GameDir { get; set; }
|
||||||
|
public string Map { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public uint AppId { get; set; }
|
||||||
|
public int Players { get; set; }
|
||||||
|
public int MaxPlayers { get; set; }
|
||||||
|
public int BotPlayers { get; set; }
|
||||||
|
public bool Passworded { get; set; }
|
||||||
|
public bool Secure { get; set; }
|
||||||
|
public uint LastTimePlayed { get; set; }
|
||||||
|
public int Version { get; set; }
|
||||||
|
public string[] Tags { get; set; }
|
||||||
|
public ulong SteamId { get; set; }
|
||||||
|
|
||||||
|
public uint Address { get; set; }
|
||||||
|
|
||||||
|
public int ConnectionPort { get; set; }
|
||||||
|
|
||||||
|
public int QueryPort { get; set; }
|
||||||
|
|
||||||
|
public string AddressString
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return string.Format( "{0}.{1}.{2}.{3}", ( Address >> 24 ) & 0xFFul, ( Address >> 16 ) & 0xFFul, ( Address >> 8 ) & 0xFFul, Address & 0xFFul );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public string ConnectionAddress
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return string.Format( "{0}.{1}.{2}.{3}:{4}", ( Address >> 24 ) & 0xFFul, ( Address >> 16 ) & 0xFFul, ( Address >> 8 ) & 0xFFul, Address & 0xFFul, ConnectionPort );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Server FromSteam( Client c, gameserveritem_t item )
|
||||||
|
{
|
||||||
|
return new Server( c )
|
||||||
|
{
|
||||||
|
Address = item.m_NetAdr.m_unIP,
|
||||||
|
ConnectionPort = item.m_NetAdr.m_usConnectionPort,
|
||||||
|
QueryPort = item.m_NetAdr.m_usQueryPort,
|
||||||
|
Name = item.m_szServerName,
|
||||||
|
Ping = item.m_nPing,
|
||||||
|
GameDir = item.m_szGameDir,
|
||||||
|
Map = item.m_szMap,
|
||||||
|
Description = item.m_szGameDescription,
|
||||||
|
AppId = item.m_nAppID,
|
||||||
|
Players = item.m_nPlayers,
|
||||||
|
MaxPlayers = item.m_nMaxPlayers,
|
||||||
|
BotPlayers = item.m_nBotPlayers,
|
||||||
|
Passworded = item.m_bPassword,
|
||||||
|
Secure = item.m_bSecure,
|
||||||
|
LastTimePlayed = item.m_ulTimeLastPlayed,
|
||||||
|
Version = item.m_nServerVersion,
|
||||||
|
Tags = item.m_szGameTags == null ? null : item.m_szGameTags.Split( ',' ),
|
||||||
|
SteamId = item.m_steamID
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, string> Rules;
|
||||||
|
public Action OnServerRules;
|
||||||
|
|
||||||
|
public void UpdateRules()
|
||||||
|
{
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// TEMPORARY, WE NEED TO WRITE OUR OWN VERSION OF THIS, DOESN'T WORK ON SPLIT PACKETS ETC
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
using ( var q = new SourceServerQuery( AddressString, ConnectionPort ) )
|
||||||
|
{
|
||||||
|
Rules = q.GetRules();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( OnServerRules != null && Rules != null )
|
||||||
|
OnServerRules();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
509
Facepunch.Steamworks/Client/ServerQuery.cs
Normal file
509
Facepunch.Steamworks/Client/ServerQuery.cs
Normal file
@ -0,0 +1,509 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
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 e )
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
77
Facepunch.Steamworks/Server.cs
Normal file
77
Facepunch.Steamworks/Server.cs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using Valve.Steamworks;
|
||||||
|
|
||||||
|
namespace Facepunch.Steamworks
|
||||||
|
{
|
||||||
|
public partial class ServerList
|
||||||
|
{
|
||||||
|
public class Server
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int Ping { get; set; }
|
||||||
|
public string GameDir { get; set; }
|
||||||
|
public string Map { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public uint AppId { get; set; }
|
||||||
|
public int Players { get; set; }
|
||||||
|
public int MaxPlayers { get; set; }
|
||||||
|
public int BotPlayers { get; set; }
|
||||||
|
public bool Passworded { get; set; }
|
||||||
|
public bool Secure { get; set; }
|
||||||
|
public uint LastTimePlayed { get; set; }
|
||||||
|
public int Version { get; set; }
|
||||||
|
public string[] Tags { get; set; }
|
||||||
|
public ulong SteamId { get; set; }
|
||||||
|
|
||||||
|
public uint Address { get; set; }
|
||||||
|
|
||||||
|
public int ConnectionPort { get; set; }
|
||||||
|
|
||||||
|
public int QueryPort { get; set; }
|
||||||
|
|
||||||
|
public string AddressString
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return string.Format( "{0}.{1}.{2}.{3}", ( Address >> 24 ) & 0xFFul, ( Address >> 16 ) & 0xFFul, ( Address >> 8 ) & 0xFFul, Address & 0xFFul );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public string ConnectionAddress
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return string.Format( "{0}.{1}.{2}.{3}:{4}", ( Address >> 24 ) & 0xFFul, ( Address >> 16 ) & 0xFFul, ( Address >> 8 ) & 0xFFul, Address & 0xFFul, ConnectionPort );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Server FromSteam( gameserveritem_t item )
|
||||||
|
{
|
||||||
|
return new Server()
|
||||||
|
{
|
||||||
|
Address = item.m_NetAdr.m_unIP,
|
||||||
|
ConnectionPort = item.m_NetAdr.m_usConnectionPort,
|
||||||
|
QueryPort = item.m_NetAdr.m_usQueryPort,
|
||||||
|
Name = item.m_szServerName,
|
||||||
|
Ping = item.m_nPing,
|
||||||
|
GameDir = item.m_szGameDir,
|
||||||
|
Map = item.m_szMap,
|
||||||
|
Description = item.m_szGameDescription,
|
||||||
|
AppId = item.m_nAppID,
|
||||||
|
Players = item.m_nPlayers,
|
||||||
|
MaxPlayers = item.m_nMaxPlayers,
|
||||||
|
BotPlayers = item.m_nBotPlayers,
|
||||||
|
Passworded = item.m_bPassword,
|
||||||
|
Secure = item.m_bSecure,
|
||||||
|
LastTimePlayed = item.m_ulTimeLastPlayed,
|
||||||
|
Version = item.m_nServerVersion,
|
||||||
|
Tags = item.m_szGameTags == null ? null : item.m_szGameTags.Split( ',' ),
|
||||||
|
SteamId = item.m_steamID
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user