Launcher protocol

From Zandronum Wiki
Revision as of 13:36, 12 May 2023 by DrinkyBird (talk | contribs) (Remove broken python huffman links)

This launcher protocol lets you to talk to servers and get information from them, allowing you to make your own custom programs like browsers, stat tools, and so on.

Protocol information

See "article history" to look up the protocol for prior versions.

The basics

All Zandronum servers use UDP as their network protocol. Additionally, all traffic is compressed using the Huffman algorithm to save bandwidth. Refer to code in src/huffman for a standalone implementation of the Huffman encoding needed to encode/decode traffic.

Definition of data types used in this article:

  • Byte: 8 bit integer
  • Short: 16 bit integer
  • Long: 32 bit integer
  • Float: Long representation of a float
  • String: null-terminated series of Bytes

Traffic is encoded in little-endian.

Getting the list of servers

This is very easy, accomplished by sending a long and a short to the master server:

Type Value Description
Long 5660028 LAUNCHER_MASTER_CHALLENGE
Short 2 MASTER_SERVER_VERSION

If your request is denied, the server will respond with one of the following:

Type Value Description
Long 3 Denied; your IP is banned (MSC_IPISBANNED)
Long 4 Denied; your IP has made a request in the past 3 seconds (MSC_REQUESTIGNORED)
Long 5 Denied; you're using an older version of the master protocol (MSC_WRONGVERSION)

If you are accepted, you will receive multiple packets, each starting with:

Type Value Description
Long 6 Beginning of list (MSC_BEGINSERVERLISTPART)
Byte 0-255 Packet number (starting with 0)
Byte 8 Beginning of server block (MSC_SERVERBLOCK)

Then you will get this next block, which is repeated for every IP with servers on it.

Type Value Description
Byte 0-255 n = Number of servers with this IP address
Byte 0-255 IP address octet
Byte 0-255 IP address octet
Byte 0-255 IP address octet
Byte 0-255 IP address octet
Short 0-65535 IP port (sent 'n' times)

Once the packet is full or all servers are transferred, you will receive 0 as number of servers on the next IP.

Type Value Description
Byte 0 There are no ports for the next server, i.e. no more servers in this packet.

Finally, the response should end with either

Type Value Description
Byte 2 End of list, you got the full server list now (MSC_ENDSERVERLIST)

or

Type Value Description
Byte 7 End of current part of the list, you'll get more packets (MSC_ENDSERVERLISTPART)

Querying individual servers

You need to choose what data you'd like. To do so, bitshift the combination of flags with a boolean OR. For example, to get the server's name and player count, you'd use (SQF_NAME|SQF_NUMPLAYERS). Use these flags appropriately; doing so saves your users' and server hosters' bandwidth.

Query flags

Name Value Description
SQF_NAME 0x00000001 The name of the server
SQF_URL 0x00000002 The associated website
SQF_EMAIL 0x00000004 Contact address
SQF_MAPNAME 0x00000008 Current map being played
SQF_MAXCLIENTS 0x00000010 Maximum amount of clients who can connect to the server
SQF_MAXPLAYERS 0x00000020 Maximum amount of players who can join the game (the rest must spectate)
SQF_PWADS 0x00000040 PWADs loaded by the server
SQF_GAMETYPE 0x00000080 Game type code
SQF_GAMENAME 0x00000100 Game mode name
SQF_IWAD 0x00000200 The IWAD used by the server
SQF_FORCEPASSWORD 0x00000400 Whether or not the server enforces a password
SQF_FORCEJOINPASSWORD 0x00000800 Whether or not the server enforces a join password
SQF_GAMESKILL 0x00001000 The skill level on the server
SQF_BOTSKILL 0x00002000 The skill level of any bots on the server
SQF_DMFLAGS 0x00004000 (Deprecated) The values of dmflags, dmflags2 and compatflags. Use SQF_ALL_DMFLAGS instead.
SQF_LIMITS 0x00010000 Timelimit, fraglimit, etc.
SQF_TEAMDAMAGE 0x00020000 Team damage factor.
SQF_TEAMSCORES 0x00040000 (Deprecated) The scores of the red and blue teams. Use SQF_TEAMINFO_* instead.
SQF_NUMPLAYERS 0x00080000 Amount of players currently on the server.
SQF_PLAYERDATA 0x00100000 Information of each player in the server.
SQF_TEAMINFO_NUMBER 0x00200000 Amount of teams available.
SQF_TEAMINFO_NAME 0x00400000 Names of teams.
SQF_TEAMINFO_COLOR 0x00800000 RGB colors of teams.
SQF_TEAMINFO_SCORE 0x01000000 Scores of teams.
SQF_TESTING_SERVER 0x02000000 Whether or not the server is a testing server, also the name of the testing binary.
SQF_DATA_MD5SUM 0x04000000 (Deprecated) Used to retrieve the MD5 checksum of skulltag_data.pk3, now obsolete and returns an empty string instead.
SQF_ALL_DMFLAGS 0x08000000 Values of various dmflags used by the server.
SQF_SECURITY_SETTINGS 0x10000000 Security setting values (for now only whether the server enforces the master banlist)
SQF_OPTIONAL_WADS 0x20000000 Which PWADs are optional
SQF_DEH 0x40000000 List of DEHACKED patches loaded by the server.
SQF_EXTENDED_INFO 0x80000000 Additional server information, see the table below for more information.

Extended flags

Name Value Description
SQF2_PWAD_HASHES 0x00000001 The MD5 hashes of the server's loaded PWADs.
SQF2_COUNTRY 0x00000002 The server's ISO 3166-1 alpha-3 country code.
SQF2_GAMEMODE_NAME 0x00000004 (development version 3.2-alpha and above only) The name of the server's current game mode.
SQF2_GAMEMODE_SHORTNAME 0x00000008 (development version 3.2-alpha and above only) The short name of the server's current game mode.

Segmented challenge

(development version 3.2-alpha and above only)

This challenge is only available on servers running Zandronum 3.2 or newer. For backwards compatibility, the old #Old protocol is still supported and can also request a segmented response, but is now deprecated. It is recommended that launchers prefer this new challenge when 3.2 reaches widespread adoption and fall back to the old challenge.

Type Value Description
Long 200 Launcher challenge
Long Flags Desired information
Long Time Current time, this will be sent back to you so you can determine ping.
Long Flags2 The extended information you want. Ensure you specify SQF_EXTENDED_INFO in the normal Flags field as well.

The server will respond with a Long, which will be one of the following:

Symbol Value Description
SERVER_LAUNCHER_IGNORING 5660024 Denied; your IP has made a request in the past sv_queryignoretime seconds
SERVER_LAUNCHER_BANNED 5660025 Denied; your IP is banned
SERVER_LAUNCHER_SEGMENTED_CHALLENGE 5660031 Accepted; segmented information follows

If you were denied, this response is followed by another Long, which is the time you sent to the server.

Old protocol challenge

Send the server the following packet:

Type Value Description
Long 199 Launcher challenge
Long Flags Desired information
Long Time Current time, this will be sent back to you so you can determine ping.
Long Flags2 Optional. The extended information you want. Ensure you specify SQF_EXTENDED_INFO in the normal Flags field as well.
Byte Segmented Optional. Set this to 1 to indicate that you want a segmented response. This exists for backwards compatibility with older servers.

The server will respond with a Long, which will be one of the following:

Symbol Value Description
SERVER_LAUNCHER_CHALLENGE 5660031 Accepted; server information follows
SERVER_LAUNCHER_IGNORING 5660024 Denied; your IP has made a request in the past sv_queryignoretime seconds
SERVER_LAUNCHER_BANNED 5660025 Denied; your IP is banned
SERVER_LAUNCHER_SEGMENTED_CHALLENGE 5660031 Accepted; segmented information follows

Segmented protocol response

If you asked for a segmented response, and the server supports it, you'll get:

Type Value Description
Byte Segment Segment number. If the most significant bit is set, then this is the last segment.
Short Size The uncompressed size of this packet.

After this, segment 0 also includes:

Type Value Description
Long Time The time you sent to the server.
String Version Server version string with revision number.

Each field set returned then contains the following information before the actual data.

Type Value Description
Byte Fieldset The field set this packet contains data for (0 -> SQF_, 1 -> SQF2_)
Long Flags The fields contained in this packet.

Single packet response

If you didn't ask for a segmented response, or the server doesn't support segmented packets, you'll get:

Type Value Description
Long Time The time you sent to the server.
String Version Server version string with revision number.
Long Flags The info you'll be receiving, possibly corrected (see below).

Field data

Field data is sent in the order the fields appear in the table below. The server will automatically correct your request if you made a mistake (like request team scores in cooperative games), so always use the flags it sends back to you when reading your data.

Flag set 0 (SQF_)

Type Flag Description
String SQF_NAME The server's name (sv_hostname)
String SQF_URL The server's WAD URL (sv_website)
String SQF_EMAIL The server host's e-mail (sv_hostemail)
String SQF_MAPNAME The current map's name
Byte SQF_MAXCLIENTS The max number of clients (sv_maxclients)
Byte SQF_MAXPLAYERS The max number of players (sv_maxplayers)
Byte SQF_PWADS The number of PWADs loaded
String SQF_PWADS The PWAD's name (Sent for each PWAD)
Byte SQF_GAMETYPE The current game mode. See below.
Byte SQF_GAMETYPE Instagib - true (1) / false (0)
Byte SQF_GAMETYPE Buckshot - true (1) / false (0)
String SQF_GAMENAME The base game's name ("DOOM", "DOOM II", "HERETIC", "HEXEN", "ERROR!")
String SQF_IWAD The IWAD's name
Byte SQF_FORCEPASSWORD Whether a password is required to join the server (sv_forcepassword)
Byte SQF_FORCEJOINPASSWORD Whether a password is required to join the game (sv_forcejoinpassword)
Byte SQF_GAMESKILL The game's difficulty (skill)
Byte SQF_BOTSKILL The bot difficulty (botskill)
Long SQF_DMFLAGS [Deprecated] Value of dmflags
Long SQF_DMFLAGS [Deprecated] Value of dmflags2
Long SQF_DMFLAGS [Deprecated] Value of compatflags
Short SQF_LIMITS Value of fraglimit
Short SQF_LIMITS Value of timelimit
Short SQF_LIMITS time left in minutes (only sent if timelimit > 0)
Short SQF_LIMITS duellimit
Short SQF_LIMITS pointlimit
Short SQF_LIMITS winlimit
Float SQF_TEAMDAMAGE The team damage scalar (teamdamage)
Short SQF_TEAMSCORES [Deprecated] Blue team's fragcount/wincount/score
Short SQF_TEAMSCORES [Deprecated] Red team's fragcount/wincount/score
Byte SQF_NUMPLAYERS The number of players in the server
Byte SQF_PLAYERDATA Segmented only: Whether the team field for each player is sent. Unlike all other fields for SQF_PLAYERDATA, this is only sent once, and not for each player.
String SQF_PLAYERDATA Player's name
Short SQF_PLAYERDATA Player's pointcount/fragcount/killcount
Short SQF_PLAYERDATA Player's ping
Byte SQF_PLAYERDATA Player is spectating - true (1) / false (0)
Byte SQF_PLAYERDATA Player is a bot - true (1) / false (0)
Byte SQF_PLAYERDATA Player's team (returned on team games, 255 is no team)
Byte SQF_PLAYERDATA Player's time on the server, in minutes. Note: SQF_PLAYERDATA information is sent once for each player on the server.
Byte SQF_TEAMINFO_NUMBER The number of teams used.
String SQF_TEAMINFO_NAME The team's name. (Sent for each team.)
Long SQF_TEAMINFO_COLOR The team's color. (Sent for each team.)
Short SQF_TEAMINFO_SCORE The team's score. (Sent for each team.)
Byte SQF_TESTING_SERVER Whether this server is running a testing binary - true (1) / false (0)
String SQF_TESTING_SERVER An empty string in case the server is running a stable binary, otherwise name of the testing binary archive found in http://www.skulltag.com/testing/files/
String SQF_DATA_MD5SUM [Deprecated] Returns an empty string.
Byte SQF_ALL_DMFLAGS The number of flags that will be sent.
Long SQF_ALL_DMFLAGS The value of the flags (Sent for each flag in the order dmflags, dmflags2, zadmflags, compatflags, zacompatflags, compatflags2)
Byte SQF_SECURITY_SETTINGS Whether the server is enforcing the master ban list - true (1) / false (0) The other bits of this byte may be used to transfer other security related settings in the future.
Byte SQF_OPTIONAL_WADS Amount of optional wad indices that follow
Byte SQF_OPTIONAL_WADS Index number int the list sent with SQF_PWADS - this wad is optional (sent for each optional Wad)
Byte SQF_DEH Amount of deh patches loaded
String SQF_DEH Deh patch name (one string for each deh patch)
Byte SQF_EXTENDED_INFO Segmented protocol: The next flag set you will receive.
Long SQF_EXTENDED_INFO Segmented protocol: The flags specifying the information from the next flag set that immediately follows.

Old protocol: The flags specifying the SQF2_data that immediately follows.

Flag set 1 (SQF2_)

Type Flag Description
Byte SQF2_PWAD_HASHES The number of hashes sent.
String SQF2_PWAD_HASHES The hash of the PWAD, sent for each PWAD. The indices are the same as sent in SQF_PWADS
Byte[3] SQF2_COUNTRY The server's ISO 3166-1 alpha-3 country code. This is sent as a raw char array of 3 length, there is no null terminator. Zandronum also has two special values for this field, XIP and XUN. See #Country codes below for how to handle this field.
String SQF2_GAMEMODE_NAME (development version 3.2-alpha and above only) The name of the server's current game mode, as defined in the GAMEMODE lump.
String SQF2_GAMEMODE_SHORTNAME (development version 3.2-alpha and above only) The short name of the server's current game mode, as defined in the GAMEMODE lump.

Notes

Game modes

Game modes are defined as:

   0   GAMEMODE_COOPERATIVE
   1   GAMEMODE_SURVIVAL
   2   GAMEMODE_INVASION
   3   GAMEMODE_DEATHMATCH
   4   GAMEMODE_TEAMPLAY
   5   GAMEMODE_DUEL
   6   GAMEMODE_TERMINATOR
   7   GAMEMODE_LASTMANSTANDING
   8   GAMEMODE_TEAMLMS
   9   GAMEMODE_POSSESSION
   10  GAMEMODE_TEAMPOSSESSION
   11  GAMEMODE_TEAMGAME
   12  GAMEMODE_CTF
   13  GAMEMODE_ONEFLAGCTF
   14  GAMEMODE_SKULLTAG
   15  GAMEMODE_DOMINATION

Country codes

Zandronum 3.1 introduces the ability to server hosts to specify the country their server is located in via a new CVAR, sv_country. This was introduced to allow hosts to combat the inaccuracies of IP geolocation that launchers rely on. This value is specified to launchers via the new SQF2_COUNTRY field, which can have the following values:

  • an ISO 3166-1 alpha-3 country code. Note that Zandronum doesn't validate whether the country code supplied in sv_country is a registered country code. If the launcher does not recognise the given code, then it should display the server's country as unknown.
  • the value XIP, which suggests to the launcher that it should attempt IP geolocation to determine the server's country. The default value of sv_country will cause it to return this by default, to preserve existing behaviour and make it easier for casual hosts. If geolocation is unsupported, disabled, or fails, then the server's country should be displayed as unknown.
  • the value XUN, in which case the launcher should display the server's country as unknown.

Also see Issue 3894: Allow servers to present their country to launchers.

Examples

Basic segmented example

In this example we query a simple co-operative server for some basic information.

Request

Type Value Description
Long 200 The launcher challenge.
Long 1234 Ping value.
Long 36700161 The fields from set 0 we want: SQF_NAME|SQF_PLAYERDATA|SQF_TEAMINFO_NUMBER|SQF_TESTING_SERVER
Long 6 The fields from set 1 we want: SQF2_COUNTRY|SQF2_GAMEMODE_NAME

Response

Type Value Description
Long 5660031 The server's response, indicating we're getting a segmented response packet.
Byte 177 The segment number. The most significant bit is set, as this is the final and only packet. Clear the MSB to reveal this is packet 0: 128 & ~0x80 = 0.
Short 143 Size of this packet in bytes.
Long 1234 The ping value we sent before, sent only in segment 0.
String 3.2-alpha-r230430-1741 on Linux 5.15.0-69-generic The server version, sent only in segment 0.
Byte 0 The initial flag set being sent.
Long 35127297 The initial flags from that set being sent: SQF_NAME|SQF_NUMPLAYERS|SQF_PLAYERDATA|SQF_TESTING_SERVER|SQF_EXTENDED_INFO. Observe how:
  • SQF_NUMPLAYERS was added by the server as we requested SQF_PLAYERDATA.
  • SQF_EXTENDED_INFO was added indicating we'll get data from another flag set later in this segment.
  • SQF_TEAMINFO_NUMBER was removed as this is a co-operative server and team information is irrelevant.
String Skipper Pavilion From SQF_NAME. The server's name.
Byte 1 From SQF_NUMPLAYERS. The number of players online.
Byte 0 From SQF_PLAYERDATA. Whether the team field will be sent for players.
String Alphus From SQF_PLAYERDATA. The name of the first player in the server.
Short 0 From SQF_PLAYERDATA. Alphus' score.
Short 0 From SQF_PLAYERDATA. Alphus' ping.
Byte 0 From SQF_PLAYERDATA. Whether Alphus is a spectator or not.
Byte 1 From SQF_PLAYERDATA. Whether Alphus is a bot or not.
Byte 5 From SQF_PLAYERDATA. How many minutes Alphus has been in the server.
Byte 1 From SQF_TESTING_SERVER. Whether the server is running a testing binary.
String downloads/testing/3.2/ZandroDev3.2-230430-1741windows.zip From SQF_TESTING_SERVER. Path to the testing binary on zandronum.com.
Byte 1 From SQF_EXTENDED_INFO. The flag set that follows.
Long 6 From SQF_EXTENDED_INFO. The fields that follow from that set: SQF2_COUNTRY|SQF2_GAMEMODE_NAME
char[3] GBR From SQF2_COUNTRY. The country code the server presents, as set by sv_country.
String Cooperative From SQF2_GAMEMODE_NAME. The name of the game mode currently being played.

Example code

This uses a loop to handle switching flag sets in the middle of a segment.

function handleFields() {
    while (readOffset < packetLength) {
        const fieldsetNum = readByte();
        const flags = readLong();
        
        if (fieldsetNum == 0) {
            if (flags & SQF_NAME) {
                const name = readString();
            }
            
            if (flags & SQF_URL) {
                const url = readString();
            }
            
            // ...
        }
        
        else if (fieldsetNum == 1) {
            if (flags & SQF2_PWAD_HASHES) {
                const numHashes = readByte();
                for (let i = 0; i < numHashes; i++) {
                    const hash = readString();
                }
            }
            
            // ...
        }
        
        if (!(flags & SQF_EXTENDED_INFO)) {
            break;
        }
    }
}

A variation that can be used with the old single-packet protocol:

function handleFields() {
    let fieldsetNum = 0;
    let flags = 0;
    
    while (readOffset < packetLength) {
        fieldsetNum = segmented ? readByte() : fieldsetNum;
        flags = readLong();
        
        if (fieldsetNum == 0) {
            // ...
        }
        
        else if (fieldsetNum == 1) {
            // ...
        }
        
        if (flags & SQF_EXTENDED_INFO) {
            fieldsetNum++;
        } else {
            break;
        }
    }
}