BitProtocol
Overview
The game client communicates with the server over a persistent TCP connection using a custom binary protocol. All data is serialized with the BitProtocol codec - a bit-level encoding scheme that compresses integers by encoding only the bytes that are actually needed.
The implementation lives in internal/network/
Wire Format
Every message follows this structure:
┌─────────────────────────────────────────────────────────┐
│ length prefix │ payload bytes │ 0x00 │
│ (varint) │ (BitStream encoding) │ (EOF) │
└─────────────────────────────────────────────────────────┘
Length prefix
The length prefix is a variable-length big-endian integer where the MSB of each byte signals whether more bytes follow:
Each group of 7 bits contributes to the value, MSB = continuation flag:
[1xxxxxxx] [1xxxxxxx] [1xxxxxxx] [0xxxxxxx]
more bytes more bytes more bytes last byte
Payload
The payload is a BitStream - a sequence of bits packed into bytes. The stream is read and written bit-by-bit; byte boundaries are only respected for raw string/byte-array data (aligned reads/writes).
After the last payload byte comes a null terminator 0x00.
BitStream Encoding
Objects (nullable)
Objects begin with a null flag bit:
[0] -> object is not null
[1] -> null / not present
Booleans
A single bit: 1 = true, 0 = false.
Compressed integers (int16, int32, int64)
Integers are compressed using a variable-width encoding that minimizes bit usage for small values.
The encoding starts with a compressed flag bit:
[1] -> compressed (fewer than max bytes needed)
followed by a count of how many bytes are needed
[0] -> 0 bytes -> value fits in 4 bits
[10] -> 1 byte -> value is 1 byte (8 bits)
[110] -> 2 bytes -> value is 2 bytes (16 bits)
...
[0] -> full width (all bytes written)
Floating-point numbers (float32, float64)
Written as raw not compressed IEEE 754 bytes.
Strings
Strings are encoded as:
- A compressed
int32for the UTF-8 byte length. - The raw UTF-8 bytes, byte-aligned (any remaining bits in the current byte are skipped before writing).
Byte arrays
Same as strings: compressed length prefix followed by byte-aligned data.
Dates
Dates are encoded relative to a fixed epoch (0001-03-01).
A null flag bit precedes the value:
[1] -> zero time (null)
[0] -> seconds since epoch, written as a raw 8-byte integer
Message Structure
Every message has a header followed by an optional body. The outer wrapper adds a null flag for the entire message object.
[0] -> message is not null
[header] -> GSF header fields
[0] -> body is not null
[body fields]
GSF Header
| Field | Type | Condition | Description |
|---|---|---|---|
flags | int32 | always | Bit flags |
svcClass | int32 | always | Message service class |
msgType | int32 | always | Message type for the given svcClass |
requestId | int32 | if IsService() | Correlates request/response pairs |
logCorrelator | string | if IsRequest() | Client-side correlation string |
resultCode | int32 | if IsResponse() | Result code (0 = success) |
appCode | int32 | if IsResponse() | Application-level error code |
appString | string | if appCode != 0 | Error description string |
appCodes | array | if appCode == 17 | Extended error code list |
Flag semantics:
| Bit | Meaning |
|---|---|
flags & 2 == 0 | IsService - a request/response pair |
flags & 1 != 0 | IsResponse (only when IsService) |
flags & 2 != 0 | IsNotify - fire-and-forget notification |
flags & 16 != 0 | IsDiscardable |
Annotated Example
The very first message the client sends after connecting is a GetClientVersionInfo request. It is 22 bytes total:
hex: 15 20 c2 5c 04 6d 0c 0c 18 41 6d 61 7a 69 6e 67 57 6f 72 6c 64 00
Breakdown:
| Bytes | Value | Meaning |
|---|---|---|
15 | 21 | Length prefix: payload is 21 bytes |
20 c2 5c 04 6d 0c 0c 18 | bit stream | Header + body |
41 6d 61 7a 69 6e 67 57 6f 72 6c 64 | AmazingWorld | String content (byte-aligned) |
00 | 0 | Null terminator |
Bit-by-bit payload decoding:
Bit 8: [0] -> message is not null
Bit 9: [0] -> header is not null
Bit 10: [1][0] -> flags compressed, 0 bytes -> 4-bit value
Bit 14: [0000] -> flags = 0
Bit 16: [1][1][0] -> svcClass compressed, 1 byte follows
Bit 19: [00010010] -> svcClass = 18 (USER_SERVER)
Bit 27: [1][1][1][0] -> msgType compressed, 2 bytes follow
Bit 31: [0000001000110110] -> msgType = 566 (GET_CLIENT_VERSION_INFO)
Bit 47: [1][0] -> requestId compressed, 0 bytes -> 4-bit value
Bit 51: [0001] -> requestId = 1
Bit 53: [1][0] -> logCorrelator length compressed, 0 bytes -> 4-bit value
Bit 57: [0000] -> length = 0 (empty string)
Bit 59: [0] -> body is not null
Bit 60: [1][1][0] -> clientName length compressed, 1 byte follows
Bit 63: [00001100] -> length = 12 (bytes in "AmazingWorld")
Bit 71: [0] align -> skip to byte boundary
Bits 72–167: -> "AmazingWorld" (12 bytes, 96 bits)