The information in this document is mostly sourced from the GPLv2 QuakeWorld source code. I'm not sure if that means this document counts as being a derivative of a GPL-ed work, so take caution if you plan to write an implementation of this protocol. This document does not contain any of the original source code. I am not a lawyer. To anyone who knows more on the subject, please get in contact with me.
Quake is a trademark of ZeniMax Media, Inc.
Type Names
[U8]: An unsigned 8-bit integer.
[I16]: A two's-compliment-signed 16-bit integer in little-endian byte order.
[I32]: A two's-compliment-signed 32-bit integer in little-endian byte order.
[F32]: A IEE 754-2008 single-precision floating point number in little-endian byte order.
[StrLine]: An 8-bit ASCII string terminated with $0A $00.
[StrNul]: An 8-bit ASCII string terminated with $00.
[Pos]: A two's-compliment-signed, 13.3 fixed point, 16-bit integer in little-endian byte order.
[Angle8]: An unsigned 8-bit integer. It encodes 1/256th of one full rotation.
[Angle16]: An unsigned 16-bit integer in little-endian byte order. It encodes 1/65536th of one full rotation.
[Scale16]: A two's-compliment-signed 16-bit integer in little-endian byte order. It the fractions of 1/32768 between -1.0 and 1.0.
[Control]: A variably-sized structure. See below.
[Control] Structure
The first byte is a [U8] bitfield describing which portions of the movement update have been sent.
Bit 0: Rotation on X
Bit 1: Rotation on Z
Bit 2: Forward/Backward Movement Axis
Bit 3: Left/Right Movement Axis
Bit 4: Up/Down Movement Axis
Bit 5: 8 binary button states (only two of which are used by vanilla)
Bit 6: 'Impulse' code, 255 one-time events that can be bound to keys or pad buttons
Bit 7: Rotation on Y
It is then followed by output of this procedure (which isn't in the same order as the bits above):
If bit 0 is set, an [Angle16] for the player's rotation X axis is sent.
If bit 7 is set, an [Angle16] for the player's rotation Y axis is sent.
If bit 1 is set, an [Angle16] for the player's rotation Z axis is sent.
If bit 2 is set, a [Scale16] for the player's forward/backward movement axis is sent.
If bit 3 is set, a [Scale16] for the player's left/right movement axis is sent.
If bit 4 is set, a [Scale16] for the player's up/down movement axis is sent. (Normally only applies when the player is swimming in a fluid)
If bit 5 is set, a [U8] containing a bitmask of the player's pressed buttons is sent. Bit 0 is for the attack button, and bit 1 for the jump button. The other bits are unused by the vanilla implementations.
If bit 6 is set, a [U8] containing the player's current 'impulse' code is sent.
Checksums
QW28 uses a CRC16 checksum in many places using the CRC-16-CCITT prime 4129.
Transport
QW28 uses UDP/IP, on port 27500 by default.
'Out-of-band' Packets
Out-of-band packets begin with the [I32] value -1, immediately followed by data.
Regular Packets
Packets begin with a header of two [I32] values. Both use the first 31 bits as a monotonic packet counter used to detect dropped packets, and the 32nd bit is used for marking attempted reliable transport. Clients will also append an [I16] receiving port number as part of this header when sending these.
After this header is a single opcode [U8], then the packet data.
Connection Handshake
The client sends an out-of-band packet containing the [StrLine] 'getchallenge'.
The server responds with an out-of-band packet containing [U8] $63, followed by [U8] $20, then a [StrLine] containing implementation-specific integer value.
The client sends an out-of-band packet containing [U8] $1C, followed by a [StrLine] containing 'connect 28 PORT CHALLENGE "USERINFO"'. 'PORT' being the port the client is connecting with, 'CHALLENGE' being the value received from the server. The content of the 'USERINFO' part is described later in the document. The ASCII double-quotes are part of the string that is sent; They are presumably there to prevent any spaces inside the info string from being interpreted as a separate command argument internally in the vanilla engine.
If the server allows the client to join, it responds with an out-of-band packet containing [U8] $6A.
If the server rejects the client, it responds with an out-of-band packet containing [U8] $6E, followed by the reason for rejection as a [Str].
Client-to-Server Packet Types
Packet ID numbers not listed here are invalid.
1: No-op
There is no data. Nothing happens.
3: Player Movement [Checksum?: U8, Drop Percent: U8, Millisec?: U8, Data: Control]
4: Player Text [Text: StrLine]
This packet is used for communicating console commands and chat messages to the server.
The vanilla client sends three reliable player text packets containing [StrNul] "drop" before dropping its connection.
5: Delta Update Request? [Unknown: U8]
Not sure yet.
6: Arbitrary Camera Position? [X: Pos, Y: Pos, Z: Pos]
This packet contains three Pos values. It seems to be used in spectator mode by the client to request viewing a certain position?
7: Data Upload [Size: I16, Percent: U8, Data: U8…]
This seems to have been used a long, long time ago for uploading .pcx format screenshots to a server at the end of a map.
Info String
This is a simple key-value pair format separated by ASCII $5C.
Each pair begins with a backslash, then the key string, another backslash, then the value. Keys and values are limited to 63 bytes long.
Pairs with keys starting with ASCII $2A are derived from certain values and cannot be set by the player.
Some implementations also include empty pairs in this string, so beware when parsing it.
'*ver' is the version of the client software. Some implementations also put their name here. The vanilla server doesn't check this.
'*ip' is the hostname the client is connecting to. The vanilla server doesn't check this.
'noaim' appears to have been a mechanism for opting out of serverside aim assist. It doesn't appear to actually do anything anymore.
'msg' is the player's preferred message priority. Text messages sent with a priority lower than this will be hidden from the player.
'rate' is how many packets per second the client is willing to send or receive.
'topcolor' and 'bottomcolor' are player-selectable color palettes used on their player model. These cannot be below 0 or above 13.
'team' is the name of the player's team.
'name' is the player's in-game nickname, visible on the leaderboard.
Some implementations also include others not listed here.
Server-to-Client Packet Types
Packet ID numbers not listed here are invalid. The apparent gaps in opcode numbers are obsolete remnants of older protocol revisions.
1: No-op
Nothing happens. There is no packet data.
2: Disconnect
Sent to the client to tell them they've been disconnected and should stop sending packets. There is no packet data.
3: Set Stat A [Stat ID: U8, Value: U8]
Updates the health, armor, and ammo values displayed on the client. The first [U8] is which stat to be updated, the second [U8] is the value to set it to.
30: End of Level Intermission [View Position X: Pos, View Position Y: Pos, View Position Z: Pos, View Angle X: Angle8, View Angle Y: Angle8, View Angle Z: Angle8]
Sets the client's view position and shows a leaderboard. The client can only leave this state when the map changes.
31: End of Episode Intermission [Text: StrNul]
Prevents player movement and slowly types out text to the center of the screen. The vanilla client also shows a 'Congratulations' header on screen. The client can only leave this state when the map changes.
32: Play Music Track [Track ID: U8]
Originally used to play tracks from a physical CD. Track 0 is not allowed.
33: Registration Info Screen
The vanilla client displays the first page of the help screen.
34: Small View Punch
35: Large View Punch
36: Set Ping [Client ID: U8, Ping: I16]
Sets a player's ping value in the scoreboard.
37: Set Join Time [Client ID: U8, Seconds: F32]
Sets how long a player has been connected.
38: Set Stat B [Stat ID: U8, Value: I32]
Similar to Set Stat A, but with a 32-bit value instead of an 8-bit one.
39: Muzzle Flash Light
Flashes a dynamic light centered on the player's position for 0.1 seconds on the vanilla client.
40: User Info Update [Client ID: U8, 'User ID'?: I32, Info: StrNul]
41: Download [Size: I16, Data: U8…]
42: Player Info [Client ID: U8, Bitflags: I16, X: Pos, Y: Pos, Z: Pos, Animation Frame: U8, …]
If bit 0 is set, a [U8] is sent containing the milliseconds since the last movement.
If bit 1 is set, a [Control] structure is sent.
If bit 2 is set, a [I16] for the velocity X component is sent.
If bit 3 is set, a [I16] for the velocity Y component is sent.
If bit 4 is set, a [I16] for the velocity Z component is sent.
If bit 5 is set, a [U8] for a custom model ID is sent.
If bit 6 is set, a [U8] for the model's skin ID is sent.
If bit 7 is set, a [U8] is sent for 'effects'?
If bit 8 is set, a [U8] is sent for the weapon view model animation frame.
If bit 9 is set, the player becomes intangible.
If bit 10 is set, the player's view is offset?
If bit 11 is set, the player has gravity disabled?
43: Spawn Projectile [Number of projectiles: U8, TODO]
This packet is a mess of bit ops.
44: 'Choke Count' [Number of 'choked' packets: U8]
45: Model ID List [Number of models: U8, TODO]
46: Sound ID List [Number of sounds: U8, TODO]
47: Object Update [TODO]
Another mess.
This is an example of why you should prefer statically-sized packets over variably-sized ones when designing net protocols.
48: Delta Object Update [TODO]
Another mess.
From what I've seen, pretty much the same as 47 Object Update but has a [U8] at the start.
49: Set Max Speed [Value: F32]
50: Set Gravity [Value: F32]
51: Set User Info Key [Client ID: U8, Key: StrNul, Value: StrNul]
52: Set Server Info Key [Key: StrNul, Value: StrNul]
53: Update Packet Loss Value [Client ID: U8, amt: U8]
I think this only changes the 'PL' value on the scoreboard.
Master Server Protocol
This took me way, way too long to figure out on my own. It's not documented anywhere and the only references to it are in a few clients' server browsers.
Step 1: Master Request
Send these bytes to a master server (usually on port 27000) and wait a bit for a response. If you don't get one within a second or two, something went wrong.
$63 $0A $00
Step 2: Master Response
$FF $FF $FF $FF $64 $0A
After this header, the packet contains tightly packed binary IPv4 address-port pairs. The first 4 bytes are the address, the last two are a big-endian port number. There's nothing that says how many you get, so you just have to read through them until you run out of packet data to read.
Step 3: Server Status
$FF $FF $FF $FF $73 $74 $61 $74 $75 $73 $0A $00
Send a packet containing these bytes to each of the addresses you got in the last step and you (should) get back '$FF $FF $FF $FF $6E' followed by a serverinfo string.