<!-- https://minecraftdocs.dev/systems/networking/protocol-phases — this page as markdown. The book's index: https://minecraftdocs.dev/llms.txt; the whole book in one file: https://minecraftdocs.dev/llms-full.txt -->

# Protocol phases

> Verified against **Minecraft 26.3** · Part IX · A login: from clicking a server in the list to standing in the world.

Click a server in the multiplayer list and one TCP connection opens, and over the next few seconds it speaks four different languages in turn: handshaking,
login, configuration, play. There is a fifth, *status*, and a login never
touches it — it is the phase the server list uses to ask a server how many
players it has, and it is a dead end by design. Each of the five is a
`ConnectionProtocol`, each has its own packet set, and each that hands over to another does so by a packet marked *terminal* — a flag on the packet class that makes the
codec which decoded it tear itself out of the pipeline as it passes ([the
connection](the-connection#what-a-terminal-packet-does-to-the-codecs)). All
but handshaking have a listener at both ends; handshaking is serverbound only,
so the client has nothing to listen with.

The surprise is not in any of that. It is that the `ServerPlayer` — the
object, its save data, its position, the chunks under it — is *prepared*
during configuration, by `PrepareSpawnTask`, and **constructed only after the
client has acknowledged that configuration is over**. For the seconds in
between, a server is holding chunk tickets for a player that does not exist,
and by the time the object is built, the server's side of the connection already encodes play.

## The cast

| class | role | thread |
|---|---|---|
| `ConnectionProtocol` | the five phases — a bare enum of labels; the codec lives in a `ProtocolInfo`, the behaviour in a `PacketListener` | — |
| `Connection` | one channel, and `Connection.setupInboundProtocol` / `Connection.setupOutboundProtocol` at every transition | Netty |
| `ServerHandshakePacketListenerImpl` | the three-way switch and the version gate | Netty |
| `ServerLoginPacketListenerImpl` | the login state machine; its handlers set a volatile state and its `ServerLoginPacketListenerImpl.tick` acts on it | Netty and the authenticator thread, ticked from Server |
| `ServerConfigurationPacketListenerImpl` | the serial task queue, and the handler that finally builds the player | mixed — see below |
| `ClientHandshakePacketListenerImpl` · `ClientConfigurationPacketListenerImpl` | the client's two halves: the session-service call, then the registries and tags it accumulates before constructing `ClientPacketListener` | Netty, the IO pool for the session call, and Render for most of the second |
| `ServerCommonPacketListenerImpl` · `CommonListenerCookie` | what configuration and play share — the packets legal in both, and the state that survives a switch between them | Netty and Server |
| `PrepareSpawnTask` | finds a spawn, tickets its chunks, waits — and later, on request, spawns the player | Server, but Netty for its start straight after a code of conduct |

## The five phases

```mermaid
stateDiagram-v2
    [*] --> HANDSHAKING : TCP accept
    HANDSHAKING --> STATUS : ClientIntentionPacket, intent STATUS
    HANDSHAKING --> LOGIN : ClientIntentionPacket, intent LOGIN or TRANSFER
    STATUS --> [*] : pong, then the server hangs up
    LOGIN --> CONFIGURATION : ClientboundLoginFinishedPacket, ServerboundLoginAcknowledgedPacket
    CONFIGURATION --> PLAY : ClientboundFinishConfigurationPacket, ServerboundFinishConfigurationPacket
    PLAY --> CONFIGURATION : ClientboundStartConfigurationPacket, ServerboundConfigurationAcknowledgedPacket
    PLAY --> [*] : disconnect
    note right of HANDSHAKING : every packet named here is terminal, so the codec that decoded it is already gone
```

*The five phases, each arrow between two of them carrying the packet — or the
pair of them — that changes one. The arrow back out of play is the only one
that goes up, which is what makes configuration the only phase entered from two
others.*

**Seven packets in the game carry the terminal flag**, and every one of them
labels an arrow above. `ClientIntentionPacket` labels two, which is the
striking one: the very first packet a connection ever sends already tears out
the codec that decoded it. The remaining six are three pairs, and every pair
enters or leaves configuration, the only phase entered from two others.

`ConnectionProtocol` is five constants — `ConnectionProtocol.HANDSHAKING`,
`ConnectionProtocol.STATUS`, `ConnectionProtocol.LOGIN`,
`ConnectionProtocol.CONFIGURATION`, `ConnectionProtocol.PLAY` — each
carrying only a string `ConnectionProtocol.id`. There is no number, no
packet table and no lookup by id. What a phase *is* lives in two other
places: the packet set, bound as a `ProtocolInfo`, and the listener that
handles it.

| phase | serverbound listener | clientbound listener | `ProtocolInfo` |
|---|---|---|---|
| `ConnectionProtocol.HANDSHAKING` | `ServerHandshakePacketListenerImpl`, or `MemoryServerHandshakePacketListenerImpl` in singleplayer | none — there is no clientbound handshake protocol | `HandshakeProtocols.SERVERBOUND`, one packet |
| `ConnectionProtocol.STATUS` | `ServerStatusPacketListenerImpl` | an anonymous `ClientStatusPacketListener` inside `ServerStatusPinger` | `StatusProtocols.SERVERBOUND` binds a **raw buffer**; `StatusProtocols.CLIENTBOUND` a `FriendlyByteBuf` |
| `ConnectionProtocol.LOGIN` | `ServerLoginPacketListenerImpl` | `ClientHandshakePacketListenerImpl` | `LoginProtocols.SERVERBOUND` / `LoginProtocols.CLIENTBOUND` |
| `ConnectionProtocol.CONFIGURATION` | `ServerConfigurationPacketListenerImpl` | `ClientConfigurationPacketListenerImpl` | `ConfigurationProtocols.SERVERBOUND` / `ConfigurationProtocols.CLIENTBOUND` |
| `ConnectionProtocol.PLAY` | `ServerGamePacketListenerImpl` | `ClientPacketListener` | **not pre-bound** — `GameProtocols.SERVERBOUND_TEMPLATE` and `GameProtocols.CLIENTBOUND_TEMPLATE` are bound per connection |

The bindings in the last column are the per-phase codec tables that
[packets and stream
codecs](packets-and-stream-codecs#where-a-packets-number-comes-from) builds,
and the
swap itself is the pipeline surgery [the
connection](the-connection#a-phase-change-is-a-message-written-down-the-pipeline)
performs; this page is what the swaps are *for*. The one thing about the
column that is a fact about the *phases* is that play is the only row not
pre-bound: its codecs write registry ids, so its two templates are bound per
connection at the configuration-to-play switch, which is the only transition
in a connection's life that ties the codecs to a set of registries as well as
changing which packets are legal.

Two listeners sit under the phases. `ServerCommonPacketListenerImpl` is the
shared base of the server's configuration and play listeners and holds
nearly everything legal in both (each subclass handles the client information
itself) — keep-alive, latency, custom payloads,
resource-pack responses and the flush suspension — with
`ClientCommonPacketListenerImpl` its client counterpart; that inheritance
is why the *common* packets in [reference/packets.md](../../reference/packets)
belong to no single phase. And the state that crosses a phase change is a
`CommonListenerCookie`: on the server a small record of profile, latency,
client information and a transferred flag; on the client a much larger one
carrying registries, feature flags, cookies, chat state and the server
brand.

## Handshake

The handshake is one packet and a three-way switch. `ClientIntentionPacket`
carries the protocol version, the address the client dialled and a
`ClientIntent`, and `ServerHandshakePacketListenerImpl.handleIntention`
branches on it. `ClientIntent.STATUS` installs the status listener, or
disconnects at once if the server does not reply to status.
`ClientIntent.TRANSFER` disconnects if the server does not accept transfers,
and otherwise joins `ClientIntent.LOGIN` in
`ServerHandshakePacketListenerImpl.beginLogin`, which compares the client's
protocol version against this build's and refuses a mismatch, and the two
refusals are not symmetric: a version **below protocol 754** is told *outdated_client*, and anything else that does not match is
told *incompatible*. All three of the handshake's refusals on the login path — these two and the transfer refusal —
go out as `ClientboundLoginDisconnectPacket` under the **login** clientbound
protocol, so the client can render a reason:
`ServerHandshakePacketListenerImpl.beginLogin` installs it for every login
before its check, and the transfer refusal installs it purely to send one. The status refusal is the exception and the
rudest: the status clientbound protocol is installed before the branch, and
then the connection is dropped with no packet at all.

The client does not wait for any of that. Whichever of its three entry
points opened the connection — `ConnectScreen` for a listed or direct
server, `Minecraft` for the integrated server's memory channel,
`RealmsConnect` — it sends `ServerboundHelloPacket` immediately after the
intention packet; there is no round trip between them. The profile id the hello carries is decoded and then never read — the
server mints or fetches identity for itself. All of this runs on the Netty
thread, and the intention packet, being terminal, has already torn out the
codec that decoded it.

## Status, the phase nobody logs in through

`ConnectionProtocol.STATUS` is two packets each way and a deliberate dead
end. `ServerStatusPacketListenerImpl` answers exactly one
`ServerboundStatusRequestPacket` — a second one disconnects the caller —
and answers `ServerboundPingRequestPacket` with
`ClientboundPongResponsePacket` and then **hangs up**. A status connection
is expected to be thrown away, which is why `ServerStatusPinger` opens one
per server in the list and why `Connection.initiateServerboundStatusConnection`
exists as a separate entry point.

## Login

The login phase is a state machine on one field, and the states are the ones
the server moves through between accepting a socket and having a profile it
believes.

```mermaid
stateDiagram-v2
    [*] --> HELLO
    HELLO --> KEY : online mode, ClientboundHelloPacket
    HELLO --> VERIFYING : the singleplayer profile
    HELLO --> VERIFYING : offline mode
    KEY --> AUTHENTICATING : ServerboundKeyPacket
    AUTHENTICATING --> VERIFYING : the authenticator thread returns
    VERIFYING --> WAITING_FOR_DUPE_DISCONNECT : tick, a duplicate is online
    VERIFYING --> PROTOCOL_SWITCHING : tick, ClientboundLoginFinishedPacket
    WAITING_FOR_DUPE_DISCONNECT --> PROTOCOL_SWITCHING : tick, the old one is gone
    PROTOCOL_SWITCHING --> ACCEPTED : ServerboundLoginAcknowledgedPacket
    note right of VERIFYING : the three tick edges are the server thread's, one the authenticator thread's, the rest Netty's
```

*Seven of the eight `ServerLoginPacketListenerImpl.State` constants, and the
three edges the server thread owns. Look at where the forks are: the machine
branches once out of the hello, on how — or whether — this login is
authenticated, and then again on whether the profile is already playing.*

No handler on `ServerLoginPacketListenerImpl` ever schedules itself onto
another thread (there is not one `PacketUtils.ensureRunningOnSameThread` call
in the class), and yet the login is finished on the server thread, which is
why its `ServerLoginPacketListenerImpl.state` field is volatile: the packet
handlers run on the Netty thread and set the state, as the authenticator thread
below does, and
`ServerLoginPacketListenerImpl.tick` — reached from
`MinecraftServer.tickConnection` through `ServerConnectionListener.tick`
and `Connection.tick`, the tick a game thread gives a connection ([the
connection](the-connection#connectiontick-the-tick-a-game-thread-gives-the-connection))
— reads it on the server thread and does the login.
The tick is also where `ServerLoginPacketListenerImpl.MAX_TICKS_BEFORE_LOGIN`,
six hundred ticks, is enforced: a client that has not reached the end of
the phase in thirty seconds is disconnected for a slow login — a transition
out of every state above but the last, and the one the diagram cannot draw.

The diagram names three states the paragraphs below do not: `ServerLoginPacketListenerImpl.State.AUTHENTICATING`
is the wait on the session service,
`ServerLoginPacketListenerImpl.State.PROTOCOL_SWITCHING` is the gap between
the server sending `ClientboundLoginFinishedPacket` and the client
acknowledging it, and `ServerLoginPacketListenerImpl.State.ACCEPTED` is where
the machine stops, because configuration has its own listener.

**Three branches out of the hello.** If the name matches the singleplayer
profile, verification starts at once with no encryption. If the server uses
authentication and this is not a memory connection, the state becomes
`ServerLoginPacketListenerImpl.State.KEY` and `ClientboundHelloPacket`
carries the server's RSA public key and a four-byte challenge. Otherwise —
offline mode — the profile is minted from the name by
`UUIDUtil.createOfflineProfile` and nothing is encrypted.

**Both sides authenticate, and the client goes first.**

```mermaid
sequenceDiagram
    box transparent the client
    participant CHPL as ClientHandshake<br/>PacketListenerImpl
    end
    box transparent the server
    participant SLPL as ServerLoginPacket<br/>ListenerImpl
    end
    participant Sess as the session service

    SLPL->>CHPL: ClientboundHelloPacket, an RSA public key and a challenge
    CHPL->>CHPL: a new AES secret, and a digest over id, secret and key
    CHPL->>Sess: joinServer, on the client's IO pool, before the key packet
    CHPL->>SLPL: ServerboundKeyPacket, RSA-encrypted, ciphers attached to the send
    SLPL->>SLPL: the challenge checked, the secret recovered, then<br/>Connection.setEncryptionKey
    SLPL->>Sess: hasJoinedServer, on a fresh User Authenticator thread
    Sess-->>SLPL: the authenticated profile, state VERIFYING
    Note over SLPL: the next server tick runs bans, whitelist, compression, duplicates
```

*Both ends authenticating against the same external service, the client first.
Read it downwards: the server has installed both ciphers by the fifth line, two
lines before it has any idea who this is.*

Read the diagram for the order, because the order is the point: **the server
installs both ciphers while handling the key packet, before its own
session-service call has begun.** An unauthenticated connection is already
encrypted. The client's asymmetry is the mirror of it — it calls the session
service *before* `ServerboundKeyPacket` goes anywhere and sends it from that
callback, attaching its own ciphers to the send, so the key packet itself
travels in the clear and nothing after it does.

**Authentication is a plain thread with two fallbacks.** It calls the
session service and, on success, reports login activity, stores the profile
and flips the state to `ServerLoginPacketListenerImpl.State.VERIFYING`. On
failure it disconnects — unless the server is a singleplayer host, in which
case both a null result and an unreachable authentication service fall back
to an offline profile. That is how a LAN world admits a guest whose account
cannot be checked. A login over a real socket against an authenticating
server starts exactly one such thread; an offline-mode, memory or
singleplayer-profile login starts none.

**The tick does the real login.**
`ServerLoginPacketListenerImpl.verifyLoginAndFinishConnectionSetup` runs on
the server thread: `PlayerList.canPlayerLogin` for bans, whitelist and
capacity; the compression switch; and
`PlayerList.disconnectAllPlayersWithProfile` for a duplicate login, after
which the machine waits in
`ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT` until the
old player has left the player list.

**Login ends with a terminal packet in each direction**, and the two sides
install their codecs in mirror order. `ClientboundLoginFinishedPacket` then
`ServerboundLoginAcknowledgedPacket` — but the server installs *outbound*
configuration when the acknowledgement arrives, in
`ServerLoginPacketListenerImpl.handleLoginAcknowledgement`, whereas the
client installs *inbound* configuration before sending it and outbound
immediately after. Both packets are terminal, so the codecs tear themselves
out as they pass ([the
connection](the-connection#what-a-terminal-packet-does-to-the-codecs)).
The client then
volunteers two packets straight away: its own `BrandPayload` and
`ServerboundClientInformationPacket`, which carries the client's language,
view distance and skin-part settings — and is therefore the packet a server
with a code of conduct reads before choosing which translation of it to send.

What disconnects a login: a version mismatch, a refused transfer, a ban, a
whitelist the player is not on, a full server, a failed session check on a
non-singleplayer host, any `ServerboundCustomQueryAnswerPacket` or cookie answer, a malformed or
out-of-order packet, or six hundred ticks. Either answer disconnects because
vanilla never asks: `ServerLoginPacketListenerImpl.State.NEGOTIATING` is
declared and never assigned, `ClientboundCustomQueryPacket` decodes every
payload as `DiscardedQueryPayload`, and nothing sends a cookie request.

## Configuration

Configuration is a queue. The order below is fixed, each task finishes before
the next begins, and the last two exist only to get a player into a world.

```mermaid
flowchart TD
    S["up to three packets, sent before any task runs"]:::server
    S --> Q
    subgraph Q["the serial queue"]
      direction TB
      R["SynchronizeRegistriesTask"]:::server --> C["ServerCodeOfConduct<br/>ConfigurationTask"]:::server
      C --> P["ServerResourcePack<br/>ConfigurationTask"]:::server
      P --> PS["PrepareSpawnTask: a spawn, and a ticket"]:::server
      PS --> J["JoinWorldTask: the terminal packet"]:::server
    end
    Q -- "ClientboundFinishConfigurationPacket" --> CL["the client rebuilds its registries"]:::client
    CL -- "ServerboundFinishConfigurationPacket" --> F["the play protocol, the gate again, the player"]:::server
```

*The phase as a queue of five tasks, strictly one at a time — the middle two
only if the server has a code of conduct or a resource pack — with the packets
that precede the queue and the round trip that ends it. The two arrows
at the foot are the phase's oddity: between them the server is holding a
ticket on chunks for a player that does not exist yet.*

`SynchronizeRegistriesTask` is the reason configuration exists, and the
queue around it is strictly serial. `ServerConfigurationPacketListenerImpl.startConfiguration`
sends up to three things outside the queue — the server's `BrandPayload`,
`ClientboundServerLinksPacket` if there are links, and
`ClientboundUpdateEnabledFeaturesPacket` — then queues the registry task, a
code-of-conduct task if the server has one (`ServerCodeOfConductConfigurationTask`,
whose two packets are `ClientboundCodeOfConductPacket` and the client's
`ServerboundAcceptCodeOfConductPacket`) and a resource-pack task if it
has one, before `ServerConfigurationPacketListenerImpl.returnToWorld`
appends `PrepareSpawnTask` and `JoinWorldTask` and starts the first. Each
`ConfigurationTask` finishes before the next begins:
`ServerConfigurationPacketListenerImpl.finishCurrentTask` rejects a
completion naming the wrong task type, and an exception out of any task
disconnects the client.

**Registry and tag sync** is why this phase exists, and *what* crosses belongs
to [identifiers and
registries](../foundations/identifiers-and-registries#when-a-world-opens) —
the known-pack negotiation, the all-or-nothing comparison, the empty payloads
for elements the client can re-read from its own jar, and the two layers the
client rebuilds. What belongs to the *phase* is the order and the count: one
`ClientboundSelectKnownPacks` and its reply, then one
`ClientboundRegistryDataPacket` **per registry**, then a single
`ClientboundUpdateTagsPacket` that covers the **networkable** registries and
the static ones too — the surprise, since the data packets are dynamic-only,
and the reason a tag has to land before any play traffic does
([tags](../foundations/tags#the-other-way-tags-cross-the-wire)). None of it
is applied as it arrives: `RegistryDataCollector` accumulates the whole
exchange and only resolves it at
`ClientConfigurationPacketListenerImpl.handleConfigurationFinished`, the
handler that goes on to construct the `ClientPacketListener`.

**The seam is not where it looks.** Four of the server's configuration
handlers hop to the Server thread —
`ServerConfigurationPacketListenerImpl.handleSelectKnownPacks`,
`ServerConfigurationPacketListenerImpl.handleConfigurationFinished` and, from
the common base, the resource-pack response and
`ServerCommonPacketListenerImpl.handleCustomClickAction` —
while the client-information and code-of-conduct handlers, like the
keep-alive and ping handlers they inherit, stay on the Netty thread. That
last one is load-bearing: accepting a code of conduct finishes a task, which
**starts the next task on the Netty thread**, and the next task may be
`PrepareSpawnTask`, whose first act is to read a player save file and
resolve a spawn position. `ServerConfigurationPacketListenerImpl.tick` runs
each tick to drive the current task and keep the spawn chunks loaded.

**`PrepareSpawnTask` is two states, and only the second builds the player, and only when asked.**
Its `PrepareSpawnTask.Preparing` state waits for the spawn position the task's
start resolved (a search, for a player with none saved), tickets the chunks and then reports *not finished* from
`ConfigurationTask.tick` until they arrive; `PrepareSpawnTask.Ready` reports
finished and does nothing but re-arm the ticket until it is asked to spawn. How it finds the spawn, why the ticket has
to be re-armed every tick, and what the two whole-file reads cost belong to
[players and
sessions](../server/players-and-sessions#preparing-a-place-to-stand).
`JoinWorldTask` then sends the terminal
packet, and only when the *client's* `ServerboundFinishConfigurationPacket`
arrives does `ServerConfigurationPacketListenerImpl.handleConfigurationFinished`
swap the outbound protocol to play, refuse a duplicate of the player, run the
admission gate a second time
([admission is a `Component` or
nothing](../server/players-and-sessions#admission-is-a-component-or-nothing)
— a ban or a full server can arrive in the seconds a configuration takes) and
call `PrepareSpawnTask.spawnPlayer`, which is where the `ServerPlayer` is
finally constructed and handed to `PlayerList.placeNewPlayer`.
**Everything between the join task and that handler is a
server holding a ticket on chunks for a player that does not exist** — which
is the phase's own oddity, and the reason this page opens on it.

What disconnects a configuration: a task that throws on start or on tick, a
completion for the wrong task, a declined resource pack the server requires,
a cookie answer, a duplicate at the finish or a refusal at the admission gate's
second run (a ban, the whitelist, a full server), and an exception while
placing the player. **Notably not a deadline on the queue.** Login has one —
six hundred ticks — and configuration's queue has none:
`ServerConfigurationPacketListenerImpl.tick` counts nothing, calling the
common base's keep-alive and then the current task, so what kills a stuck
task is a client that stops answering keep-alives rather than one that takes
too long. Only the round trip that ends the phase has a clock, for anyone but the
singleplayer host: the terminal
packet closes the listener, which stops sending keep-alives and allows
fifteen seconds for the answer ([the
connection](the-connection#how-a-connection-dies)). A slow configuration
can be the server still finding chunks for the player, and the queue lets it
be.

## Play, and the way back

The play protocol is installed in two halves on each side: the server swaps
outbound early in the finish handler and inbound inside
`PlayerList.placeNewPlayer`; the client, in one handler, swaps inbound, sends
the finish packet, then swaps outbound. That is why the server's side already encodes play
while still nominally in the configuration listener, and a refusal at the
finish goes out as a play packet. Chat session
keys are not part of any of this: they are negotiated in play, after the
client learns the server's mode from `ClientboundLoginPacket`
([chat and signing](chat-and-signing#what-the-signature-covers)).

**A reconfigure does not re-run configuration.**
`ServerGamePacketListenerImpl.switchToConfig` removes the player from the
world, sends `ClientboundStartConfigurationPacket` and swaps outbound; the
client's `ClientPacketListener.handleConfigurationStart` flushes its chat
queue, stashes the chat state and carries the registry access, feature
flags, brand and server links forward in a rebuilt cookie, then answers
`ServerboundConfigurationAcknowledgedPacket`; and
`ServerGamePacketListenerImpl.handleConfigurationAcknowledged` installs a
configuration listener **without** calling
`ServerConfigurationPacketListenerImpl.startConfiguration`. No registries,
tags, feature flags or brand are re-sent, and none need to be. The player
parks in configuration with an empty queue until
`ServerConfigurationPacketListenerImpl.returnToWorld` re-queues the spawn
and join tasks. Both directions are reachable in vanilla only from
`DebugConfigCommand`, which the game registers only in a dedicated server's command set, and only when started with its debug system properties.

## Cookies, transfers and the chat reset are proxy hooks

A
`ServerboundCookieResponsePacket` arriving at any server listener is an
unexpected query and a disconnect, and nothing in the tree constructs
`ClientboundStoreCookiePacket` or `ClientboundCookieRequestPacket`. Of the
transfer machinery only `ClientboundTransferPacket` has a vanilla caller,
`/transfer`: it sends the client to another server, which sees a
`ClientIntent.TRANSFER` handshake and a transferred flag in its cookie, and
`ClientCommonPacketListenerImpl.shouldHandleMessage` keeps accepting
store-cookie and transfer packets while a transfer is in flight, which is
what lets a proxy's trailing state land. `ClientboundResetChatPacket` is
registered and handled and never sent. The rest is fully implemented on the
client and unused by the server.

> **For a 1.21-era reader.** The assumption that "packet handlers run on the
> game thread" is exactly backwards for the first two phases: the handshake
> and login listeners' handlers run to completion on the Netty thread, and the first
> `PacketUtils.ensureRunningOnSameThread` in a connection's life is in
> configuration.

## Where to look

Read the four listeners, the server's three in the order a login meets them,
and the whole page is in them. **`ServerHandshakePacketListenerImpl`** is
little more than one switch. **`ServerLoginPacketListenerImpl`** is the one to spend time on: read
`ServerLoginPacketListenerImpl.tick` first, then the handlers below it, and
the volatile state field above both will explain itself.
**`ServerConfigurationPacketListenerImpl`** is the task queue, and
`ServerConfigurationPacketListenerImpl.handleConfigurationFinished` is where
the player is finally built. **`ClientHandshakePacketListenerImpl`** is the
other side of the encryption exchange and worth reading against the server's.

Then the tasks themselves — **`ConfigurationTask`** is a three-method
interface, and **`PrepareSpawnTask`** and **`JoinWorldTask`** are the two that
matter here; **`SynchronizeRegistriesTask`** is the reason the phase exists
and is Part II's subject as much as this page's.

Two doors this page only points at: **`ConnectionProtocol`**, which takes ten
seconds to read and is worth it for how little is in it, and **`Crypt`**, the
key, digest, cipher and salt helpers the login and chat signing share, in one
utility class.

---

*Rules: names, never code · how the system works, not how the code reads ·
newest version only · every backticked name passes `tools/verify_names.py`.*
