---
mvp: yes
subsystem: packet-protocol
---

# Packet Protocol (server-side narrative companion)

This document is the prose companion to [./protocol.md](./protocol.md) (the canonical opcode table emitted by Phase 3 plan 02 from `extracted/server-5-4/scripts/0359-server_receive.gml`'s switch statement). It explains the **how** of the wire (39dll framing, sendmessage / receivemessage call discipline, dispatcher shape) so a Phase 4 server author can read protocol.json and immediately know what each opcode-row means at the byte level.

The byte-level wire format is documented canonically in [decomp/wiki/08-39dll-networking.md](../../decomp/wiki/08-39dll-networking.md) — per Phase 1 D-19 thin-wrapper, this MD links there rather than duplicating it.

## 39dll initialization (`0014-dllinit.gml`)

The original loads `39dll.dll` once at server start. The init script is a long sequence of `external_define` calls binding ~70 DLL symbols into globals (`global._BufA`, `global._BufB`, …):

```gml
// 0014-dllinit.gml (excerpt)
global._39dll = argument0;
if(is_real(argument0)) global._39dll = "39dll.dll";
external_call(external_define(global._39dll, "dllInit", dll_cdecl, ty_real, 0));
//Buffer
global._BufA = external_define(global._39dll, "writebyte", dll_cdecl, ty_real, 2, ty_real, ty_real);
global._BufB = external_define(global._39dll, "writestring", dll_cdecl, ty_real, 2, ty_string, ty_real);
global._BufC = external_define(global._39dll, "writeshort", dll_cdecl, ty_real, 2, ty_real, ty_real);
global._BufD = external_define(global._39dll, "writeint", dll_cdecl, ty_real, 2, ty_real, ty_real);
// ... ~70 more bind rows ...
```

The bound symbols are then trivially wrapped by scripts `0023-sendmessage.gml`, `0024-receivemessage.gml`, `0038-writebyte.gml` … `0057-readstring.gml`, etc. Each is a one-line `return external_call(global._XXX, …)` redirector. From `0023-sendmessage.gml`:

```gml
// 0023-sendmessage.gml
return external_call(global._SokD, argument0, argument1, argument2, argument3);
```

The 39dll wrappers (scripts 14-84) are shared verbatim with the [client-side networking layer](../extracted-engine/client-networking.md) — the same DLL is embedded in both client and server binaries. See [./client-server-bridge.md](./client-server-bridge.md) for the symmetry mapping.

## Buffer build → send pattern

The canonical send sequence, from inside `0349-operations.gml` and `0359-server_receive.gml`:

```gml
clearbuffer();              // Reset write position to 0
writebyte(OPCODE);          // 1 byte — distinguishes message types
writeint(some_int);         // 4 bytes
writestring(some_string);   // length-prefixed string
sendmessage(global.p_tcpsocket[pid]);  // Flush buffer to socket
```

All multi-byte integers are 39dll-default endianness (network byte order, big-endian). Strings are length-prefix + raw bytes (no NUL terminator). There is no per-frame length prefix — the TCP socket is configured with `setformat()` in `0026-setformat.gml` to use 39dll's "auto-format" mode, which puts a 4-byte big-endian length prefix on the buffer before each `sendmessage`. The receiving side uses `peekmessage()` (script 0025) to check whether enough bytes are on the wire before draining a frame with `receivemessage()`.

## Opcode dispatcher (`0359-server_receive.gml`)

`server_receive` runs from inside `0349-operations.gml` step event after a successful `receivemessage()`. It reads the first byte (opcode), then `switch`-es on it:

```gml
// 0359-server_receive.gml — dispatcher shape
case 0:  /* login: readstring username */ ...
case 1:  /* room change: readbyte updateonly + readint room_id */ ...
case 2:  /* sprite_index update (UDP) */ ...
case 3:  /* x,y pos update (UDP) */ ...
case 4:  /* chat line: readstring */ ...
case 5:  /* user log on/off — typically broadcast s2c, this is the migration ACK case */ ...
case 6:  /* (in-flight feature) */ ...
case 7:  /* request user list */ ...
case 8:  /* login response (s2c slot) */ ...
case 11: /* room snapshot (s2c only — but the case in dispatcher handles client-asked-for-resync) */ ...
case 12: /* execute_string payload (REJECTED-AS-PORTED — see admin-anti-port.md) */ ...
case 13, 14, 15, 16, 18, 19, 20, 21, 22, 23: /* …per-feature opcodes — see protocol.md for the full table */ ...
```

The full opcode table — including direction (c2s vs s2c), payload field types, and MVP-tier flag — is the auto-generated content in [./protocol.md](./protocol.md), refreshed by `pnpm protocol-doc:catalog` (Plan 02). This MD's job is the prose explanation of dispatcher shape; the auto-generated rows below complement it.

## Wire-level invariants (Phase 4 SRV-01..03 contract)

These properties of the original wire have to be preserved (or explicitly broken with a noted ADR) by the Phase 4 rebuild:

1. **Opcode is 1 byte.** Range 0-23 used; 24-255 unused. Phase 4 opcode space stays 1-byte for protocol parity.
2. **Strings are length-prefix UTF-8 bytes.** No NUL terminator. The 39dll length prefix is `writeshort` (2 bytes, max 65535).
3. **Per-frame length prefix is 4 bytes big-endian.** Set by `setformat()` mode 5; documented in 39dll source.
4. **No reserved opcodes for keepalive or ping.** The original relies on TCP keepalive (OS layer). Phase 4 likely needs an explicit ping/pong since browser WebSockets sit behind proxies that idle-close.
5. **No frame versioning byte.** Adding a version byte is a Phase 4 ADR call (forward-link plan 03-08 canonical-snapshot ADR).

## Methodology (D-07: 39dll = call order)

Per CLAUDE.md hard rule #5, we reverse the protocol from the **call order** of `writebyte` / `writeint` / `writestring` / `readbyte` / `readint` / `readstring` inside `0359-server_receive.gml` and the broadcast sites scattered through `0349-operations.gml`. We do NOT hex-inspect packet captures — the byte format is whatever the 39dll DLL emits, and the project's own GML is the only source of truth for the field-by-field grammar of each opcode.

Plan 02 (`pnpm protocol-doc:catalog`) is the tool that mechanically scans these scripts and emits [./protocol.json](./protocol.json) (machine) + [./protocol.md](./protocol.md) (human-readable, with the AUTOGEN tables Phase 4 servers consume).

## Rebuild guidance (Phase 4 SRV-01..03)

- **Replace 39dll with WebSocket binary frames.** Per `.planning/research/STACK.md`, transport is WSS + Colyseus + msgpackr. The 39dll DLL is gone.
- **Reuse the opcode names**, even where the byte layout changes. Phase 4 server emits `writebyte(1)` as Colyseus `room.send('room-change', {room: 5})` — same semantic, different transport.
- **`packages/protocol`** holds the shared TypeScript types. Both client and server import; protocol.json drives codegen.
- **No DLL load step**, no `external_define`. Plain TypeScript imports.

## See also

- [./protocol.md](./protocol.md) — canonical opcode table (auto-generated by plan 02)
- [./protocol.json](./protocol.json) — machine-readable opcode rows
- [./client-server-bridge.md](./client-server-bridge.md) — client / server opcode handler symmetry
- [../extracted-engine/client-networking.md](../extracted-engine/client-networking.md) — client-side mirror of this layer
- [../../decomp/wiki/08-39dll-networking.md](../../decomp/wiki/08-39dll-networking.md) — canonical wire-protocol reference

## Scripts referenced in this subsystem

<!-- AUTOGEN:scripts:start -->
| Script ID | Name | Lines | Used in objects |
|-----------|------|-------|------------------|
| 0014 | dllinit | 85 | — |
| 0015 | dllfree | 2 | — |
| 0016 | tcpconnect | 12 | — |
| 0017 | tcplisten | 12 | — |
| 0018 | tcpaccept | 8 | — |
| 0019 | tcpip | 4 | — |
| 0020 | setnagle | 7 | — |
| 0021 | tcpconnected | 6 | — |
| 0022 | udpconnect | 9 | — |
| 0023 | sendmessage | 16 | — |
| 0024 | receivemessage | 15 | — |
| 0025 | peekmessage | 16 | — |
| 0026 | setformat | 20 | — |
| 0027 | lastinIP | 10 | — |
| 0028 | lastinPort | 8 | — |
| 0029 | setsync | 7 | — |
| 0030 | closesocket | 6 | — |
| 0031 | socklasterror | 4 | — |
| 0032 | myhost | 3 | — |
| 0033 | compareip | 10 | — |
| 0034 | sockexit | 4 | — |
| 0035 | sockstart | 3 | — |
| 0036 | hostip | 7 | — |
| 0037 | getsockid | 6 | — |
| 0038 | writebyte | 8 | — |
| 0039 | writeshort | 9 | — |
| 0040 | writeushort | 9 | — |
| 0041 | writeint | 9 | — |
| 0042 | writeuint | 9 | — |
| 0043 | writefloat | 8 | — |
| 0044 | writedouble | 9 | — |
| 0045 | writechars | 8 | — |
| 0046 | writestring | 10 | — |
| 0047 | copybuffer | 8 | — |
| 0048 | copybuffer2 | 9 | — |
| 0049 | readbyte | 7 | — |
| 0050 | readshort | 7 | — |
| 0051 | readushort | 7 | — |
| 0052 | readint | 7 | — |
| 0053 | readuint | 7 | — |
| 0054 | readfloat | 7 | — |
| 0055 | readdouble | 7 | — |
| 0056 | readchars | 8 | — |
| 0057 | readstring | 7 | — |
| 0058 | readsep | 13 | — |
| 0059 | readbit | 8 | — |
| 0060 | buildbyte | 6 | — |
| 0061 | getpos | 8 | — |
| 0062 | clearbuffer | 6 | — |
| 0063 | buffsize | 7 | — |
| 0064 | setpos | 8 | — |
| 0065 | bytesleft | 7 | — |
| 0066 | createbuffer | 5 | — |
| 0067 | freebuffer | 6 | — |
| 0068 | bufferexists | 6 | — |
| 0069 | md5string | 6 | — |
| 0070 | md5buffer | 5 | — |
| 0071 | bufferencrypt | 6 | — |
| 0072 | bufferdecrypt | 6 | — |
| 0073 | fileopen | 8 | — |
| 0074 | fileclose | 6 | — |
| 0075 | filewrite | 8 | — |
| 0076 | fileread | 10 | — |
| 0077 | filepos | 6 | — |
| 0078 | filesetpos | 7 | — |
| 0079 | filesize | 6 | — |
| 0080 | adler32 | 9 | — |
| 0081 | getmacaddress | 6 | — |
| 0082 | iptouint | 9 | — |
| 0083 | uinttoip | 7 | — |
| 0084 | netconnected | 6 | — |
| 0359 | server_receive | 628 | — |
<!-- AUTOGEN:scripts:end -->

## Objects referenced in this subsystem

<!-- AUTOGEN:objects:start -->
| Object ID | Name | Sprite | Mask | Events |
|-----------|------|--------|------|--------|
<!-- AUTOGEN:objects:end -->

## Engine functions used

<!-- AUTOGEN:gml-functions:start -->
| GML function | Call sites | Sample script | Wiki link |
|--------------|------------|---------------|-----------|
| `external_define` | 1 | 0014-dllinit | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `external_call` | 68 | 0014-dllinit | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `sendmessage` | 5 | 0008-ChtCmdRec | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `receivemessage` | 2 | 0014-dllinit | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `writebyte` | 5 | 0008-ChtCmdRec | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `readbyte` | 2 | 0014-dllinit | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `writestring` | 5 | 0008-ChtCmdRec | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `readstring` | 2 | 0014-dllinit | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `writeint` | 4 | 0014-dllinit | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `readint` | 2 | 0014-dllinit | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
<!-- AUTOGEN:gml-functions:end -->
