diff --git a/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch b/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch new file mode 100644 index 0000000000..90f421d2a8 --- /dev/null +++ b/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch @@ -0,0 +1,1142 @@ +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -112,14 +_,16 @@ + private static final int SEND_PLAYER_INFO_INTERVAL = 600; + private static final SimpleDateFormat BAN_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z"); + private final MinecraftServer server; +- public final List players = Lists.newArrayList(); ++ public final List players = new java.util.concurrent.CopyOnWriteArrayList(); // CraftBukkit - ArrayList -> CopyOnWriteArrayList: Iterator safety + private final Map playersByUUID = Maps.newHashMap(); + private final UserBanList bans = new UserBanList(USERBANLIST_FILE); + private final IpBanList ipBans = new IpBanList(IPBANLIST_FILE); + private final ServerOpList ops = new ServerOpList(OPLIST_FILE); + private final UserWhiteList whitelist = new UserWhiteList(WHITELIST_FILE); +- private final Map stats = Maps.newHashMap(); +- private final Map advancements = Maps.newHashMap(); ++ // CraftBukkit start ++ // private final Map stats = Maps.newHashMap(); ++ // private final Map advancements = Maps.newHashMap(); ++ // CraftBukkit end + public final PlayerDataStorage playerIo; + private boolean doWhiteList; + private final LayeredRegistryAccess registries; +@@ -130,14 +_,26 @@ + private static final boolean ALLOW_LOGOUTIVATOR = false; + private int sendAllPlayerInfoIn; + ++ // CraftBukkit start ++ private org.bukkit.craftbukkit.CraftServer cserver; ++ private final Map playersByName = new java.util.HashMap<>(); ++ public @Nullable String collideRuleTeamName; // Paper - Configurable player collision ++ + public PlayerList(MinecraftServer server, LayeredRegistryAccess registries, PlayerDataStorage playerIo, int maxPlayers) { ++ this.cserver = server.server = new org.bukkit.craftbukkit.CraftServer((net.minecraft.server.dedicated.DedicatedServer) server, this); ++ server.console = new com.destroystokyo.paper.console.TerminalConsoleCommandSender(); // Paper ++ // CraftBukkit end + this.server = server; + this.registries = registries; + this.maxPlayers = maxPlayers; + this.playerIo = playerIo; + } + ++ abstract public void loadAndSaveFiles(); // Paper - fix converting txt to json file; moved from DedicatedPlayerList constructor ++ + public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie cookie) { ++ player.isRealPlayer = true; // Paper ++ player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed + GameProfile gameProfile = player.getGameProfile(); + GameProfileCache profileCache = this.server.getProfileCache(); + String string; +@@ -150,30 +_,94 @@ + } + + Optional optional = this.load(player); +- ResourceKey resourceKey = optional.>flatMap( +- compoundTag -> DimensionType.parseLegacy(new Dynamic<>(NbtOps.INSTANCE, compoundTag.get("Dimension"))).resultOrPartial(LOGGER::error) ++ // CraftBukkit start - Better rename detection ++ if (optional.isPresent()) { ++ CompoundTag nbttagcompound = optional.get(); ++ if (nbttagcompound.contains("bukkit")) { ++ CompoundTag bukkit = nbttagcompound.getCompound("bukkit"); ++ string = bukkit.contains("lastKnownName", 8) ? bukkit.getString("lastKnownName") : string; ++ } ++ } ++ // CraftBukkit end ++ // Paper start - move logic in Entity to here, to use bukkit supplied world UUID & reset to main world spawn if no valid world is found ++ ResourceKey resourceKey = null; // Paper ++ boolean[] invalidPlayerWorld = {false}; ++ bukkitData: if (optional.isPresent()) { ++ // The main way for bukkit worlds to store the world is the world UUID despite mojang adding custom worlds ++ final org.bukkit.World bWorld; ++ if (optional.get().contains("WorldUUIDMost") && optional.get().contains("WorldUUIDLeast")) { ++ bWorld = org.bukkit.Bukkit.getServer().getWorld(new UUID(optional.get().getLong("WorldUUIDMost"), optional.get().getLong("WorldUUIDLeast"))); ++ } else if (optional.get().contains("world", net.minecraft.nbt.Tag.TAG_STRING)) { // Paper - legacy bukkit world name ++ bWorld = org.bukkit.Bukkit.getServer().getWorld(optional.get().getString("world")); ++ } else { ++ break bukkitData; // if neither of the bukkit data points exist, proceed to the vanilla migration section ++ } ++ if (bWorld != null) { ++ resourceKey = ((org.bukkit.craftbukkit.CraftWorld) bWorld).getHandle().dimension(); ++ } else { ++ resourceKey = Level.OVERWORLD; ++ invalidPlayerWorld[0] = true; ++ } ++ } ++ if (resourceKey == null) { // only run the vanilla logic if we haven't found a world from the bukkit data ++ // Below is the vanilla way of getting the dimension, this is for migration from vanilla servers ++ resourceKey = optional.>flatMap( ++ compoundTag -> { ++ com.mojang.serialization.DataResult> dataResult = DimensionType.parseLegacy(new Dynamic<>(NbtOps.INSTANCE, compoundTag.get("Dimension"))); ++ final Optional> result = dataResult.resultOrPartial(LOGGER::error); ++ invalidPlayerWorld[0] = result.isEmpty(); // reset to main world spawn if no valid world is found ++ return result; ++ } + ) +- .orElse(Level.OVERWORLD); ++ .orElse(Level.OVERWORLD); // revert to vanilla default main world, this isn't an "invalid world" since no player data existed ++ } ++ // Paper end + ServerLevel level = this.server.getLevel(resourceKey); + ServerLevel serverLevel; + if (level == null) { + LOGGER.warn("Unknown respawn dimension {}, defaulting to overworld", resourceKey); + serverLevel = this.server.overworld(); ++ invalidPlayerWorld[0] = true; // Paper - reset to main world if no world with parsed value is found + } else { + serverLevel = level; + } + ++ // Paper start - Entity#getEntitySpawnReason ++ if (optional.isEmpty()) { ++ player.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT; // set Player SpawnReason to DEFAULT on first login ++ // Paper start - reset to main world spawn if first spawn or invalid world ++ } ++ if (optional.isEmpty() || invalidPlayerWorld[0]) { ++ // Paper end - reset to main world spawn if first spawn or invalid world ++ player.moveTo(player.adjustSpawnLocation(serverLevel, serverLevel.getSharedSpawnPos()).getBottomCenter(), serverLevel.getSharedSpawnAngle(), 0.0F); // Paper - MC-200092 - fix first spawn pos yaw being ignored ++ } ++ // Paper end - Entity#getEntitySpawnReason + player.setServerLevel(serverLevel); + String loggableAddress = connection.getLoggableAddress(this.server.logIPs()); +- LOGGER.info( +- "{}[{}] logged in with entity id {} at ({}, {}, {})", +- player.getName().getString(), +- loggableAddress, +- player.getId(), +- player.getX(), +- player.getY(), +- player.getZ() +- ); ++ // Spigot start - spawn location event ++ org.bukkit.entity.Player spawnPlayer = player.getBukkitEntity(); ++ org.spigotmc.event.player.PlayerSpawnLocationEvent ev = new org.spigotmc.event.player.PlayerSpawnLocationEvent(spawnPlayer, spawnPlayer.getLocation()); ++ this.cserver.getPluginManager().callEvent(ev); ++ ++ org.bukkit.Location loc = ev.getSpawnLocation(); ++ serverLevel = ((org.bukkit.craftbukkit.CraftWorld) loc.getWorld()).getHandle(); ++ ++ player.spawnIn(serverLevel); ++ player.gameMode.setLevel((ServerLevel) player.level()); ++ // Paper start - set raw so we aren't fully joined to the world (not added to chunk or world) ++ player.setPosRaw(loc.getX(), loc.getY(), loc.getZ()); ++ player.setRot(loc.getYaw(), loc.getPitch()); ++ // Paper end - set raw so we aren't fully joined to the world ++ // Spigot end ++ // LOGGER.info( // CraftBukkit - Moved message to after join ++ // "{}[{}] logged in with entity id {} at ({}, {}, {})", ++ // player.getName().getString(), ++ // loggableAddress, ++ // player.getId(), ++ // player.getX(), ++ // player.getY(), ++ // player.getZ() ++ // ); + LevelData levelData = serverLevel.getLevelData(); + player.loadGameTypes(optional.orElse(null)); + ServerGamePacketListenerImpl serverGamePacketListenerImpl = new ServerGamePacketListenerImpl(this.server, connection, player, cookie); +@@ -190,8 +_,8 @@ + levelData.isHardcore(), + this.server.levelKeys(), + this.getMaxPlayers(), +- this.viewDistance, +- this.simulationDistance, ++ serverLevel.spigotConfig.viewDistance,// Spigot - view distance ++ serverLevel.spigotConfig.simulationDistance, + _boolean1, + !_boolean, + _boolean2, +@@ -199,6 +_,7 @@ + this.server.enforceSecureProfile() + ) + ); ++ player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit + serverGamePacketListenerImpl.send(new ClientboundChangeDifficultyPacket(levelData.getDifficulty(), levelData.isDifficultyLocked())); + serverGamePacketListenerImpl.send(new ClientboundPlayerAbilitiesPacket(player.getAbilities())); + serverGamePacketListenerImpl.send(new ClientboundSetHeldSlotPacket(player.getInventory().selected)); +@@ -218,24 +_,117 @@ + mutableComponent = Component.translatable("multiplayer.player.joined.renamed", player.getDisplayName(), string); + } + +- this.broadcastSystemMessage(mutableComponent.withStyle(ChatFormatting.YELLOW), false); ++ // CraftBukkit start ++ mutableComponent.withStyle(ChatFormatting.YELLOW); ++ Component joinMessage = mutableComponent; // Paper - Adventure + serverGamePacketListenerImpl.teleport(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()); + ServerStatus status = this.server.getStatus(); + if (status != null && !cookie.transferred()) { + player.sendServerStatus(status); + } + +- player.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(this.players)); ++ // player.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(this.players)); // CraftBukkit - replaced with loop below + this.players.add(player); ++ this.playersByName.put(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT), player); // Spigot + this.playersByUUID.put(player.getUUID(), player); +- this.broadcastAll(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player))); +- this.sendLevelInfo(player, serverLevel); ++ // this.broadcastAll(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player))); // CraftBukkit - replaced with loop below ++ // Paper start - Fire PlayerJoinEvent when Player is actually ready; correctly register player BEFORE PlayerJoinEvent, so the entity is valid and doesn't require tick delay hacks ++ player.supressTrackerForLogin = true; + serverLevel.addNewPlayer(player); +- this.server.getCustomBossEvents().onPlayerConnect(player); +- this.sendActivePlayerEffects(player); ++ this.server.getCustomBossEvents().onPlayerConnect(player); // see commented out section below serverLevel.addPlayerJoin(player); ++ // Paper end - Fire PlayerJoinEvent when Player is actually ready + player.loadAndSpawnEnderpearls(optional); + player.loadAndSpawnParentVehicle(optional); ++ // CraftBukkit start ++ org.bukkit.craftbukkit.entity.CraftPlayer bukkitPlayer = player.getBukkitEntity(); ++ ++ // Ensure that player inventory is populated with its viewer ++ player.containerMenu.transferTo(player.containerMenu, bukkitPlayer); ++ ++ org.bukkit.event.player.PlayerJoinEvent playerJoinEvent = new org.bukkit.event.player.PlayerJoinEvent(bukkitPlayer, io.papermc.paper.adventure.PaperAdventure.asAdventure(mutableComponent)); // Paper - Adventure ++ this.cserver.getPluginManager().callEvent(playerJoinEvent); ++ ++ if (!player.connection.isAcceptingMessages()) { ++ return; ++ } ++ ++ final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage(); ++ ++ if (jm != null && !jm.equals(net.kyori.adventure.text.Component.empty())) { // Paper - Adventure ++ joinMessage = io.papermc.paper.adventure.PaperAdventure.asVanilla(jm); // Paper - Adventure ++ this.server.getPlayerList().broadcastSystemMessage(joinMessage, false); // Paper - Adventure ++ } ++ // CraftBukkit end ++ ++ // CraftBukkit start - sendAll above replaced with this loop ++ ClientboundPlayerInfoUpdatePacket packet = ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player)); // Paper - Add Listing API for Player ++ ++ final List onlinePlayers = Lists.newArrayListWithExpectedSize(this.players.size() - 1); // Paper - Use single player info update packet on join ++ for (int i = 0; i < this.players.size(); ++i) { ++ ServerPlayer entityplayer1 = (ServerPlayer) this.players.get(i); ++ ++ if (entityplayer1.getBukkitEntity().canSee(bukkitPlayer)) { ++ // Paper start - Add Listing API for Player ++ if (entityplayer1.getBukkitEntity().isListed(bukkitPlayer)) { ++ // Paper end - Add Listing API for Player ++ entityplayer1.connection.send(packet); ++ // Paper start - Add Listing API for Player ++ } else { ++ entityplayer1.connection.send(ClientboundPlayerInfoUpdatePacket.createSinglePlayerInitializing(player, false)); ++ } ++ // Paper end - Add Listing API for Player ++ } ++ ++ if (entityplayer1 == player || !bukkitPlayer.canSee(entityplayer1.getBukkitEntity())) { // Paper - Use single player info update packet on join; Don't include joining player ++ continue; ++ } ++ ++ onlinePlayers.add(entityplayer1); // Paper - Use single player info update packet on join ++ } ++ // Paper start - Use single player info update packet on join ++ if (!onlinePlayers.isEmpty()) { ++ player.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(onlinePlayers, player)); // Paper - Add Listing API for Player ++ } ++ // Paper end - Use single player info update packet on join ++ player.sentListPacket = true; ++ player.supressTrackerForLogin = false; // Paper - Fire PlayerJoinEvent when Player is actually ready ++ ((ServerLevel)player.level()).getChunkSource().chunkMap.addEntity(player); // Paper - Fire PlayerJoinEvent when Player is actually ready; track entity now ++ // CraftBukkit end ++ ++ //player.refreshEntityData(player); // CraftBukkit - BungeeCord#2321, send complete data to self on spawn // Paper - THIS IS NOT NEEDED ANYMORE ++ ++ this.sendLevelInfo(player, serverLevel); ++ ++ // CraftBukkit start - Only add if the player wasn't moved in the event ++ if (player.level() == serverLevel && !serverLevel.players().contains(player)) { ++ serverLevel.addNewPlayer(player); ++ this.server.getCustomBossEvents().onPlayerConnect(player); ++ } ++ ++ serverLevel = player.serverLevel(); // CraftBukkit - Update in case join event changed it ++ // CraftBukkit end ++ this.sendActivePlayerEffects(player); ++ // Paper - move loading pearls / parent vehicle up + player.initInventoryMenu(); ++ // CraftBukkit - Moved from above, added world ++ // Paper start - Configurable player collision; Add to collideRule team if needed ++ final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard(); ++ final PlayerTeam collideRuleTeam = scoreboard.getPlayerTeam(this.collideRuleTeamName); ++ if (this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) { ++ scoreboard.addPlayerToTeam(player.getScoreboardName(), collideRuleTeam); ++ } ++ // Paper end - Configurable player collision ++ PlayerList.LOGGER.info("{}[{}] logged in with entity id {} at ([{}]{}, {}, {})", player.getName().getString(), loggableAddress, player.getId(), serverLevel.serverLevelData.getLevelName(), player.getX(), player.getY(), player.getZ()); ++ // Paper start - Send empty chunk, so players aren't stuck in the world loading screen with our chunk system not sending chunks when dead ++ if (player.isDeadOrDying()) { ++ net.minecraft.core.Holder plains = serverLevel.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.BIOME) ++ .getOrThrow(net.minecraft.world.level.biome.Biomes.PLAINS); ++ player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket( ++ new net.minecraft.world.level.chunk.EmptyLevelChunk(serverLevel, player.chunkPosition(), plains), ++ serverLevel.getLightEngine(), (java.util.BitSet)null, (java.util.BitSet) null) ++ ); ++ } ++ // Paper end - Send empty chunk + } + + public void updateEntireScoreboard(ServerScoreboard scoreboard, ServerPlayer player) { +@@ -258,30 +_,31 @@ + } + + public void addWorldborderListener(ServerLevel level) { ++ if (this.playerIo != null) return; // CraftBukkit + level.getWorldBorder().addListener(new BorderChangeListener() { + @Override + public void onBorderSizeSet(WorldBorder border, double size) { +- PlayerList.this.broadcastAll(new ClientboundSetBorderSizePacket(border)); ++ PlayerList.this.broadcastAll(new ClientboundSetBorderSizePacket(border), border.world); // CraftBukkit + } + + @Override + public void onBorderSizeLerping(WorldBorder border, double oldSize, double newSize, long time) { +- PlayerList.this.broadcastAll(new ClientboundSetBorderLerpSizePacket(border)); ++ PlayerList.this.broadcastAll(new ClientboundSetBorderLerpSizePacket(border), border.world); // CraftBukkit + } + + @Override + public void onBorderCenterSet(WorldBorder border, double x, double z) { +- PlayerList.this.broadcastAll(new ClientboundSetBorderCenterPacket(border)); ++ PlayerList.this.broadcastAll(new ClientboundSetBorderCenterPacket(border), border.world); // CraftBukkit + } + + @Override + public void onBorderSetWarningTime(WorldBorder border, int warningTime) { +- PlayerList.this.broadcastAll(new ClientboundSetBorderWarningDelayPacket(border)); ++ PlayerList.this.broadcastAll(new ClientboundSetBorderWarningDelayPacket(border), border.world); // CraftBukkit + } + + @Override + public void onBorderSetWarningBlocks(WorldBorder border, int warningBlocks) { +- PlayerList.this.broadcastAll(new ClientboundSetBorderWarningDistancePacket(border)); ++ PlayerList.this.broadcastAll(new ClientboundSetBorderWarningDistancePacket(border), border.world); // CraftBukkit + } + + @Override +@@ -309,67 +_,175 @@ + } + + protected void save(ServerPlayer player) { ++ if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit + this.playerIo.save(player); +- ServerStatsCounter serverStatsCounter = this.stats.get(player.getUUID()); ++ ServerStatsCounter serverStatsCounter = player.getStats(); // CraftBukkit + if (serverStatsCounter != null) { + serverStatsCounter.save(); + } + +- PlayerAdvancements playerAdvancements = this.advancements.get(player.getUUID()); ++ PlayerAdvancements playerAdvancements = player.getAdvancements(); // CraftBukkit + if (playerAdvancements != null) { + playerAdvancements.save(); + } + } + +- public void remove(ServerPlayer player) { ++ public net.kyori.adventure.text.Component remove(ServerPlayer player) { // CraftBukkit - return string // Paper - return Component ++ // Paper start - Fix kick event leave message not being sent ++ return this.remove(player, net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? player.getBukkitEntity().displayName() : io.papermc.paper.adventure.PaperAdventure.asAdventure(player.getDisplayName()))); ++ } ++ public net.kyori.adventure.text.Component remove(ServerPlayer player, net.kyori.adventure.text.Component leaveMessage) { ++ // Paper end - Fix kick event leave message not being sent + ServerLevel serverLevel = player.serverLevel(); + player.awardStat(Stats.LEAVE_GAME); ++ // CraftBukkit start - Quitting must be before we do final save of data, in case plugins need to modify it ++ // See SPIGOT-5799, SPIGOT-6145 ++ if (player.containerMenu != player.inventoryMenu) { ++ player.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.DISCONNECT); // Paper - Inventory close reason ++ } ++ ++ org.bukkit.event.player.PlayerQuitEvent playerQuitEvent = new org.bukkit.event.player.PlayerQuitEvent(player.getBukkitEntity(), leaveMessage, player.quitReason); // Paper - Adventure & Add API for quit reason ++ this.cserver.getPluginManager().callEvent(playerQuitEvent); ++ player.getBukkitEntity().disconnect(playerQuitEvent.getQuitMessage()); ++ ++ player.doTick(); // SPIGOT-924 ++ // CraftBukkit end ++ ++ // Paper start - Configurable player collision; Remove from collideRule team if needed ++ if (this.collideRuleTeamName != null) { ++ final net.minecraft.world.scores.Scoreboard scoreBoard = this.server.getLevel(Level.OVERWORLD).getScoreboard(); ++ final PlayerTeam team = scoreBoard.getPlayersTeam(this.collideRuleTeamName); ++ if (player.getTeam() == team && team != null) { ++ scoreBoard.removePlayerFromTeam(player.getScoreboardName(), team); ++ } ++ } ++ // Paper end - Configurable player collision ++ ++ // Paper - Drop carried item when player has disconnected ++ if (!player.containerMenu.getCarried().isEmpty()) { ++ net.minecraft.world.item.ItemStack carried = player.containerMenu.getCarried(); ++ player.containerMenu.setCarried(net.minecraft.world.item.ItemStack.EMPTY); ++ player.drop(carried, false); ++ } ++ // Paper end - Drop carried item when player has disconnected + this.save(player); + if (player.isPassenger()) { + Entity rootVehicle = player.getRootVehicle(); + if (rootVehicle.hasExactlyOnePlayerPassenger()) { + LOGGER.debug("Removing player mount"); + player.stopRiding(); +- rootVehicle.getPassengersAndSelf().forEach(entity -> entity.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER)); ++ rootVehicle.getPassengersAndSelf().forEach(entity -> { ++ // Paper start - Fix villager boat exploit ++ if (entity instanceof net.minecraft.world.entity.npc.AbstractVillager villager) { ++ final net.minecraft.world.entity.player.Player human = villager.getTradingPlayer(); ++ if (human != null) { ++ villager.setTradingPlayer(null); ++ } ++ } ++ // Paper end - Fix villager boat exploit ++ entity.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER, org.bukkit.event.entity.EntityRemoveEvent.Cause.PLAYER_QUIT); // CraftBukkit - add Bukkit remove cause ++ }); + } + } + + player.unRide(); + + for (ThrownEnderpearl thrownEnderpearl : player.getEnderPearls()) { +- thrownEnderpearl.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER); ++ // Paper start - Allow using old ender pearl behavior ++ if (!thrownEnderpearl.level().paperConfig().misc.legacyEnderPearlBehavior) { ++ thrownEnderpearl.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER, org.bukkit.event.entity.EntityRemoveEvent.Cause.PLAYER_QUIT); // CraftBukkit - add Bukkit remove cause ++ } else { ++ thrownEnderpearl.cachedOwner = null; ++ } ++ // Paper end - Allow using old ender pearl behavior + } + + serverLevel.removePlayerImmediately(player, Entity.RemovalReason.UNLOADED_WITH_PLAYER); ++ player.retireScheduler(); // Paper - Folia schedulers + player.getAdvancements().stopListening(); + this.players.remove(player); ++ this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot + this.server.getCustomBossEvents().onPlayerDisconnect(player); + UUID uuid = player.getUUID(); + ServerPlayer serverPlayer = this.playersByUUID.get(uuid); + if (serverPlayer == player) { + this.playersByUUID.remove(uuid); +- this.stats.remove(uuid); +- this.advancements.remove(uuid); +- } +- +- this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID()))); ++ // CraftBukkit start ++ // this.stats.remove(uuid); ++ // this.advancements.remove(uuid); ++ // CraftBukkit end ++ } ++ ++ // CraftBukkit start ++ // this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID()))); ++ ClientboundPlayerInfoRemovePacket packet = new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID())); ++ for (int i = 0; i < this.players.size(); i++) { ++ ServerPlayer entityplayer2 = (ServerPlayer) this.players.get(i); ++ ++ if (entityplayer2.getBukkitEntity().canSee(player.getBukkitEntity())) { ++ entityplayer2.connection.send(packet); ++ } else { ++ entityplayer2.getBukkitEntity().onEntityRemove(player); ++ } ++ } ++ // This removes the scoreboard (and player reference) for the specific player in the manager ++ this.cserver.getScoreboardManager().removePlayer(player.getBukkitEntity()); ++ // CraftBukkit end ++ return playerQuitEvent.quitMessage(); // Paper - Adventure + } + +- @Nullable +- public Component canPlayerLogin(SocketAddress socketAddress, GameProfile gameProfile) { +- if (this.bans.isBanned(gameProfile)) { +- UserBanListEntry userBanListEntry = this.bans.get(gameProfile); +- MutableComponent mutableComponent = Component.translatable("multiplayer.disconnect.banned.reason", userBanListEntry.getReason()); +- if (userBanListEntry.getExpires() != null) { ++ // CraftBukkit start - Whole method, SocketAddress to LoginListener, added hostname to signature, return EntityPlayer ++ public ServerPlayer canPlayerLogin(net.minecraft.server.network.ServerLoginPacketListenerImpl loginlistener, GameProfile gameProfile) { ++ // if (this.bans.isBanned(gameProfile)) { ++ // UserBanListEntry userBanListEntry = this.bans.get(gameProfile); ++ // Moved from processLogin ++ UUID uuid = gameProfile.getId(); ++ List list = Lists.newArrayList(); ++ ++ ServerPlayer entityplayer; ++ ++ for (int i = 0; i < this.players.size(); ++i) { ++ entityplayer = (ServerPlayer) this.players.get(i); ++ if (entityplayer.getUUID().equals(uuid) || (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() && entityplayer.getGameProfile().getName().equalsIgnoreCase(gameProfile.getName()))) { // Paper - validate usernames ++ list.add(entityplayer); ++ } ++ } ++ ++ java.util.Iterator iterator = list.iterator(); ++ ++ while (iterator.hasNext()) { ++ entityplayer = (ServerPlayer) iterator.next(); ++ this.save(entityplayer); // CraftBukkit - Force the player's inventory to be saved ++ entityplayer.connection.disconnect(Component.translatable("multiplayer.disconnect.duplicate_login"), org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); // Paper - kick event cause ++ } ++ ++ // Instead of kicking then returning, we need to store the kick reason ++ // in the event, check with plugins to see if it's ok, and THEN kick ++ // depending on the outcome. ++ SocketAddress socketAddress = loginlistener.connection.getRemoteAddress(); ++ ++ ServerPlayer entity = new ServerPlayer(this.server, this.server.getLevel(Level.OVERWORLD), gameProfile, ClientInformation.createDefault()); ++ entity.transferCookieConnection = loginlistener; ++ org.bukkit.entity.Player player = entity.getBukkitEntity(); ++ org.bukkit.event.player.PlayerLoginEvent event = new org.bukkit.event.player.PlayerLoginEvent(player, loginlistener.connection.hostname, ((java.net.InetSocketAddress) socketAddress).getAddress(), ((java.net.InetSocketAddress) loginlistener.connection.channel.remoteAddress()).getAddress()); ++ ++ // Paper start - Fix MC-158900 ++ UserBanListEntry gameprofilebanentry; ++ if (this.bans.isBanned(gameProfile) && (gameprofilebanentry = this.bans.get(gameProfile)) != null) { ++ // Paper end - Fix MC-158900 ++ MutableComponent mutableComponent = Component.translatable("multiplayer.disconnect.banned.reason", gameprofilebanentry.getReason()); ++ if (gameprofilebanentry.getExpires() != null) { + mutableComponent.append( +- Component.translatable("multiplayer.disconnect.banned.expiration", BAN_DATE_FORMAT.format(userBanListEntry.getExpires())) ++ Component.translatable("multiplayer.disconnect.banned.expiration", BAN_DATE_FORMAT.format(gameprofilebanentry.getExpires())) + ); + } + +- return mutableComponent; +- } else if (!this.isWhiteListed(gameProfile)) { +- return Component.translatable("multiplayer.disconnect.not_whitelisted"); +- } else if (this.ipBans.isBanned(socketAddress)) { ++ // return mutableComponent; ++ event.disallow(org.bukkit.event.player.PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.PaperAdventure.asAdventure(mutableComponent)); // Paper - Adventure ++ } else if (!this.isWhiteListed(gameProfile, event)) { // Paper - ProfileWhitelistVerifyEvent ++ // return Component.translatable("multiplayer.disconnect.not_whitelisted"); ++ //event.disallow(PlayerLoginEvent.Result.KICK_WHITELIST, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.whitelistMessage)); // Spigot // Paper - Adventure - moved to isWhitelisted ++ } else if (this.ipBans.isBanned(socketAddress) && getIpBans().get(socketAddress) != null && !this.getIpBans().get(socketAddress).hasExpired()) { // Paper - fix NPE with temp ip bans + IpBanListEntry ipBanListEntry = this.ipBans.get(socketAddress); + MutableComponent mutableComponent = Component.translatable("multiplayer.disconnect.banned_ip.reason", ipBanListEntry.getReason()); + if (ipBanListEntry.getExpires() != null) { +@@ -378,69 +_,129 @@ + ); + } + +- return mutableComponent; ++ // return mutableComponent; ++ event.disallow(org.bukkit.event.player.PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.PaperAdventure.asAdventure(mutableComponent)); // Paper - Adventure + } else { +- return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile) +- ? Component.translatable("multiplayer.disconnect.server_full") +- : null; +- } +- } +- +- public ServerPlayer getPlayerForLogin(GameProfile gameProfile, ClientInformation clientInformation) { +- return new ServerPlayer(this.server, this.server.overworld(), gameProfile, clientInformation); +- } +- +- public boolean disconnectAllPlayersWithProfile(GameProfile gameProfile) { +- UUID id = gameProfile.getId(); +- Set set = Sets.newIdentityHashSet(); +- +- for (ServerPlayer serverPlayer : this.players) { +- if (serverPlayer.getUUID().equals(id)) { +- set.add(serverPlayer); ++ // return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile) ++ // ? Component.translatable("multiplayer.disconnect.server_full") ++ // : null; ++ if (this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile)) { ++ event.disallow(org.bukkit.event.player.PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure + } + } +- +- ServerPlayer serverPlayer1 = this.playersByUUID.get(gameProfile.getId()); +- if (serverPlayer1 != null) { +- set.add(serverPlayer1); +- } +- +- for (ServerPlayer serverPlayer2 : set) { +- serverPlayer2.connection.disconnect(DUPLICATE_LOGIN_DISCONNECT_MESSAGE); +- } +- +- return !set.isEmpty(); +- } +- +- public ServerPlayer respawn(ServerPlayer player, boolean keepInventory, Entity.RemovalReason reason) { ++ this.cserver.getPluginManager().callEvent(event); ++ if (event.getResult() != org.bukkit.event.player.PlayerLoginEvent.Result.ALLOWED) { ++ loginlistener.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.kickMessage())); // Paper - Adventure ++ return null; ++ } ++ return entity; ++ } ++ ++ // CraftBukkit start - added EntityPlayer ++ public ServerPlayer getPlayerForLogin(GameProfile gameProfile, ClientInformation clientInformation, ServerPlayer player) { ++ player.updateOptions(clientInformation); ++ return player; ++ // CraftBukkit end ++ } ++ ++ public boolean disconnectAllPlayersWithProfile(GameProfile gameProfile, ServerPlayer player) { // CraftBukkit - added ServerPlayer ++ // CraftBukkit start - Moved up ++ // UUID id = gameProfile.getId(); ++ // Set set = Sets.newIdentityHashSet(); ++ // ++ // for (ServerPlayer serverPlayer : this.players) { ++ // if (serverPlayer.getUUID().equals(id)) { ++ // set.add(serverPlayer); ++ // } ++ // } ++ // ++ // ServerPlayer serverPlayer1 = this.playersByUUID.get(gameProfile.getId()); ++ // if (serverPlayer1 != null) { ++ // set.add(serverPlayer1); ++ // } ++ // ++ // for (ServerPlayer serverPlayer2 : set) { ++ // serverPlayer2.connection.disconnect(DUPLICATE_LOGIN_DISCONNECT_MESSAGE); ++ // } ++ // ++ // return !set.isEmpty(); ++ return player == null; ++ // CraftBukkit end ++ } ++ ++ // CraftBukkit start ++ public ServerPlayer respawn(ServerPlayer player, boolean keepInventory, Entity.RemovalReason reason, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason eventReason) { ++ return this.respawn(player, keepInventory, reason, eventReason, null); ++ } ++ public ServerPlayer respawn(ServerPlayer player, boolean keepInventory, Entity.RemovalReason reason, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason eventReason, org.bukkit.Location location) { ++ player.stopRiding(); // CraftBukkit + this.players.remove(player); ++ this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot + player.serverLevel().removePlayerImmediately(player, reason); +- TeleportTransition teleportTransition = player.findRespawnPositionAndUseSpawnBlock(!keepInventory, TeleportTransition.DO_NOTHING); +- ServerLevel level = teleportTransition.newLevel(); +- ServerPlayer serverPlayer = new ServerPlayer(this.server, level, player.getGameProfile(), player.clientInformation()); ++ // TeleportTransition teleportTransition = player.findRespawnPositionAndUseSpawnBlock(!keepInventory, TeleportTransition.DO_NOTHING); ++ // ServerLevel level = teleportTransition.newLevel(); ++ // ServerPlayer serverPlayer = new ServerPlayer(this.server, level, player.getGameProfile(), player.clientInformation()); ++ ServerPlayer serverPlayer = player; ++ Level fromWorld = player.level(); ++ player.wonGame = false; ++ // CraftBukkit end + serverPlayer.connection = player.connection; + serverPlayer.restoreFrom(player, keepInventory); + serverPlayer.setId(player.getId()); + serverPlayer.setMainArm(player.getMainArm()); +- if (!teleportTransition.missingRespawnBlock()) { +- serverPlayer.copyRespawnPosition(player); +- } ++ // CraftBukkit - not required, just copies old location into reused entity ++ // if (!teleportTransition.missingRespawnBlock()) { ++ // serverPlayer.copyRespawnPosition(player); ++ // } + + for (String string : player.getTags()) { + serverPlayer.addTag(string); + } +- ++ // Paper start - Add PlayerPostRespawnEvent ++ boolean isBedSpawn = false; ++ boolean isRespawn = false; ++ // Paper end - Add PlayerPostRespawnEvent ++ ++ // CraftBukkit start - fire PlayerRespawnEvent ++ TeleportTransition teleportTransition; ++ if (location == null) { ++ teleportTransition = player.findRespawnPositionAndUseSpawnBlock(!keepInventory, TeleportTransition.DO_NOTHING, eventReason); ++ ++ if (!keepInventory) player.reset(); // SPIGOT-4785 ++ // Paper start - Add PlayerPostRespawnEvent ++ if (teleportTransition == null) return player; // Early exit, mirrors belows early return for disconnected players in respawn event ++ isRespawn = true; ++ location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(teleportTransition.position(), teleportTransition.newLevel().getWorld(), teleportTransition.yRot(), teleportTransition.xRot()); ++ // Paper end - Add PlayerPostRespawnEvent ++ } else { ++ teleportTransition = new TeleportTransition(((org.bukkit.craftbukkit.CraftWorld) location.getWorld()).getHandle(), org.bukkit.craftbukkit.util.CraftLocation.toVec3D(location), Vec3.ZERO, location.getYaw(), location.getPitch(), TeleportTransition.DO_NOTHING); ++ } ++ // Spigot Start ++ if (teleportTransition == null) { // Paper - Add PlayerPostRespawnEvent - diff on change - spigot early returns if respawn pos is null, that is how they handle disconnected player in respawn event ++ return player; ++ } ++ // Spigot End ++ ServerLevel level = teleportTransition.newLevel(); ++ serverPlayer.spawnIn(level); ++ serverPlayer.unsetRemoved(); ++ serverPlayer.setShiftKeyDown(false); + Vec3 vec3 = teleportTransition.position(); +- serverPlayer.moveTo(vec3.x, vec3.y, vec3.z, teleportTransition.yRot(), teleportTransition.xRot()); ++ serverPlayer.forceSetPositionRotation(vec3.x, vec3.y, vec3.z, teleportTransition.yRot(), teleportTransition.xRot()); ++ level.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(net.minecraft.util.Mth.floor(vec3.x()) >> 4, net.minecraft.util.Mth.floor(vec3.z()) >> 4), 1, player.getId()); // Paper - post teleport ticket type ++ // CraftBukkit end + if (teleportTransition.missingRespawnBlock()) { + serverPlayer.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.NO_RESPAWN_BLOCK_AVAILABLE, 0.0F)); ++ serverPlayer.setRespawnPosition(null, null, 0f, false, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN); // CraftBukkit - SPIGOT-5988: Clear respawn location when obstructed // Paper - PlayerSetSpawnEvent + } + + byte b = (byte)(keepInventory ? 1 : 0); + ServerLevel serverLevel = serverPlayer.serverLevel(); + LevelData levelData = serverLevel.getLevelData(); + serverPlayer.connection.send(new ClientboundRespawnPacket(serverPlayer.createCommonSpawnInfo(serverLevel), b)); +- serverPlayer.connection.teleport(serverPlayer.getX(), serverPlayer.getY(), serverPlayer.getZ(), serverPlayer.getYRot(), serverPlayer.getXRot()); ++ // serverPlayer.connection.teleport(serverPlayer.getX(), serverPlayer.getY(), serverPlayer.getZ(), serverPlayer.getYRot(), serverPlayer.getXRot()); ++ serverPlayer.connection.send(new ClientboundSetChunkCacheRadiusPacket(serverLevel.spigotConfig.viewDistance)); // Spigot ++ serverPlayer.connection.send(new ClientboundSetSimulationDistancePacket(serverLevel.spigotConfig.simulationDistance)); // Spigot ++ serverPlayer.connection.teleport(org.bukkit.craftbukkit.util.CraftLocation.toBukkit(serverPlayer.position(), serverLevel.getWorld(), serverPlayer.getYRot(), serverPlayer.getXRot())); // CraftBukkit + serverPlayer.connection.send(new ClientboundSetDefaultSpawnPositionPacket(level.getSharedSpawnPos(), level.getSharedSpawnAngle())); + serverPlayer.connection.send(new ClientboundChangeDifficultyPacket(levelData.getDifficulty(), levelData.isDifficultyLocked())); + serverPlayer.connection +@@ -448,10 +_,13 @@ + this.sendActivePlayerEffects(serverPlayer); + this.sendLevelInfo(serverPlayer, level); + this.sendPlayerPermissionLevel(serverPlayer); +- level.addRespawnedPlayer(serverPlayer); +- this.players.add(serverPlayer); +- this.playersByUUID.put(serverPlayer.getUUID(), serverPlayer); +- serverPlayer.initInventoryMenu(); ++ if (!serverPlayer.connection.isDisconnected()) { ++ level.addRespawnedPlayer(serverPlayer); ++ this.players.add(serverPlayer); ++ this.playersByName.put(serverPlayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT), serverPlayer); // Spigot ++ this.playersByUUID.put(serverPlayer.getUUID(), serverPlayer); ++ } ++ // serverPlayer.initInventoryMenu(); + serverPlayer.setHealth(serverPlayer.getHealth()); + BlockPos respawnPosition = serverPlayer.getRespawnPosition(); + ServerLevel level1 = this.server.getLevel(serverPlayer.getRespawnDimension()); +@@ -472,7 +_,40 @@ + ) + ); + } +- } ++ // Paper start - Add PlayerPostRespawnEvent ++ if (blockState.is(net.minecraft.tags.BlockTags.BEDS) && !teleportTransition.missingRespawnBlock()) { ++ isBedSpawn = true; ++ } ++ // Paper end - Add PlayerPostRespawnEvent ++ } ++ // Added from changeDimension ++ this.sendAllPlayerInfo(player); // Update health, etc... ++ player.onUpdateAbilities(); ++ for (MobEffectInstance mobEffect : player.getActiveEffects()) { ++ player.connection.send(new ClientboundUpdateMobEffectPacket(player.getId(), mobEffect, false)); // blend = false ++ } ++ ++ // Fire advancement trigger ++ player.triggerDimensionChangeTriggers(level); ++ ++ // Don't fire on respawn ++ if (fromWorld != level) { ++ org.bukkit.event.player.PlayerChangedWorldEvent event = new org.bukkit.event.player.PlayerChangedWorldEvent(player.getBukkitEntity(), fromWorld.getWorld()); ++ this.server.server.getPluginManager().callEvent(event); ++ } ++ ++ // Save player file again if they were disconnected ++ if (player.connection.isDisconnected()) { ++ this.save(player); ++ } ++ ++ // Paper start - Add PlayerPostRespawnEvent ++ if (isRespawn) { ++ cserver.getPluginManager().callEvent(new com.destroystokyo.paper.event.player.PlayerPostRespawnEvent(player.getBukkitEntity(), location, isBedSpawn)); ++ } ++ // Paper end - Add PlayerPostRespawnEvent ++ ++ // CraftBukkit end + + return serverPlayer; + } +@@ -482,24 +_,65 @@ + } + + public void sendActiveEffects(LivingEntity entity, ServerGamePacketListenerImpl connection) { ++ // Paper start - collect packets ++ this.sendActiveEffects(entity, connection::send); ++ } ++ public void sendActiveEffects(LivingEntity entity, java.util.function.Consumer> packetConsumer) { ++ // Paper end - collect packets + for (MobEffectInstance mobEffectInstance : entity.getActiveEffects()) { +- connection.send(new ClientboundUpdateMobEffectPacket(entity.getId(), mobEffectInstance, false)); ++ packetConsumer.accept(new ClientboundUpdateMobEffectPacket(entity.getId(), mobEffectInstance, false)); // Paper - collect packets + } + } + + public void sendPlayerPermissionLevel(ServerPlayer player) { ++ // Paper start - avoid recalculating permissions if possible ++ this.sendPlayerPermissionLevel(player, true); ++ } ++ ++ public void sendPlayerPermissionLevel(ServerPlayer player, boolean recalculatePermissions) { ++ // Paper end - avoid recalculating permissions if possible + GameProfile gameProfile = player.getGameProfile(); + int profilePermissions = this.server.getProfilePermissions(gameProfile); +- this.sendPlayerPermissionLevel(player, profilePermissions); ++ this.sendPlayerPermissionLevel(player, profilePermissions, recalculatePermissions); // Paper - avoid recalculating permissions if possible + } + + public void tick() { + if (++this.sendAllPlayerInfoIn > 600) { +- this.broadcastAll(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), this.players)); ++ // CraftBukkit start ++ for (int i = 0; i < this.players.size(); ++i) { ++ final ServerPlayer target = (ServerPlayer) this.players.get(i); ++ ++ target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), this.players.stream().filter(new Predicate() { ++ @Override ++ public boolean test(ServerPlayer input) { ++ return target.getBukkitEntity().canSee(input.getBukkitEntity()); ++ } ++ }).collect(java.util.stream.Collectors.toList()))); ++ } ++ // CraftBukkit end + this.sendAllPlayerInfoIn = 0; + } + } + ++ // CraftBukkit start - add a world/entity limited version ++ public void broadcastAll(Packet packet, net.minecraft.world.entity.player.Player entityhuman) { ++ for (int i = 0; i < this.players.size(); ++i) { ++ ServerPlayer entityplayer = this.players.get(i); ++ if (entityhuman != null && !entityplayer.getBukkitEntity().canSee(entityhuman.getBukkitEntity())) { ++ continue; ++ } ++ ((ServerPlayer) this.players.get(i)).connection.send(packet); ++ } ++ } ++ ++ public void broadcastAll(Packet packet, Level world) { ++ for (int i = 0; i < world.players().size(); ++i) { ++ ((ServerPlayer) world.players().get(i)).connection.send(packet); ++ } ++ ++ } ++ // CraftBukkit end ++ + public void broadcastAll(Packet packet) { + for (ServerPlayer serverPlayer : this.players) { + serverPlayer.connection.send(packet); +@@ -575,6 +_,11 @@ + } + + private void sendPlayerPermissionLevel(ServerPlayer player, int permLevel) { ++ // Paper start - Add sendOpLevel API ++ this.sendPlayerPermissionLevel(player, permLevel, true); ++ } ++ public void sendPlayerPermissionLevel(ServerPlayer player, int permLevel, boolean recalculatePermissions) { ++ // Paper end - Add sendOpLevel API + if (player.connection != null) { + byte b; + if (permLevel <= 0) { +@@ -588,11 +_,32 @@ + player.connection.send(new ClientboundEntityEventPacket(player, b)); + } + ++ if (recalculatePermissions) { // Paper - Add sendOpLevel API ++ player.getBukkitEntity().recalculatePermissions(); // CraftBukkit + this.server.getCommands().sendCommands(player); ++ } // Paper - Add sendOpLevel API + } + + public boolean isWhiteListed(GameProfile profile) { +- return !this.doWhiteList || this.ops.contains(profile) || this.whitelist.contains(profile); ++ // Paper start - ProfileWhitelistVerifyEvent ++ return this.isWhiteListed(profile, null); ++ } ++ public boolean isWhiteListed(GameProfile gameprofile, @Nullable org.bukkit.event.player.PlayerLoginEvent loginEvent) { ++ boolean isOp = this.ops.contains(gameprofile); ++ boolean isWhitelisted = !this.doWhiteList || isOp || this.whitelist.contains(gameprofile); ++ final com.destroystokyo.paper.event.profile.ProfileWhitelistVerifyEvent event; ++ ++ final net.kyori.adventure.text.Component configuredMessage = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.whitelistMessage); ++ event = new com.destroystokyo.paper.event.profile.ProfileWhitelistVerifyEvent(com.destroystokyo.paper.profile.CraftPlayerProfile.asBukkitMirror(gameprofile), this.doWhiteList, isWhitelisted, isOp, configuredMessage); ++ event.callEvent(); ++ if (!event.isWhitelisted()) { ++ if (loginEvent != null) { ++ loginEvent.disallow(org.bukkit.event.player.PlayerLoginEvent.Result.KICK_WHITELIST, event.kickMessage() == null ? configuredMessage : event.kickMessage()); ++ } ++ return false; ++ } ++ return true; ++ // Paper end - ProfileWhitelistVerifyEvent + } + + public boolean isOp(GameProfile profile) { +@@ -603,21 +_,17 @@ + + @Nullable + public ServerPlayer getPlayerByName(String username) { +- int size = this.players.size(); +- +- for (int i = 0; i < size; i++) { +- ServerPlayer serverPlayer = this.players.get(i); +- if (serverPlayer.getGameProfile().getName().equalsIgnoreCase(username)) { +- return serverPlayer; +- } +- } +- +- return null; ++ return this.playersByName.get(username.toLowerCase(java.util.Locale.ROOT)); // Spigot + } + + public void broadcast(@Nullable Player except, double x, double y, double z, double radius, ResourceKey dimension, Packet packet) { + for (int i = 0; i < this.players.size(); i++) { + ServerPlayer serverPlayer = this.players.get(i); ++ // CraftBukkit start - Test if player receiving packet can see the source of the packet ++ if (except != null && !serverPlayer.getBukkitEntity().canSee(except.getBukkitEntity())) { ++ continue; ++ } ++ // CraftBukkit end + if (serverPlayer != except && serverPlayer.level().dimension() == dimension) { + double d = x - serverPlayer.getX(); + double d1 = y - serverPlayer.getY(); +@@ -630,9 +_,11 @@ + } + + public void saveAll() { ++ io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main + for (int i = 0; i < this.players.size(); i++) { + this.save(this.players.get(i)); + } ++ return null; }); // Paper - ensure main + } + + public UserWhiteList getWhiteList() { +@@ -655,14 +_,18 @@ + } + + public void sendLevelInfo(ServerPlayer player, ServerLevel level) { +- WorldBorder worldBorder = this.server.overworld().getWorldBorder(); ++ WorldBorder worldBorder = player.level().getWorldBorder(); // CraftBukkit + player.connection.send(new ClientboundInitializeBorderPacket(worldBorder)); + player.connection.send(new ClientboundSetTimePacket(level.getGameTime(), level.getDayTime(), level.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT))); + player.connection.send(new ClientboundSetDefaultSpawnPositionPacket(level.getSharedSpawnPos(), level.getSharedSpawnAngle())); + if (level.isRaining()) { +- player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0.0F)); +- player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, level.getRainLevel(1.0F))); +- player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, level.getThunderLevel(1.0F))); ++ // CraftBukkit start - handle player weather ++ // player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0.0F)); ++ // player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, level.getRainLevel(1.0F))); ++ // player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, level.getThunderLevel(1.0F))); ++ player.setPlayerWeather(org.bukkit.WeatherType.DOWNFALL, false); ++ player.updateWeather(-level.rainLevel, level.rainLevel, -level.thunderLevel, level.thunderLevel); ++ // CraftBukkit end + } + + player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.LEVEL_CHUNKS_LOAD_START, 0.0F)); +@@ -671,8 +_,16 @@ + + public void sendAllPlayerInfo(ServerPlayer player) { + player.inventoryMenu.sendAllDataToRemote(); +- player.resetSentInfo(); ++ // entityplayer.resetSentInfo(); ++ player.getBukkitEntity().updateScaledHealth(); // CraftBukkit - Update scaled health on respawn and worldchange ++ player.refreshEntityData(player); // CraftBukkkit - SPIGOT-7218: sync metadata + player.connection.send(new ClientboundSetHeldSlotPacket(player.getInventory().selected)); ++ // CraftBukkit start - from GameRules ++ int i = player.serverLevel().getGameRules().getBoolean(GameRules.RULE_REDUCEDDEBUGINFO) ? 22 : 23; ++ player.connection.send(new ClientboundEntityEventPacket(player, (byte) i)); ++ float immediateRespawn = player.serverLevel().getGameRules().getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN) ? 1.0F: 0.0F; ++ player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.IMMEDIATE_RESPAWN, immediateRespawn)); ++ // CraftBukkit end + } + + public int getPlayerCount() { +@@ -688,6 +_,7 @@ + } + + public void setUsingWhiteList(boolean whitelistEnabled) { ++ new com.destroystokyo.paper.event.server.WhitelistToggleEvent(whitelistEnabled).callEvent(); // Paper - WhitelistToggleEvent + this.doWhiteList = whitelistEnabled; + } + +@@ -725,10 +_,35 @@ + } + + public void removeAll() { +- for (int i = 0; i < this.players.size(); i++) { +- this.players.get(i).connection.disconnect(Component.translatable("multiplayer.disconnect.server_shutdown")); +- } +- } ++ // Paper start - Extract method to allow for restarting flag ++ this.removeAll(false); ++ } ++ ++ public void removeAll(boolean isRestarting) { ++ // Paper end ++ // CraftBukkit start - disconnect safely ++ for (ServerPlayer player : this.players) { ++ if (isRestarting) player.connection.disconnect(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.restartMessage), org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN); else // Paper - kick event cause (cause is never used here) ++ player.connection.disconnect(java.util.Objects.requireNonNullElseGet(this.server.server.shutdownMessage(), net.kyori.adventure.text.Component::empty)); // CraftBukkit - add custom shutdown message // Paper - Adventure ++ } ++ // CraftBukkit end ++ ++ // Paper start - Configurable player collision; Remove collideRule team if it exists ++ if (this.collideRuleTeamName != null) { ++ final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard(); ++ final PlayerTeam team = scoreboard.getPlayersTeam(this.collideRuleTeamName); ++ if (team != null) scoreboard.removePlayerTeam(team); ++ } ++ // Paper end - Configurable player collision ++ } ++ ++ // CraftBukkit start ++ public void broadcastMessage(Component[] iChatBaseComponents) { ++ for (Component component : iChatBaseComponents) { ++ this.broadcastSystemMessage(component, false); ++ } ++ } ++ // CraftBukkit end + + public void broadcastSystemMessage(Component message, boolean bypassHiddenChat) { + this.broadcastSystemMessage(message, serverPlayer -> message, bypassHiddenChat); +@@ -750,20 +_,39 @@ + } + + public void broadcastChatMessage(PlayerChatMessage message, ServerPlayer sender, ChatType.Bound boundChatType) { +- this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender, boundChatType); ++ // Paper start ++ this.broadcastChatMessage(message, sender, boundChatType, null); ++ } ++ public void broadcastChatMessage(PlayerChatMessage message, ServerPlayer sender, ChatType.Bound boundChatType, @Nullable Function unsignedFunction) { ++ // Paper end ++ this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender, boundChatType, unsignedFunction); // Paper + } + + private void broadcastChatMessage( + PlayerChatMessage message, Predicate shouldFilterMessageTo, @Nullable ServerPlayer sender, ChatType.Bound boundChatType + ) { ++ // Paper start ++ this.broadcastChatMessage(message, shouldFilterMessageTo, sender, boundChatType, null); ++ } ++ public void broadcastChatMessage(PlayerChatMessage message, Predicate shouldFilterMessageTo, @Nullable ServerPlayer sender, ChatType.Bound boundChatType, @Nullable Function unsignedFunction) { ++ // Paper end + boolean flag = this.verifyChatTrusted(message); +- this.server.logChatMessage(message.decoratedContent(), boundChatType, flag ? null : "Not Secure"); ++ this.server.logChatMessage((unsignedFunction == null ? message.decoratedContent() : unsignedFunction.apply(this.server.console)), boundChatType, flag ? null : "Not Secure"); // Paper + OutgoingChatMessage outgoingChatMessage = OutgoingChatMessage.create(message); + boolean flag1 = false; + ++ Packet disguised = sender != null && unsignedFunction == null ? new net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket(outgoingChatMessage.content(), boundChatType) : null; // Paper - don't send player chat packets from vanished players + for (ServerPlayer serverPlayer : this.players) { + boolean flag2 = shouldFilterMessageTo.test(serverPlayer); +- serverPlayer.sendChatMessage(outgoingChatMessage, flag2, boundChatType); ++ // Paper start - don't send player chat packets from vanished players ++ if (sender != null && !serverPlayer.getBukkitEntity().canSee(sender.getBukkitEntity())) { ++ serverPlayer.connection.send(unsignedFunction != null ++ ? new net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket(unsignedFunction.apply(serverPlayer.getBukkitEntity()), boundChatType) ++ : disguised); ++ continue; ++ } ++ // Paper end ++ sender.sendChatMessage(outgoingChatMessage, flag2, boundChatType, unsignedFunction == null ? null : unsignedFunction.apply(serverPlayer.getBukkitEntity())); // Paper + flag1 |= flag2 && message.isFullyFiltered(); + } + +@@ -772,18 +_,25 @@ + } + } + +- private boolean verifyChatTrusted(PlayerChatMessage message) { ++ public boolean verifyChatTrusted(PlayerChatMessage message) { // Paper - private -> public + return message.hasSignature() && !message.hasExpiredServer(Instant.now()); + } + +- public ServerStatsCounter getPlayerStats(Player player) { +- UUID uuid = player.getUUID(); +- ServerStatsCounter serverStatsCounter = this.stats.get(uuid); ++ // CraftBukkit start ++ public ServerStatsCounter getPlayerStats(ServerPlayer player) { ++ ServerStatsCounter serverstatisticmanager = player.getStats(); ++ return serverstatisticmanager == null ? this.getPlayerStats(player.getUUID(), player.getGameProfile().getName()) : serverstatisticmanager; // Paper - use username and not display name ++ } ++ ++ public ServerStatsCounter getPlayerStats(UUID uuid, String displayName) { ++ ServerPlayer player = this.getPlayer(uuid); ++ ServerStatsCounter serverStatsCounter = player == null ? null : player.getStats(); ++ // CraftBukkit end + if (serverStatsCounter == null) { + File file = this.server.getWorldPath(LevelResource.PLAYER_STATS_DIR).toFile(); + File file1 = new File(file, uuid + ".json"); + if (!file1.exists()) { +- File file2 = new File(file, player.getName().getString() + ".json"); ++ File file2 = new File(file, displayName + ".json"); // CraftBukkit + Path path = file2.toPath(); + if (FileUtil.isPathNormalized(path) && FileUtil.isPathPortable(path) && path.startsWith(file.getPath()) && file2.isFile()) { + file2.renameTo(file1); +@@ -791,7 +_,7 @@ + } + + serverStatsCounter = new ServerStatsCounter(this.server, file1); +- this.stats.put(uuid, serverStatsCounter); ++ // this.stats.put(uuid, serverStatsCounter); // CraftBukkit + } + + return serverStatsCounter; +@@ -799,11 +_,11 @@ + + public PlayerAdvancements getPlayerAdvancements(ServerPlayer player) { + UUID uuid = player.getUUID(); +- PlayerAdvancements playerAdvancements = this.advancements.get(uuid); ++ PlayerAdvancements playerAdvancements = player.getAdvancements(); // CraftBukkit + if (playerAdvancements == null) { + Path path = this.server.getWorldPath(LevelResource.PLAYER_ADVANCEMENTS_DIR).resolve(uuid + ".json"); + playerAdvancements = new PlayerAdvancements(this.server.getFixerUpper(), this, this.server.getAdvancements(), path, player); +- this.advancements.put(uuid, playerAdvancements); ++ // this.advancements.put(uuid, playerAdvancements); // CraftBukkit + } + + playerAdvancements.setPlayer(player); +@@ -846,11 +_,34 @@ + } + + public void reloadResources() { +- for (PlayerAdvancements playerAdvancements : this.advancements.values()) { +- playerAdvancements.reload(this.server.getAdvancements()); ++ // Paper start - API for updating recipes on clients ++ this.reloadAdvancementData(); ++ this.reloadTagData(); ++ this.reloadRecipes(); ++ } ++ public void reloadAdvancementData() { ++ // Paper end - API for updating recipes on clients ++ // CraftBukkit start ++ // for (PlayerAdvancements playerAdvancements : this.advancements.values()) { ++ // playerAdvancements.reload(this.server.getAdvancements()); ++ // } ++ for (ServerPlayer player : this.players) { ++ player.getAdvancements().reload(this.server.getAdvancements()); ++ player.getAdvancements().flushDirty(player); // CraftBukkit - trigger immediate flush of advancements + } ++ // CraftBukkit end + ++ // Paper start - API for updating recipes on clients ++ } ++ public void reloadTagData() { + this.broadcastAll(new ClientboundUpdateTagsPacket(TagNetworkSerialization.serializeTagsToNetwork(this.registries))); ++ // CraftBukkit start ++ // this.reloadRecipes(); // Paper - do not reload recipes just because tag data was reloaded ++ // Paper end - API for updating recipes on clients ++ } ++ ++ public void reloadRecipes() { ++ // CraftBukkit end + RecipeManager recipeManager = this.server.getRecipeManager(); + ClientboundUpdateRecipesPacket clientboundUpdateRecipesPacket = new ClientboundUpdateRecipesPacket( + recipeManager.getSynchronizedItemProperties(), recipeManager.getSynchronizedStonecutterRecipes() diff --git a/paper-server/patches/unapplied/net/minecraft/server/players/PlayerList.java.patch b/paper-server/patches/unapplied/net/minecraft/server/players/PlayerList.java.patch deleted file mode 100644 index 06288fc4a0..0000000000 --- a/paper-server/patches/unapplied/net/minecraft/server/players/PlayerList.java.patch +++ /dev/null @@ -1,1276 +0,0 @@ ---- a/net/minecraft/server/players/PlayerList.java -+++ b/net/minecraft/server/players/PlayerList.java -@@ -76,6 +76,7 @@ - import net.minecraft.server.level.ServerPlayer; - import net.minecraft.server.network.CommonListenerCookie; - import net.minecraft.server.network.ServerGamePacketListenerImpl; -+import net.minecraft.server.network.ServerLoginPacketListenerImpl; - import net.minecraft.sounds.SoundEvents; - import net.minecraft.sounds.SoundSource; - import net.minecraft.stats.ServerStatsCounter; -@@ -84,7 +85,6 @@ - import net.minecraft.world.effect.MobEffectInstance; - import net.minecraft.world.entity.Entity; - import net.minecraft.world.entity.LivingEntity; --import net.minecraft.world.entity.player.Player; - import net.minecraft.world.entity.projectile.ThrownEnderpearl; - import net.minecraft.world.item.crafting.RecipeManager; - import net.minecraft.world.level.GameRules; -@@ -104,6 +104,26 @@ - import net.minecraft.world.scores.PlayerTeam; - import org.slf4j.Logger; - -+// CraftBukkit start -+import java.util.stream.Collectors; -+import net.minecraft.server.dedicated.DedicatedServer; -+import org.bukkit.Location; -+import org.bukkit.craftbukkit.CraftServer; -+import org.bukkit.craftbukkit.CraftWorld; -+import org.bukkit.craftbukkit.entity.CraftPlayer; -+import org.bukkit.craftbukkit.util.CraftChatMessage; -+import org.bukkit.craftbukkit.util.CraftLocation; -+import org.bukkit.entity.Player; -+import org.bukkit.event.entity.EntityRemoveEvent; -+import org.bukkit.event.player.PlayerChangedWorldEvent; -+import org.bukkit.event.player.PlayerJoinEvent; -+import org.bukkit.event.player.PlayerLoginEvent; -+import org.bukkit.event.player.PlayerQuitEvent; -+import org.bukkit.event.player.PlayerRespawnEvent; -+import org.bukkit.event.player.PlayerRespawnEvent.RespawnReason; -+import org.bukkit.event.player.PlayerSpawnChangeEvent; -+// CraftBukkit end -+ - public abstract class PlayerList { - - public static final File USERBANLIST_FILE = new File("banned-players.json"); -@@ -116,14 +136,16 @@ - private static final int SEND_PLAYER_INFO_INTERVAL = 600; - private static final SimpleDateFormat BAN_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z"); - private final MinecraftServer server; -- public final List players = Lists.newArrayList(); -+ public final List players = new java.util.concurrent.CopyOnWriteArrayList(); // CraftBukkit - ArrayList -> CopyOnWriteArrayList: Iterator safety - private final Map playersByUUID = Maps.newHashMap(); - private final UserBanList bans; - private final IpBanList ipBans; - private final ServerOpList ops; - private final UserWhiteList whitelist; -- private final Map stats; -- private final Map advancements; -+ // CraftBukkit start -+ // private final Map stats; -+ // private final Map advancements; -+ // CraftBukkit end - public final PlayerDataStorage playerIo; - private boolean doWhiteList; - private final LayeredRegistryAccess registries; -@@ -134,58 +156,137 @@ - private static final boolean ALLOW_LOGOUTIVATOR = false; - private int sendAllPlayerInfoIn; - -+ // CraftBukkit start -+ private CraftServer cserver; -+ private final Map playersByName = new java.util.HashMap<>(); -+ public @Nullable String collideRuleTeamName; // Paper - Configurable player collision -+ - public PlayerList(MinecraftServer server, LayeredRegistryAccess registryManager, PlayerDataStorage saveHandler, int maxPlayers) { -+ this.cserver = server.server = new CraftServer((DedicatedServer) server, this); -+ server.console = new com.destroystokyo.paper.console.TerminalConsoleCommandSender(); // Paper -+ // CraftBukkit end -+ - this.bans = new UserBanList(PlayerList.USERBANLIST_FILE); - this.ipBans = new IpBanList(PlayerList.IPBANLIST_FILE); - this.ops = new ServerOpList(PlayerList.OPLIST_FILE); - this.whitelist = new UserWhiteList(PlayerList.WHITELIST_FILE); -- this.stats = Maps.newHashMap(); -- this.advancements = Maps.newHashMap(); -+ // CraftBukkit start -+ // this.stats = Maps.newHashMap(); -+ // this.advancements = Maps.newHashMap(); -+ // CraftBukkit end - this.server = server; - this.registries = registryManager; - this.maxPlayers = maxPlayers; - this.playerIo = saveHandler; - } -+ abstract public void loadAndSaveFiles(); // Paper - fix converting txt to json file; moved from DedicatedPlayerList constructor - - public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie clientData) { -+ player.isRealPlayer = true; // Paper -+ player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed - GameProfile gameprofile = player.getGameProfile(); - GameProfileCache usercache = this.server.getProfileCache(); -- Optional optional; -+ // Optional optional; // CraftBukkit - decompile error - String s; - - if (usercache != null) { -- optional = usercache.get(gameprofile.getId()); -+ Optional optional = usercache.get(gameprofile.getId()); // CraftBukkit - decompile error - s = (String) optional.map(GameProfile::getName).orElse(gameprofile.getName()); - usercache.add(gameprofile); - } else { - s = gameprofile.getName(); - } - -- optional = this.load(player); -- ResourceKey resourcekey = (ResourceKey) optional.flatMap((nbttagcompound) -> { -- DataResult dataresult = DimensionType.parseLegacy(new Dynamic(NbtOps.INSTANCE, nbttagcompound.get("Dimension"))); -+ Optional optional = this.load(player); // CraftBukkit - decompile error -+ ResourceKey resourcekey = null; // Paper -+ // CraftBukkit start - Better rename detection -+ if (optional.isPresent()) { -+ CompoundTag nbttagcompound = optional.get(); -+ if (nbttagcompound.contains("bukkit")) { -+ CompoundTag bukkit = nbttagcompound.getCompound("bukkit"); -+ s = bukkit.contains("lastKnownName", 8) ? bukkit.getString("lastKnownName") : s; -+ } -+ } -+ // CraftBukkit end -+ // Paper start - move logic in Entity to here, to use bukkit supplied world UUID & reset to main world spawn if no valid world is found -+ boolean[] invalidPlayerWorld = {false}; -+ bukkitData: if (optional.isPresent()) { -+ // The main way for bukkit worlds to store the world is the world UUID despite mojang adding custom worlds -+ final org.bukkit.World bWorld; -+ if (optional.get().contains("WorldUUIDMost") && optional.get().contains("WorldUUIDLeast")) { -+ bWorld = org.bukkit.Bukkit.getServer().getWorld(new UUID(optional.get().getLong("WorldUUIDMost"), optional.get().getLong("WorldUUIDLeast"))); -+ } else if (optional.get().contains("world", net.minecraft.nbt.Tag.TAG_STRING)) { // Paper - legacy bukkit world name -+ bWorld = org.bukkit.Bukkit.getServer().getWorld(optional.get().getString("world")); -+ } else { -+ break bukkitData; // if neither of the bukkit data points exist, proceed to the vanilla migration section -+ } -+ if (bWorld != null) { -+ resourcekey = ((CraftWorld) bWorld).getHandle().dimension(); -+ } else { -+ resourcekey = Level.OVERWORLD; -+ invalidPlayerWorld[0] = true; -+ } -+ } -+ if (resourcekey == null) { // only run the vanilla logic if we haven't found a world from the bukkit data -+ // Below is the vanilla way of getting the dimension, this is for migration from vanilla servers -+ resourcekey = optional.flatMap((nbttagcompound) -> { -+ // Paper end -+ DataResult> dataresult = DimensionType.parseLegacy(new Dynamic(NbtOps.INSTANCE, nbttagcompound.get("Dimension"))); // CraftBukkit - decompile error - Logger logger = PlayerList.LOGGER; - - Objects.requireNonNull(logger); -- return dataresult.resultOrPartial(logger::error); -- }).orElse(Level.OVERWORLD); -+ // Paper start - reset to main world spawn if no valid world is found -+ final Optional> result = dataresult.resultOrPartial(logger::error); -+ invalidPlayerWorld[0] = result.isEmpty(); -+ return result; -+ }).orElse(Level.OVERWORLD); // Paper - revert to vanilla default main world, this isn't an "invalid world" since no player data existed -+ } -+ // Paper end - ServerLevel worldserver = this.server.getLevel(resourcekey); - ServerLevel worldserver1; - - if (worldserver == null) { - PlayerList.LOGGER.warn("Unknown respawn dimension {}, defaulting to overworld", resourcekey); - worldserver1 = this.server.overworld(); -+ invalidPlayerWorld[0] = true; // Paper - reset to main world if no world with parsed value is found - } else { - worldserver1 = worldserver; - } - -+ // Paper start - Entity#getEntitySpawnReason -+ if (optional.isEmpty()) { -+ player.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT; // set Player SpawnReason to DEFAULT on first login -+ // Paper start - reset to main world spawn if first spawn or invalid world -+ } -+ if (optional.isEmpty() || invalidPlayerWorld[0]) { -+ // Paper end - reset to main world spawn if first spawn or invalid world -+ player.moveTo(player.adjustSpawnLocation(worldserver1, worldserver1.getSharedSpawnPos()).getBottomCenter(), worldserver1.getSharedSpawnAngle(), 0.0F); // Paper - MC-200092 - fix first spawn pos yaw being ignored -+ } -+ // Paper end - Entity#getEntitySpawnReason - player.setServerLevel(worldserver1); - String s1 = connection.getLoggableAddress(this.server.logIPs()); - -- PlayerList.LOGGER.info("{}[{}] logged in with entity id {} at ({}, {}, {})", new Object[]{player.getName().getString(), s1, player.getId(), player.getX(), player.getY(), player.getZ()}); -+ // Spigot start - spawn location event -+ Player spawnPlayer = player.getBukkitEntity(); -+ org.spigotmc.event.player.PlayerSpawnLocationEvent ev = new org.spigotmc.event.player.PlayerSpawnLocationEvent(spawnPlayer, spawnPlayer.getLocation()); -+ this.cserver.getPluginManager().callEvent(ev); -+ -+ Location loc = ev.getSpawnLocation(); -+ worldserver1 = ((CraftWorld) loc.getWorld()).getHandle(); -+ -+ player.spawnIn(worldserver1); -+ player.gameMode.setLevel((ServerLevel) player.level()); -+ // Paper start - set raw so we aren't fully joined to the world (not added to chunk or world) -+ player.setPosRaw(loc.getX(), loc.getY(), loc.getZ()); -+ player.setRot(loc.getYaw(), loc.getPitch()); -+ // Paper end - set raw so we aren't fully joined to the world -+ // Spigot end -+ -+ // CraftBukkit - Moved message to after join -+ // PlayerList.LOGGER.info("{}[{}] logged in with entity id {} at ({}, {}, {})", new Object[]{entityplayer.getName().getString(), s1, entityplayer.getId(), entityplayer.getX(), entityplayer.getY(), entityplayer.getZ()}); - LevelData worlddata = worldserver1.getLevelData(); - -- player.loadGameTypes((CompoundTag) optional.orElse((Object) null)); -+ player.loadGameTypes((CompoundTag) optional.orElse(null)); // CraftBukkit - decompile error - ServerGamePacketListenerImpl playerconnection = new ServerGamePacketListenerImpl(this.server, connection, player, clientData); - - connection.setupInboundProtocol(GameProtocols.SERVERBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(this.server.registryAccess())), playerconnection); -@@ -194,7 +295,9 @@ - boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO); - boolean flag2 = gamerules.getBoolean(GameRules.RULE_LIMITED_CRAFTING); - -- playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), this.server.levelKeys(), this.getMaxPlayers(), this.viewDistance, this.simulationDistance, flag1, !flag, flag2, player.createCommonSpawnInfo(worldserver1), this.server.enforceSecureProfile())); -+ // Spigot - view distance -+ playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), this.server.levelKeys(), this.getMaxPlayers(), worldserver1.spigotConfig.viewDistance, worldserver1.spigotConfig.simulationDistance, flag1, !flag, flag2, player.createCommonSpawnInfo(worldserver1), this.server.enforceSecureProfile())); -+ player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit - playerconnection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked())); - playerconnection.send(new ClientboundPlayerAbilitiesPacket(player.getAbilities())); - playerconnection.send(new ClientboundSetHeldSlotPacket(player.getInventory().selected)); -@@ -213,8 +316,10 @@ - } else { - ichatmutablecomponent = Component.translatable("multiplayer.player.joined.renamed", player.getDisplayName(), s); - } -+ // CraftBukkit start -+ ichatmutablecomponent.withStyle(ChatFormatting.YELLOW); -+ Component joinMessage = ichatmutablecomponent; // Paper - Adventure - -- this.broadcastSystemMessage(ichatmutablecomponent.withStyle(ChatFormatting.YELLOW), false); - playerconnection.teleport(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()); - ServerStatus serverping = this.server.getStatus(); - -@@ -222,17 +327,109 @@ - player.sendServerStatus(serverping); - } - -- player.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(this.players)); -+ // entityplayer.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(this.players)); // CraftBukkit - replaced with loop below - this.players.add(player); -+ this.playersByName.put(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT), player); // Spigot - this.playersByUUID.put(player.getUUID(), player); -- this.broadcastAll(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player))); -- this.sendLevelInfo(player, worldserver1); -+ // this.broadcastAll(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityplayer))); // CraftBukkit - replaced with loop below -+ -+ // Paper start - Fire PlayerJoinEvent when Player is actually ready; correctly register player BEFORE PlayerJoinEvent, so the entity is valid and doesn't require tick delay hacks -+ player.supressTrackerForLogin = true; - worldserver1.addNewPlayer(player); -- this.server.getCustomBossEvents().onPlayerConnect(player); -- this.sendActivePlayerEffects(player); -+ this.server.getCustomBossEvents().onPlayerConnect(player); // see commented out section below worldserver.addPlayerJoin(entityplayer); - player.loadAndSpawnEnderpearls(optional); - player.loadAndSpawnParentVehicle(optional); -+ // Paper end - Fire PlayerJoinEvent when Player is actually ready -+ // CraftBukkit start -+ CraftPlayer bukkitPlayer = player.getBukkitEntity(); -+ -+ // Ensure that player inventory is populated with its viewer -+ player.containerMenu.transferTo(player.containerMenu, bukkitPlayer); -+ -+ PlayerJoinEvent playerJoinEvent = new PlayerJoinEvent(bukkitPlayer, io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure -+ this.cserver.getPluginManager().callEvent(playerJoinEvent); -+ -+ if (!player.connection.isAcceptingMessages()) { -+ return; -+ } -+ -+ final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage(); -+ -+ if (jm != null && !jm.equals(net.kyori.adventure.text.Component.empty())) { // Paper - Adventure -+ joinMessage = io.papermc.paper.adventure.PaperAdventure.asVanilla(jm); // Paper - Adventure -+ this.server.getPlayerList().broadcastSystemMessage(joinMessage, false); // Paper - Adventure -+ } -+ // CraftBukkit end -+ -+ // CraftBukkit start - sendAll above replaced with this loop -+ ClientboundPlayerInfoUpdatePacket packet = ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player)); // Paper - Add Listing API for Player -+ -+ final List onlinePlayers = Lists.newArrayListWithExpectedSize(this.players.size() - 1); // Paper - Use single player info update packet on join -+ for (int i = 0; i < this.players.size(); ++i) { -+ ServerPlayer entityplayer1 = (ServerPlayer) this.players.get(i); -+ -+ if (entityplayer1.getBukkitEntity().canSee(bukkitPlayer)) { -+ // Paper start - Add Listing API for Player -+ if (entityplayer1.getBukkitEntity().isListed(bukkitPlayer)) { -+ // Paper end - Add Listing API for Player -+ entityplayer1.connection.send(packet); -+ // Paper start - Add Listing API for Player -+ } else { -+ entityplayer1.connection.send(ClientboundPlayerInfoUpdatePacket.createSinglePlayerInitializing(player, false)); -+ } -+ // Paper end - Add Listing API for Player -+ } -+ -+ if (entityplayer1 == player || !bukkitPlayer.canSee(entityplayer1.getBukkitEntity())) { // Paper - Use single player info update packet on join; Don't include joining player -+ continue; -+ } -+ -+ onlinePlayers.add(entityplayer1); // Paper - Use single player info update packet on join -+ } -+ // Paper start - Use single player info update packet on join -+ if (!onlinePlayers.isEmpty()) { -+ player.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(onlinePlayers, player)); // Paper - Add Listing API for Player -+ } -+ // Paper end - Use single player info update packet on join -+ player.sentListPacket = true; -+ player.supressTrackerForLogin = false; // Paper - Fire PlayerJoinEvent when Player is actually ready -+ ((ServerLevel)player.level()).getChunkSource().chunkMap.addEntity(player); // Paper - Fire PlayerJoinEvent when Player is actually ready; track entity now -+ // CraftBukkit end -+ -+ //player.refreshEntityData(player); // CraftBukkit - BungeeCord#2321, send complete data to self on spawn // Paper - THIS IS NOT NEEDED ANYMORE -+ -+ this.sendLevelInfo(player, worldserver1); -+ -+ // CraftBukkit start - Only add if the player wasn't moved in the event -+ if (player.level() == worldserver1 && !worldserver1.players().contains(player)) { -+ worldserver1.addNewPlayer(player); -+ this.server.getCustomBossEvents().onPlayerConnect(player); -+ } -+ -+ worldserver1 = player.serverLevel(); // CraftBukkit - Update in case join event changed it -+ // CraftBukkit end -+ this.sendActivePlayerEffects(player); -+ // Paper - move loading pearls / parent vehicle up - player.initInventoryMenu(); -+ // CraftBukkit - Moved from above, added world -+ // Paper start - Configurable player collision; Add to collideRule team if needed -+ final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard(); -+ final PlayerTeam collideRuleTeam = scoreboard.getPlayerTeam(this.collideRuleTeamName); -+ if (this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) { -+ scoreboard.addPlayerToTeam(player.getScoreboardName(), collideRuleTeam); -+ } -+ // Paper end - Configurable player collision -+ PlayerList.LOGGER.info("{}[{}] logged in with entity id {} at ([{}]{}, {}, {})", player.getName().getString(), s1, player.getId(), worldserver1.serverLevelData.getLevelName(), player.getX(), player.getY(), player.getZ()); -+ // Paper start - Send empty chunk, so players aren't stuck in the world loading screen with our chunk system not sending chunks when dead -+ if (player.isDeadOrDying()) { -+ net.minecraft.core.Holder plains = worldserver1.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.BIOME) -+ .getOrThrow(net.minecraft.world.level.biome.Biomes.PLAINS); -+ player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket( -+ new net.minecraft.world.level.chunk.EmptyLevelChunk(worldserver1, player.chunkPosition(), plains), -+ worldserver1.getLightEngine(), (java.util.BitSet)null, (java.util.BitSet) null) -+ ); -+ } -+ // Paper end - Send empty chunk - } - - public void updateEntireScoreboard(ServerScoreboard scoreboard, ServerPlayer player) { -@@ -269,30 +466,31 @@ - } - - public void addWorldborderListener(ServerLevel world) { -+ if (this.playerIo != null) return; // CraftBukkit - world.getWorldBorder().addListener(new BorderChangeListener() { - @Override - public void onBorderSizeSet(WorldBorder border, double size) { -- PlayerList.this.broadcastAll(new ClientboundSetBorderSizePacket(border)); -+ PlayerList.this.broadcastAll(new ClientboundSetBorderSizePacket(border), border.world); // CraftBukkit - } - - @Override - public void onBorderSizeLerping(WorldBorder border, double fromSize, double toSize, long time) { -- PlayerList.this.broadcastAll(new ClientboundSetBorderLerpSizePacket(border)); -+ PlayerList.this.broadcastAll(new ClientboundSetBorderLerpSizePacket(border), border.world); // CraftBukkit - } - - @Override - public void onBorderCenterSet(WorldBorder border, double centerX, double centerZ) { -- PlayerList.this.broadcastAll(new ClientboundSetBorderCenterPacket(border)); -+ PlayerList.this.broadcastAll(new ClientboundSetBorderCenterPacket(border), border.world); // CraftBukkit - } - - @Override - public void onBorderSetWarningTime(WorldBorder border, int warningTime) { -- PlayerList.this.broadcastAll(new ClientboundSetBorderWarningDelayPacket(border)); -+ PlayerList.this.broadcastAll(new ClientboundSetBorderWarningDelayPacket(border), border.world); // CraftBukkit - } - - @Override - public void onBorderSetWarningBlocks(WorldBorder border, int warningBlockDistance) { -- PlayerList.this.broadcastAll(new ClientboundSetBorderWarningDistancePacket(border)); -+ PlayerList.this.broadcastAll(new ClientboundSetBorderWarningDistancePacket(border), border.world); // CraftBukkit - } - - @Override -@@ -319,14 +517,15 @@ - } - - protected void save(ServerPlayer player) { -+ if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit - this.playerIo.save(player); -- ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) this.stats.get(player.getUUID()); -+ ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit - - if (serverstatisticmanager != null) { - serverstatisticmanager.save(); - } - -- PlayerAdvancements advancementdataplayer = (PlayerAdvancements) this.advancements.get(player.getUUID()); -+ PlayerAdvancements advancementdataplayer = (PlayerAdvancements) player.getAdvancements(); // CraftBukkit - - if (advancementdataplayer != null) { - advancementdataplayer.save(); -@@ -334,95 +533,216 @@ - - } - -- public void remove(ServerPlayer player) { -- ServerLevel worldserver = player.serverLevel(); -+ public net.kyori.adventure.text.Component remove(ServerPlayer entityplayer) { // CraftBukkit - return string // Paper - return Component -+ // Paper start - Fix kick event leave message not being sent -+ return this.remove(entityplayer, net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? entityplayer.getBukkitEntity().displayName() : io.papermc.paper.adventure.PaperAdventure.asAdventure(entityplayer.getDisplayName()))); -+ } -+ public net.kyori.adventure.text.Component remove(ServerPlayer entityplayer, net.kyori.adventure.text.Component leaveMessage) { -+ // Paper end - Fix kick event leave message not being sent -+ ServerLevel worldserver = entityplayer.serverLevel(); - -- player.awardStat(Stats.LEAVE_GAME); -- this.save(player); -- if (player.isPassenger()) { -- Entity entity = player.getRootVehicle(); -+ entityplayer.awardStat(Stats.LEAVE_GAME); - -+ // CraftBukkit start - Quitting must be before we do final save of data, in case plugins need to modify it -+ // See SPIGOT-5799, SPIGOT-6145 -+ if (entityplayer.containerMenu != entityplayer.inventoryMenu) { -+ entityplayer.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.DISCONNECT); // Paper - Inventory close reason -+ } -+ -+ PlayerQuitEvent playerQuitEvent = new PlayerQuitEvent(entityplayer.getBukkitEntity(), leaveMessage, entityplayer.quitReason); // Paper - Adventure & Add API for quit reason -+ this.cserver.getPluginManager().callEvent(playerQuitEvent); -+ entityplayer.getBukkitEntity().disconnect(playerQuitEvent.getQuitMessage()); -+ -+ entityplayer.doTick(); // SPIGOT-924 -+ // CraftBukkit end -+ -+ // Paper start - Configurable player collision; Remove from collideRule team if needed -+ if (this.collideRuleTeamName != null) { -+ final net.minecraft.world.scores.Scoreboard scoreBoard = this.server.getLevel(Level.OVERWORLD).getScoreboard(); -+ final PlayerTeam team = scoreBoard.getPlayersTeam(this.collideRuleTeamName); -+ if (entityplayer.getTeam() == team && team != null) { -+ scoreBoard.removePlayerFromTeam(entityplayer.getScoreboardName(), team); -+ } -+ } -+ // Paper end - Configurable player collision -+ -+ // Paper - Drop carried item when player has disconnected -+ if (!entityplayer.containerMenu.getCarried().isEmpty()) { -+ net.minecraft.world.item.ItemStack carried = entityplayer.containerMenu.getCarried(); -+ entityplayer.containerMenu.setCarried(net.minecraft.world.item.ItemStack.EMPTY); -+ entityplayer.drop(carried, false); -+ } -+ // Paper end - Drop carried item when player has disconnected -+ -+ this.save(entityplayer); -+ if (entityplayer.isPassenger()) { -+ Entity entity = entityplayer.getRootVehicle(); -+ - if (entity.hasExactlyOnePlayerPassenger()) { - PlayerList.LOGGER.debug("Removing player mount"); -- player.stopRiding(); -+ entityplayer.stopRiding(); - entity.getPassengersAndSelf().forEach((entity1) -> { -- entity1.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER); -+ // Paper start - Fix villager boat exploit -+ if (entity1 instanceof net.minecraft.world.entity.npc.AbstractVillager villager) { -+ final net.minecraft.world.entity.player.Player human = villager.getTradingPlayer(); -+ if (human != null) { -+ villager.setTradingPlayer(null); -+ } -+ } -+ // Paper end - Fix villager boat exploit -+ entity1.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER, EntityRemoveEvent.Cause.PLAYER_QUIT); // CraftBukkit - add Bukkit remove cause - }); - } - } - -- player.unRide(); -- Iterator iterator = player.getEnderPearls().iterator(); -+ entityplayer.unRide(); -+ Iterator iterator = entityplayer.getEnderPearls().iterator(); - - while (iterator.hasNext()) { - ThrownEnderpearl entityenderpearl = (ThrownEnderpearl) iterator.next(); - -- entityenderpearl.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER); -+ // Paper start - Allow using old ender pearl behavior -+ if (!entityenderpearl.level().paperConfig().misc.legacyEnderPearlBehavior) { -+ entityenderpearl.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER, EntityRemoveEvent.Cause.PLAYER_QUIT); // CraftBukkit - add Bukkit remove cause -+ } else { -+ entityenderpearl.cachedOwner = null; -+ } -+ // Paper end - Allow using old ender pearl behavior - } - -- worldserver.removePlayerImmediately(player, Entity.RemovalReason.UNLOADED_WITH_PLAYER); -- player.getAdvancements().stopListening(); -- this.players.remove(player); -- this.server.getCustomBossEvents().onPlayerDisconnect(player); -- UUID uuid = player.getUUID(); -+ worldserver.removePlayerImmediately(entityplayer, Entity.RemovalReason.UNLOADED_WITH_PLAYER); -+ entityplayer.retireScheduler(); // Paper - Folia schedulers -+ entityplayer.getAdvancements().stopListening(); -+ this.players.remove(entityplayer); -+ this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot -+ this.server.getCustomBossEvents().onPlayerDisconnect(entityplayer); -+ UUID uuid = entityplayer.getUUID(); - ServerPlayer entityplayer1 = (ServerPlayer) this.playersByUUID.get(uuid); - -- if (entityplayer1 == player) { -+ if (entityplayer1 == entityplayer) { - this.playersByUUID.remove(uuid); -- this.stats.remove(uuid); -- this.advancements.remove(uuid); -+ // CraftBukkit start -+ // this.stats.remove(uuid); -+ // this.advancements.remove(uuid); -+ // CraftBukkit end - } - -- this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID()))); -+ // CraftBukkit start -+ // this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(entityplayer.getUUID()))); -+ ClientboundPlayerInfoRemovePacket packet = new ClientboundPlayerInfoRemovePacket(List.of(entityplayer.getUUID())); -+ for (int i = 0; i < this.players.size(); i++) { -+ ServerPlayer entityplayer2 = (ServerPlayer) this.players.get(i); -+ -+ if (entityplayer2.getBukkitEntity().canSee(entityplayer.getBukkitEntity())) { -+ entityplayer2.connection.send(packet); -+ } else { -+ entityplayer2.getBukkitEntity().onEntityRemove(entityplayer); -+ } -+ } -+ // This removes the scoreboard (and player reference) for the specific player in the manager -+ this.cserver.getScoreboardManager().removePlayer(entityplayer.getBukkitEntity()); -+ // CraftBukkit end -+ -+ return playerQuitEvent.quitMessage(); // Paper - Adventure - } - -- @Nullable -- public Component canPlayerLogin(SocketAddress address, GameProfile profile) { -+ // CraftBukkit start - Whole method, SocketAddress to LoginListener, added hostname to signature, return EntityPlayer -+ public ServerPlayer canPlayerLogin(ServerLoginPacketListenerImpl loginlistener, GameProfile gameprofile) { - MutableComponent ichatmutablecomponent; - -- if (this.bans.isBanned(profile)) { -- UserBanListEntry gameprofilebanentry = (UserBanListEntry) this.bans.get(profile); -+ // Moved from processLogin -+ UUID uuid = gameprofile.getId(); -+ List list = Lists.newArrayList(); - -+ ServerPlayer entityplayer; -+ -+ for (int i = 0; i < this.players.size(); ++i) { -+ entityplayer = (ServerPlayer) this.players.get(i); -+ if (entityplayer.getUUID().equals(uuid) || (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() && entityplayer.getGameProfile().getName().equalsIgnoreCase(gameprofile.getName()))) { // Paper - validate usernames -+ list.add(entityplayer); -+ } -+ } -+ -+ Iterator iterator = list.iterator(); -+ -+ while (iterator.hasNext()) { -+ entityplayer = (ServerPlayer) iterator.next(); -+ this.save(entityplayer); // CraftBukkit - Force the player's inventory to be saved -+ entityplayer.connection.disconnect(Component.translatable("multiplayer.disconnect.duplicate_login"), org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); // Paper - kick event cause -+ } -+ -+ // Instead of kicking then returning, we need to store the kick reason -+ // in the event, check with plugins to see if it's ok, and THEN kick -+ // depending on the outcome. -+ SocketAddress socketaddress = loginlistener.connection.getRemoteAddress(); -+ -+ ServerPlayer entity = new ServerPlayer(this.server, this.server.getLevel(Level.OVERWORLD), gameprofile, ClientInformation.createDefault()); -+ entity.transferCookieConnection = loginlistener; -+ Player player = entity.getBukkitEntity(); -+ PlayerLoginEvent event = new PlayerLoginEvent(player, loginlistener.connection.hostname, ((java.net.InetSocketAddress) socketaddress).getAddress(), ((java.net.InetSocketAddress) loginlistener.connection.channel.remoteAddress()).getAddress()); -+ -+ // Paper start - Fix MC-158900 -+ UserBanListEntry gameprofilebanentry; -+ if (this.bans.isBanned(gameprofile) && (gameprofilebanentry = this.bans.get(gameprofile)) != null) { -+ // Paper end - Fix MC-158900 -+ - ichatmutablecomponent = Component.translatable("multiplayer.disconnect.banned.reason", gameprofilebanentry.getReason()); - if (gameprofilebanentry.getExpires() != null) { - ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned.expiration", PlayerList.BAN_DATE_FORMAT.format(gameprofilebanentry.getExpires()))); - } - -- return ichatmutablecomponent; -- } else if (!this.isWhiteListed(profile)) { -- return Component.translatable("multiplayer.disconnect.not_whitelisted"); -- } else if (this.ipBans.isBanned(address)) { -- IpBanListEntry ipbanentry = this.ipBans.get(address); -+ // return chatmessage; -+ event.disallow(PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure -+ } else if (!this.isWhiteListed(gameprofile, event)) { // Paper - ProfileWhitelistVerifyEvent -+ //ichatmutablecomponent = Component.translatable("multiplayer.disconnect.not_whitelisted"); // Paper -+ //event.disallow(PlayerLoginEvent.Result.KICK_WHITELIST, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.whitelistMessage)); // Spigot // Paper - Adventure - moved to isWhitelisted -+ } else if (this.getIpBans().isBanned(socketaddress) && getIpBans().get(socketaddress) != null && !this.getIpBans().get(socketaddress).hasExpired()) { // Paper - fix NPE with temp ip bans -+ IpBanListEntry ipbanentry = this.ipBans.get(socketaddress); - - ichatmutablecomponent = Component.translatable("multiplayer.disconnect.banned_ip.reason", ipbanentry.getReason()); - if (ipbanentry.getExpires() != null) { - ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned_ip.expiration", PlayerList.BAN_DATE_FORMAT.format(ipbanentry.getExpires()))); - } - -- return ichatmutablecomponent; -+ // return chatmessage; -+ event.disallow(PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure - } else { -- return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(profile) ? Component.translatable("multiplayer.disconnect.server_full") : null; -+ // return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile) ? IChatBaseComponent.translatable("multiplayer.disconnect.server_full") : null; -+ if (this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile)) { -+ event.disallow(PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure -+ } - } -+ -+ this.cserver.getPluginManager().callEvent(event); -+ if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) { -+ loginlistener.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.kickMessage())); // Paper - Adventure -+ return null; -+ } -+ return entity; - } - -- public ServerPlayer getPlayerForLogin(GameProfile profile, ClientInformation syncedOptions) { -- return new ServerPlayer(this.server, this.server.overworld(), profile, syncedOptions); -+ // CraftBukkit start - added EntityPlayer -+ public ServerPlayer getPlayerForLogin(GameProfile gameprofile, ClientInformation clientinformation, ServerPlayer player) { -+ player.updateOptions(clientinformation); -+ return player; -+ // CraftBukkit end - } - -- public boolean disconnectAllPlayersWithProfile(GameProfile profile) { -- UUID uuid = profile.getId(); -- Set set = Sets.newIdentityHashSet(); -+ public boolean disconnectAllPlayersWithProfile(GameProfile gameprofile, ServerPlayer player) { // CraftBukkit - added EntityPlayer -+ /* CraftBukkit startMoved up -+ UUID uuid = gameprofile.getId(); -+ Set set = Sets.newIdentityHashSet(); - Iterator iterator = this.players.iterator(); - - while (iterator.hasNext()) { -- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); -+ EntityPlayer entityplayer = (EntityPlayer) iterator.next(); - - if (entityplayer.getUUID().equals(uuid)) { - set.add(entityplayer); - } - } - -- ServerPlayer entityplayer1 = (ServerPlayer) this.playersByUUID.get(profile.getId()); -+ EntityPlayer entityplayer1 = (EntityPlayer) this.playersByUUID.get(gameprofile.getId()); - - if (entityplayer1 != null) { - set.add(entityplayer1); -@@ -431,72 +751,160 @@ - Iterator iterator1 = set.iterator(); - - while (iterator1.hasNext()) { -- ServerPlayer entityplayer2 = (ServerPlayer) iterator1.next(); -+ EntityPlayer entityplayer2 = (EntityPlayer) iterator1.next(); - - entityplayer2.connection.disconnect(PlayerList.DUPLICATE_LOGIN_DISCONNECT_MESSAGE); - } - - return !set.isEmpty(); -+ */ -+ return player == null; -+ // CraftBukkit end - } - -- public ServerPlayer respawn(ServerPlayer player, boolean alive, Entity.RemovalReason removalReason) { -- this.players.remove(player); -- player.serverLevel().removePlayerImmediately(player, removalReason); -- TeleportTransition teleporttransition = player.findRespawnPositionAndUseSpawnBlock(!alive, TeleportTransition.DO_NOTHING); -- ServerLevel worldserver = teleporttransition.newLevel(); -- ServerPlayer entityplayer1 = new ServerPlayer(this.server, worldserver, player.getGameProfile(), player.clientInformation()); -+ // CraftBukkit start -+ public ServerPlayer respawn(ServerPlayer entityplayer, boolean flag, Entity.RemovalReason entity_removalreason, RespawnReason reason) { -+ return this.respawn(entityplayer, flag, entity_removalreason, reason, null); -+ } - -- entityplayer1.connection = player.connection; -- entityplayer1.restoreFrom(player, alive); -- entityplayer1.setId(player.getId()); -- entityplayer1.setMainArm(player.getMainArm()); -+ public ServerPlayer respawn(ServerPlayer entityplayer, boolean flag, Entity.RemovalReason entity_removalreason, RespawnReason reason, Location location) { -+ entityplayer.stopRiding(); // CraftBukkit -+ this.players.remove(entityplayer); -+ this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot -+ entityplayer.serverLevel().removePlayerImmediately(entityplayer, entity_removalreason); -+ /* CraftBukkit start -+ TeleportTransition teleporttransition = entityplayer.findRespawnPositionAndUseSpawnBlock(!flag, TeleportTransition.DO_NOTHING); -+ WorldServer worldserver = teleporttransition.newLevel(); -+ EntityPlayer entityplayer1 = new EntityPlayer(this.server, worldserver, entityplayer.getGameProfile(), entityplayer.clientInformation()); -+ // */ -+ ServerPlayer entityplayer1 = entityplayer; -+ Level fromWorld = entityplayer.level(); -+ entityplayer.wonGame = false; -+ // CraftBukkit end -+ -+ entityplayer1.connection = entityplayer.connection; -+ entityplayer1.restoreFrom(entityplayer, flag); -+ entityplayer1.setId(entityplayer.getId()); -+ entityplayer1.setMainArm(entityplayer.getMainArm()); -+ // CraftBukkit - not required, just copies old location into reused entity -+ /* - if (!teleporttransition.missingRespawnBlock()) { -- entityplayer1.copyRespawnPosition(player); -+ entityplayer1.copyRespawnPosition(entityplayer); - } -+ */ -+ // CraftBukkit end - -- Iterator iterator = player.getTags().iterator(); -+ Iterator iterator = entityplayer.getTags().iterator(); - - while (iterator.hasNext()) { - String s = (String) iterator.next(); - - entityplayer1.addTag(s); - } -+ // Paper start - Add PlayerPostRespawnEvent -+ boolean isBedSpawn = false; -+ boolean isRespawn = false; -+ // Paper end - Add PlayerPostRespawnEvent - -+ // CraftBukkit start - fire PlayerRespawnEvent -+ TeleportTransition teleporttransition; -+ if (location == null) { -+ teleporttransition = entityplayer.findRespawnPositionAndUseSpawnBlock(!flag, TeleportTransition.DO_NOTHING, reason); -+ -+ if (!flag) entityplayer.reset(); // SPIGOT-4785 -+ // Paper start - Add PlayerPostRespawnEvent -+ if (teleporttransition == null) return entityplayer; // Early exit, mirrors belows early return for disconnected players in respawn event -+ isRespawn = true; -+ location = CraftLocation.toBukkit(teleporttransition.position(), teleporttransition.newLevel().getWorld(), teleporttransition.yRot(), teleporttransition.xRot()); -+ // Paper end - Add PlayerPostRespawnEvent -+ } else { -+ teleporttransition = new TeleportTransition(((CraftWorld) location.getWorld()).getHandle(), CraftLocation.toVec3D(location), Vec3.ZERO, location.getYaw(), location.getPitch(), TeleportTransition.DO_NOTHING); -+ } -+ // Spigot Start -+ if (teleporttransition == null) { // Paper - Add PlayerPostRespawnEvent - diff on change - spigot early returns if respawn pos is null, that is how they handle disconnected player in respawn event -+ return entityplayer; -+ } -+ // Spigot End -+ ServerLevel worldserver = teleporttransition.newLevel(); -+ entityplayer1.spawnIn(worldserver); -+ entityplayer1.unsetRemoved(); -+ entityplayer1.setShiftKeyDown(false); - Vec3 vec3d = teleporttransition.position(); - -- entityplayer1.moveTo(vec3d.x, vec3d.y, vec3d.z, teleporttransition.yRot(), teleporttransition.xRot()); -+ entityplayer1.forceSetPositionRotation(vec3d.x, vec3d.y, vec3d.z, teleporttransition.yRot(), teleporttransition.xRot()); -+ worldserver.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(net.minecraft.util.Mth.floor(vec3d.x()) >> 4, net.minecraft.util.Mth.floor(vec3d.z()) >> 4), 1, entityplayer.getId()); // Paper - post teleport ticket type -+ // CraftBukkit end - if (teleporttransition.missingRespawnBlock()) { - entityplayer1.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.NO_RESPAWN_BLOCK_AVAILABLE, 0.0F)); -+ entityplayer1.setRespawnPosition(null, null, 0f, false, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN); // CraftBukkit - SPIGOT-5988: Clear respawn location when obstructed // Paper - PlayerSetSpawnEvent - } - -- int i = alive ? 1 : 0; -+ int i = flag ? 1 : 0; - ServerLevel worldserver1 = entityplayer1.serverLevel(); - LevelData worlddata = worldserver1.getLevelData(); - - entityplayer1.connection.send(new ClientboundRespawnPacket(entityplayer1.createCommonSpawnInfo(worldserver1), (byte) i)); -- entityplayer1.connection.teleport(entityplayer1.getX(), entityplayer1.getY(), entityplayer1.getZ(), entityplayer1.getYRot(), entityplayer1.getXRot()); -+ entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.spigotConfig.viewDistance)); // Spigot -+ entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.spigotConfig.simulationDistance)); // Spigot -+ entityplayer1.connection.teleport(CraftLocation.toBukkit(entityplayer1.position(), worldserver1.getWorld(), entityplayer1.getYRot(), entityplayer1.getXRot())); // CraftBukkit - entityplayer1.connection.send(new ClientboundSetDefaultSpawnPositionPacket(worldserver.getSharedSpawnPos(), worldserver.getSharedSpawnAngle())); - entityplayer1.connection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked())); - entityplayer1.connection.send(new ClientboundSetExperiencePacket(entityplayer1.experienceProgress, entityplayer1.totalExperience, entityplayer1.experienceLevel)); - this.sendActivePlayerEffects(entityplayer1); - this.sendLevelInfo(entityplayer1, worldserver); - this.sendPlayerPermissionLevel(entityplayer1); -- worldserver.addRespawnedPlayer(entityplayer1); -- this.players.add(entityplayer1); -- this.playersByUUID.put(entityplayer1.getUUID(), entityplayer1); -- entityplayer1.initInventoryMenu(); -+ if (!entityplayer.connection.isDisconnected()) { -+ worldserver.addRespawnedPlayer(entityplayer1); -+ this.players.add(entityplayer1); -+ this.playersByName.put(entityplayer1.getScoreboardName().toLowerCase(java.util.Locale.ROOT), entityplayer1); // Spigot -+ this.playersByUUID.put(entityplayer1.getUUID(), entityplayer1); -+ } -+ // entityplayer1.initInventoryMenu(); - entityplayer1.setHealth(entityplayer1.getHealth()); - BlockPos blockposition = entityplayer1.getRespawnPosition(); - ServerLevel worldserver2 = this.server.getLevel(entityplayer1.getRespawnDimension()); - -- if (!alive && blockposition != null && worldserver2 != null) { -+ if (!flag && blockposition != null && worldserver2 != null) { - BlockState iblockdata = worldserver2.getBlockState(blockposition); - - if (iblockdata.is(Blocks.RESPAWN_ANCHOR)) { - entityplayer1.connection.send(new ClientboundSoundPacket(SoundEvents.RESPAWN_ANCHOR_DEPLETE, SoundSource.BLOCKS, (double) blockposition.getX(), (double) blockposition.getY(), (double) blockposition.getZ(), 1.0F, 1.0F, worldserver.getRandom().nextLong())); - } -+ // Paper start - Add PlayerPostRespawnEvent -+ if (iblockdata.is(net.minecraft.tags.BlockTags.BEDS) && !teleporttransition.missingRespawnBlock()) { -+ isBedSpawn = true; -+ } -+ // Paper end - Add PlayerPostRespawnEvent - } -+ // Added from changeDimension -+ this.sendAllPlayerInfo(entityplayer); // Update health, etc... -+ entityplayer.onUpdateAbilities(); -+ for (MobEffectInstance mobEffect : entityplayer.getActiveEffects()) { -+ entityplayer.connection.send(new ClientboundUpdateMobEffectPacket(entityplayer.getId(), mobEffect, false)); // blend = false -+ } - -+ // Fire advancement trigger -+ entityplayer.triggerDimensionChangeTriggers(worldserver); -+ -+ // Don't fire on respawn -+ if (fromWorld != worldserver) { -+ PlayerChangedWorldEvent event = new PlayerChangedWorldEvent(entityplayer.getBukkitEntity(), fromWorld.getWorld()); -+ this.server.server.getPluginManager().callEvent(event); -+ } -+ -+ // Save player file again if they were disconnected -+ if (entityplayer.connection.isDisconnected()) { -+ this.save(entityplayer); -+ } -+ -+ // Paper start - Add PlayerPostRespawnEvent -+ if (isRespawn) { -+ cserver.getPluginManager().callEvent(new com.destroystokyo.paper.event.player.PlayerPostRespawnEvent(entityplayer.getBukkitEntity(), location, isBedSpawn)); -+ } -+ // Paper end - Add PlayerPostRespawnEvent -+ -+ // CraftBukkit end -+ - return entityplayer1; - } - -@@ -505,26 +913,48 @@ - } - - public void sendActiveEffects(LivingEntity entity, ServerGamePacketListenerImpl networkHandler) { -+ // Paper start - collect packets -+ this.sendActiveEffects(entity, networkHandler::send); -+ } -+ public void sendActiveEffects(LivingEntity entity, java.util.function.Consumer> packetConsumer) { -+ // Paper end - collect packets - Iterator iterator = entity.getActiveEffects().iterator(); - - while (iterator.hasNext()) { - MobEffectInstance mobeffect = (MobEffectInstance) iterator.next(); - -- networkHandler.send(new ClientboundUpdateMobEffectPacket(entity.getId(), mobeffect, false)); -+ packetConsumer.accept(new ClientboundUpdateMobEffectPacket(entity.getId(), mobeffect, false)); // Paper - collect packets - } - - } - - public void sendPlayerPermissionLevel(ServerPlayer player) { -+ // Paper start - avoid recalculating permissions if possible -+ this.sendPlayerPermissionLevel(player, true); -+ } -+ -+ public void sendPlayerPermissionLevel(ServerPlayer player, boolean recalculatePermissions) { -+ // Paper end - avoid recalculating permissions if possible - GameProfile gameprofile = player.getGameProfile(); - int i = this.server.getProfilePermissions(gameprofile); - -- this.sendPlayerPermissionLevel(player, i); -+ this.sendPlayerPermissionLevel(player, i, recalculatePermissions); // Paper - avoid recalculating permissions if possible - } - - public void tick() { - if (++this.sendAllPlayerInfoIn > 600) { -- this.broadcastAll(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), this.players)); -+ // CraftBukkit start -+ for (int i = 0; i < this.players.size(); ++i) { -+ final ServerPlayer target = (ServerPlayer) this.players.get(i); -+ -+ target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), this.players.stream().filter(new Predicate() { -+ @Override -+ public boolean test(ServerPlayer input) { -+ return target.getBukkitEntity().canSee(input.getBukkitEntity()); -+ } -+ }).collect(Collectors.toList()))); -+ } -+ // CraftBukkit end - this.sendAllPlayerInfoIn = 0; - } - -@@ -541,6 +971,25 @@ - - } - -+ // CraftBukkit start - add a world/entity limited version -+ public void broadcastAll(Packet packet, net.minecraft.world.entity.player.Player entityhuman) { -+ for (int i = 0; i < this.players.size(); ++i) { -+ ServerPlayer entityplayer = this.players.get(i); -+ if (entityhuman != null && !entityplayer.getBukkitEntity().canSee(entityhuman.getBukkitEntity())) { -+ continue; -+ } -+ ((ServerPlayer) this.players.get(i)).connection.send(packet); -+ } -+ } -+ -+ public void broadcastAll(Packet packet, Level world) { -+ for (int i = 0; i < world.players().size(); ++i) { -+ ((ServerPlayer) world.players().get(i)).connection.send(packet); -+ } -+ -+ } -+ // CraftBukkit end -+ - public void broadcastAll(Packet packet, ResourceKey dimension) { - Iterator iterator = this.players.iterator(); - -@@ -554,7 +1003,7 @@ - - } - -- public void broadcastSystemToTeam(Player source, Component message) { -+ public void broadcastSystemToTeam(net.minecraft.world.entity.player.Player source, Component message) { - PlayerTeam scoreboardteam = source.getTeam(); - - if (scoreboardteam != null) { -@@ -573,7 +1022,7 @@ - } - } - -- public void broadcastSystemToAllExceptTeam(Player source, Component message) { -+ public void broadcastSystemToAllExceptTeam(net.minecraft.world.entity.player.Player source, Component message) { - PlayerTeam scoreboardteam = source.getTeam(); - - if (scoreboardteam == null) { -@@ -619,7 +1068,7 @@ - } - - public void deop(GameProfile profile) { -- this.ops.remove((Object) profile); -+ this.ops.remove(profile); // CraftBukkit - decompile error - ServerPlayer entityplayer = this.getPlayer(profile.getId()); - - if (entityplayer != null) { -@@ -629,6 +1078,11 @@ - } - - private void sendPlayerPermissionLevel(ServerPlayer player, int permissionLevel) { -+ // Paper start - Add sendOpLevel API -+ this.sendPlayerPermissionLevel(player, permissionLevel, true); -+ } -+ public void sendPlayerPermissionLevel(ServerPlayer player, int permissionLevel, boolean recalculatePermissions) { -+ // Paper end - Add sendOpLevel API - if (player.connection != null) { - byte b0; - -@@ -643,36 +1097,53 @@ - player.connection.send(new ClientboundEntityEventPacket(player, b0)); - } - -+ if (recalculatePermissions) { // Paper - Add sendOpLevel API -+ player.getBukkitEntity().recalculatePermissions(); // CraftBukkit - this.server.getCommands().sendCommands(player); -+ } // Paper - Add sendOpLevel API - } - - public boolean isWhiteListed(GameProfile profile) { -- return !this.doWhiteList || this.ops.contains(profile) || this.whitelist.contains(profile); -+ // Paper start - ProfileWhitelistVerifyEvent -+ return this.isWhiteListed(profile, null); - } -+ public boolean isWhiteListed(GameProfile gameprofile, @Nullable org.bukkit.event.player.PlayerLoginEvent loginEvent) { -+ boolean isOp = this.ops.contains(gameprofile); -+ boolean isWhitelisted = !this.doWhiteList || isOp || this.whitelist.contains(gameprofile); -+ final com.destroystokyo.paper.event.profile.ProfileWhitelistVerifyEvent event; - -+ final net.kyori.adventure.text.Component configuredMessage = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.whitelistMessage); -+ event = new com.destroystokyo.paper.event.profile.ProfileWhitelistVerifyEvent(com.destroystokyo.paper.profile.CraftPlayerProfile.asBukkitMirror(gameprofile), this.doWhiteList, isWhitelisted, isOp, configuredMessage); -+ event.callEvent(); -+ if (!event.isWhitelisted()) { -+ if (loginEvent != null) { -+ loginEvent.disallow(PlayerLoginEvent.Result.KICK_WHITELIST, event.kickMessage() == null ? configuredMessage : event.kickMessage()); -+ } -+ return false; -+ } -+ return true; -+ // Paper end - ProfileWhitelistVerifyEvent -+ } -+ - public boolean isOp(GameProfile profile) { - return this.ops.contains(profile) || this.server.isSingleplayerOwner(profile) && this.server.getWorldData().isAllowCommands() || this.allowCommandsForAllPlayers; - } - - @Nullable - public ServerPlayer getPlayerByName(String name) { -- int i = this.players.size(); -- -- for (int j = 0; j < i; ++j) { -- ServerPlayer entityplayer = (ServerPlayer) this.players.get(j); -- -- if (entityplayer.getGameProfile().getName().equalsIgnoreCase(name)) { -- return entityplayer; -- } -- } -- -- return null; -+ return this.playersByName.get(name.toLowerCase(java.util.Locale.ROOT)); // Spigot - } - -- public void broadcast(@Nullable Player player, double x, double y, double z, double distance, ResourceKey worldKey, Packet packet) { -+ public void broadcast(@Nullable net.minecraft.world.entity.player.Player player, double x, double y, double z, double distance, ResourceKey worldKey, Packet packet) { - for (int i = 0; i < this.players.size(); ++i) { - ServerPlayer entityplayer = (ServerPlayer) this.players.get(i); - -+ // CraftBukkit start - Test if player receiving packet can see the source of the packet -+ if (player != null && !entityplayer.getBukkitEntity().canSee(player.getBukkitEntity())) { -+ continue; -+ } -+ // CraftBukkit end -+ - if (entityplayer != player && entityplayer.level().dimension() == worldKey) { - double d4 = x - entityplayer.getX(); - double d5 = y - entityplayer.getY(); -@@ -687,10 +1158,12 @@ - } - - public void saveAll() { -+ io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main - for (int i = 0; i < this.players.size(); ++i) { - this.save((ServerPlayer) this.players.get(i)); - } - -+ return null; }); // Paper - ensure main - } - - public UserWhiteList getWhiteList() { -@@ -712,15 +1185,19 @@ - public void reloadWhiteList() {} - - public void sendLevelInfo(ServerPlayer player, ServerLevel world) { -- WorldBorder worldborder = this.server.overworld().getWorldBorder(); -+ WorldBorder worldborder = player.level().getWorldBorder(); // CraftBukkit - - player.connection.send(new ClientboundInitializeBorderPacket(worldborder)); - player.connection.send(new ClientboundSetTimePacket(world.getGameTime(), world.getDayTime(), world.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT))); - player.connection.send(new ClientboundSetDefaultSpawnPositionPacket(world.getSharedSpawnPos(), world.getSharedSpawnAngle())); - if (world.isRaining()) { -- player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0.0F)); -- player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, world.getRainLevel(1.0F))); -- player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, world.getThunderLevel(1.0F))); -+ // CraftBukkit start - handle player weather -+ // entityplayer.connection.send(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.START_RAINING, 0.0F)); -+ // entityplayer.connection.send(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.RAIN_LEVEL_CHANGE, worldserver.getRainLevel(1.0F))); -+ // entityplayer.connection.send(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.THUNDER_LEVEL_CHANGE, worldserver.getThunderLevel(1.0F))); -+ player.setPlayerWeather(org.bukkit.WeatherType.DOWNFALL, false); -+ player.updateWeather(-world.rainLevel, world.rainLevel, -world.thunderLevel, world.thunderLevel); -+ // CraftBukkit end - } - - player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.LEVEL_CHUNKS_LOAD_START, 0.0F)); -@@ -729,8 +1206,16 @@ - - public void sendAllPlayerInfo(ServerPlayer player) { - player.inventoryMenu.sendAllDataToRemote(); -- player.resetSentInfo(); -+ // entityplayer.resetSentInfo(); -+ player.getBukkitEntity().updateScaledHealth(); // CraftBukkit - Update scaled health on respawn and worldchange -+ player.refreshEntityData(player); // CraftBukkkit - SPIGOT-7218: sync metadata - player.connection.send(new ClientboundSetHeldSlotPacket(player.getInventory().selected)); -+ // CraftBukkit start - from GameRules -+ int i = player.serverLevel().getGameRules().getBoolean(GameRules.RULE_REDUCEDDEBUGINFO) ? 22 : 23; -+ player.connection.send(new ClientboundEntityEventPacket(player, (byte) i)); -+ float immediateRespawn = player.serverLevel().getGameRules().getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN) ? 1.0F: 0.0F; -+ player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.IMMEDIATE_RESPAWN, immediateRespawn)); -+ // CraftBukkit end - } - - public int getPlayerCount() { -@@ -746,6 +1231,7 @@ - } - - public void setUsingWhiteList(boolean whitelistEnabled) { -+ new com.destroystokyo.paper.event.server.WhitelistToggleEvent(whitelistEnabled).callEvent(); // Paper - WhitelistToggleEvent - this.doWhiteList = whitelistEnabled; - } - -@@ -786,11 +1272,35 @@ - } - - public void removeAll() { -- for (int i = 0; i < this.players.size(); ++i) { -- ((ServerPlayer) this.players.get(i)).connection.disconnect((Component) Component.translatable("multiplayer.disconnect.server_shutdown")); -+ // Paper start - Extract method to allow for restarting flag -+ this.removeAll(false); -+ } -+ -+ public void removeAll(boolean isRestarting) { -+ // Paper end -+ // CraftBukkit start - disconnect safely -+ for (ServerPlayer player : this.players) { -+ if (isRestarting) player.connection.disconnect(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.restartMessage), org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN); else // Paper - kick event cause (cause is never used here) -+ player.connection.disconnect(java.util.Objects.requireNonNullElseGet(this.server.server.shutdownMessage(), net.kyori.adventure.text.Component::empty)); // CraftBukkit - add custom shutdown message // Paper - Adventure -+ } -+ // CraftBukkit end -+ -+ // Paper start - Configurable player collision; Remove collideRule team if it exists -+ if (this.collideRuleTeamName != null) { -+ final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard(); -+ final PlayerTeam team = scoreboard.getPlayersTeam(this.collideRuleTeamName); -+ if (team != null) scoreboard.removePlayerTeam(team); - } -+ // Paper end - Configurable player collision -+ } - -+ // CraftBukkit start -+ public void broadcastMessage(Component[] iChatBaseComponents) { -+ for (Component component : iChatBaseComponents) { -+ this.broadcastSystemMessage(component, false); -+ } - } -+ // CraftBukkit end - - public void broadcastSystemMessage(Component message, boolean overlay) { - this.broadcastSystemMessage(message, (entityplayer) -> { -@@ -819,24 +1329,43 @@ - } - - public void broadcastChatMessage(PlayerChatMessage message, ServerPlayer sender, ChatType.Bound params) { -+ // Paper start -+ this.broadcastChatMessage(message, sender, params, null); -+ } -+ public void broadcastChatMessage(PlayerChatMessage message, ServerPlayer sender, ChatType.Bound params, @Nullable Function unsignedFunction) { -+ // Paper end - Objects.requireNonNull(sender); -- this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender, params); -+ this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender, params, unsignedFunction); // Paper - } - - private void broadcastChatMessage(PlayerChatMessage message, Predicate shouldSendFiltered, @Nullable ServerPlayer sender, ChatType.Bound params) { -+ // Paper start -+ this.broadcastChatMessage(message, shouldSendFiltered, sender, params, null); -+ } -+ public void broadcastChatMessage(PlayerChatMessage message, Predicate shouldSendFiltered, @Nullable ServerPlayer sender, ChatType.Bound params, @Nullable Function unsignedFunction) { -+ // Paper end - boolean flag = this.verifyChatTrusted(message); - -- this.server.logChatMessage(message.decoratedContent(), params, flag ? null : "Not Secure"); -+ this.server.logChatMessage((unsignedFunction == null ? message.decoratedContent() : unsignedFunction.apply(this.server.console)), params, flag ? null : "Not Secure"); // Paper - OutgoingChatMessage outgoingchatmessage = OutgoingChatMessage.create(message); - boolean flag1 = false; - - boolean flag2; -+ Packet disguised = sender != null && unsignedFunction == null ? new net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket(outgoingchatmessage.content(), params) : null; // Paper - don't send player chat packets from vanished players - - for (Iterator iterator = this.players.iterator(); iterator.hasNext(); flag1 |= flag2 && message.isFullyFiltered()) { - ServerPlayer entityplayer1 = (ServerPlayer) iterator.next(); - - flag2 = shouldSendFiltered.test(entityplayer1); -- entityplayer1.sendChatMessage(outgoingchatmessage, flag2, params); -+ // Paper start - don't send player chat packets from vanished players -+ if (sender != null && !entityplayer1.getBukkitEntity().canSee(sender.getBukkitEntity())) { -+ entityplayer1.connection.send(unsignedFunction != null -+ ? new net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket(unsignedFunction.apply(entityplayer1.getBukkitEntity()), params) -+ : disguised); -+ continue; -+ } -+ // Paper end -+ entityplayer1.sendChatMessage(outgoingchatmessage, flag2, params, unsignedFunction == null ? null : unsignedFunction.apply(entityplayer1.getBukkitEntity())); // Paper - } - - if (flag1 && sender != null) { -@@ -845,20 +1374,27 @@ - - } - -- private boolean verifyChatTrusted(PlayerChatMessage message) { -+ public boolean verifyChatTrusted(PlayerChatMessage message) { // Paper - private -> public - return message.hasSignature() && !message.hasExpiredServer(Instant.now()); - } - -- public ServerStatsCounter getPlayerStats(Player player) { -- UUID uuid = player.getUUID(); -- ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) this.stats.get(uuid); -+ // CraftBukkit start -+ public ServerStatsCounter getPlayerStats(ServerPlayer entityhuman) { -+ ServerStatsCounter serverstatisticmanager = entityhuman.getStats(); -+ return serverstatisticmanager == null ? this.getPlayerStats(entityhuman.getUUID(), entityhuman.getGameProfile().getName()) : serverstatisticmanager; // Paper - use username and not display name -+ } - -+ public ServerStatsCounter getPlayerStats(UUID uuid, String displayName) { -+ ServerPlayer entityhuman = this.getPlayer(uuid); -+ ServerStatsCounter serverstatisticmanager = entityhuman == null ? null : (ServerStatsCounter) entityhuman.getStats(); -+ // CraftBukkit end -+ - if (serverstatisticmanager == null) { - File file = this.server.getWorldPath(LevelResource.PLAYER_STATS_DIR).toFile(); - File file1 = new File(file, String.valueOf(uuid) + ".json"); - - if (!file1.exists()) { -- File file2 = new File(file, player.getName().getString() + ".json"); -+ File file2 = new File(file, displayName + ".json"); // CraftBukkit - Path path = file2.toPath(); - - if (FileUtil.isPathNormalized(path) && FileUtil.isPathPortable(path) && path.startsWith(file.getPath()) && file2.isFile()) { -@@ -867,7 +1403,7 @@ - } - - serverstatisticmanager = new ServerStatsCounter(this.server, file1); -- this.stats.put(uuid, serverstatisticmanager); -+ // this.stats.put(uuid, serverstatisticmanager); // CraftBukkit - } - - return serverstatisticmanager; -@@ -875,13 +1411,13 @@ - - public PlayerAdvancements getPlayerAdvancements(ServerPlayer player) { - UUID uuid = player.getUUID(); -- PlayerAdvancements advancementdataplayer = (PlayerAdvancements) this.advancements.get(uuid); -+ PlayerAdvancements advancementdataplayer = (PlayerAdvancements) player.getAdvancements(); // CraftBukkit - - if (advancementdataplayer == null) { - Path path = this.server.getWorldPath(LevelResource.PLAYER_ADVANCEMENTS_DIR).resolve(String.valueOf(uuid) + ".json"); - - advancementdataplayer = new PlayerAdvancements(this.server.getFixerUpper(), this, this.server.getAdvancements(), path, player); -- this.advancements.put(uuid, advancementdataplayer); -+ // this.advancements.put(uuid, advancementdataplayer); // CraftBukkit - } - - advancementdataplayer.setPlayer(player); -@@ -932,15 +1468,39 @@ - } - - public void reloadResources() { -- Iterator iterator = this.advancements.values().iterator(); -+ // Paper start - API for updating recipes on clients -+ this.reloadAdvancementData(); -+ this.reloadTagData(); -+ this.reloadRecipes(); -+ } -+ public void reloadAdvancementData() { -+ // Paper end - API for updating recipes on clients -+ // CraftBukkit start -+ /*Iterator iterator = this.advancements.values().iterator(); - - while (iterator.hasNext()) { -- PlayerAdvancements advancementdataplayer = (PlayerAdvancements) iterator.next(); -+ AdvancementDataPlayer advancementdataplayer = (AdvancementDataPlayer) iterator.next(); - - advancementdataplayer.reload(this.server.getAdvancements()); -+ }*/ -+ -+ for (ServerPlayer player : this.players) { -+ player.getAdvancements().reload(this.server.getAdvancements()); -+ player.getAdvancements().flushDirty(player); // CraftBukkit - trigger immediate flush of advancements - } -+ // CraftBukkit end - -+ // Paper start - API for updating recipes on clients -+ } -+ public void reloadTagData() { - this.broadcastAll(new ClientboundUpdateTagsPacket(TagNetworkSerialization.serializeTagsToNetwork(this.registries))); -+ // CraftBukkit start -+ // this.reloadRecipes(); // Paper - do not reload recipes just because tag data was reloaded -+ // Paper end - API for updating recipes on clients -+ } -+ -+ public void reloadRecipes() { -+ // CraftBukkit end - RecipeManager craftingmanager = this.server.getRecipeManager(); - ClientboundUpdateRecipesPacket packetplayoutrecipeupdate = new ClientboundUpdateRecipesPacket(craftingmanager.getSynchronizedItemProperties(), craftingmanager.getSynchronizedStonecutterRecipes()); - Iterator iterator1 = this.players.iterator();