Client protocol - NexusTomorrow/server GitHub Wiki
How the 16042 client talks to a server: its connections, the envelopes and keys every message travels in, and the conversation from logging in to the end of the loading screen. It covers what the message audit has verified so far; world messages join it as their areas are checked. Each message's fields, with their widths and meanings, are in the generated message reference, which lists every message by opcode and links the page for its area; pregame's are all on Messages-Pregame.
The shape
Three connections
A client talks to three servers in turn, and keeps more than one connection open at once.
| Connection | Opened | Carries |
|---|---|---|
| Sign-on | First, to the address on the client's command line | A text exchange that proves the password and hands the client a game token |
| Login | After sign-on | The login itself, then the hand-off to a realm. It stays open after the hand-off |
| World | When the login server hands the client to a realm | Character select, realm select and the game itself |
sequenceDiagram
participant C as Client
participant L as Login server
participant W as World server
L->>C: ServerHello 0x0003, bare
C->>L: LoginRequest 0x0592
L->>C: LoginAccepted 0x0591
L->>C: RealmHandoff 0x03DB
C->>W: opens the world connection
W->>C: ServerHello 0x0003
C->>W: WorldHello 0x058F, then re-keys
C->>W: CharacterListRequest 0x07E0
W->>C: CharacterList 0x0117
C->>W: CharacterSelect 0x07DD
W->>C: EnterWorld 0x00AD
The diagram leaves out the envelopes below, which wrap every message after the first greeting.
Envelopes and keys
Everything after the first greeting is encrypted. The client sends every message inside a ClientEnvelope (0x0244): a 32-bit length in clear, then the message, encrypted with the key of the connection it's sent on. Servers answer inside one of two envelopes, and the client picks the key by the envelope's opcode, not by the connection it arrives on:
| Envelope | Opcode | Key |
|---|---|---|
LoginEnvelope |
0x0076 |
The login connection's |
WorldEnvelope |
0x03DC |
The world connection's |
The client drops any server message that arrives bare, except a ServerHello on the login connection.
The keys:
- The login connection uses a key fixed by the build.
- A world connection starts under that same fixed key.
- Once the client has greeted a world server, it switches that connection to a key derived from the session key the login server handed it.
Inside the envelope, a message travels on its own or under a tag. A TaggedMessage (0x038C) carries one whole message under a 5-bit tag, 0x03, 0x0B or 0x13, and a message of a given opcode always travels under the same tag. The client sends tagged messages before the world as well as in it: at character select, one goes out under tag 0x13. Other messages go on their own with no tag, among them LoginRequest, WorldHello, CharacterListRequest, CharacterSelect, CharacterDelete and PregameKeepAlive. What each tag value is for isn't established yet; Server core design has how the design reads them.
Character creation travels as a NestedMessage (0x025C): one already-encoded message, unencrypted inside the envelope. It's the only message the client sends that way.
The greeting checks the build
Every connection opens with the server's ServerHello (0x0003). On the login connection the client checks two of its fields and ends the login unless both match:
| Field | Must be |
|---|---|
| The build | 16042 |
| The protocol version | 0x097998A0 |
On a world connection the client checks neither. It reads only the realm id, and only on the world connection the login opens, where it keeps that id as its current realm. The rest of the greeting's fields carry no meaning to the client.
Pregame, step by step
Signing on
Before any of the messages below, the client signs on over a separate text exchange. It proves the account's password with SRP6 without ever sending it. Both sides then switch the exchange to RC4 under the key that proof produced, and the client receives a game token for the login.
[!NOTE] Sign-on isn't part of the message audit yet: it's a text protocol, not the bit-packed messages the audit covers. What's here is carried over from the old code, which a real client signs on against. Our one fix to it so far makes the proof's key always 64 bytes, where the old code produced 63 on about one login in 256.
Logging in
- The login server greets the client with a bare
ServerHello. If the greeting passes its checks, the client answers withLoginRequest(0x0592), once it has an account name: its build, the account name as typed, the user id and game token sign-on gave it, and a description of the client and its machine. - The server accepts with
LoginAccepted(0x0591) or refuses withLoginDenied(0x063D). A refusal carries a result the client turns into a message of its own, then it closes its connections and returns to the login screen. - After accepting, the server sends
RealmHandoff(0x03DB). Realm notices (RealmNotices,0x0763) can arrive at any point.
Handing off to the world server
RealmHandoff tells the client where to go next: the world server's address and port, the session key, a value to echo back, and the realm's name, flags and PvP type.
- The client opens a connection to that world server and waits there for its
ServerHello. The login connection stays open. - It answers with
WorldHello(0x058F): the echo value and the session key, both sent back unchanged, and the account name the login server accepted. - Right after sending it, the client switches the world connection to the key derived from the session key.
Character select
Each time the client enters character select it sends CharacterListRequest (0x07E0): after a world connection's greeting, on choosing the current realm or backing out of realm select, and after leaving the world. The server answers with CharacterList (0x0117), which replaces the client's list. It holds the account's characters and its character creation options.
| The client sends | The server answers | Notes |
|---|---|---|
CharacterCreate (0x025B), inside a NestedMessage |
CharacterCreateResult (0x00DC) |
On success the client selects the new character itself |
CharacterDelete (0x0352) |
CharacterDeleteResult (0x00E6) |
|
CharacterRename (0x0786) |
CharacterRenameResult (0x014B) |
A name that fails the client's own check is refused without sending |
CharacterSelect (0x07DD) |
EnterWorld (0x00AD), or CharacterSelectFailed (0x0162) |
See Entering the world |
Each answer is taken only while its request is pending.
The server can also send these on character select, unprompted:
| Message | Opcode | Does |
|---|---|---|
DeletedCharacterList |
0x0116 |
The account's deleted characters |
CreationLevelLimit |
0x0036 |
A level that limits the creation options the client offers |
SubscriptionState |
0x033D |
The account's subscription state |
CharacterDisabled |
0x00E7 |
Marks one listed character disabled or enabled |
LoginQueueStatus, LoginQueueFinished |
0x0717, 0x0715 |
The login queue's progress and its end |
RealmAnnouncement |
0x03E1 |
A realm-wide announcement, taken on every screen |
PtrCopyQueued |
0x06EA |
A copy of a character to the test realm was queued |
While the client sits on character select or realm select, it sends PregameKeepAlive (0x0241) at most about once a minute.
Realm select and transfers
- From character select, the client asks for the realm list with
RealmListRequest(0x07A4), and realm select takes over the connection. The server answers withRealmList(0x0761), which also carries the realm notices. - The player's choice goes back as
RealmChoice(0x07DF).- The current realm: the client carries on in character select without waiting.
- Another realm: the server answers with
RealmRedirect(0x07A1): the new world server, a session key and the realm's details. The client connects there and waits for its greeting, as in the hand-off above.
Transfers. While the subscription hasn't expired, the client can ask which realms a character could move to with TransferDestinationsRequest (0x03EE). The answer is TransferDestinationList (0x0862), and a transfer's outcome comes back as RealmTransferResult (0x0765).
Entering the world
EnterWorld (0x00AD) answers a pending CharacterSelect. The client shows the world's loading screen and loads the world, around a point the message gives. That point isn't the character's position, which arrives later in the world's own messages. The same message changes world from inside the game.
Then the server puts the player into the world, and the client says when it's ready. The loading screen stays up until the server's last message:
sequenceDiagram
participant C as Client
participant W as World server
W->>C: EnterWorld 0x00AD
Note over C: loading screen, loads the world
W->>C: UnitCreate 0x0262, the player's unit
W->>C: PlayerUnit 0x019B
W->>C: WorldSettings 0x00F1
W->>C: MovementControl 0x0636
C->>W: MovementControlReceipt 0x0635
C->>W: ZoneChange 0x063A
W->>C: PathState 0x06BC
W->>C: PlayerState 0x025E
C->>W: WorldReady 0x00F2
W->>C: WorldEntryComplete 0x0061
Note over C: leaves the loading screen
- The player's own unit arrives in a
UnitCreate(0x0262), andPlayerUnit(0x019B) names it as the player: the unit the client moves. The client refusesPlayerUnitwhen no unit has its id, so the create comes first. It also refuses one whose unit has no base faction, but only after taking that unit as the player, and the refusal undoes nothing: the client keeps that player and refuses every laterPlayerUnitwhile it has it. So the player's unit carries its base faction from the start. WorldSettings(0x00F1) can come any time afterEnterWorld. It carries the world's difficulty and prime level for the interface, and the period of the client's movement timer.MovementControl(0x0636) hands the client a unit to move. The client sends the message's ticket straight back in aMovementControlReceipt(0x0635), takes the unit over and starts sending its movement (ClientMovement,0x0637). On some takeovers of the player's own unit it also sends aZoneChange(0x063A) at once: always the first, and typically one after aMovementControlEnd. A mark on the unit the client last moved decides it: aMovementControlEndsets the mark, a new unit starts with it, and taking a unit over clears it.PathState(0x06BC) sets the player's path: the active one, each path's XP and which paths are unlocked.PlayerState(0x025E) follows with the player's own values, kept apart from the unit: the amount of each currency, experience, item proficiencies, entitlements and the like. The client sends nothing back, and doesn't need it to finish entering the world.- Once the world is loaded,
PlayerUnithas named the player andWorldSettingshas arrived, the client sendsWorldReady(0x00F2), once per world. - The server answers with
WorldEntryComplete(0x0061). No other message lets the client leave the loading screen.
Until PlayerUnit arrives, the client ignores MovementControl, drops PathState and refuses PlayerState, keeping none of it, so those three always follow it.
Moving. The client stamps its movement with its own clock, never the server's. Each time its movement timer runs it checks the zone of the unit it moves, and sends a ZoneChange when that changes; the zone id is the same one WorldReady carries. A MovementControlEnd (0x0639) takes control back: the client sends no more movement until the next MovementControl, and drops what it gathered but hadn't sent. Other units' movement reaches it in UnitMovement (0x0638). What comes after, from the movement messages' fields to chatting, belongs to the areas the audit checks next; milestone one's list of messages is on the Roadmap.
Leaving
| Message | Opcode | Taken on | The client |
|---|---|---|---|
Kicked |
0x036A |
Character select, realm select and in the world | Drops its connections and shows the reason |
WorldLogout |
0x0594 |
In the world only; the pregame screens drop it | Ends the session |
Vocabulary
| Term | Means here |
|---|---|
| Login connection | The connection to the login server, after sign-on |
| World connection | A connection to a realm's world server |
| Envelope | The encrypted wrapper every message after the first greeting travels in |
| Nested message | One already-encoded message carried unencrypted inside an envelope; only character creation uses it |
| The build's fixed key | The key every login and new world connection starts under |
| Session key | The key the login server hands the client for the world connection, sent back in WorldHello |
How sure this is
| Part | Basis |
|---|---|
Every message named here but UnitCreate, PlayerState, ClientMovement and UnitMovement |
Verified to the Fields rung: two independent readings of the client agree on every stated fact, and the area was reviewed |
UnitCreate (0x0262), PlayerState (0x025E), ClientMovement (0x0637) and UnitMovement (0x0638) |
Verified to Purpose: what each is for is settled, but not yet all of its fields |
| The order and the conditions on each step | Established from the client, not yet seen in a game. They become the check at milestone one |
| Sign-on | Carried over from the old code, which a real client signs on against; not yet part of the audit |
Where the code lives
| Piece | Path |
|---|---|
| The greeting and the client's envelopes | Source/NexusTomorrow.Network/Message/Model/ |
| The login server's messages | Source/NexusTomorrow.Network.Auth/Message/Model/ |
Character select, realm select and EnterWorld |
Source/NexusTomorrow.Network.World/Message/Model/Pregame/ |
| The rest of world entry, one file per message until the client's areas are settled | Source/NexusTomorrow.Network.World/Message/Model/ |
| The sign-on exchange | Source/NexusTomorrow.Network.Sts/ |
| SRP6 and the packet cipher | Source/NexusTomorrow.Cryptography/ |
| The check that holds each message to its rung | Tests/NexusTomorrow.Network.Codec.Tests/Verification/ |