Launcher protocol: Difference between revisions

From Zandronum Wiki
(Clarify the response may have multiple packets)
Tag: Source edit
(Remove the data example for now)
Tag: Source edit
 
(3 intermediate revisions by the same user not shown)
Line 206: Line 206:
|}
|}


The server will respond with one or more packets (depending on if a segmented response was requested and supported by the server), each starting with a Long, which will be one of the following:
The server will respond with one or more datagrams (depending on if a segmented response was requested and supported by the server), each starting with a Long, which will be one of the following:


{| class="wikitable"
{| class="wikitable"
Line 229: Line 229:


==== Segmented protocol response ====
==== Segmented protocol response ====
If you asked for a segmented response, and the server supports it, you'll get:
If you asked for a segmented response, and the server supports it, you'll get in each datagram:


{| class="wikitable"
{| class="wikitable"
! Type||Value||Description
! Type||Value||Description
|-
|-
|Byte  || Segment  || Segment number. If the most significant bit is set, then this is the last segment.
|Byte  || Segment  || Segment number.
|-
|-
|Byte  || NumSegments    || Total number of segments that will be sent.
|Byte  || NumSegments    || Total number of segments that will be sent.
|-
|-
|Short  || Size     || The size of this.
|Short  || Offset     || Offset into the full packet this datagram contains.
|-
|-
|Short  || Offset     || Offset into the full packet this packet contains.
|Short  || Size     || The size of the segment, not including this header. (The size of the datagram is this value plus 12 bytes.)
|-
|-
|Short  || TotalSize    || Total size of the data to be sent.
|Short  || TotalSize    || Total size of the assembled packet.
|}
|}


The segmented responses contain a full launcher packet split up into multiple segments. To use them, create a buffer of TotalSize bytes, then copy the data from each segment at the given offset.
Following this header, the segmented responses contain a full launcher packet split up into multiple segments. To use them, create a buffer of TotalSize bytes, then copy the data from each segment at the given offset.


=== Packet contents ===
=== Packet contents ===
Line 384: Line 384:
| <tt>SQF_NUMPLAYERS</tt>
| <tt>SQF_NUMPLAYERS</tt>
| The number of players in the server
| The number of players in the server
|-
| Byte
| <tt>SQF_PLAYERDATA</tt>
| '''Segmented only:''' Whether the team field for each player is sent. Unlike all other fields for <tt>SQF_PLAYERDATA</tt>, this is only sent once, and '''not''' for each player.
|-
|-
| String
| String
Line 472: Line 468:
| <tt>SQF_DEH</tt>
| <tt>SQF_DEH</tt>
| Deh patch name (one string for each deh patch)
| Deh patch name (one string for each deh patch)
|-
|-  
| Byte
| <tt>SQF_EXTENDED_INFO</tt>
| '''Segmented protocol:''' The next flag set you will receive.
|-
| Long
| Long
| <tt>SQF_EXTENDED_INFO</tt>
| <tt>SQF_EXTENDED_INFO</tt>
| '''Segmented protocol:''' The flags specifying the information from the next flag set that immediately follows.
| The flags specifying the <tt>SQF2_</tt>data that immediately follows.
'''Old protocol:''' The flags specifying the <tt>SQF2_</tt>data that immediately follows.
|}
|}


Line 546: Line 537:


== Examples ==
== Examples ==
=== Basic segmented example ===
In this example we query a simple co-operative server for some basic information.
'''Request'''
{| class="wikitable"
! Type || Value || Description
|-
| Long || 199 || The launcher challenge.
|-
| Long || 1234 || Ping value.
|-
| Long || 36700161 || The flags we want: <tt><nowiki>SQF_NAME|SQF_PLAYERDATA|SQF_TEAMINFO_NUMBER|SQF_TESTING_SERVER</nowiki></tt>
|-
| Long || 6 || The extended flags we want: <tt><nowiki>SQF2_COUNTRY|SQF2_GAMEMODE_NAME</nowiki></tt>
|-
| Byte || 2 || Indicates we want a segmented response, if the server supports it.
|}
'''Response'''
{| class="wikitable"
! Type || Value || Description
|-
| Long || 5660032 || The server's response, indicating we're getting a segmented response packet.
|-
| Byte || 0 || The segment number.
|-
| Byte || 1 || Total number of segments.
|-
| Short || 0 || Offset of the packet.
|-
| Short || 747 || Size of this packet
|-
| Short || 747 || Total amount of data being sent.
|-
| colspan = 3 | Packet data follows after this point.
|-
| Long || 1234 || The ping value we sent to the server.
|-
| String || 3.2-alpha-r240718-2351 on Linux 6.9.9-1-default || The server's version.
|-
| Long || 35127297 || The fields contained in the response. <tt><nowiki>SQF_NAME|SQF_NUMPLAYERS|SQF_PLAYERDATA|SQF_TESTING_SERVER|SQF_EXTENDED_INFO</nowiki></tt>. Observe how:
* <tt>SQF_NUMPLAYERS</tt> was added by the server as we requested <tt>SQF_PLAYERDATA</tt>.
* <tt>SQF_EXTENDED_INFO</tt> was added indicating we'll get data from another flag set later in this segment.
* <tt>SQF_TEAMINFO_NUMBER</tt> was removed as this is a co-operative server and team information is irrelevant.
|-
| String || <tt>Skipper Pavilion</tt> || From <tt>SQF_NAME</tt>. The server's name.
|-
| Byte || 1 || From <tt>SQF_NUMPLAYERS</tt>. The number of players online.
|-
| Byte|| 0 || From <tt>SQF_PLAYERDATA</tt>. Whether the team field will be sent for players.
|-
| String || <tt>Alphus</tt> || From <tt>SQF_PLAYERDATA</tt>. The name of the first player in the server.
|-
| Short || 0 || From <tt>SQF_PLAYERDATA</tt>. Alphus' score.
|-
| Short || 0 || From <tt>SQF_PLAYERDATA</tt>. Alphus' ping.
|-
| Byte || 0 || From <tt>SQF_PLAYERDATA</tt>. Whether Alphus is a spectator or not.
|-
| Byte || 1 || From <tt>SQF_PLAYERDATA</tt>. Whether Alphus is a bot or not.
|-
| Byte || 5 || From <tt>SQF_PLAYERDATA</tt>. How many minutes Alphus has been in the server.
|-
| Byte || 1 || From <tt>SQF_TESTING_SERVER</tt>. Whether the server is running a testing binary.
|-
| String || <tt>downloads/testing/3.2/ZandroDev3.2-230430-1741windows.zip</tt> || From <tt>SQF_TESTING_SERVER</tt>. Path to the testing binary on zandronum.com.
|-
| Byte || 1 || From <tt>SQF_EXTENDED_INFO</tt>. The flag set that follows.
|-
| Long || 6 || From <tt>SQF_EXTENDED_INFO</tt>. The fields that follow from that set: <tt><nowiki>SQF2_COUNTRY|SQF2_GAMEMODE_NAME</nowiki></tt>
|-
| char[3] || <tt>GBR</tt> || From <tt>SQF2_COUNTRY</tt>. The country code the server presents, as set by sv_country.
|-
| String|| <tt>Cooperative</tt> || From <tt>SQF2_GAMEMODE_NAME</tt>. The name of the game mode currently being played.
|}


=== Example code ===
=== Example code ===

Latest revision as of 13:44, 27 July 2024

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.
SQF2_VOICECHAT 0x00000010 (development version 3.2-alpha and above only) The server's voice chat setting.

Challenge packet

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 2 to indicate that you want a segmented response.

The server will respond with one or more datagrams (depending on if a segmented response was requested and supported by the server), each starting with a Long, which will be one of the following:

Symbol Value Description
SERVER_LAUNCHER_CHALLENGE 5660023 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_CHALLENGE_SEGMENTED 5660032 Accepted; segmented information follows

Segmented protocol response

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

Type Value Description
Byte Segment Segment number.
Byte NumSegments Total number of segments that will be sent.
Short Offset Offset into the full packet this datagram contains.
Short Size The size of the segment, not including this header. (The size of the datagram is this value plus 12 bytes.)
Short TotalSize Total size of the assembled packet.

Following this header, the segmented responses contain a full launcher packet split up into multiple segments. To use them, create a buffer of TotalSize bytes, then copy the data from each segment at the given offset.

Packet contents

The full packet contains the following header:

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

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
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 into 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)
Long SQF_EXTENDED_INFO The flags specifying the SQF2_data that immediately follows.

Extended flags

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.
Byte SQF2_VOICECHAT (development version 3.2-alpha and above only) The server's voice chat setting which is based on sv_allowvoicechat.

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

Example code

Validation of the given offsets and sizes has been omitted here.

let receivedSegments = 0;

function handlePacket(data) {
    const response = data.readLong();
    switch (response) {
        case SERVER_LAUNCHER_CHALLENGE: {
            parseResponse(data);

            break;
        }

        case SERVER_LAUNCHER_CHALLENGE_SEGMENTED: {
            const segmentNumber = data.readByte();
            const totalSegments = data.readByte();
            const offset = data.readShort();
            const size = data.readShort();
            const totalSize = data.readShort();

            if (buffer == null) {
                buffer = newBuffer(totalSize);
            }

            copyBuffer(from: data, to: buffer, toOffset: offset);
            receivedSegments++;

            if (receivedSegments == totalSegments) {
                parseResponse(buffer);
            }

            break;
        }

        case SERVER_LAUNCHER_BANNED: {
            // handle error

            break;
        }

        case SERVER_LAUNCHER_IGNORING: {
            // handle error

            break;
        }
    }

}

function parseResponse(data) { 
    const time = data.readLong();
    const version = data.readString();
    const flags = data.readLong();

    if (flags & SQF_NAME) {
        const name = data.readString();
    }

    // ...
}