<!-- https://minecraftdocs.dev/systems/server/players-and-sessions — 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 -->

# Players and sessions

> Verified against **Minecraft 26.3** · Part III · One connection, four events: a player joins, dies, walks into a Nether portal, and logs out.

A player clicks a server in the list, watches a progress bar, and is
standing in a world. An hour later they fall in lava, press *Respawn*, walk
through a portal and log off for the night. On the server those are four
events on one object graph, and they disagree about what survives. Dying
**destroys** the `ServerPlayer` and builds a new one; a trip to the Nether
keeps the same object and moves it. Both keep the player's entity id, and
both keep the very same `ServerGamePacketListenerImpl` — a respawn does not
rebuild the connection, it repoints it, in one field assignment, at the new
player. Which is why anyone still holding the old `ServerPlayer` after a
respawn is holding a corpse: an object flagged removed, out of every list,
that will keep answering questions all day.

## The cast

| class | what it decides | thread |
|---|---|---|
| `PlayerList` | who is admitted, who is in the tab list, and the exact order of the join burst | Server |
| `ServerPlayer` | one player's world state, and what a new one inherits from an old one | Server |
| `ServerGamePacketListenerImpl` | the session: the player it points at, the flush suspension, the load gate, the kicks | Netty to decode, Server to handle |
| `ServerConfigurationPacketListenerImpl` | the strictly serial task queue a join is prepared in | Netty, with four handlers hopping to Server |
| `PrepareSpawnTask` | where the player will stand, and when the `ServerPlayer` is finally constructed | Server, or Netty when accepting a code of conduct starts it |
| `PlayerDataStorage` | the `.dat` file, its *.dat_old* twin and its corrupt-copy rescue | whichever thread starts the task, then Server |
| `PlayerChunkSender` | how many chunks a client is trusted with, starting at one unacknowledged batch | Server |
| `CommonListenerCookie` | the four facts that survive a phase change: profile, latency, client options, transferred | — |

The phases this trace passes through — login, configuration, play, and the
terminal packets between them — belong to
[protocol phases](../networking/protocol-phases). This page starts where
that one hands over: with `PlayerList` deciding whether the login is allowed
at all.

## Admission is a `Component` or nothing

Identity here is a `NameAndId`: a record of a UUID and a name, made from the
authenticated `GameProfile`, and the key that the ban list, the whitelist, every
op lookup and every save file are addressed by (the IP ban list keys on the
address). `UserNameToIdResolver`,
reached through `MinecraftServer.services`, is the cache that maps between
the two halves — a `CachedUserNameToIdResolver` over *usercache.json*, with a
`GameProfileRepository` behind it for the lookups the file cannot answer — and
comparing its remembered name against the profile's is how the server notices
a player has been renamed since their last visit.

### Four questions, and the two ways past them

`PlayerList.canPlayerLogin` returns a `Component` — the reason to refuse,
rendered on the disconnect screen — or null for *let them in*. It asks
four questions in order — the ban list, the whitelist, the IP ban list, then
capacity — and the first three ask the same kind of object. A `StoredUserList`
is a JSON file of `StoredUserEntry` records keyed by identity (the IP bans by
address), rewritten whole
on every change, and there are four of them: `UserBanList`, `IpBanList`,
`UserWhiteList` and the `ServerOpList` that both surprises below turn on. The two
ban lists share a `BanListEntry` carrying a source, a reason and an expiry,
and nothing sweeps that expiry on a schedule: `StoredUserList.get` drops what
has lapsed before it answers, but the login gate asks `UserBanList.isBanned`
or `IpBanList.isBanned` first, which do not. So the first login after a
temporary ban lapses finds the ban, fetches an entry that is no longer there,
and throws; the server logs *Internal server error* and closes the connection,
and the sweep that caused it lets the next attempt in. The two ways past the gate are not the ones a reader expects.
The
whitelist is bypassed by being an **op**: `PlayerList.isWhiteListed` is
satisfied by presence in the op list, and `DedicatedPlayerList.isWhiteListed`
overrides it to route through `PlayerList.isOp`, which also asks whether
the identity is the singleplayer owner — a branch that never fires here,
because `DedicatedServer.isSingleplayerOwner` returns false for everyone.
The *bypassesPlayerLimit* flag in *ops.json* is a
separate thing entirely and reaches exactly one of the four questions: of
them, only the capacity test reads `ServerOpListEntry.bypassesPlayerLimit`,
through `DedicatedPlayerList.canBypassPlayerLimit`. A banned op is still banned.

### The gate runs twice, and disagrees with itself

The gate runs **twice**, and the second run is not a repeat of the first.
`ServerLoginPacketListenerImpl.verifyLoginAndFinishConnectionSetup` runs it
during login; `ServerConfigurationPacketListenerImpl.handleConfigurationFinished`
runs it again when the client says configuration is over, because a ban or a
newly full server can land in the seconds a configuration takes. Duplicate
logins differ between the two. At login the newcomer wins:
`PlayerList.disconnectAllPlayersWithProfile` kicks every player holding
that UUID with `PlayerList.DUPLICATE_LOGIN_DISCONNECT_MESSAGE`, and the login parks until the old player has left the player list. At the second check the
newcomer loses — an existing player with that id is a flat rejection, since
by then there is a prepared spawn to throw away rather than a session to
evict.

## Preparing a place to stand

```mermaid
sequenceDiagram
    participant SLPL as ServerLoginPacket<br/>ListenerImpl
    participant PL as PlayerList
    participant SCPL as ServerConfiguration<br/>PacketListenerImpl
    participant PST as PrepareSpawnTask
    participant PDS as PlayerDataStorage
    participant SL as ServerLevel

    SLPL->>PL: canPlayerLogin, then disconnectAllPlayersWithProfile
    SLPL->>SCPL: startConfiguration, on a listener just built
    Note over SCPL: one task at a time, never two at once
    SCPL->>SCPL: Synchronize<br/>RegistriesTask<br/>first, then the<br/>code of conduct<br/>or a resource pack
    SCPL->>PST: start, appended by returnToWorld with a JoinWorldTask behind it
    PST->>PL: loadPlayerData, for the saved position
    PL->>PDS: load, the whole file read and datafixed
    PST->>SL: a PLAYER_SPAWN ticket at radius 3, through its chunk source
    rect rgba(0, 0, 0, 0.04)
        Note over SCPL,SL: every tick until the chunks land, the client still in configuration
        SL-->>PST: the load future completes, Preparing becomes Ready
    end
    SCPL->>SCPL: JoinWorldTask sends the finish packet
    SCPL->>PL: the duplicate check again, then canPlayerLogin
    SCPL->>PST: spawnPlayer
```

*A join assembled while the client waits: the queue above the band runs one
task at a time and never overlaps, and the band is where the waiting happens —
a world being built for a client that has no idea.*

Nothing in that queue overlaps.
`ServerConfigurationPacketListenerImpl.startNextTask` throws rather than
start a task while another is unfinished, so the registry transfer completes
before `PrepareSpawnTask` reads a byte, and the two optional tasks sit
between them. What *does* run alongside it is the chunk load and the
client's wait: once the ticket is placed the task simply reports *not
finished* from `ConfigurationTask.tick` each tick, and the client spends
that time in configuration with no idea a world is being assembled for it.

The ticket needs re-arming, but not yet. `TicketType.PLAYER_SPAWN` is
registered with a timeout of twenty ticks and with `TicketType.FLAG_LOADING` as its only
flag, so `TicketType.canExpireIfUnloaded` is false and the timeout does not
begin while the chunks it asked for are still on their way — which is what
lets `ServerChunkCache.addTicketAndLoadWithRadius` accept this type at all,
since it throws for any type that could expire before it loads. From the
moment the task reaches `PrepareSpawnTask.Ready` the clock does run, and
`PrepareSpawnTask.keepAlive` — called from
`ServerConfigurationPacketListenerImpl.tick` — re-adds the ticket at
**radius 3**, the radius the figure shows, every tick. Without that, a client
slow to acknowledge the finish packet would arrive to find its spawn chunks
expired underneath it.

A player with no save file gets a search instead of a position, except on a
world whose game mode is Adventure, where the world spawn is used without a
search, only its height corrected.
`PlayerSpawnFinder.findSpawn` walks up to
`PlayerSpawnFinder.ABSOLUTE_MAX_ATTEMPTS` candidates — a thousand and
twenty-four, or fewer if `GameRules.RESPAWN_RADIUS` or the world border says
so — in a strided order from a random offset, loading each
candidate's chunk under a one-tick `TicketType.SPAWN_SEARCH` ticket and
returning a future. The `ChunkLoadCounter` beside it feeds only the
server's `LevelLoadListener`, through
`LevelLoadListener.Stage.LOAD_PLAYER_CHUNKS` — the client's progress bar on
an integrated server, and nothing a remote player ever sees on a dedicated
one, where the listener is a `LoggingLevelLoadListener` that boot already
closed.

## The save file is read twice, and both reads are the whole file

`PrepareSpawnTask.start` reads the `.dat` and decodes
`ServerPlayer.SavedPosition` from it — three optional fields, dimension,
position and rotation, and nothing else. That is all it *decodes*. It is not
all it does: `PlayerDataStorage.load` reads the compressed tag with an
unlimited accounter and runs `DataFixTypes.PLAYER` over the entire document
before any codec sees it ([codecs, NBT and
JSON](../foundations/codecs-nbt-json)). So the narrow read saves decoding,
not I/O, and a join pays the migration cost twice — once here, and once in
`PrepareSpawnTask.Ready`, where the file is loaded again for the full
`Entity.load`.

### Between the two reads, the player is built

`ServerLevel.waitForEntities`
blocks the Server thread until the entities in the spawn chunks have
finished loading, and then the `ServerPlayer` constructor runs, pulling its
`ServerStatsCounter` and `PlayerAdvancements` out of `PlayerList` on the way
past and applying the `ClientInformation` the client sent in configuration. The second read fills the
object in, the player is snapped to the prepared position,
`PlayerList.placeNewPlayer` runs, and only afterwards do
`ServerPlayer.loadAndSpawnEnderPearls` and
`ServerPlayer.loadAndSpawnParentVehicle` put back the ender pearls the player
had in flight and what they were sitting on when they left, both re-created
from the file.

### Two rescues wired into the read

Two rescues are wired into the read, and the first is the file's own.
`PlayerDataStorage.load` tries *\<uuid\>.dat*; if that comes back with
nothing — the file is missing, or `NbtIo.readCompressed` threw and logged
*Failed to load player data* — it copies whatever is there aside as
*\<uuid\>_corrupted_\<timestamp\>.dat* and then tries *\<uuid\>.dat_old*,
the twin the previous write rotated out. So a corrupted file is kept — under
the *corrupted* name, for whoever wants to look at it — and the player loses
what changed since the save before last rather than everything; a player with no readable file and no
twin is built from nothing, which is a new spawn rather than an error. Only
what survives that is datafixed.

The second rescue is about identity, and only an integrated server has it.
`PlayerList.loadPlayerData` checks whether the joining identity is the
singleplayer owner and, if the world
records a *singleplayer_uuid* in its level data, loads **that** file rather
than the one named for the joining id. It works for one session: the save writes under
the current id and `MinecraftServer.saveAllChunks` stamps the current
owner's id into *level.dat*, so the next time the world is opened the old file
is orphaned.

## `PlayerList.placeNewPlayer` sends a world in one write

```mermaid
sequenceDiagram
    participant PL as PlayerList
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant SL as ServerLevel
    participant CM as ChunkMap
    participant Wire as the network

    Note over PL,Wire: in the packet drain at the top of a tick
    PL->>SGPL: suspendFlushing, on a listener already set to play
    PL->>SGPL: ClientboundLoginPacket, difficulty, abilities, held slot, recipes
    PL->>SGPL: the permission level as an entity event, then the command tree
    PL->>SGPL: recipe book, scoreboard, teleport, status
    PL->>SGPL: the tab list as it stands, everyone already here
    PL->>PL: the joiner is added to PlayerList.players
    PL->>SGPL: the joiner, to everyone, themselves included
    PL->>SGPL: the border, the clocks, the spawn, the rain, LEVEL_CHUNKS_LOAD_START, the tick rate
    PL->>SL: addNewPlayer
    SL->>CM: addEntity, then updatePlayerStatus and the first ChunkTrackingView
    CM->>SGPL: every ready chunk in that view marked pending on PlayerChunkSender
    PL->>SGPL: the boss bars, the effects, the inventory menu, then resumeFlushing
    SGPL->>Wire: one write
    Note over SL,Wire: later in the same tick, the first chunk batch, and only one
```

*Everything `PlayerList.placeNewPlayer` sends the joiner, and the single write
at the foot that carries it; the self-message in the middle is the step the
tab-list order turns on.*

The whole method sits inside one suspension, so everything above — a login
packet, a command tree, a scoreboard, a tab list, a world border — leaves as a
single write; what it tells the players already there goes out on their own
connections, one packet at a time. It is the same
`ServerCommonPacketListenerImpl.suspendFlushing` bracket [the server
tick](server-tick#the-two-writes-each-client-gets) puts around every client
every tick, and the one place in the game it is opened by hand: a join is
handled in the packet drain that runs *before*
`MinecraftServer.tickChildren` opens the tick's own.

`ClientboundLoginPacket` is where the client learns the entity id it is
about to be given, the hardcore flag, every dimension key the server has,
the view and simulation distances, whether the server authenticates, and a
`CommonPlayerSpawnInfo` from `ServerPlayer.createCommonSpawnInfo` carrying
the dimension type, the obfuscated seed, both game modes, the last death
location, the portal cooldown and the sea level. Three game rules are pinned
into it as plain booleans and never re-sent by this path:
`GameRules.REDUCED_DEBUG_INFO`, `GameRules.LIMITED_CRAFTING`, and
`GameRules.IMMEDIATE_RESPAWN` inverted into *show the death screen*.

The permission level arrives twice over, as a `ClientboundEntityEventPacket`
carrying one of five event ids and as the whole command tree from
`Commands.sendCommands`, both from `PlayerList.sendPlayerPermissionLevel`
resolving a `LevelBasedPermissionSet` out of
`MinecraftServer.getProfilePermissions`. Where such a set comes from is
[permissions](../commands/permissions#where-a-set-comes-from); all a join
needs to know is that both halves are re-sent whenever `PlayerList.op` or
`PlayerList.deop` changes it.

The tab list goes out in a deliberate order, and it is the middle step that
makes it one: the joiner is sent everyone already present, *then* added to
`PlayerList.players`, *then* everyone — themselves included — is sent the
joiner. Near the end of `PlayerList.sendLevelInfo`, with only the tick rate
after it, comes `ClientboundGameEventPacket.LEVEL_CHUNKS_LOAD_START`, the
client's signal to stop waiting for the server and start waiting for its own
chunk. And the chat join message, which goes to the players already there and
not to the joiner, uses *multiplayer.player.joined.renamed* when the name in
the profile differs from the one the name cache remembered, which is why that
name is read at the top of the method, before the cache is overwritten.

Entering the level is the step that starts the terrain, though it is not
the last: the boss bars, the active effects, the inventory menu and
`NotificationManager.playerJoined` — which is not the chat message, sent
before the player entered the level — all follow it, and `ServerCommonPacketListenerImpl.resumeFlushing`
closes the single write.
`ServerLevel.addNewPlayer` hands the player to
`PersistentEntitySectionManager.addNewEntity`, whose callback adds it to
`ServerLevel.players` and to the chunk source; `ChunkMap.updatePlayerStatus`
registers the player with `DistanceManager`, resets its chunk tracking to
`ChunkTrackingView.EMPTY` and computes the real one, and every chunk inside
that view that is ready to send is marked pending on the connection's
`PlayerChunkSender`. So
there are two player lists, written by different systems:
`PlayerList.players` is *who is on the server* and `PlayerList` alone writes
it, while `ServerLevel.players` is *whose entity is in this level* and only
the entity manager's tracking callbacks write it. A player halfway through a
dimension change is in the first and in neither copy of the second.

**A joining client is trusted with one batch**, which is the only thing about
the chunk-sending loop that is a fact about *joining*: `PlayerChunkSender`
starts with a budget of a single unacknowledged batch, so the first batch a
player ever receives is a hard round trip before the second can leave. The
loop it then settles into — the client's measured rate, the clamps and the
nearest-first order — belongs to [what the client is
told](../networking/what-the-client-is-told#the-rate-the-client-asks-for),
and which chunks are in the player's set at all, to [tickets and
loading](../world/tickets-and-loading#which-chunks-a-player-is-owed-and-what-makes-one-eligible).

## Loaded is something the client says

The player is in the world before the client can play.
`ServerGamePacketListenerImpl.hasClientLoaded` gates movement, block
breaking, item use, sprinting and sneaking, and it answers false until
either the client sends `ServerboundPlayerLoadedPacket` or a sixty-tick
timer runs out — `ServerGamePacketListenerImpl.CLIENT_LOADED_TIMEOUT_TIME`,
started in the listener's constructor and counted down from
`ServerPlayer.tick`, which is to say from the level's entity loop rather
than from the connection.

Death re-arms the same gate by a different mechanism.
`ServerGamePacketListenerImpl.markClientUnloadedAfterDeath`, called at the
end of `ServerPlayer.die`, sets a flag that no timer clears: a dead player's
client counts as unloaded indefinitely, and only
`ServerGamePacketListenerImpl.restartClientLoadTimerAfterRespawn` — reached
from the respawn, or from a brand-new listener — clears the flag and starts
the sixty ticks again. The death screen is held open by the same gate as the
*Joining world* screen, through a different field.

That the countdown ticks from `ServerPlayer.tick` while the food, health and
stat sync tick from `ServerPlayer.doTick` is the whole reason a player is
ticked from two places every tick; [the two-phase
tick](../player/the-two-phase-tick#both-halves-in-the-order-they-run) is
the lecture on that. The fact worth
carrying out of here is that the level's entity loop ticks players
*regardless* of entity-ticking range.

## Four ways the session changes

The join is one story; what happens afterwards is a comparison. Three of
these a player will meet tonight and the fourth exists for one debug
command, and they disagree about almost everything.

| | respawn | dimension change | disconnect | `ServerGamePacketListenerImpl.switchToConfig` |
|---|---|---|---|---|
| **who runs it** | `ServerGamePacketListenerImpl.handleClientCommand` | `ServerPlayer.teleport` | `Connection.handleDisconnection` | `DebugConfigCommand` |
| **same `ServerPlayer`?** | no — `PlayerList.respawn` constructs a new one | yes | yes, briefly | yes, then a new one on the way back |
| **same entity id?** | yes, copied over by `Entity.setId` | yes | — | no, the rebuilt player gets a fresh one |
| **same connection listener?** | yes, `ServerPlayer.connection` is reassigned | yes | — | no, a new one from `PlayerList.placeNewPlayer` |
| **what carries over** | whatever `ServerPlayer.restoreFrom` copies | everything, because nothing is copied | — | whatever the `.dat` holds |
| **is the `.dat` written?** | no | no | yes, inside `PlayerList.remove` | yes, the same call |
| **the client is told** | `ClientboundRespawnPacket`, keeping nothing, or attribute modifiers | `ClientboundRespawnPacket`, `ClientboundRespawnPacket.KEEP_ALL_DATA` | nothing, the channel is already gone | `ClientboundStartConfigurationPacket` |
| **removal reason** | `Entity.RemovalReason.KILLED` | `Entity.RemovalReason.CHANGED_DIMENSION` | `Entity.RemovalReason.UNLOADED_WITH_PLAYER` | the same |

### The object, and the reference that outlives it

`PlayerList.respawn` builds the new `ServerPlayer` from the old one's
profile and client information, then performs three assignments that keep
the difference out of every tab list: `ServerPlayer.connection` is set to
the old player's listener, `Entity.setId` copies the entity id, and the
listener's own player field is reassigned by its caller. To every other
client's tab list nothing has happened — same id, same UUID, same row — though
entity tracking removes the old body and adds the new one, and a hardcore
respawn sends the row's new game mode. To
anything inside the server holding the old object, everything has happened:
it was removed from its level before the new one existed, and it is not in
`PlayerList.players` any more.

Only two of the four paths take a `TeleportTransition`, and they are the two
that move you. `ServerPlayer.findRespawnPositionAndUseSpawnBlock` turns the
saved `ServerPlayer.RespawnConfig` — respawn data plus a *forced* flag —
into one: a respawn anchor is found and a charge spent unless the point is
forced, a bed is found and costs nothing,
`TeleportTransition.missingRespawnBlock` says the block is gone, and
`TeleportTransition.createDefault` falls back to the world spawn. Both of
those last two are the expensive branch, not just the fallback: each builds
its position with `TeleportTransition.findAdjustedSharedSpawnPos`, so each
runs `ServerPlayer.adjustSpawnLocation` — the same `PlayerSpawnFinder` search
a join runs asynchronously — and blocks the Server thread on the future with
`BlockableEventLoop.managedBlock`.

### What comes across when you die

`ServerPlayer.restoreFrom` has two branches, and the interesting one is not
the one people name it for. Its *restore everything* branch — permanent
attribute modifiers, health, hunger, every active effect, the inventory, the
portal state — is reached only when `PlayerList.respawn` is called with
`Entity.RemovalReason.CHANGED_DIMENSION`, which happens in exactly one place:
a player leaving the end credits, with `ServerPlayer.wonGame`
set. That is the End-portal return, not *keepInventory*.

Which makes the end credits a fifth way out of a `ServerLevel`, and the
reason the table above has four columns rather than five:
`ServerPlayer.showEndCredits` removes the player with
`Entity.RemovalReason.CHANGED_DIMENSION` like a dimension change, and the
client, when the credits end or are closed, sends the respawn that hands it to
`PlayerList.respawn` like a death. It is not a way a session
changes so much as the one place two of them meet.

An ordinary death takes the other branch. Health is reset to maximum,
effects are gone, and `GameRules.KEEP_INVENTORY` — or having died as a
spectator — decides only whether `ServerPlayer.restoreFrom` runs
`ServerPlayer.transferInventoryXpAndScore`, which moves the inventory, the
experience and the score and nothing else. What the same rule decided on the
way *out*, when the old player died, belongs to [damage and
death](../entities/damage-and-death#the-death-screen-and-what-the-client-does-alone).
Outside the branch, and so true of every death, a long tail is copied
unconditionally: the ender chest, the enchantment seed, both game modes,
base attribute values, the recipe book, the warden spawn tracker, the chat
session, the skin customisation, the last death location. The ender chest
surviving death is not a game rule; it is a field assignment.

### Why the Nether keeps your potion effects

Because nothing is copied. `ServerPlayer.teleport` across a dimension
boundary sets `ServerPlayer.isChangingDimension`, sends the client a
`ClientboundRespawnPacket` marked `ClientboundRespawnPacket.KEEP_ALL_DATA`,
removes the entity from the old level with
`Entity.RemovalReason.CHANGED_DIMENSION` — one of the two reasons that
neither destroy an entity nor write it into a chunk ([entity
lifecycle](../entities/entity-lifecycle#five-reasons-one-label) has the
five) — immediately calls
`Entity.unsetRemoved`, points it at the new level with
`ServerPlayer.setServerLevel`, and adds it back with
`ServerLevel.addDuringTeleport`. It is one object the whole way. The effects
survive because they were never touched, and what the server then re-sends
— level info, the inventory through `PlayerList.sendAllPlayerInfo`, the
active effects, the abilities, the permission level — is not a restoration
but a **re-sync**, because the client threw its own world away the moment
the respawn packet arrived.

A same-dimension teleport is not this path at all. It is
`ServerGamePacketListenerImpl.teleport` followed by
`ServerGamePacketListenerImpl.resetPosition`, with no client-side world
discarded and no respawn packet sent.

### Where your llama goes when you log out

The channel closes on a Netty thread and nothing happens until
`ServerConnectionListener.tick`, in the connection step of the server tick,
notices and calls `Connection.handleDisconnection` ([the
connection](../networking/the-connection#how-a-connection-dies) is the
channel's own account of dying). That reaches
`ServerGamePacketListenerImpl.removePlayerFromWorld`: the leave message,
`ServerPlayer.disconnect` to eject passengers and stop sleeping, and
`PlayerList.remove`.

The order inside `PlayerList.remove` is the answer to the question. The
`.dat` is written **first**, with the stats and advancements JSON beside it,
and `ServerPlayer.saveParentVehicle` writes the entire root vehicle into it
under *RootVehicle* — but only if that vehicle has exactly one player
passenger. The same test then decides whether the vehicle chain is removed
from the world with `Entity.RemovalReason.UNLOADED_WITH_PLAYER` — the other
of those two non-destroying reasons, which is exactly what you want for
something already written into a different file.
In-flight ender pearls go the same way, saved into the file and removed from
the world. So the llama leaves the world with you, rides in your save, and
is put back by `ServerPlayer.loadAndSpawnParentVehicle` after your next
`PlayerList.placeNewPlayer` — unless somebody else was riding it too, in
which case it stays where it is and you land beside it. On an integrated
server one further thing happens:
`ServerCommonPacketListenerImpl.onDisconnect` sees that the leaver is the
singleplayer owner and halts the server ([how a server
dies](how-a-server-dies), which owns `PlayerList.saveAll` and
`PlayerList.removeAll` at shutdown).

`ServerGamePacketListenerImpl.switchToConfig` is that same removal with the
connection left alive. It runs `ServerGamePacketListenerImpl.removePlayerFromWorld` — leave message,
saved file, tab-list removal and all — which is the half that belongs here:
as far as this page's four ways are concerned, a reconfigure *is* a leave
that keeps the socket. What the connection then does with the phase, and why
no registry is re-sent on the way back, belong to [protocol
phases](../networking/protocol-phases#play-and-the-way-back). `CommonListenerCookie` carries the client's options and the transferred
flag across that return, as it does across every phase change.

### What everyone else is told

Very little, and nearly all of it on the way out. A respawn and a dimension change send
other clients nothing for the tab list, a hardcore respawn's game mode aside,
because the identity a tab list is keyed on never changed. A disconnect sends one
`ClientboundPlayerInfoRemovePacket` to everybody, and otherwise a tab list
hears a row's game mode, hat or chat session when one changes and the latency
sweep `PlayerList.tick` broadcasts on its own slow counter ([the server
tick](server-tick#what-minecraftservertickchildren-runs-and-in-what-order)).
The rest of what other players see is entity tracking in `ChunkMap`, and the
removal reasons in the table above have already told it what to do.

## The three kicks that come from the tick

`ServerGamePacketListenerImpl.tick` ends sessions of its own accord. An idle
player is disconnected after `MinecraftServer.playerIdleTimeout` minutes
with no action, unless they are sitting on the end credits. A player whose
client reports itself airborne for too long is kicked for flying, on a budget
that stretches as gravity falls and against a list of exemptions the movement
page owns ([input to
movement](../player/input-to-movement#floating-and-everything-exempted-from-it)). And
keep-alive disconnects a client that has stopped answering — a mechanism that
belongs to the connection rather than to the session, because it runs in the
configuration phase too ([the
connection](../networking/the-connection#how-a-connection-dies)).

The asymmetry is what this section is about. **The singleplayer owner is
exempt from the keep-alive, the one kick of the three that could reach it**:
the keep-alive alone asks
`ServerCommonPacketListenerImpl.isSingleplayerOwner`, and the host exists only
on an integrated server, which allows flight and sets no idle timeout, so
nobody there is kicked for either of the others.



## Where to look

`PlayerList.canPlayerLogin` · `PlayerList.placeNewPlayer` ·
`PlayerList.respawn` · `PlayerList.remove` · `PlayerList.loadPlayerData` ·
`PlayerList.sendLevelInfo` · `PrepareSpawnTask` · `JoinWorldTask` ·
`PlayerSpawnFinder` · `PlayerDataStorage` · `ServerPlayer.restoreFrom` ·
`ServerPlayer.teleport` ·
`ServerPlayer.findRespawnPositionAndUseSpawnBlock` · `ServerPlayer.die` ·
`ServerPlayer.doTick` ·
`ServerGamePacketListenerImpl.removePlayerFromWorld` ·
`ServerGamePacketListenerImpl.switchToConfig` ·
`ServerGamePacketListenerImpl.hasClientLoaded` ·
`ServerCommonPacketListenerImpl` · `PlayerChunkSender` ·
`ChunkMap.updatePlayerStatus` · `TeleportTransition`

---

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