Getting Live Half-Life Game Stats From ColdFusion

by Lewis Sellers

So, you want to proudly post the stats of your Half-Life or Counter-Strike game servers on your web site. Live.

No problem.

No, really. With a few lines of CFM and a general purpose COM object for sending and receiving UDP packets that I wrote there's not much to it. First off though, let's get into a little beginner-level background info, then we'll talk about the COM and how to make the scripts.

The internet, as you may or may not know, runs off of the TCP/IP network protocol suite. TCP/IP consists of three protocols called: ICMP, UDP and TCP. ICMP is used only to PING for the distance between computers and TCP is used to do most of the heavy-lifting on the internet, powering such familiar things as HTTP, FTP and all your email through POP3/IMAP4 and SMTP.

UDP is the network protocol that almost every game since the late ?90s has used to send information back and forth from game severs to a player?s game client. This is the important part. If you have the ability to send and receive data through the UDP protocol, you can TALK TO ANY GAME on the internet. ?Hi, how ya doing? So, who?s online tonight and how many frags do they have??

Now, sadly, ColdFusion doesn?t ship with any way to inherently use UDP in a general purpose manner. (Or at least it didn?t until CFMX exposed some JAVA functions that can be used for UDP. Using those functions however can be rather intimidating to most folks.) For this reason, sometime back I wrote a simple-to-use, general purpose little COM object specifically so that I could talk to game servers with ColdFusion scripts.

Before we go further, and show you the actual scripts we should go into a little bit more detail on just exactly what this thing ?UDP? actually is. Otherwise you might be scratching you head trying to understand what?s going on later.

UDP stands for ?User Datagram Protocol?. In simple terms, UDP allows you to send a ?packet? of data from one computer to another. By a ?packet? we mean a block of BYTEs (or string of characters such as ?A?, ?B?, etc if you prefer) in a chunk that can range in size anywhere from 0 (that is no bytes at all) up to a little less than 64 kilobytes (or that is technically up to a maximum of 65,507 bytes). You definitely couldn?t send say an entire MP3 or an ebook version of the Bible (or Koran) as a single packet, but that is plenty of space for sending around say lists of people who are currently playing on a game server or things like updated x,y,z positions of a couple dozen people and flying rockets 60 times a second or so.

You?re probably starting to get the idea of what UDP is and what it?s used for (and if you?re starting to relate it to a sort of binary XML feed that's not a bad way for a CFer to start relating to it), but one last important thing before we get to the scripts themselves. UDP is an ?unreliable? protocol. What this means is that if you tell your computer to send a UDP packet to someone else?s computer, the protocol does NOT guarantee that it will be received. This is important. Unlike the heavier protocol TCP (which we mentioned before) you could send out say 10 packets to someone, and they might report back that they only got 3. What happened to the others? *SHRUG*. Now this might seem like a BAD THING, a reason NOT to use UDP, but in truth the technical reasons behind this are actually what make it the prime choice among game programmers as far as protocols to use. In short, this unreliableness makes it FAST. Unlike the heavier TCP, it?s a fire-and-forget protocol. The attitude is: ?Send it off, and if it arrives, fine, if not, we don?t really care.? The heavier TCP protocol corrects for any lost packets but takes up more bandwidth and creates high(er) ping response times doing so. (As an aside, UNREAL I originally used TCP when it shipped, but in later patches dropped it in favor of UDP when trying to tweak out better responsiveness for its internet multiplay.)

Ok. That?s it for the background lecture. We?re not going to get into any more of the technical aspects of UDP else this article will threaten to become a chapter in a book. You should know enough now to more or less follow what we?re going to do in the scripts.

PART TWO: The Scripts

Let?s start with the most basic thing: Getting a list of everyone currently playing on a Half-Life/Counter-Strike game server.

All that you really have to do is send a UDP packet to the game server you?re interested in. This packet will contain a request for a list of players. Assuming that the server got the packet (UDP is unreliable remember ? a flakey Ethernet controller in Iraq could have ate the packet), it will send you BACK a packet containing the information you requested. Then it?s simply your job to use the string handling function of ColdFusion to parse through the data you got back and CFOUTPUT it to your web page.

That?s all there is to it.

Well, except for the details. The devil is in the details they say, and it?s true. Some games send data back and forth in pure, lovely, easy to handle, plain text. Half-life . . . well, now Half-Life is not one of those, unfortunately. It sends its data around in ugly (but compact, and thus fast) chunks of binary data.

Not to worry though, the COM object I mentioned, and which you need to make the scripts work (see the APPENDIX for the download URL) has a couple little functions that you can use to pull out the binary formatted data into CFM variables.

Ok, the details. The command string (or request) you need to send to the game server to list players is:

	chr(255) & chr(255) & chr(255) & chr(255) & "players" & chr(10)

Yes, like I said, Half-Life uses some unprintable binary data in its request/responses but you don?t have to understand what it means. All you have to do is just send it, as is, to the game server.

Now, what you get back is a packet that contains:

	(255, 255, 255, 255)  : four chr(255)?s. (An OOB). Ignore this.
	?D?                   : A response code that means ?player data follows?.
	                      : You see, UDP can send packets back in a
	                      : completely different order than you
	                      : sent them out. Without such codes...
	                      : you could have a problem.
	BYTE                  : An 8-bit BYTE that is the ?active player count?.
	                      : Can technically be 0 to 255.

Then there follows a series of recordsets that you can CFLOOP through containing specific information for each of the players on the server. If ?active player count? is 0 then there is no further data following. Each player record consists if the following data:

	BYTE                  : The game server?s index number (ie, Primary Key)
	                      : for the player. Can be 0 to 255.
	STRING                : A NULL (ie, CHR(0)) terminated ASCII string
	                      : containing the player?s name.
	INT32                 : A signed 32-bit integer number (4 BYTES)
	                      : which is the players FRAG total.
	FLOAT32               : And finally, a 32-bit floating-point number
	                      : which represents the time the player has been in 
	                      : the game.

That?s it. Aside from a short lecture on the actual functions the COM object has to send and receive the data and parse through the binary data, you now know how to list the players on a Half-Life server. Smile. ;-)

Ok, drum-roll please. Actual code follows.

First, we create an instance of the COM object and call it ?obj?.

<CFOBJECT
	ACTION=CREATE
	NAME=obj
	CLASS=Intrafoundation.UDPClient
>
	<h1>PLAYERS</h1>

Simple enough. Now we tell the COM the internet address and port number of the game server we?re interested in by ?open?ing a connect to it. gaming.twcny.rr.com always seems to be up, so we?ll use them as an example. (BTW, by default Half-Life is always on port 27015).

<cfset c=obj.Open(?gaming.twcny.rr.com?,27015)>
<CFIF c IS "1">

?Open? returns a ?1? if it could establish a connection to the server and a ?0? if it couldn?t. We save this in the variable ?c?. We then check if it?s ?1? and print a message saying we couldn?t connect if not.

	<cfset s=chr(255) & chr(255) & chr(255) & chr(255) & "players" & chr(10)>

We set ?s? to the ?players? request string.

	<cfset obj.timeout=2.000>

Before we do anything else we tell the COM object to wait around up 2 full seconds before giving up on anything it tries to send or receive. You can change this to anything you feel comfortable with. If the game server is sitting on a rack right next to your web server you could change it to 0.050 (or 50 milliseconds) if you want. (Because of a thing called the ?Nagle? algorithm there?s usually not much point in making it less than 20ms.)

	<cfset c=obj.Send(s)>

Now we send the packet with the ?send? command. That wasn?t hard was it? Again, if the command fails for some reason it?ll return a ?0?, otherwise it?ll return a count of the BYTEs it actually sent off. UDP should NEVER fail unless there is a serious problem with the Windows operating system itself.

	<CFIF c IS "0">
UDP Packet send failure (players).<br>
	<cfelse>
UDP Packet sent... (players)  (<cfoutput>#c#</cfoutput> bytes).<br>
	</cfif>

The above codes tests if the ?send? actually worked. I?ve never seen it fail.

	<cfset obj.RecvPacket()>

And this invokes the command to receive the packet. If we expect only pure text we could use the ?Recv? command and immediately print it to the web page. Since we know it's binary data though we use the special ?RecvPacket? command which buffers the data into a binary data cache. We can now issue a few of the special functions I mentioned which can parse through the pesky binary data packets for you.

(NOTE: The COM object does have complete documentation of all these functions. Don't worry about that now though. For clarity, the important thing to understand here is that "recvpacket" stores the entire last packet received in a buffer in the COM object itself. The special binary packet handling functions will pull data from this buffer for you in a sequential manner, handling all of what would be a rather ugly business if you tried to handle it yourself in say ColdFusion version 3.x. Support for binary strings is better in latter versions of CF though, if you want to do it by hand. Feel free.)

	<cfif obj.packetlength is 0>
UDP packet recvpacket failure...<Br>

First though, we check ?packetlength? to see just how many bytes of data we got back. If the game server sends back a packet with 0 bytes, well, something very odd is up with that server. But we check anyway.

	<cfelse>
UDP packet recv... (<cfoutput>#obj.packetlength#</cfoutput> bytes)<Br>

Print the count of bytes received.

<cfset oob=obj.packetnumber("int32")>
oob = <cfoutput>#oob#</cfoutput><br>

Ok. Here we go. We use the ?packetnumber? function to grab ?numbers? from the ?packet? buffer. The four chr(255) we mentioned above should be here. Actually, if you really want to know, the four chr(255) are what the number ?-1? looks like as a binary 32-bit integer. Oh, you didn?t want to know? Sorry. Pretend I didn?t tell you. It?s not important. All you need to know is that?s it?s a 32-bit integer so we tell ?packetnumber? to please grab an int32 for us.

Which we promptly ignore. (It?s a special UDP header control code called an OOB ? Out Of Bounds. Didn?t want to know that either, eh?)

<cfset ch=obj.packetchar()>
ch = <cfoutput>#ch#</cfoutput><br>

Guess what? We use ?packetchar? to grab a character (a single BYTE expressed as an ASCII character) from the packet stream and pray to whatever deities will listen that it?s a ?D?, cause ?D? (as mentioned far above) means player listing data follows.

<cfif ch is "D">
**PACKET "PLAYERS" DETECTED**<br>

Yippie. It is the packet with player data in it.

<cfset acc=obj.packetnumber("BYTE")>
active client count = <cfoutput>#acc#</cfoutput><br>

We use ?packetnumber? again to grab the next BYTE in the packet buffer and save it as a number to the CF variable ?acc?. This number should tell us exactly how many players are, right at this very moment, in the game trying to kill each other.

<table border=1>
<tr>
<th>#</th><th>Player Name</th><th>Frags</th><th>in-game Time</th>
</tr>
<cfloop index="i" from="1" to="#acc#">
	<tr>
	<cfset id=obj.packetnumber("BYTE")>
	<cfset playername=obj.packetstring()>
	<cfset frags=obj.packetnumber("int32")>
	<cfset ingame=obj.packetnumber("float32")>
	<cfoutput><th>#id#</th><td>#playername#</td><td>#frags#</td><td>#ingame#</td></cfoutput>
	</tr>
</cfloop>
</table>

The table above should be somewhat obvious by now as we?ve used the ?packetnumber? function a couple times and we went into detail on the breakdown of the contents of the packet as well. The ?packetstring? function is new, but also obvious we should think.

What we?ve done is just CFLOOP through the remaining player data and print up a table based on the info we extract from the binary packet buffer.

<cfelse>
**PACKET WRONG-TYPE or NOT DETECTED**<br>
</cfif>
</cfif>

The remains of a few of our conditional CFIF tests.

<cfset obj.Close()>

And finally, now that we?re done, we close the connection we opened with the ?Open? command.

<cfelse>
Couldn?t establish connection.<br>
</cfif>

Hey. That?s it. We?re done man (or woman, as the case may be).

You now know more about this subject than probably every one else on the planet excepting perhaps a few thousand other people. Now go on and build yourself a stats page.

fini

--min
Lewis Sellers
April 2003
http://www.intrafoundation.com

APPENDIX A: The COM Compoment

This article made use of a COM component called ?UDPClient?. It?s free, completely open-sourced and available for immediate download at http://www.intrafoundation.com/UDPClient.asp. It?s written in C++, not that it matters as the compiled .DLL is included in the .ZIP. All you have to do is unzip the .DLL into your web folder somewhere and register it. I.e. at a console prompt in that folder type:

	regsvr32 UDPClientcom.dll

There's also an install.bat in the .ZIP's folder you can click on. But now it?s exposed for anything on your computer to use it, including your CFM scripts.

APPENDIX B: Other Half-Life Commands

PING

Send:

	chr(255) & chr(255) & chr(255) & chr(255) & "ping" & chr(10)

Receive:

	INT32       : -1
	?j?         :

PLAYERS

Send:

	chr(255) & chr(255) & chr(255) & chr(255) & "players" & chr(10)

Receive:

	INT32       : -1
	?D?         :
	BYTE        : active client count

Followed by a record-set of active client count

	BYTE        : client index
	STRING      : player name
	INT32       : frags
	FLOAT32     : total time in game

RULES

Send:

	chr(255) & chr(255) & chr(255) & chr(255) & "rules" & chr(10)

Receive:

	INT32       : -1
	?E?         :
	INT16       : count of rules

Followed by a record-set of rules

	STRING      : rule name
	STRING      : rule value

INFO

Send:

	chr(255) & chr(255) & chr(255) & chr(255) & "info" & chr(10)

Receive:

	INT32       : -1
	?C?         :
	STRING      : server network address
	STRING      : host name
	STRING      : map name
	STRING      : game name
	STRING      : game description
	BYTE        : active client count
	BYTE        : maximum number of clients (players) allowed
	BYTE        : network protocol version

DETAILS

Send:

	chr(255) & chr(255) & chr(255) & chr(255) & "details" & chr(10)

Receive:

	INT32       : -1
	?m?         :
	STRING      : server network address
	STRING      : host name
	STRING      : map name
	STRING      : game directory
	STRING      : game description
	BYTE        : active client count
	BYTE        : maximum number of clients (players) allowed
	BYTE        : protocol version
	BYTE        : ?l? = listen server, ?d? = dedicated server
	BYTE        : 'w' = windows, 'l' = linux
	BYTE        : password? 1 = yes, 0 = no
	BYTE        : mod? 1 = yes, 0 = no

If mod? is ?1? then extra data follows:

	STRING      : url for mod website
	STRING      : url for mod download
	INT32       : mod version
	INT32       : mod byte size
	BYTE        : mod only to server?  1 = yes, 0 = no
	BYTE        : custom mod client required?  1 = yes, 0 = no

SDK Extension:

	BYTE        : secure server?  1 = yes, 0 = no

GETCHALLENGE

Send:

	chr(255) & chr(255) & chr(255) & chr(255) & "getchallenge" & chr(10)

Um... Ok. Wait a minute. Maybe you don't need to know this. This command initiates an actual remote player connection. Yes... I know what's in your head now. You're thinking "Hey! If you just tell me the rest of the command codes I can make my web page play a game of Half-Life all by itself, dude!"

Yes, that's technically true, but.... Maybe in another article. ;-)

- v1.2 -

About This Tutorial
Author: Lewis Sellers
Skill Level: Advanced 
 
 
 
Platforms Tested: CF4,CF5
Total Views: 88,282
Submission Date: April 30, 2003
Last Update Date: June 05, 2009
All Tutorials By This Autor: 1
Discuss This Tutorial
  • Hacker problems of late. Sorry about that. Made a work around until the server can be de-kludged. Try downloaded it now. BTW. A slightly updated version of UDPClient (and TCPClient) are sitting on my hard drive. I suppose I could try finishing them over the weekend and uploading. --min

  • Can someone post a link to the the udpclient for cfmx. It seems I cannot dl from the link above anymore. Thanks!! zbob

  • pretty good

Advertisement

Sponsored By...
Healing Touch Massage - $39.00 - 50 Minute Deep Tissue Massage Dripping Springs, Texas!