|
|||||||||||||||||||||||||
|
|||||||||||||||||||||||||
|
Announcements
Services
Chapters
Feature Zones
|
What the hell...Today first person shooter games like Counter Strike, Quake and Unreal Tournament are played online mostly. Each game ships with a built-in server-browser to show active servers on the net. A lot of people would like to have such a server-browser or similar tools as standalone applications. Maybe they want to look for more than one game at the same time, maybe standalone tools have additional features (like enhanced filtering) or are lightweight applications that do their specific job much faster than in the in-game-browsers, whatever. With Unreal Tournament 2003 being available soon, I thought it would be a good idea to share my code. Provided with this backend-class, you are able to develop your own server-browser, server-watching-tool or whatever you like. IntroductionThere are several sources of information on how to get the latest status of an Unreal Tournament server (see the Links-section below). (I personally play none of the other games, so I don't know about their query-protocols, but i believe, they are similar to the Unreal-ones presented here.) When speaking about Unreal Tournament 2003, there are two protocols available. The GameSpy-protocol that already worked with Unreal Tournament (the old version of the game) and a brand-new protocol, that was first seen with the Unreal Tournament 2003 Demo available in mid-September 2002 (when this article was written). The 'old' GameSpy-protocol is well documented by Epic (the company that does all the Unreal-series games). On Epic's Unreal Technology Site there is a page on server-communication that gives us a lot of information. In short: send '\status\' via UDP to the server, but add 1 to the official gameport (e.g: server-address = 62.93.201.43:8980 - send query to 62.93.201.43:8981). What you get back is a bunch of packets, each in the following format: "\property\value\property\value\...\queryid\X.Y". Each query is assigned a unique ID to - all the packets you receive from one query should have the same X-value. The second number Y is the packet-number (1-indexed). This is essential, since UDP is a connectionless protocol, thus packages may take different routes through the internet and may arrive out of order. Additionally from time to time packets get lost. Checking the packet-numbers you can asure, that you received all packets in the correct ordering and that none is missing. The last package ends with the string '\final\'. To give you an idea, of how a typical answer looks like, here is a very simple one: \gamename\ut2d\gamever\1080\minnetver\1000\location\0
\hostname\www.inUnreal.de UT2k3 TDM2 @Clanserver4u\hostport\8980
\maptitle\Citadel\mapname\CTF-Citadel\gametype\xCTFGame
\numplayers\0\maxplayers\12\gamemode\openplaying\gamever\1080
\minnetver\1000\AdminName\hEx\AdminEMail\hex@inunreal.de\queryid\66.1\final\
Currently there's no player active on the server and we received all the information in one single packet. Querying an 'old' UT-server, you would use Ip:gameport+1 with that GameSpy-protocol (as written in that official document I mentioned above). Now in times of UT2003, that protocol was moved over to gameport+10. On gameport+1 there's this new, let's call it Unreal-protocol, which is byte-oriented. There's no official documentation on this protocol, but since it is far more robust (no backslash-bug, see below) and faster to parse, it will be used a lot in the future. See the Links-section for pages of people that looked at the packets (sent by the original built-in UT2003 server-browser) and have written down, what they found out. Update: After observing the two protocols for a few weeks now, there are certain important things to mention. See the Protocol Discussion Section below. You guess what the two 'in-depth' parts of this article (and the main problems writing the code for our purpose) are: connect to the server and parse the answer. But before getting into detail, let's have a look at how to use this class: UsageFirst thing to do is to add the provided C#-file UT2003Server.cs into your project. Next you construct your server-object and call at least once the server = new UT2003Server("62.93.201.43",8980);
// ...
try
{
server.Refresh();
textBoxServerName.Text = server.HostName;
textBoxMapTitle.Text = server.MapTitle;
textBoxPlayerCount.Text = server.NumPlayers.ToString();
for(int i=0; i<server.Players.Length;++i)
{
listBox.Add(server.Players[i].Name);
}
// other properties are Version, MinClientVersion, MapFilename,
// Gametype (DM, CTF, ...)
// MaxPlayers, AdminName, AdminEmail, Player-array containing
// name,ping,frags for each player
// and more...
}
catch (GameserverQueryException ex)
{
MessageBox.Show(ex.Message,"Error");
}
Pretty simple. I would strongly recommend to put the Refinement I: Using Refinement II: Using Attention: Be aware that some properties of the server-object may equal to Update: In version 2.1 there was a new property introduced to the class, named for(int i=0; i<server.ServerVars.Count; ++i) { ListViewItem prop = new ListViewItem(new String[] { UT2003Server.Prop2En(server.ServerVars.GetKey(i)), server.ServerVars[i]}); listViewServer.Items.Add(prop); } The new static function There are a few things left to mention about The rule is: When you want to display all server-properties in a list or similar, loop through Querying the MasterserverThere is no real querying of the masterserver, at least if you don't know the authentication-algorithm, that UT2003 uses to connect to the masterserver. EPIC doesn't want everyone to query the server via TCP (as in good old UT times), but rather updates a textfile every 20 seconds (or something in that dimension) which is free for download. There's one file for the UT2003 demo-version and another for the retail:
The Format is pretty simple: one line for each server and each line containing three tab-separated values, which are IP → Gameport → GameSpy-Queryport
I've added two little static functions to the UT2003Server-class for each list. In your code, querying the masterserver will look like this: ...
UT2003Server [] serverList;
serverList = UT2003Server.QueryMaster();
...
Protocol DiscussionHow to ask?The None of the two protocols is better in each and every case. It seems, that the output of the new Unreal-protocol is cached on the server and far from being up-to-date. If you query a server every 2 seconds, you see that the output of the GameSpy-protocol changes everytime something happened (especially the ping and frags of the players) while the Unreal-protocol still delivers things from the past. On the other hand, the GameSpy-protocol gives you less information on the game and the server in general. Thus getting information about mutators, friendly fire, gamespeed, etc., you have to use the new Unreal-protocol. Summary: only using both protocols gives you complete and up-to-date information. That's why I introduced the The following table outlines the properties, the protocols deliver exclusively (i.e. the other one doesn't):
Where to ask?Some Servers don't have their GameSpy-Queryport on Gameport+10, since this is freely configurable by the server-admin. When I wrote this, there were 1196 servers online, 951 (79.5%) of them using Gameport+10 (default), 118 (9.8%) using Gameport+12, 67 (5.6%) using Gameport+1 and 60 (5.0%) using other Offsets (even negative ones, i.e. Gameport > Queryport). That means: there's no 100% reliable way to get the Queryport out of the Gameport, but with +10 or +12 you are right with a probability of 90%. Using Gameport+1 will work with half of the remaining 10%, but I have no idea, where these servers put the Unreal-protocol Queryport (since Gameport+1 is the default value here). But it turns out, that the entire story isn't that frustrating, because EPICs masterserver just tells you the GameSpy-Queryport - so if you get your servers from the masterserver, everything is alright. If you want to observe a certain server given by IP and Gameport, you have to ask the administrator for the GameSpy-Queryport or try yourself. The message is: it is not always Gameport+10! If you just want to use the class and don't care much about details and the problems I had to face, you can stop reading here and do a little C# instead. ;) Connect to the ServerWell, now lets dive into the Details. First some preparatory work: The UdpClient::Client.SetSocketOption(SocketOptionLevel.Socket,
SocketOptionName.ReceiveTimeout, 10000);
but that just didn't work - would have been too nice. Notice the public class TimedUdpClient : UdpClient
{
private int timeout = 1000; // 1 sec default
private Thread timeoutWatchdog;
public int Timeout {
get { return timeout; }
set { timeout = value; }
}
public new byte[] Receive(ref IPEndPoint remote)
{
byte [] ret;
timeoutWatchdog = new Thread(new ThreadStart(StartWatchdog));
timeoutWatchdog.Start();
try
{
ret = base.Receive(ref remote);
}
catch (SocketException) { ret=null; }
finally { timeoutWatchdog.Abort(); }
return ret;
}
private void StartWatchdog()
{
Thread.Sleep(timeout);
this.Send(new byte[] {0x00},1,"",8000); // port is arbitrary
}
}
It makes a new Having that and the base functionality of The first version of this article had a lot of code here, which was kind of straightforward and only lengthened the page needlessly. I will give you only the core parts now, if you're interested you will look at the code anyway ;) First lets have a look at the GameSpy-Query part: // set the server
remote = new IPEndPoint(ip,port+gameSpyQueryOffset);
// form the query
byte[] query = Encoding.ASCII.GetBytes("\\status\\");
// send the query
udp.Send(query,query.Length,remote);
// receive result
answer = "";
byte[] receive = null;
DateTime start = DateTime.Now;
TimeSpan diff;
do
{
receive=udp.Receive(ref remote);
// turn received byte array into a string
if (receive!=null)
answer += Encoding.ASCII.GetString(receive);
DateTime now = DateTime.Now;
diff = now-start;
} while (!answer.EndsWith("\\final\\") && receive!=null &&
(diff.Seconds*1000+diff.Milliseconds < timeout));
if (receive==null || (diff.Seconds*1000+diff.Milliseconds >= timeout))
throw new ServerNotReachableException("timed out");
// evaluate
AnalyseGameSpyAnswer();
Sending the query via UDP needs the query itself to be in a Update: I found one server on the masterlist, that returned nonsense, when asked with '\status\' on the correct port. Since I want that class to work with every server on the masterlist, I updated the piece of code above with an additional timeout-condition, when there is something received, but no '\final\' could be found within the given timeout. The ominous server sent an increasing stream of zero-bytes... The Unreal-Query is devided into three parts (see the documentation for details), where all three queries are sent and evaluated, when the Unreal-Protocol is used, and only the second query is used, when working with the mixed protocol (since basic and player information is already there from the GameSpy-Answer in that case). // server props query
remote = new IPEndPoint(ip,port+unrealQueryOffset);
byte [] queryServer = {0x78,0x00,0x00,0x00,0x01};
udp.Send(queryServer,queryServer.Length,remote);
receiveServer=udp.Receive(ref remote);
This is the code for the second query, which receives the server properties packet. The two other ones are analog. Afterwards the Parse the AnswerGameSpy ProtocolFirst, I'll give you a real world example: \gamename\ut2d\gamever\1077\minnetver\1000\location\0
\hostname\The Drunk Snipers UT2003 CTF Instagib (DEMO) #2\hostport\7777
\maptitle\Citadel\mapname\CTF-Citadel\gametype\xCTFGame\
numplayers\11\maxplayers\16
\gamemode\openplaying\gamever\1077\minnetver\1000\AdminName\
Sirius\AdminEMail\sirius@drunksnipers.com
\player_0\Player\frags_0\1\ping_0\ 63\team_0\0
\player_1\Perdition\frags_1\0\ping_1\ 221\team_1\1
\player_2\Porter\frags_2\10\ping_2\ 143\team_2\1
\player_3\JoeDozer\frags_3\20\ping_3\ 289\team_3\0
\player_4\Kee\/n\frags_4\38\ping_4\ 117\team_4\0
\player_5\Rekklih\frags_5\42\ping_5\ 166\team_5\1
\player_6\Hanover_Fist\frags_6\4\ping_6\ 196\team_6\0
\player_7\HELLAS-4774CK\frags_7\101\ping_7\ 107\team_7\1
\player_8\Psycho\frags_8\49\ping_8\ 105\team_8\1\queryid\29.1
\player_9\Levodopa\frags_9\45\ping_9\ 36\team_9\0
\player_10\Psy[Duck]\frags_10\49\ping_10\ 73\team_10\0\queryid\29.2\final\
This answer comes in two packets (29.1 and 29.2) in correct ordering and 29.2 is definitely the last packet, because it ends with '\final\'. There are 11 players currently on the server - the demo-version had a bug, that caused the server to send a weird team-value. The class takes care of that and simply sets the team-value to an empty string, when querying a UT2003-Demo server. There's another problem in that answer, that is well known to GameSpy-Style-Query-Programmers. The 'very cool' Player named 'Kee\/n' confuses our nice little But hey, you're a programmer and programmers are there to solve problems. Well, at least I solved the problem with the following hack: if the next property immediately after 'player_x' is not 'frags_x' or 'queryid' a playername with a backslash is detected and handled appropriately. In fact a backslash ('\') and the nonsense-property-name ('/n') (which is the second part of the players name) are appended to the name and the nonsense-value ('frags_4') becomes the new property-name. Put that into a while loop (to handle even cooler players using more than one backslash in their name) and everything goes fine. Since there's nothing technically new and it should be an easy read, just go ahead and look at the code, if you're interested in the details. UT2003-protocolWhen you stick to the specification, parsing the byte-arrays is pretty straightforward. I needed a function that extracts a Notice, that this protocol may change with each patch version released for UT2003. Currently it is working with the UT2003 Demo and Retail version! Maybe some patched servers deliver additional information, maybe something is altered - please contact me, if you think you've found a server, that sends things that are not taken care of by my class. Other interesting ProblemsWhat about the player-properties? I included a very basic Btw: I won't rely on the 'numplayers'-property to be there all the time. Using that (and assuming it always is delivered before the first player-property) might be a solution to know the size of the player-array before the first player occurs. A little request: if you're using that class when debugging your application or for private use, I would be glad if you set Links
ChangelogVersion 2.2 - 10/17/2002
Version 2.1 - 10/16/2002This version is compatible with 2.0 - so replacing UT2003Server.cs with that new version in an existing project, should work fine.
Version 2.0 - 10/08/2002
Version 1.1 - 09/25/2002
Version 1.0 - 09/25/2002
AftermathSorry for my bad English. I'm trying to improve it, but here and there, you see I'm natively German-speaking. :rolleyes: Prospective: When future patches are released, I will make sure, the class runs with all versions of UT2003. This is my first article. Feel free to make suggestions, both on the code and on the article itself. Email: ruepel@gmx.li | ||||||||||||||||||||||||