Introduction
Most gaming servers support status queries to get information like server configuration or the number of players. There are some programs that query master servers to receive all IPs from the gaming servers and list them. The most popular ones are Gamespy, The All Seeing Eye and kQuery.
I did not intend to build another program with just the same functionality, but to create a library that allows easy handling to query the servers and offering their information on your special purpose.
Background
The status queries use specific protocols: the most common are Doom3, Gamespy, Gamespy v2, Half-Life, Quake3, Source (Half-Life 2) and The All Seeing Eye (ASE). These protocols provide basically the same information, but their structure is different. In general, there are three sections:
Infos
The basic information like server name, map name, maximum players and password protection.
Rules
The detailed server information with game specific data like teams or response time.
Players
A list of all players on the server. These information differs a lot: the different protocols have only the player name in common, depending on the protocol there are also the players' score, ping or team.
Protocol Overview
Here is a short overview of the different protocols and how they work:
Doom3
This protocol is currently used only in so called game, but will probably be used in upcoming games built on the same engine. The request starts with a 0xFF 0xFF
, followed by a simple getInfo
. The server response comes in one big block and is separated by 0x00
. That makes the response look like a Key0x00Value
– the server information and the player information are separated by a 0x00
0x00
(a double NULL
).
Gamespy
Gamespy is one of the oldest and most broadly used protocols. Its structure is very simple: to get the server's information, you just need to send a simple \info\
; for the rules, send \rules\
; and for the players, \players\
. You can also combine these requests by sending something like \infos\\players\\rules\
.
The response looks like \Key\Value\Key\Value\
, the player info is extended with an integer to group them. They look like \player_0\Name\frags_0\10\ping_0\100\
. This is important because some servers send all \player_x\
flags, followed by all \frags_x\
and so on.
Gamespy v2
The name indicates, Gamespy v2 is the revision of the Gamespy protocol. It differs from the first version by managing information more with byte values than with strings. All requests begin with a 0xFE 0xFD 0x00
, followed by a string that is used as a ping value. Now we append three more bytes which stand for the information we want to know: the first byte for server information, the second for rules and the third for players. You can set them with 0x00
for NO or 0xFF
for YES. The structure of the response is the same as in Doom3.
Half-Life
The request starts with a 0xFF 0xFF 0xFF 0xFF
, followed by details
, rules
or players
, so it is pretty much the same as in Gamespy v2, but with strings. The structure of the response is the same as in Doom3.
Quake3
The request is similar to Half-Life: we start with a 0xFF 0xFF 0xFF 0xFF
, followed by the string getstatus
. The response includes all information in multiple text lines and can be handled with simple string parsing. The first line can be ignored, the second provides the server information in the same format as in Gamespy, (\Key\Value\Key\Value\
). The following lines represent the player information, one player per line, in the form Scores Ping „Playername“
.
Source (Half-Life 2)
The request structure is nearly the same as in the former Half-Life protocol: only the strings are replaced with single bytes. The query now starts with 0xFF 0xFF 0xFF 0xFF
, followed by 0x54
for the server information, 0x55
for the rules and 0x56
for the player information. The structure of the response is the same as in Doom3.
The All Seeing Eye (ASE)
The protocol to the homonymous program is the most complex and also the rarely used one. The request is a simple s
, but different from all other protocols. The query must be sent to the server port + 123, so don't send to the game server port. The response is a little bit more difficult, because it has no separators: The structure is „Byte String Byte String“, in which the byte represents the length of the next string, (the byte is self-including). The separator between the server and player information is an empty string, so it's the length of the byte itself - a simple 0x01
. The player information looks similar, but starts with an extra byte by using the flags telling which information is included. I do not describe this protocol any further, the only games currently using this protocol are GTA and Farcry.
An excellent documentation and samples for all of these protocols can be found in the newsgroups dev.int64 and dev.kquery, which I used while creating this library.
Using the Code
Using the library is very simple. First, include it in your project, and then simply call it as shown:
GameServer server = new GameServer( "192.168.0.15", 27960, GameType.Quake3 );
Server.QueryServer();
QueryServer()
gets all the information from the server. Depending on the timeout, this may take a while. You can change this with the Timeout
property.
Server.Timeout = 1500;
The basic information is available over properties and collections, depending on which protocol you are querying. Not all values are set. Here are the diagrams for all of them:
| Using the Players property, you can get a PlayerCollection containing the following data:
The Parameters property contains a StringCollection with all information from the server. The values strongly depend on the protocols being used!
|
Connecting to Servers
The complete connection and parsing process is handled through the abstract protocol class. Here is the main connection part:
protected void Connect( string host, int port )
{
_serverConnection = new Socket( AddressFamily.InterNetwork,
SocketType.Dgram, ProtocolType.Udp );
_serverConnection.SetSocketOption( SocketOptionLevel.Socket,
SocketOptionName.ReceiveTimeout, _timeout );
IPAddress ip;
try
{
ip = IPAddress.Parse( host );
}
catch( System.FormatException )
{
ip = Dns.Resolve( host ).AddressList[0];
}
_remoteIpEndPoint = new IPEndPoint( ip, port );
}
Server Communication
The communication always uses UDP supplied by the methods Socket.SendTo()
and Socket.ReceiveFrom()
.
_readBuffer = new byte[100 * 1024];
EndPoint _remoteEndPoint = (EndPoint)_remoteIpEndPoint;
_packages = 0;
int read = 0, bufferOffset = 0;
_sendBuffer = System.Text.Encoding.Default.GetBytes( request );
_serverConnection.SendTo( _sendBuffer, _remoteIpEndPoint );
...
read = _serverConnection.ReceiveFrom( _readBuffer, ref _remoteEndPoint );
Most servers send packages with a maximum length of 1400 to 1500 bytes. For long status information, this is not enough memory to send it in one package. Usually, they fit into two packets. UDP is a stateless connection, so we must implement how to handle these multi-packages and how to merge them.
Half-Life
In Half-Life, these UDP-packages start with a 9 byte-long header, different from single-package responses. The ninth position is telling us how many packages the response includes, and which package we are currently working on.
byte[] _tempBuffer = new byte[100 * 1024];
read = _serverConnection.ReceiveFrom(
_tempBuffer, ref _remoteEndPoint );
int packets = ( _tempBuffer[8] & 15 );
int packetNr = ( _tempBuffer[8] >> 4 ) + 1;
if ( packetNr < packets )
{
Array.Copy( _readBuffer, 9, _tempBuffer, read, bufferOffset );
_readBuffer = _tempBuffer;
}
else
{
Array.Copy( _tempBuffer, 9, _readBuffer, bufferOffset, read );
}
Gamespy v1 and v2
All Gamespy responses are built in a Key/Value scheme, so it doesn’t really matter if we merge them in the right order. The protocol itself is smart enough to always split at a separator and not inside a value, so we can simply attach them at the end of the readBuffer
.
read = _serverConnection.ReceiveFrom( _readBuffer, bufferOffset,
( _readBuffer.Length - bufferOffset ),
SocketFlags.None, ref _remoteEndPoint );
Parsing the Response
The main part of parsing the strings that are separated by a NULL
byte (0x00):
protected string ReadNextParam()
{
string temp = "";
for ( ; _offset < _readBuffer.Length; _offset++ )
{
if ( _readBuffer[_offset] == 0 )
{
_offset++;
break;
}
temp += (char)_readBuffer[_offset];
}
return temp;
}
String-separated responses are even more simple:
AddParams( ResponseString.Split( '\\' ) );
protected void AddParams( string[] parts )
{
if ( !IsOnline )
{
return;
}
string key, val;
for ( int i = 0; i < parts.Length; i++ )
{
if ( parts[i] == "" )
{
continue;
}
key = parts[i++];
val = parts[i];
if ( key == "final" )
{
break;
}
if ( key == "querid" )
{
continue;
}
_params[key] = val;
}
}
I avoided explaining how to parse the ASE protocol, because it doesn’t work 100%. I’m missing some good documentation, and it seems that many people besides me are also thinking that it’s not worth the work.
Links
I can't thank enough these guys. For more information, check these pages:
Summary
I learned something about UDP protocol and used classes and methods I never used before, like the BitConverter()
, so I think it was worth the work. I hope this library can be useful in your future projects.
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.