Skip to content

Transport and framing

Every Wulfram II game packet is an opcode followed by a body, carried either in a length-prefixed TCP frame or in a UDP datagram whose per-opcode stream mode decides whether it gets a sequence number, ordering and acknowledgements.

The client builds a single game protocol object with 87 opcode slots, 0x00 through 0x56 (0x0046B390 → 0x0046C080 → 0x0050A030). For each opcode the object holds a receive callback, an outgoing stream mode, optional retry and retirement callbacks, and an optional named sequence group. 0x00509A90 finalizes the setup. recovered

Opcodes 0x00–0x04 are transport controls handled by the protocol itself. Game packets start above them. Voice and other service connections use their own protocol instances and reuse the same opcode numbers, so an opcode only has meaning within the game protocol. recovered

A registered receive callback shows what the client accepts. It does not prove that a particular sender used UDP rather than TCP, or that the opcode was ever sent.

  • Multi-byte values use network byte order (big-endian). Fixed-point values are signed 16.16. recovered
  • Strings are a u16 length that includes the terminating NUL, followed by that many bytes (0x00509340, 0x00509600). An empty string normally has length 1. The receiver forces the last byte to NUL. recovered
  • Bodies are written as a bit stream, so some fields are narrower than a byte. At the end of a record the sender pads to the next byte boundary; the receiver skips the padding without checking that it is zero (0x00509770, 0x00509DC0, 0x00509410). recovered

Layouts on these pages give body offsets, excluding the opcode and whatever envelope the transport adds.

TCP carries each packet in a frame with a two-byte size prefix. The size is a u16, big-endian, and counts the whole frame including the prefix itself. recovered

The body follows the opcode. The size includes the two prefix bytes.

The frame reader at 0x0050A4E0 subtracts two from the size (0x0050A536) and accepts a body of up to 0x2800 (10240) bytes, inclusive (0x0050A54B). So the largest frame is 10242 bytes. recovered

TCP frames ignore the UDP stream modes below: an opcode configured as sequenced does not get a sequence header when it travels over TCP. Transport controls 0x01–0x07 are rejected on TCP, and 0x00 has its own stream-check handling. recovered

The client’s UDP socket receives into a 3072-byte (0xC00) buffer. The constructor at 0x00507530 sets that capacity, and the receive path at 0x00507A30 passes it to recvfrom (0x00507AD7). A larger datagram fails with Winsock error 10040 (WSAEMSGSIZE) and is lost. recovered

A datagram can hold several records back to back: the receiver walks them with one shared cursor (0x0046B830 → 0x00507A30 → 0x00508C20 → 0x00505170). How each record is wrapped depends on the stream mode configured for its opcode.

moderecorddelivery
0 rawopcode, bodyNo sequence, no acknowledgement.
1 latestopcode, seq, length, bodyA newer sequence is delivered and acknowledged, and moves the receive cursor past any missing predecessors. Older or out-of-window records are skipped without an acknowledgement (0x00504EB0).
2sequence-only headerEvery record is delivered and acknowledged, without the duplicate suppression of mode 3 (0x00504E30).
3 orderedopcode, seq, length, bodyThe exact next sequence is delivered, then any queued successors. A future record inside the window is queued and acknowledged. A stale record is acknowledged and dropped (0x00504F50, 0x005049B0).

recovered Mode 2’s exact header was not decoded; don’t read “reliable but unordered” into the number alone.

Modes 1 and 3 share a five-byte header. The sender backpatches the length after writing the body (0x0050A210). For example, an empty 0x19 tank resend request is five bytes, and a 0x35 viewpoint record carrying one i32 is nine. recovered

Mode 1 and mode 3 record header. length is the total record length; the body follows at offset 5.

Sequence numbers run modulo 65535, not 65536, so they wrap from 65534 to 0. A receive stream starts expecting sequence 1 (0x00503AD0). The receiver accepts five positions ahead of its cursor; the sender keeps at most 15 unacknowledged records in flight (0x00503F10). If a pending record arrives twice, the first copy is kept (0x00503B40). recovered

Sequence state is per connection, per direction and per stream. Most opcodes get a private stream, but named groups share one: recovered

  • Beacon: 0x3A, 0x3B
  • Squad: 0x42, 0x46, 0x49, 0x4A

0x47 is not in the Squad group; the client sends it raw.

The sender keeps each unacknowledged record and resends either the same bytes or a record rebuilt by the opcode’s retry callback. A mode 1 rebuild may take a new sequence number. An acknowledgement retires the record (0x005047D0, 0x00505920, 0x00503D50). recovered

The retry timeout comes from a rolling average of ten round-trip samples, seeded at 500 ms. Each sample is multiplied by 1.5, the average is truncated and clamped to 200–1000 ms (0x00503CA0). On a timeout the resender feeds 1.5 times the current timeout back into the same average. This is not exponential backoff. recovered

opcodenamebody
0x00diagnosticcount u8, then that many bytes.
0x01D_IGNOREtarget opcode u8, sequence u16. Marks a sequence to skip in an ordered stream, then drains and sends ACK subtype 1.
0x02D_ACKsubtype u8, then one of the three forms below.
0x03D_HANDSHAKEStream configuration; see below.
0x04D_SET_STARTtarget opcode u8, start u16. Resets only a mode 1 receive cursor, and always answers with ACK subtype 2.
ACK subtypefields after subtype
0timestamp u32
1opcode u8, sequence u16
2timestamp u32, opcode u8, sequence u16

recovered A future D_IGNORE is stored as a marker and only drains the stream once it fills the next expected position. A matching subtype 2 acknowledgement unpauses the sender’s mode 1 stream (0x00505790, 0x00504750, 0x00505AD0).

These are bodies only. Controls travel in whatever stream mode the handshake gave them, so the size of an ACK on the wire depends on configuration.

D_HANDSHAKE (0x005053F0) carries a timestamp u32, connection ID u32 and group count u32. Each group is a string, a member count u32 and that many opcode u32s. Then comes a configuration count u32 and that many pairs of opcode u32 and mode u32. The handshake sets the modes the peer should expect for incoming records, independently of the local outgoing table. recovered

The client limits above are what matter. Our server checks every outgoing packet against them before it reaches the socket: policy

formatmaximumcounts
UDP datagram3072everything, including opcode and envelope
mode 1/3 payload3068opcode and body; the envelope adds four bytes
TCP payload10240opcode and body; framed size up to 10242
entity array255 recordsalso bound by the UDP byte limit

An oversized packet is logged and dropped, never truncated. Large entity updates are split into several datagrams that share one publication timestamp. The client accepts that: it rejects an entity update only when it is strictly older than that entity’s last one, with no packet-wide duplicate check (0x0047D760). Long chat and server messages are split at 3052 bytes or an earlier word or line break. recovered policy

Which opcodes our server sequences, and how, is also its own choice rather than a recovered table. It advertises raw mode for the transport controls, ordered mode for chat (0x1F), and one ordered group for entity lifecycle and roster messages. When too many ordered messages back up for one session, it disconnects that session instead of dropping one. policy

  • Whether the official server wrapped raw UDP datagrams in a size prefix or FF FF delimiter. The client’s receive path shows no such wrapper, so our server’s handling of them is a compatibility assumption.
  • The exact mode 2 header and where it was used.
  • The byte order of the mode 1/3 sequence and length fields is assumed to follow the general big-endian rule; the sources don’t state it separately.
  • Near the wrap, the client’s window check can accept sequence 65535 even though its sender never produces it. Our server rejects it.
  • Which opcodes the official server sent over TCP, which over UDP, and in which stream mode.
  • Recovery when a control message or its acknowledgement is lost.

reviewed 2026-09-25

A fan preservation project, not affiliated with the original developers. No original game files are hosted. Text licensed CC BY 4.0; site code MIT.