---
mvp: yes
subsystem: client-server-bridge
---

# Client/Server Bridge

This document is the **Rosetta stone** between client-emit and server-handle. It exists so a Phase 6 client author can read one table to know exactly which server opcode their client-side action targets, and so a Phase 4 server author can confirm coverage in the opposite direction. The two directions are separately documented in:

- [../extracted-engine/client-networking.md](../extracted-engine/client-networking.md) — client-side send/receive logic, scripts 0094 (`begin_client_receive`) and 0097 (`client_receive`)
- [./packet-protocol.md](./packet-protocol.md) — server-side dispatch (`0359-server_receive.gml`)

The wire protocol is symmetric — same opcodes, same byte format, same 39dll DLL on both sides. The bridge is just "which side calls which handler when".

## CLI-08 MVP slice (movement + chat)

The Phase 6 CLI-08 milestone gates on these opcodes working end-to-end. Two players in Chrome, walking around, chatting in real time.

| Client emits | Opcode | Server handles | Source script | Notes |
|--------------|--------|----------------|---------------|-------|
| Login (username announce) | **0** c2s | `case 0:` | `0359-server_receive.gml` | Username paired w/ password via `case 5` migration ACK |
| Room change request | **1** c2s | `case 1:` | `0359-server_receive.gml` | Carries `updateonly` flag + `room_id` |
| Sprite index update | **2** c2s | `case 2:` | `0359-server_receive.gml` | UDP — animation state |
| X/Y position update | **3** c2s | `case 3:` | `0359-server_receive.gml` | UDP — high-frequency movement |
| Chat line | **4** c2s | `case 4:` | `0359-server_receive.gml` | Server broadcasts back as opcode 4 s2c filtered by area |
| Login response | **8** s2c | (client `client_receive` opcode 8) | `0097-client_receive.gml` (client) | Sent after `case 5` migration ACK |
| User log-on/off broadcast | **5** s2c | (client `client_receive` opcode 5) | `0097-client_receive.gml` (client) | Driven by `uninit_user` + new `init_user` |
| Room snapshot | **11** s2c | (client `client_receive` opcode 11) | `0097-client_receive.gml` (client) | Server emits to joiner after `case 1` |

## Beyond MVP (Phase 7 parity scope)

The following opcodes are in `0359-server_receive.gml` but NOT required for CLI-08. Phase 7 PAR-02 walks them; the full row table lives in [./protocol.md](./protocol.md).

- Opcodes 6, 7, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22, 23 — feature-specific (inventory updates, hexport bookmarks, message-board interactions, user-news flags, etc.)
- Opcode 12 — **REJECTED-AS-PORTED** per [./admin-anti-port.md](./admin-anti-port.md). The wire channel is deleted in the rebuild; no replacement.

## 39dll DLL symmetry

Both the original client and the original server load `39dll.dll` and use the same `external_define` bind list. Scripts 0014-0084 in both `extracted/client-5-8/scripts/` and `extracted/server-5-4/scripts/` are bit-for-bit identical (modulo a couple of debug-string differences). This is why a Phase 4 server can mock the wire format from server GML alone — anything the server reads, the client must have written, with the same buffer-build sequence.

## Rebuild bridge contract

The shared bridge in the rebuild is `packages/protocol/`:

```typescript
// packages/protocol/src/messages.ts (Phase 4 / Phase 6 contract)
export type ClientToServer =
  | { type: 'login'; payload: { username: string; password: string } }
  | { type: 'room-change'; payload: { roomId: number; updateOnly: boolean } }
  | { type: 'walk'; payload: { dir: 'n' | 's' | 'e' | 'w' } }
  | { type: 'chat'; payload: { line: string } };

export type ServerToClient =
  | { type: 'login-response'; payload: { ok: boolean; reason?: string } }
  | { type: 'user-online'; payload: { accountId: string; pid: number; spriteIndex: number } }
  | { type: 'user-offline'; payload: { accountId: string } }
  | { type: 'room-snapshot'; payload: { roomId: number; players: PlayerSnapshot[] } }
  | { type: 'chat-broadcast'; payload: { senderId: string; line: string } }
  | { type: 'state-delta'; payload: PlayerStateDelta };
```

Both client and server import from `packages/protocol`. Any wire change is a single-PR diff that touches both consumers atomically.

## Per-side opcode coverage check

Phase 9 (the verification phase) runs a coverage script that:

1. Lists every opcode case in client `0097-client_receive.gml`.
2. Lists every opcode case in server `0359-server_receive.gml`.
3. Asserts every c2s opcode the client emits has a server handler, and every s2c opcode the server emits has a client handler.

The coverage script is owned by Phase 3 plan 02 (`pnpm protocol-doc:catalog` already enumerates opcodes; Phase 9 adds the symmetry check).

## See also

- [./packet-protocol.md](./packet-protocol.md) — server-side dispatcher narrative
- [../extracted-engine/client-networking.md](../extracted-engine/client-networking.md) — client-side dispatcher narrative
- [./protocol.md](./protocol.md) — full opcode table (auto-generated)
- [./admin-anti-port.md](./admin-anti-port.md) — opcode 12 disposition

## Scripts referenced in this subsystem

<!-- AUTOGEN:scripts:start -->
| Script ID | Name | Lines | Used in objects |
|-----------|------|-------|------------------|
<!-- 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 |
|--------------|------------|---------------|-----------|
<!-- AUTOGEN:gml-functions:end -->
