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. 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
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). 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
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 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; 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,
and which chunks are in the player’s set at all, to tickets and
loading.
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 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.
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 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 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, 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. 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).
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). 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).
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.