Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

classwhat it decidesthread
PlayerListwho is admitted, who is in the tab list, and the exact order of the join burstServer
ServerPlayerone player’s world state, and what a new one inherits from an old oneServer
ServerGamePacketListenerImplthe session: the player it points at, the flush suspension, the load gate, the kicksNetty to decode, Server to handle
ServerConfigurationPacketListenerImplthe strictly serial task queue a join is prepared inNetty, with four handlers hopping to Server
PrepareSpawnTaskwhere the player will stand, and when the ServerPlayer is finally constructedServer, or Netty when accepting a code of conduct starts it
PlayerDataStoragethe .dat file, its .dat_old twin and its corrupt-copy rescuewhichever thread starts the task, then Server
PlayerChunkSenderhow many chunks a client is trusted with, starting at one unacknowledged batchServer
CommonListenerCookiethe 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.

respawndimension changedisconnectServerGamePacketListenerImpl.switchToConfig
who runs itServerGamePacketListenerImpl.handleClientCommandServerPlayer.teleportConnection.handleDisconnectionDebugConfigCommand
same ServerPlayer?no — PlayerList.respawn constructs a new oneyesyes, brieflyyes, then a new one on the way back
same entity id?yes, copied over by Entity.setIdyes—no, the rebuilt player gets a fresh one
same connection listener?yes, ServerPlayer.connection is reassignedyes—no, a new one from PlayerList.placeNewPlayer
what carries overwhatever ServerPlayer.restoreFrom copieseverything, because nothing is copied—whatever the .dat holds
is the .dat written?nonoyes, inside PlayerList.removeyes, the same call
the client is toldClientboundRespawnPacket, keeping nothing, or attribute modifiersClientboundRespawnPacket, ClientboundRespawnPacket.KEEP_ALL_DATAnothing, the channel is already goneClientboundStartConfigurationPacket
removal reasonEntity.RemovalReason.KILLEDEntity.RemovalReason.CHANGED_DIMENSIONEntity.RemovalReason.UNLOADED_WITH_PLAYERthe 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.