The master Server Protocol

When Hyperbol wants to know what servers are available, it contacts the Master Server, and queries it for live servers. The Master Server responds with a list of all live servers, how many players are on them, etc.

The hyperbol Master Server address is currently:

hbms.iocainestudios.com PORT 20203 PROTOCOL TCP

... but at a future time we may add additional servers or make the address configurable. That server is always sitting on that port, waiting for TCP connections to it.

Note: In this protocol document, a 'char' is a single byte character, an int is a 32-bit integer, and a uint is a 32-bit unsigned integer. All data is in little-endian (intel / x86) data format except for possibly the INET-ADDR of the server, which is in standard INET-ADDR order (?) All structures use default C++ packing (sorry), so I'm actually just guessing the offsets here. Its probably actually aligning by 32 bits for some stuff and not for others, have fun.

When the server detects a client connection, the first thing it does is immediately send the following data packet to the client:

Offset

Type

Description

0

char[4]

always the four characters "HBSL" - serves as an identifier that you are connected to a server that is sending Hyperbol Server Lists. If you don't see these characters, you are not connected to a hyperbol master server.

4

uint

Security Key - This is more or less an outdated feature that we will eventually get rid of. The client must currently parrot this key back to the server before it gets server listings.

8

uint

The number of players playing hyperbol (total). This can be used to detect activity without asking for a full server list. It probably does not include bots.

Once the client has received this packet, it has just a few seconds to ask for the full list. If it does not reply, the connection is terminated and cleaned up pretty quickly. If the client wants the full list of servers, it must respond with the following packet:

Offset

Type

Description

0

uint

security key. This must be the same key sent to the client when it connected to the server.

4

byte[4]

Four bytes that can be used to filter the results. Currently, only the first byte is considered. It is a bitflag, with the following bits still having meaning (binary, not hex) 16 = Show unofficial servers as well as official ones

It is recommended that you always just send a 255 first byte to guarantee that you always get all servers (since other filters may be interoduced later). The structure sizes MUST match up. Once it recieves the request, the server will immediately send you all the servers it knows about, end-to-end. It will not prefix the list with a count, and once it finishes, it will immediately close the connection. Each server item has the following structure:

Offset

Type

Description

0

uint

INET-ADDR compliant IP address (four bytes, in network address order)

4

uint

port (note, this is a uint, not a ushort)

8

byte

server 'flavor' - ie, officialness and gametype

9

3 bytes

3 bytes of padding so that the structure ends up exactly 12 bytes long

Mind the padding!

Note that the master server is authorative on the 'flavor' of server. 0 means its an unofficial server. Other values indicate official servers of various kinds (deathmatch, etc). The reason the master server is authorative on this is so that servers cannot spoof that they are official when they are not.

Also note that all that the master server tells you is the port and IP Address that the server is located at. In order to get the actual data such as name, etc, from the server, you will have to hurl a UDP packet at the actual server itself (using the IP and Port given) for more details!

Server Query Protocol

To get the details from individual servers, you will need to throw a UDP packet at them (given the port and IP from the master server). The UDP packet structure should be as follows

Offset

Type

Description

0

byte

Must be the byte 2

1

uint

A timestamp - this timestamp will be echoed back so its not that important what you use, but you can use it to determine actual ping time to server. Note that alignment is actually overridden to be single-byte-aligned here, it is at offset 1, not 4

The server, if its alive, will respond as quickly as it can, with the following packet of data:

Offset

Type

Description

0

byte

Will be the byte 27 or else you're not talking to a server

1

uint

A timestamp - whatever you sent as the timestamp when you sent the request. Again, note that alignment is overridden here, and is actually 1 and not eg 32-bit aligned, at 4. However, this element and the element before are packed seperately to the following elements, and the following elements ARE C++ stndard 32-byte aligned from offset 5

5

char[32]

a string containing the server name

37

char[32]

a string containing the game type name

69

byte

current number of players

70

byte

maximum number of players

71

byte

unused

72

char[64]

name of map

136

char[20]

host address in dotted ip string format for easy printing

156

byte

unused padding

157

uint

host port - should match what the master server says or something is fishy!

161

uint

flags

165

uint

Internal host address - often a LAN address

169

char[16]

version number suitable for displaying (like '1.8a')

185

uint

unused

189

uint

unused

193

uint

unused

197

uint

unused

201

byte

unused

202

byte[20]

Reserved

222

byte[3]

Number of characters currently playing in each faction

225

byte

Skill level of the server

226

byte

unused

227

byte

unused

228

byte

unused

The entire structure is 229 bytes long - 224 bytes for the actual payload and 5 for the header.

Notes:

Faction 0 is the IMF, 1 is syndicate, 2 is republic.

"Flavors" of servers are whats in the servers hostoptions.cfg file and are as follows:

"Flags" of servers is a bitmask of the following bits. Note that they are bit masked.

// Here's the C++ compliant structure of the server response, for the lazy:
// the following two bytes are sent first, and specially have a strict packing (no padding for alignment:)
// char specialbyte; // always 27 (decimal, not hex)
// unsigned int timeStamp; // whatever you sent, it echoes back.
// they aren't really part of the below structure, the network subsystem actually prepends them to the payload.
// directly after that (starting at byte number 5), the following structure is packed, with regular 32-bit C++ alignment relative to the start of it (byte 5).
// by sheer coincidence, and not planning, the members line up anyway.
struct _s_ServerInfo
{
        char m_szServerName[32];
        char m_szGameTypeName[32];
        unsigned char m_ucCurrentPlayers;
        unsigned char m_ucMaxPlayers;
        unsigned char reserved0;
        char m_szMap[64];
        char m_szHostAddress[20];
         // note that there's one padding byte here for whatever reason.
        unsigned int m_uiHostPort;
        unsigned int m_Flags;
        unsigned long m_ulHostAddr_INET; // MUST be filled at all times.
        char m_szVersion[16]; // packed version #
        unsigned int reserved1;
        unsigned int reserved2;
        unsigned int reserved3;
        unsigned int reserved4;
        unsigned char reserved5;
        unsigned char reserved6[20];
        unsigned char m_ucCharactersPerFaction[3];
        unsigned char m_ucSkillFlavor;
};

As a bonus, here's a python script which gets the list, deciphers it, pings the servers, saves the results to XML, writes the results to a file, and optionally uploads the file to an FTP:

import socket
import sys
import struct
import time
import xml.dom.minidom
import ftplib

HOST = 'hbms.datavertex.com'
PORT = 20203

UPLOAD_TO_FTP = False
FTP_UPLOAD_HOST = r'ftp.hostname.com'
FTP_UPLOAD_PORT = 21
FTP_UPLOAD_USER = r'ftpusername'
FTP_UPLOAD_PASS = r'ftppassword'
FTP_UPLOAD_PATH = "/"
FTP_UPLOAD_FILE = "servers.xml"

def DoTheEntireThing():
  
  # create the socket
  try:
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  except socket.error, msg:
    sys.stderr.write("Error: unable to create the socket : %s\n" % msg[1])
    return 1
  
  sock.settimeout(5.0)
  
  # connect to the master server
  try:
    sock.connect((HOST, PORT))
  except socket.error, msg:
    sys.stderr.write("Error: Unable to connect to the server: %s\n" % msg[1])
    return 2
  
  # receive the initial packet
  try:
    data = sock.recv(12)
  except socket.error, msg:
    sys.stderr.write("Error reading from socket: %s\n" % msg[1])
    return 3
  
  #make sure bytes 0-4 match up with expected
  if (data[0:4] != "HBSL"):
    sys.stderr.write("Unexpected token from host.")
    return 4
  
  # Decipher bytes 4 to 12 as two little-endian ints. 
  # according to the python docs, the '<' means little endian, I means a 32-bit (four byte) unsigned integer
  # unpack returns an array just like PHP does, but python lets you save members of an array
  # into variables immediately
  # I Could have easily done something like 
  # somearray = unpack(blah blah)
  # followed by 
  # magic_number = somearray[0] 
  # players_playing = somearray[1] 
  # python is better and cleaner than that...
  # data[4:12] just means use the 4th to the 12th bytes.  (Ie, skipping the "HBSL") 
  magic_number, players_playing = struct.unpack("<II",data[4:12])
  
  sys.stdout.write("Magic Number: %i\n" % magic_number)
  sys.stdout.write("Players Playing: %i\n" % players_playing)
  
  # ask the server for the full list of servers
  # we're sending the magic number as a little endian 4-byte int
  # followed by 4 characters: 255 0 0 0 as BYTES.
  
  responsepack = struct.pack("<IBBBB",magic_number,255,0,0,0)
  
  try:
    sock.send(responsepack)
  except socket.error, msg:
    sys.stderr.write("Error, unable to get server list from host: %s\n" % msg[1])
    return 5
  
  # this is incredibly inefficient way of concatenating a string but whatever
  serverlist = ""
  
  while 1:
    data = sock.recv(256) # recevie up to 256 bytes at a time
    if not data: break # sock.recv will return an empty string when the server closes the connection
    serverlist += data # and append it to our string we're growing.
   
  if (len(serverlist)==0):
    sys.stderr.write("Error, 0 bytes received from server list!  No servers up?")
    return 6
  
  sys.stdout.write("Recieved %i bytes from server\n" % len(serverlist))
  
  # parse this.  each server takes 12 bytes, so chomp it up until less than 12 remains
  # note that I'm performing a silly trick here, by interpretting the four-byte integer
  # inet-address as four seperate bytes so I can print it out nicely.  If I was going
  # to actually connect I'd probably keep it as the inet-addr and read it as <IIB instead 
  curpos = 0 # start at beginning
  listlen = len(serverlist)
  
  #lets start an XML doc.
  doc = xml.dom.minidom.Document()
  head_element = doc.createElement("serverlist")
  head_element.setAttribute("localtime",str(time.localtime()))
  head_element.setAttribute("gmtime",str(time.gmtime()))
  doc.appendChild(head_element)
  
  while (curpos + 12 <= listlen): #while theres more than or exactly 12 bytes left
    i1,i2,i3,i4, port, flavor = struct.unpack("<BBBBIB",serverlist[curpos:curpos+9])
    sockaddr_as_string = ("%i.%i.%i.%i" % (i1,i2,i3,i4))
    curpos = curpos + 12 # advance the reading by the size of the structure
    #as you parse each server, we may as well ping it right here and now.
    # create a UDP socket with that ip and port
    pingsock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    # send the specially crafted "ping packet"
    # lets use time clock
    starttime = time.clock()
    packetrequester = struct.pack("<BI",2,(int)(time.clock() * 1000))
    sys.stdout.write("PINGING Server ip %s port %i flavor %i\n" % (sockaddr_as_string,port,flavor))
    pingsock.sendto(packetrequester,(sockaddr_as_string,port))
    pingsock.settimeout(3)
    server = doc.createElement("server")
    server.setAttribute("ip",sockaddr_as_string)
    server.setAttribute("port",str(port))
    # receive the ping response.
    try:
      pingresponse, addressfrom = pingsock.recvfrom(229)
    except socket.error, msg:
      sys.stderr.write("Server timed out...\n")
      server.setAttribute("responding","0")
      head_element.appendChild(server)
      continue
    finally:
      pingsock.close()
      
    endtime = time.clock()
    # how long did that take?
    ping_milliseconds = int((endtime - starttime) * 1000.0)
    
    try:
      # parse it using the layout described int the wiki.  Mind the padding!
      firstpiece = pingresponse[0:5]
      secondpiece = pingresponse[5:]
      bigfatparse = struct.unpack("<32s32sBBB64s20sxIIBBBB16sIIIIB20s3BBxxx",secondpiece)
    except struct.error, msg:
      sys.stderr.write("Unable to parse server reply\n")
      server.setAttribute("responding","0")
      head_element.appendChild(server)
      continue
    
    # now save various fields to the XML
    sys.stdout.write("-------------------------------------\n")
    sys.stdout.write("Server name: %s\n" % (bigfatparse[0]))
    sys.stdout.write("Server game type: %s\n" % (bigfatparse[1]))
    sys.stdout.write("Players: %i/%i\n" % (bigfatparse[2],bigfatparse[3]))
    sys.stdout.write("Map name: %s\n" % (bigfatparse[5]))
    sys.stdout.write("Version: %s\n" % (bigfatparse[13]))
    sys.stdout.write("Skill Level: %i\n" % (bigfatparse[23]))
    sys.stdout.write("Flavor: %i\n"% flavor)
    sys.stdout.write("Ping(ms): %ims\n" % ping_milliseconds)

   
    server.setAttribute("responding","1")
    server.setAttribute("servername",bigfatparse[0].replace('\0',""))
    server.setAttribute("gametype",bigfatparse[1].replace('\0',""))
    server.setAttribute("playercount",str(bigfatparse[2]))
    server.setAttribute("maxplayercount",str(bigfatparse[3]))
    server.setAttribute("mapname",bigfatparse[5].replace('\0',""))
    server.setAttribute("version",bigfatparse[13].replace('\0',""))
    server.setAttribute("skill",str(bigfatparse[23]))
    server.setAttribute("flavor",str(flavor))
    server.setAttribute("ping_milliseconds",str(ping_milliseconds))
    
    head_element.appendChild(server)
    
  sys.stdout.write("-------------------------------------\n")
  
  try:
    xmlfile = file(FTP_UPLOAD_FILE,"wb")
    try:
      doc.writexml(writer=xmlfile,encoding="UTF-8")
    finally:
      xmlfile.close()
  except IOError:
    sys.stdout.write("Unable to open the server list file to save!\n")
    return 7
    
  # save this xml file to a ftp site
  
  if (UPLOAD_TO_FTP is True):
    try:
      readfile = file(FTP_UPLOAD_FILE,"rb")
      try:
        ftpconnection = ftplib.FTP()
        ftpconnection.set_debuglevel(1)
        ftpconnection.connect(FTP_UPLOAD_HOST,FTP_UPLOAD_PORT)
        try:
          ftpconnection.login(FTP_UPLOAD_USER,FTP_UPLOAD_PASS)
          ftpconnection.cwd(FTP_UPLOAD_PATH)
          ftpconnection.storbinary("STOR " + FTP_UPLOAD_FILE,readfile)
        finally:
          ftpconnection.quit()
      except ftplib.all_errors, e:
        sys.stdout.write(str(e))
      finally:
        readfile.close()
    except IOError:
      sys.stdout.write("Unable to read or open the file or something...\n")
      return 8
  
  return 0 # exit with success

try:
  while 1:
    DoTheEntireThing()
    sys.stdout.write("Waiting for 5 minutes before going again... ctrl+C to quit\n")
    time.sleep(60*5)
except KeyboardInterrupt:
  sys.stdout.write("Bye now!")

Master Server Protocol (last edited 2009-10-31 21:35:34 by VekTuz)