diff --git a/build-data/paper.at b/build-data/paper.at
index 1692a54925..594a83bd5f 100644
--- a/build-data/paper.at
+++ b/build-data/paper.at
@@ -104,6 +104,9 @@ public net.minecraft.server.level.ServerPlayer wardenSpawnTracker
public net.minecraft.server.level.ServerPlayer$RespawnPosAngle
public net.minecraft.server.level.ServerPlayerGameMode level
public net.minecraft.server.level.TicketType register(Ljava/lang/String;JZLnet/minecraft/server/level/TicketType$TicketUse;)Lnet/minecraft/server/level/TicketType;
+public net.minecraft.server.network.ServerConfigurationPacketListenerImpl clientInformation
+public net.minecraft.server.network.ServerConfigurationPacketListenerImpl currentTask
+public net.minecraft.server.network.ServerConfigurationPacketListenerImpl finishCurrentTask(Lnet/minecraft/server/network/ConfigurationTask$Type;)V
public net.minecraft.server.network.ServerGamePacketListenerImpl isChatMessageIllegal(Ljava/lang/String;)Z
public net.minecraft.server.network.ServerLoginPacketListenerImpl authenticatedProfile
public net.minecraft.server.network.ServerLoginPacketListenerImpl connection
diff --git a/paper-api/src/main/java/io/papermc/paper/connection/PlayerCommonConnection.java b/paper-api/src/main/java/io/papermc/paper/connection/PlayerCommonConnection.java
new file mode 100644
index 0000000000..cae2e5ba5d
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/connection/PlayerCommonConnection.java
@@ -0,0 +1,45 @@
+package io.papermc.paper.connection;
+
+import java.util.Map;
+
+import com.destroystokyo.paper.ClientOption;
+import org.bukkit.ServerLinks;
+
+/**
+ * Represents a connection that has properties shared between the GAME and CONFIG stage.
+ */
+public interface PlayerCommonConnection extends WritablePlayerCookieConnection, ReadablePlayerCookieConnection {
+
+ /**
+ * Sends data to appear in this connection's report logs.
+ * This is useful for debugging server state that may be causing
+ * player disconnects.
+ *
+ * These are formatted as key - value, where keys are limited to a length of 128 characters,
+ * values are limited to 4096, and 32 maximum entries can be sent.
+ *
+ * @param details report details
+ */
+ void sendReportDetails(Map details);
+
+ /**
+ * Sends the given server links to this connection.
+ *
+ * @param links links to send
+ */
+ void sendLinks(ServerLinks links);
+
+ /**
+ * Transfers this connection to another server.
+ *
+ * @param host host
+ * @param port port
+ */
+ void transfer(String host, int port);
+
+ /**
+ * @param type client option
+ * @return the client option value of the player
+ */
+ T getClientOption(ClientOption type);
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/connection/PlayerConfigurationConnection.java b/paper-api/src/main/java/io/papermc/paper/connection/PlayerConfigurationConnection.java
new file mode 100644
index 0000000000..dc68010fca
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/connection/PlayerConfigurationConnection.java
@@ -0,0 +1,37 @@
+package io.papermc.paper.connection;
+
+import com.destroystokyo.paper.ClientOption;
+import com.destroystokyo.paper.profile.PlayerProfile;
+import net.kyori.adventure.audience.Audience;
+
+public interface PlayerConfigurationConnection extends PlayerCommonConnection {
+
+ /**
+ * Returns the audience representing the player in configuration mode.
+ * This can be used to interact with the Adventure API during the configuration stage.
+ * This is guaranteed to be an instance of {@link PlayerConfigurationConnection}
+ *
+ * @return the configuring player audience
+ */
+ Audience getAudience();
+
+ /**
+ * Gets the profile for this connection.
+ *
+ * @return profile
+ */
+ PlayerProfile getProfile();
+
+ /**
+ * Clears the players chat history and their local chat.
+ */
+ void clearChat();
+
+ /**
+ * Completes the configuration for this player, which will cause this player to reenter the game.
+ *
+ * Note, this should be only be called if you are reconfiguring the player.
+ */
+ void completeReconfiguration();
+
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/connection/PlayerConnection.java b/paper-api/src/main/java/io/papermc/paper/connection/PlayerConnection.java
new file mode 100644
index 0000000000..9430d33edf
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/connection/PlayerConnection.java
@@ -0,0 +1,60 @@
+package io.papermc.paper.connection;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import net.kyori.adventure.text.Component;
+import org.jspecify.annotations.Nullable;
+
+public interface PlayerConnection {
+
+ /**
+ * Disconnects the player connection.
+ *
+ * Note that calling this during connection related events may caused undefined behavior.
+ *
+ * @param component disconnect reason
+ */
+ void disconnect(Component component);
+
+ /**
+ * Gets if this connection originated from a transferred connection.
+ *
+ * Do note that this is sent and stored on the client.
+ *
+ * @return is transferred
+ */
+ boolean isTransferred();
+
+ /**
+ * Gets the raw remote address of the connection. This may be a proxy address
+ * or a Unix domain socket address, depending on how the channel was established.
+ *
+ * @return the remote {@link SocketAddress} of the channel
+ */
+ SocketAddress getAddress();
+
+ /**
+ * Gets the real client address of the player. If the connection is behind a proxy,
+ * this will be the actual player’s IP address extracted from the proxy handshake.
+ *
+ * @return the client {@link InetSocketAddress}
+ */
+ InetSocketAddress getClientAddress();
+
+ /**
+ * Returns the virtual host the client is connected to.
+ *
+ *
The virtual host refers to the hostname/port the client used to
+ * connect to the server.
+ *
+ * @return The client's virtual host, or {@code null} if unknown
+ */
+ @Nullable InetSocketAddress getVirtualHost();
+
+ /**
+ * Gets the socket address of this player's proxy
+ *
+ * @return the player's proxy address, null if the server doesn't have Proxy Protocol enabled, or the player didn't connect to an HAProxy instance
+ */
+ @Nullable InetSocketAddress getHAProxyAddress();
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/connection/PlayerGameConnection.java b/paper-api/src/main/java/io/papermc/paper/connection/PlayerGameConnection.java
new file mode 100644
index 0000000000..da74c5f390
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/connection/PlayerGameConnection.java
@@ -0,0 +1,21 @@
+package io.papermc.paper.connection;
+
+import org.bukkit.entity.Player;
+
+public interface PlayerGameConnection extends PlayerCommonConnection {
+
+ /**
+ * Bumps the player to the configuration stage.
+ *
+ * This will, by default, cause the player to stay until their connection is released by
+ * {@link PlayerConfigurationConnection#completeReconfiguration()}
+ */
+ void reenterConfiguration();
+
+ /**
+ * Gets the player that is associated with this game connection.
+ *
+ * @return player
+ */
+ Player getPlayer();
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/connection/PlayerLoginConnection.java b/paper-api/src/main/java/io/papermc/paper/connection/PlayerLoginConnection.java
new file mode 100644
index 0000000000..dfe95ad426
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/connection/PlayerLoginConnection.java
@@ -0,0 +1,23 @@
+package io.papermc.paper.connection;
+
+import com.destroystokyo.paper.profile.PlayerProfile;
+import org.jspecify.annotations.Nullable;
+
+public interface PlayerLoginConnection extends ReadablePlayerCookieConnection {
+
+ /**
+ * Gets the authenticated profile for this connection.
+ * This may return null depending on what stage this connection is at.
+ *
+ * @return authenticated profile, or null if not present
+ */
+ @Nullable PlayerProfile getAuthenticatedProfile();
+
+ /**
+ * Gets the player profile that this connection is requesting to authenticate as.
+ *
+ * @return the unsafe unauthenticated profile, or null if not sent
+ */
+ @Nullable
+ PlayerProfile getUnsafeProfile();
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/connection/ReadablePlayerCookieConnection.java b/paper-api/src/main/java/io/papermc/paper/connection/ReadablePlayerCookieConnection.java
new file mode 100644
index 0000000000..d51fe45989
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/connection/ReadablePlayerCookieConnection.java
@@ -0,0 +1,18 @@
+package io.papermc.paper.connection;
+
+import java.util.concurrent.CompletableFuture;
+import org.bukkit.NamespacedKey;
+
+public interface ReadablePlayerCookieConnection extends PlayerConnection {
+
+ /**
+ * Retrieves a cookie from this connection.
+ *
+ * @param key the key identifying the cookie
+ * @return a {@link CompletableFuture} that will be completed when the
+ * Cookie response is received or otherwise available. If the cookie is not
+ * set in the client, the {@link CompletableFuture} will complete with a
+ * null value.
+ */
+ CompletableFuture retrieveCookie(NamespacedKey key);
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/connection/WritablePlayerCookieConnection.java b/paper-api/src/main/java/io/papermc/paper/connection/WritablePlayerCookieConnection.java
new file mode 100644
index 0000000000..e9d3e7127b
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/connection/WritablePlayerCookieConnection.java
@@ -0,0 +1,15 @@
+package io.papermc.paper.connection;
+
+import org.bukkit.NamespacedKey;
+
+public interface WritablePlayerCookieConnection extends PlayerConnection {
+
+ /**
+ * Stores a cookie in this player's client.
+ *
+ * @param key the key identifying the cookie
+ * @param value the data to store in the cookie
+ * @throws IllegalStateException if a cookie cannot be stored at this time
+ */
+ void storeCookie(NamespacedKey key, byte[] value);
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/connection/package-info.java b/paper-api/src/main/java/io/papermc/paper/connection/package-info.java
new file mode 100644
index 0000000000..bc3c1ad42a
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/connection/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * This package contains events related to player connections, such as joining and leaving the server.
+ */
+@ApiStatus.Experimental
+@NullMarked
+package io.papermc.paper.connection;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jspecify.annotations.NullMarked;
diff --git a/paper-api/src/main/java/io/papermc/paper/event/connection/PlayerConnectionValidateLoginEvent.java b/paper-api/src/main/java/io/papermc/paper/event/connection/PlayerConnectionValidateLoginEvent.java
new file mode 100644
index 0000000000..ed06472e4c
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/event/connection/PlayerConnectionValidateLoginEvent.java
@@ -0,0 +1,86 @@
+package io.papermc.paper.event.connection;
+
+import io.papermc.paper.connection.PlayerConnection;
+import net.kyori.adventure.text.Component;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.ApiStatus;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Validates whether a player connection is able to log in.
+ *
+ * Called when is attempting to log in for the first time, or is finishing up
+ * being configured.
+ */
+public class PlayerConnectionValidateLoginEvent extends Event {
+
+ private static final HandlerList HANDLER_LIST = new HandlerList();
+
+ private final PlayerConnection connection;
+ private @Nullable Component kickMessage;
+
+ @ApiStatus.Internal
+ public PlayerConnectionValidateLoginEvent(final PlayerConnection connection, final @Nullable Component kickMessage) {
+ super(false);
+ this.connection = connection;
+ this.kickMessage = kickMessage;
+ }
+
+ /**
+ * Gets the connection of the player in this event.
+ * Note, the type of this connection is not guaranteed to be stable across versions.
+ * Additionally, disconnecting the player through this connection / using any methods that may send packets
+ * is not supported.
+ *
+ * @return connection
+ */
+ public PlayerConnection getConnection() {
+ return this.connection;
+ }
+
+ /**
+ * Allows the player to log in.
+ * This skips any login validation checks.
+ */
+ public void allow() {
+ this.kickMessage = null;
+ }
+
+ /**
+ * Disallows the player from logging in, with the given reason
+ *
+ * @param message Kick message to display to the user
+ */
+ public void kickMessage(final Component message) {
+ this.kickMessage = message;
+ }
+
+ /**
+ * Gets the reason for why a player is not allowed to join the server.
+ * This will be null in the case that the player is allowed to log in.
+ *
+ * @return disallow reason
+ */
+ public @Nullable Component getKickMessage() {
+ return this.kickMessage;
+ }
+
+ /**
+ * Gets if the player is allowed to enter the next stage.
+ *
+ * @return if allowed
+ */
+ public boolean isAllowed() {
+ return this.kickMessage == null;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLER_LIST;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLER_LIST;
+ }
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/event/connection/configuration/AsyncPlayerConnectionConfigureEvent.java b/paper-api/src/main/java/io/papermc/paper/event/connection/configuration/AsyncPlayerConnectionConfigureEvent.java
new file mode 100644
index 0000000000..02bae8e427
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/event/connection/configuration/AsyncPlayerConnectionConfigureEvent.java
@@ -0,0 +1,39 @@
+package io.papermc.paper.event.connection.configuration;
+
+import io.papermc.paper.connection.PlayerConfigurationConnection;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * An event that allows you to configure the player.
+ * This is async and allows you to run configuration code on the player.
+ * Once this event has finished execution, the player connection will continue.
+ *
+ * This occurs after configuration, but before the player has entered the world.
+ */
+public class AsyncPlayerConnectionConfigureEvent extends Event {
+
+ private static final HandlerList HANDLER_LIST = new HandlerList();
+
+ private final PlayerConfigurationConnection connection;
+
+ @ApiStatus.Internal
+ public AsyncPlayerConnectionConfigureEvent(final PlayerConfigurationConnection connection) {
+ super(true);
+ this.connection = connection;
+ }
+
+ public PlayerConfigurationConnection getConnection() {
+ return this.connection;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLER_LIST;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLER_LIST;
+ }
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/event/connection/configuration/PlayerConnectionInitialConfigureEvent.java b/paper-api/src/main/java/io/papermc/paper/event/connection/configuration/PlayerConnectionInitialConfigureEvent.java
new file mode 100644
index 0000000000..ac63562936
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/event/connection/configuration/PlayerConnectionInitialConfigureEvent.java
@@ -0,0 +1,36 @@
+package io.papermc.paper.event.connection.configuration;
+
+import io.papermc.paper.connection.PlayerConfigurationConnection;
+import org.bukkit.Bukkit;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * Indicates that this player is being configured for the first time, meaning that the connection will start being configured automatically
+ */
+public class PlayerConnectionInitialConfigureEvent extends Event {
+
+ private static final HandlerList HANDLER_LIST = new HandlerList();
+
+ private final PlayerConfigurationConnection connection;
+
+ @ApiStatus.Internal
+ public PlayerConnectionInitialConfigureEvent(final PlayerConfigurationConnection connection) {
+ super(!Bukkit.isPrimaryThread());
+ this.connection = connection;
+ }
+
+ public PlayerConfigurationConnection getConnection() {
+ return this.connection;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLER_LIST;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLER_LIST;
+ }
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/event/connection/configuration/PlayerConnectionReconfigureEvent.java b/paper-api/src/main/java/io/papermc/paper/event/connection/configuration/PlayerConnectionReconfigureEvent.java
new file mode 100644
index 0000000000..e6fa54aa1a
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/event/connection/configuration/PlayerConnectionReconfigureEvent.java
@@ -0,0 +1,37 @@
+package io.papermc.paper.event.connection.configuration;
+
+import io.papermc.paper.connection.PlayerConfigurationConnection;
+import org.bukkit.Bukkit;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * Indicates that this player is being reconfigured, meaning that this connection will be held in the configuration
+ * stage unless kicked out through {@link PlayerConfigurationConnection#completeReconfiguration()}
+ */
+public class PlayerConnectionReconfigureEvent extends Event {
+
+ private static final HandlerList HANDLER_LIST = new HandlerList();
+
+ private final PlayerConfigurationConnection connection;
+
+ @ApiStatus.Internal
+ public PlayerConnectionReconfigureEvent(final PlayerConfigurationConnection connection) {
+ super(!Bukkit.isPrimaryThread());
+ this.connection = connection;
+ }
+
+ public PlayerConfigurationConnection getConnection() {
+ return this.connection;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLER_LIST;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLER_LIST;
+ }
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/event/connection/configuration/package-info.java b/paper-api/src/main/java/io/papermc/paper/event/connection/configuration/package-info.java
new file mode 100644
index 0000000000..41c6599aa5
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/event/connection/configuration/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * Configuration connection events.
+ */
+@ApiStatus.Experimental
+@NullMarked
+package io.papermc.paper.event.connection.configuration;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jspecify.annotations.NullMarked;
diff --git a/paper-api/src/main/java/io/papermc/paper/event/connection/package-info.java b/paper-api/src/main/java/io/papermc/paper/event/connection/package-info.java
new file mode 100644
index 0000000000..d67749daac
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/event/connection/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * Common connection events.
+ */
+@NullMarked
+@ApiStatus.Experimental
+package io.papermc.paper.event.connection;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jspecify.annotations.NullMarked;
diff --git a/paper-api/src/main/java/io/papermc/paper/event/player/PlayerServerFullCheckEvent.java b/paper-api/src/main/java/io/papermc/paper/event/player/PlayerServerFullCheckEvent.java
new file mode 100644
index 0000000000..8bee45911f
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/event/player/PlayerServerFullCheckEvent.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2017 - Daniel Ennis (Aikar) - MIT License
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package io.papermc.paper.event.player;
+
+import com.destroystokyo.paper.profile.PlayerProfile;
+import net.kyori.adventure.text.Component;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
+import org.jspecify.annotations.NullMarked;
+
+/**
+ * Fires when computing if a server is currently considered full for a player.
+ */
+@NullMarked
+public class PlayerServerFullCheckEvent extends Event {
+
+ private static final HandlerList HANDLER_LIST = new HandlerList();
+
+ private final PlayerProfile profile;
+ private Component kickMessage;
+ private boolean allow;
+
+ @ApiStatus.Internal
+ public PlayerServerFullCheckEvent(final PlayerProfile profile, final Component kickMessage, final boolean shouldKick) {
+ this.profile = profile;
+ this.kickMessage = kickMessage;
+ this.allow = !shouldKick;
+ }
+
+ /**
+ * @return the currently planned message to send to the user if they are unable to join the server
+ */
+ @Contract(pure = true)
+ public Component kickMessage() {
+ return this.kickMessage;
+ }
+
+ /**
+ * @param kickMessage The message to send to the player on kick if not able to join.
+ */
+ public void deny(final Component kickMessage) {
+ this.kickMessage = kickMessage;
+ }
+
+ /**
+ * @return The profile of the player trying to connect
+ */
+ public PlayerProfile getPlayerProfile() {
+ return this.profile;
+ }
+
+ /**
+ * Sets whether the player is able to join this server.
+ * @param allow can join the server
+ */
+ public void allow(final boolean allow) {
+ this.allow = allow;
+ }
+
+ /**
+ * Gets if the player is currently able to join the server.
+ *
+ * @return can join the server, or false if the server should be considered full
+ */
+ public boolean isAllowed() {
+ return this.allow;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLER_LIST;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLER_LIST;
+ }
+}
diff --git a/paper-api/src/main/java/org/bukkit/entity/Player.java b/paper-api/src/main/java/org/bukkit/entity/Player.java
index d34419693f..acb7a39ac6 100644
--- a/paper-api/src/main/java/org/bukkit/entity/Player.java
+++ b/paper-api/src/main/java/org/bukkit/entity/Player.java
@@ -10,6 +10,7 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
+import io.papermc.paper.connection.PlayerGameConnection;
import io.papermc.paper.entity.LookAnchor;
import io.papermc.paper.entity.PlayerGiveResult;
import io.papermc.paper.math.Position;
@@ -3925,4 +3926,12 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
* @param score New death screen score of player
*/
void setDeathScreenScore(int score);
+
+ /**
+ * Gets the game connection for this player.
+ *
+ * @return the game connection
+ */
+ @ApiStatus.Experimental
+ PlayerGameConnection getConnection();
}
diff --git a/paper-api/src/main/java/org/bukkit/event/player/AsyncPlayerPreLoginEvent.java b/paper-api/src/main/java/org/bukkit/event/player/AsyncPlayerPreLoginEvent.java
index cf7593ce00..cd5eb46544 100644
--- a/paper-api/src/main/java/org/bukkit/event/player/AsyncPlayerPreLoginEvent.java
+++ b/paper-api/src/main/java/org/bukkit/event/player/AsyncPlayerPreLoginEvent.java
@@ -5,6 +5,7 @@ import java.util.UUID;
import com.destroystokyo.paper.profile.PlayerProfile;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import io.papermc.paper.connection.PlayerLoginConnection;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.ApiStatus;
@@ -33,6 +34,7 @@ public class AsyncPlayerPreLoginEvent extends Event {
private Result result;
private Component message;
private PlayerProfile profile;
+ private final PlayerLoginConnection playerLoginConnection;
@ApiStatus.Internal
@Deprecated(since = "1.7.5", forRemoval = true)
@@ -60,11 +62,11 @@ public class AsyncPlayerPreLoginEvent extends Event {
@ApiStatus.Internal
@Deprecated(forRemoval = true)
public AsyncPlayerPreLoginEvent(@NotNull final String name, @NotNull final InetAddress ipAddress, @NotNull final InetAddress rawAddress, @NotNull final UUID uniqueId, boolean transferred, @NotNull com.destroystokyo.paper.profile.PlayerProfile profile) {
- this(name, ipAddress, rawAddress, uniqueId, transferred, profile, "");
+ this(name, ipAddress, rawAddress, uniqueId, transferred, profile, "", null);
}
@ApiStatus.Internal
- public AsyncPlayerPreLoginEvent(@NotNull final String name, @NotNull final InetAddress ipAddress, @NotNull final InetAddress rawAddress, @NotNull final UUID uniqueId, boolean transferred, @NotNull com.destroystokyo.paper.profile.PlayerProfile profile, @NotNull String hostname) {
+ public AsyncPlayerPreLoginEvent(@NotNull final String name, @NotNull final InetAddress ipAddress, @NotNull final InetAddress rawAddress, @NotNull final UUID uniqueId, boolean transferred, @NotNull com.destroystokyo.paper.profile.PlayerProfile profile, @NotNull String hostname, final PlayerLoginConnection playerLoginConnection) {
super(true);
this.result = Result.ALLOWED;
this.message = Component.empty();
@@ -73,6 +75,7 @@ public class AsyncPlayerPreLoginEvent extends Event {
this.rawAddress = rawAddress;
this.hostname = hostname;
this.transferred = transferred;
+ this.playerLoginConnection = playerLoginConnection;
}
/**
@@ -301,6 +304,16 @@ public class AsyncPlayerPreLoginEvent extends Event {
return this.transferred;
}
+ /**
+ * Gets the connection for the player logging in.
+ * @return connection
+ */
+ @ApiStatus.Experimental
+ @NotNull
+ public PlayerLoginConnection getConnection() {
+ return playerLoginConnection;
+ }
+
@NotNull
@Override
public HandlerList getHandlers() {
diff --git a/paper-api/src/main/java/org/bukkit/event/player/PlayerLinksSendEvent.java b/paper-api/src/main/java/org/bukkit/event/player/PlayerLinksSendEvent.java
index 6fc2aa25da..9fe9133ffb 100644
--- a/paper-api/src/main/java/org/bukkit/event/player/PlayerLinksSendEvent.java
+++ b/paper-api/src/main/java/org/bukkit/event/player/PlayerLinksSendEvent.java
@@ -1,7 +1,9 @@
package org.bukkit.event.player;
+import io.papermc.paper.connection.PlayerCommonConnection;
+import io.papermc.paper.connection.PlayerConfigurationConnection;
import org.bukkit.ServerLinks;
-import org.bukkit.entity.Player;
+import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
@@ -10,18 +12,28 @@ import org.jetbrains.annotations.NotNull;
* This event is called when the list of links is sent to the player.
*/
@ApiStatus.Experimental
-public class PlayerLinksSendEvent extends PlayerEvent {
+public class PlayerLinksSendEvent extends Event {
private static final HandlerList HANDLER_LIST = new HandlerList();
private final ServerLinks links;
+ private final PlayerCommonConnection connection;
@ApiStatus.Internal
- public PlayerLinksSendEvent(@NotNull final Player player, @NotNull final ServerLinks links) {
- super(player);
+ public PlayerLinksSendEvent(@NotNull final PlayerConfigurationConnection connection, @NotNull final ServerLinks links) {
+ this.connection = connection;
this.links = links;
}
+ /**
+ * Gets the connection that received the links.
+ * @return connection
+ */
+ @NotNull
+ public PlayerCommonConnection getConnection() {
+ return connection;
+ }
+
/**
* Gets the links to be sent, for modification.
*
diff --git a/paper-api/src/main/java/org/bukkit/event/player/PlayerLoginEvent.java b/paper-api/src/main/java/org/bukkit/event/player/PlayerLoginEvent.java
index 996fffc845..7f2e0d895d 100644
--- a/paper-api/src/main/java/org/bukkit/event/player/PlayerLoginEvent.java
+++ b/paper-api/src/main/java/org/bukkit/event/player/PlayerLoginEvent.java
@@ -1,6 +1,8 @@
package org.bukkit.event.player;
import java.net.InetAddress;
+import io.papermc.paper.event.connection.PlayerConnectionValidateLoginEvent;
+import org.bukkit.Warning;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.entity.Player;
@@ -14,7 +16,15 @@ import org.jetbrains.annotations.NotNull;
* Note that this event is called early in the player initialization
* process. It is recommended that most options involving the Player
* entity be postponed to the {@link PlayerJoinEvent} instead.
+ * @deprecated Use {@link PlayerConnectionValidateLoginEvent} to handle pre-login logic
+ * (e.g. authentication or ban checks), or {@link io.papermc.paper.event.player.PlayerServerFullCheckEvent} to allow
+ * players to bypass the server's maximum player limit.
+ * Minecraft triggers this twice internally, using this event skips one of the validation checks done by the server.
+ * Additionally, this event causes the full player entity to be created much earlier than it would be in Vanilla,
+ * leaving it with mostly disfunctional methods and state.
*/
+@Warning(reason = "Listening to this event causes the player to be created early.")
+@Deprecated(since = "1.21.6")
public class PlayerLoginEvent extends PlayerEvent {
private static final HandlerList HANDLER_LIST = new HandlerList();
diff --git a/paper-api/src/main/java/org/bukkit/plugin/messaging/PluginMessageListener.java b/paper-api/src/main/java/org/bukkit/plugin/messaging/PluginMessageListener.java
index 70463cd059..a9561e3ba4 100644
--- a/paper-api/src/main/java/org/bukkit/plugin/messaging/PluginMessageListener.java
+++ b/paper-api/src/main/java/org/bukkit/plugin/messaging/PluginMessageListener.java
@@ -1,5 +1,6 @@
package org.bukkit.plugin.messaging;
+import io.papermc.paper.connection.PlayerCommonConnection;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@@ -18,4 +19,16 @@ public interface PluginMessageListener {
* @param message The raw message that was sent.
*/
public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, byte @NotNull [] message);
+
+ /**
+ * A method that will be invoked when a PluginMessageSource sends a plugin
+ * message on a registered channel.
+ *
+ * @param channel Channel that the message was sent through.
+ * @param connection Source of the message.
+ * @param message The raw message that was sent.
+ */
+ default void onPluginMessageReceived(@NotNull String channel, @NotNull PlayerCommonConnection connection, byte @NotNull [] message) {
+
+ }
}
diff --git a/paper-server/patches/features/0001-Optimize-Network-Manager-and-add-advanced-packet-sup.patch b/paper-server/patches/features/0001-Optimize-Network-Manager-and-add-advanced-packet-sup.patch
index 296185dc6a..b839e8f1b0 100644
--- a/paper-server/patches/features/0001-Optimize-Network-Manager-and-add-advanced-packet-sup.patch
+++ b/paper-server/patches/features/0001-Optimize-Network-Manager-and-add-advanced-packet-sup.patch
@@ -28,7 +28,7 @@ and then catch exceptions and close if they fire.
Part of this commit was authored by: Spottedleaf, sandtechnology
diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java
-index 2252c7c3e78c1d6dc9b56c0f6f01aec04699f072..235a076f982247552fef6d86d7e1b141eb8c72b5 100644
+index 14a25f4d9c6da9b9d7646cf88b80a04f5b4d0823..682fb4cbf9975b21171ae210defd6e338de3b718 100644
--- a/net/minecraft/network/Connection.java
+++ b/net/minecraft/network/Connection.java
@@ -85,7 +85,7 @@ public class Connection extends SimpleChannelInboundHandler> {
@@ -40,10 +40,10 @@ index 2252c7c3e78c1d6dc9b56c0f6f01aec04699f072..235a076f982247552fef6d86d7e1b141
public Channel channel;
public SocketAddress address;
// Spigot start
-@@ -145,6 +145,10 @@ public class Connection extends SimpleChannelInboundHandler> {
- }
- // Paper end - packet limiter
+@@ -146,6 +146,10 @@ public class Connection extends SimpleChannelInboundHandler> {
@Nullable public SocketAddress haProxyAddress; // Paper - Add API to get player's proxy address
+ public @Nullable java.util.Optional legacySavedLoginEventResultOverride; // Paper - playerloginevent
+ public @Nullable net.minecraft.server.level.ServerPlayer savedPlayerForLoginEventLegacy; // Paper - playerloginevent
+ // Paper start - Optimize network
+ public boolean isPending = true;
+ public boolean queueImmunity;
@@ -51,7 +51,7 @@ index 2252c7c3e78c1d6dc9b56c0f6f01aec04699f072..235a076f982247552fef6d86d7e1b141
public Connection(PacketFlow receiving) {
this.receiving = receiving;
-@@ -419,11 +423,38 @@ public class Connection extends SimpleChannelInboundHandler> {
+@@ -420,11 +424,38 @@ public class Connection extends SimpleChannelInboundHandler> {
}
public void send(Packet> packet, @Nullable ChannelFutureListener channelFutureListener, boolean flag) {
@@ -93,7 +93,7 @@ index 2252c7c3e78c1d6dc9b56c0f6f01aec04699f072..235a076f982247552fef6d86d7e1b141
}
}
-@@ -432,7 +463,7 @@ public class Connection extends SimpleChannelInboundHandler> {
+@@ -433,7 +464,7 @@ public class Connection extends SimpleChannelInboundHandler> {
this.flushQueue();
action.accept(this);
} else {
@@ -102,7 +102,7 @@ index 2252c7c3e78c1d6dc9b56c0f6f01aec04699f072..235a076f982247552fef6d86d7e1b141
}
}
-@@ -446,21 +477,41 @@ public class Connection extends SimpleChannelInboundHandler> {
+@@ -447,21 +478,41 @@ public class Connection extends SimpleChannelInboundHandler> {
}
private void doSendPacket(Packet> packet, @Nullable ChannelFutureListener channelFutureListener, boolean flag) {
@@ -148,7 +148,7 @@ index 2252c7c3e78c1d6dc9b56c0f6f01aec04699f072..235a076f982247552fef6d86d7e1b141
}
}
-@@ -472,16 +523,57 @@ public class Connection extends SimpleChannelInboundHandler> {
+@@ -473,16 +524,57 @@ public class Connection extends SimpleChannelInboundHandler> {
}
}
@@ -211,7 +211,7 @@ index 2252c7c3e78c1d6dc9b56c0f6f01aec04699f072..235a076f982247552fef6d86d7e1b141
private static final int MAX_PER_TICK = io.papermc.paper.configuration.GlobalConfiguration.get().misc.maxJoinsPerTick; // Paper - Buffer joins to world
private static int joinAttemptsThisTick; // Paper - Buffer joins to world
-@@ -551,6 +643,7 @@ public class Connection extends SimpleChannelInboundHandler> {
+@@ -552,6 +644,7 @@ public class Connection extends SimpleChannelInboundHandler> {
public void disconnect(DisconnectionDetails disconnectionDetails) {
this.preparing = false; // Spigot
@@ -219,7 +219,7 @@ index 2252c7c3e78c1d6dc9b56c0f6f01aec04699f072..235a076f982247552fef6d86d7e1b141
if (this.channel == null) {
this.delayedDisconnect = disconnectionDetails;
}
-@@ -739,7 +832,7 @@ public class Connection extends SimpleChannelInboundHandler> {
+@@ -740,7 +833,7 @@ public class Connection extends SimpleChannelInboundHandler> {
public void handleDisconnection() {
if (this.channel != null && !this.channel.isOpen()) {
if (this.disconnectionHandled) {
@@ -228,7 +228,7 @@ index 2252c7c3e78c1d6dc9b56c0f6f01aec04699f072..235a076f982247552fef6d86d7e1b141
} else {
this.disconnectionHandled = true;
PacketListener packetListener = this.getPacketListener();
-@@ -750,7 +843,7 @@ public class Connection extends SimpleChannelInboundHandler> {
+@@ -751,7 +844,7 @@ public class Connection extends SimpleChannelInboundHandler> {
);
packetListener1.onDisconnect(disconnectionDetails);
}
@@ -237,7 +237,7 @@ index 2252c7c3e78c1d6dc9b56c0f6f01aec04699f072..235a076f982247552fef6d86d7e1b141
// Paper start - Add PlayerConnectionCloseEvent
if (packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) {
/* Player was logged in, either game listener or configuration listener */
-@@ -785,4 +878,97 @@ public class Connection extends SimpleChannelInboundHandler> {
+@@ -786,4 +879,97 @@ public class Connection extends SimpleChannelInboundHandler> {
public void setBandwidthLogger(LocalSampleLogger bandwithLogger) {
this.bandwidthDebugMonitor = new BandwidthDebugMonitor(bandwithLogger);
}
diff --git a/paper-server/patches/features/0005-Use-Velocity-compression-and-cipher-natives.patch b/paper-server/patches/features/0005-Use-Velocity-compression-and-cipher-natives.patch
index 2c16d1c118..abf97b350c 100644
--- a/paper-server/patches/features/0005-Use-Velocity-compression-and-cipher-natives.patch
+++ b/paper-server/patches/features/0005-Use-Velocity-compression-and-cipher-natives.patch
@@ -269,10 +269,10 @@ index bc674b08a41d5529fe06c6d3f077051cf4138f73..ea8a894158c44c2e7943dea43ecd8e1f
+ // Paper end - Use Velocity cipher
}
diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java
-index 235a076f982247552fef6d86d7e1b141eb8c72b5..34524dc5a503bebcec99ada0d9560d6f4df48cdf 100644
+index 682fb4cbf9975b21171ae210defd6e338de3b718..8bab2c26e10e8495fd39be470bcb02917fe56f40 100644
--- a/net/minecraft/network/Connection.java
+++ b/net/minecraft/network/Connection.java
-@@ -762,11 +762,22 @@ public class Connection extends SimpleChannelInboundHandler> {
+@@ -763,11 +763,22 @@ public class Connection extends SimpleChannelInboundHandler> {
return connection;
}
@@ -299,7 +299,7 @@ index 235a076f982247552fef6d86d7e1b141eb8c72b5..34524dc5a503bebcec99ada0d9560d6f
public boolean isEncrypted() {
return this.encrypted;
-@@ -805,16 +816,17 @@ public class Connection extends SimpleChannelInboundHandler> {
+@@ -806,16 +817,17 @@ public class Connection extends SimpleChannelInboundHandler> {
// Paper end - add proper async disconnect
public void setupCompression(int threshold, boolean validateDecompressed) {
if (threshold >= 0) {
@@ -336,10 +336,10 @@ index 7de11ba404f0b60e7f7b7c16954811a343688219..bd07e6a5aa1883786d789ea71711a0c0
this.channels
.add(
diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
-index 5d48487568994860c153351ad4c6c33bc8aa5309..7950f4f88d8a83ed5610b7af4e134557d32da3f0 100644
+index 0b0e57cc534862e265108f0e9cbb324ffbc2bcd1..ebf4d4516233c002b33084f1679044b23869d848 100644
--- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
-@@ -275,11 +275,9 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
+@@ -242,11 +242,9 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
}
SecretKey secretKey = packet.getSecretKey(_private);
diff --git a/paper-server/patches/features/0016-Moonrise-optimisation-patches.patch b/paper-server/patches/features/0016-Moonrise-optimisation-patches.patch
index 29aae5b778..f29705d6b2 100644
--- a/paper-server/patches/features/0016-Moonrise-optimisation-patches.patch
+++ b/paper-server/patches/features/0016-Moonrise-optimisation-patches.patch
@@ -27703,8 +27703,8 @@ index 6f7f92cc43c56a7453b289f205502d089474ef6d..b390ba657b8b880e431c84e9dd948ac9
private static final Logger LOGGER = LogUtils.getLogger();
private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32;
private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10;
-@@ -426,6 +426,36 @@ public class ServerPlayer extends Player {
- public @Nullable String clientBrandName = null; // Paper - Brand support
+@@ -424,6 +424,36 @@ public class ServerPlayer extends Player {
+ public @Nullable com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper - PlayerNaturallySpawnCreaturesEvent
public @Nullable org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - Add API for quit reason; there are a lot of changes to do if we change all methods leading to the event
+ // Paper start - rewrite chunk system
@@ -28333,10 +28333,10 @@ index b30f56fbc1fd17259a1d05dc9155fffcab292ca1..11fed81a4696ba18440e755c3b8a5ca3
this.generatingStep = generatingStep;
this.cache = cache;
diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java
-index 891d3e13057c6034c59a78e594935c433921de04..a3aa852b04e6deccccf995cfc57f95fa79860c04 100644
+index 21b7b2f88708eb9e29d4c327b9bca79119b7124d..5a60527c9feb3606d85488b3cf215c451ca033de 100644
--- a/net/minecraft/server/players/PlayerList.java
+++ b/net/minecraft/server/players/PlayerList.java
-@@ -1328,7 +1328,7 @@ public abstract class PlayerList {
+@@ -1312,7 +1312,7 @@ public abstract class PlayerList {
public void setViewDistance(int viewDistance) {
this.viewDistance = viewDistance;
@@ -28345,7 +28345,7 @@ index 891d3e13057c6034c59a78e594935c433921de04..a3aa852b04e6deccccf995cfc57f95fa
for (ServerLevel serverLevel : this.server.getAllLevels()) {
if (serverLevel != null) {
-@@ -1339,7 +1339,7 @@ public abstract class PlayerList {
+@@ -1323,7 +1323,7 @@ public abstract class PlayerList {
public void setSimulationDistance(int simulationDistance) {
this.simulationDistance = simulationDistance;
diff --git a/paper-server/patches/features/0024-Incremental-chunk-and-player-saving.patch b/paper-server/patches/features/0024-Incremental-chunk-and-player-saving.patch
index d05c65bf8b..2b92a0550b 100644
--- a/paper-server/patches/features/0024-Incremental-chunk-and-player-saving.patch
+++ b/paper-server/patches/features/0024-Incremental-chunk-and-player-saving.patch
@@ -83,7 +83,7 @@ index 34b7769663e235b93c6388ab0c92c00f0297e42f..dda8d38ef61672cc714d9e5a475f9b04
// Paper start - add close param
this.save(progress, flush, skipSave, false);
diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java
-index c0b74d408340101bc3aac4cb4b7232c5cc78b08a..b70929df38389d789dad46c0a6d94f6c08aa7eba 100644
+index ff2f4a0fd221c0b632066cdc93719ec6d66c2ff7..9cbadb677d2ab3e9e3ee45df6647a1cbd28fcfd7 100644
--- a/net/minecraft/server/level/ServerPlayer.java
+++ b/net/minecraft/server/level/ServerPlayer.java
@@ -195,6 +195,7 @@ import org.slf4j.Logger;
@@ -95,7 +95,7 @@ index c0b74d408340101bc3aac4cb4b7232c5cc78b08a..b70929df38389d789dad46c0a6d94f6c
private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10;
private static final int FLY_STAT_RECORDING_SPEED = 25;
diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java
-index a3aa852b04e6deccccf995cfc57f95fa79860c04..b1524279c02cd3be82338a6bd0320cb125a134d5 100644
+index 18aaa9bfb08ce36177a5f357644093630ce91264..fe3f36b3cd90766f026c6383cfedc42fa7ae5508 100644
--- a/net/minecraft/server/players/PlayerList.java
+++ b/net/minecraft/server/players/PlayerList.java
@@ -489,6 +489,7 @@ public abstract class PlayerList {
@@ -106,7 +106,7 @@ index a3aa852b04e6deccccf995cfc57f95fa79860c04..b1524279c02cd3be82338a6bd0320cb1
this.playerIo.save(player);
ServerStatsCounter serverStatsCounter = player.getStats(); // CraftBukkit
if (serverStatsCounter != null) {
-@@ -1075,9 +1076,23 @@ public abstract class PlayerList {
+@@ -1059,9 +1060,23 @@ public abstract class PlayerList {
}
public void saveAll() {
diff --git a/paper-server/patches/features/0026-Optional-per-player-mob-spawns.patch b/paper-server/patches/features/0026-Optional-per-player-mob-spawns.patch
index 93df896305..f9cc310f3f 100644
--- a/paper-server/patches/features/0026-Optional-per-player-mob-spawns.patch
+++ b/paper-server/patches/features/0026-Optional-per-player-mob-spawns.patch
@@ -5,7 +5,7 @@ Subject: [PATCH] Optional per player mob spawns
diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java
-index 86ba02916c4a1c1311eeca5b1fb10a46626ba9ab..b40b94c49dc0997654aca0a1a0dffe482cd14eac 100644
+index 56a1d081a28e8b38384cfca732b103462693e322..c1e61a9ccea0c66a664439309593c7f7e6e9e867 100644
--- a/net/minecraft/server/level/ChunkMap.java
+++ b/net/minecraft/server/level/ChunkMap.java
@@ -243,11 +243,29 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
@@ -43,7 +43,7 @@ index 86ba02916c4a1c1311eeca5b1fb10a46626ba9ab..b40b94c49dc0997654aca0a1a0dffe48
protected ChunkGenerator generator() {
return this.worldGenContext.generator();
diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java
-index 8e9988130d9b912e5b858cd7792bdcefdeb66ada..25d74f866546362a17505b5d4abf85382c0df20c 100644
+index 7c9a2eed4441f816723562e0012f918db265912e..cc8638a6dab16ffbdf6951fa10182bd5763df90f 100644
--- a/net/minecraft/server/level/ServerChunkCache.java
+++ b/net/minecraft/server/level/ServerChunkCache.java
@@ -541,9 +541,18 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
@@ -78,7 +78,7 @@ index 8e9988130d9b912e5b858cd7792bdcefdeb66ada..25d74f866546362a17505b5d4abf8538
profiler.popPush("tickSpawningChunks");
diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java
-index b70929df38389d789dad46c0a6d94f6c08aa7eba..e96bb37bb7a7340a7ee33046820652ecd4accc53 100644
+index 9cbadb677d2ab3e9e3ee45df6647a1cbd28fcfd7..d2e78e9e113c9b3e35ae401f2eb055a98df12c06 100644
--- a/net/minecraft/server/level/ServerPlayer.java
+++ b/net/minecraft/server/level/ServerPlayer.java
@@ -406,6 +406,10 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
@@ -90,8 +90,8 @@ index b70929df38389d789dad46c0a6d94f6c08aa7eba..e96bb37bb7a7340a7ee33046820652ec
+ public final int[] mobCounts = new int[MOBCATEGORY_TOTAL_ENUMS];
+ // Paper end - Optional per player mob spawns
// CraftBukkit start
- public org.bukkit.craftbukkit.entity.CraftPlayer.TransferCookieConnection transferCookieConnection;
public String displayName;
+ public net.kyori.adventure.text.Component adventure$displayName; // Paper
diff --git a/net/minecraft/world/level/NaturalSpawner.java b/net/minecraft/world/level/NaturalSpawner.java
index 3a864c568cd66a680760bb4df2cb020e323e9a9d..c710e08ab48075ce7854e56826adb8f0364b025b 100644
--- a/net/minecraft/world/level/NaturalSpawner.java
diff --git a/paper-server/patches/features/0027-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch b/paper-server/patches/features/0027-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch
index 7a941a9142..a2da25bd63 100644
--- a/paper-server/patches/features/0027-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch
+++ b/paper-server/patches/features/0027-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch
@@ -6,7 +6,7 @@ Subject: [PATCH] Improve cancelling PreCreatureSpawnEvent with per player mob
diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java
-index b40b94c49dc0997654aca0a1a0dffe482cd14eac..b72000ce6bdcb97b787bfab79236f60bdb4aa2ee 100644
+index c1e61a9ccea0c66a664439309593c7f7e6e9e867..eb352aa4296abc3ed4cf31c590bc0be66daf4de3 100644
--- a/net/minecraft/server/level/ChunkMap.java
+++ b/net/minecraft/server/level/ChunkMap.java
@@ -262,8 +262,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
@@ -37,7 +37,7 @@ index b40b94c49dc0997654aca0a1a0dffe482cd14eac..b72000ce6bdcb97b787bfab79236f60b
// Paper end - Optional per player mob spawns
diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java
-index 25d74f866546362a17505b5d4abf85382c0df20c..7d78d270b7a076f595301acd9b3c6275e804496e 100644
+index cc8638a6dab16ffbdf6951fa10182bd5763df90f..2882cd829d4d8e1f8615f085f6908efcdf68ac62 100644
--- a/net/minecraft/server/level/ServerChunkCache.java
+++ b/net/minecraft/server/level/ServerChunkCache.java
@@ -546,7 +546,17 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
@@ -60,7 +60,7 @@ index 25d74f866546362a17505b5d4abf85382c0df20c..7d78d270b7a076f595301acd9b3c6275
spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true);
} else {
diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java
-index e96bb37bb7a7340a7ee33046820652ecd4accc53..53f038e1b5e7a13a08a0c925c8bd3f8a40868195 100644
+index d2e78e9e113c9b3e35ae401f2eb055a98df12c06..34e5cbb7eb58590f82510601e73c4e442a2addec 100644
--- a/net/minecraft/server/level/ServerPlayer.java
+++ b/net/minecraft/server/level/ServerPlayer.java
@@ -410,6 +410,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
@@ -69,8 +69,8 @@ index e96bb37bb7a7340a7ee33046820652ecd4accc53..53f038e1b5e7a13a08a0c925c8bd3f8a
// Paper end - Optional per player mob spawns
+ public final int[] mobBackoffCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper - per player mob count backoff
// CraftBukkit start
- public org.bukkit.craftbukkit.entity.CraftPlayer.TransferCookieConnection transferCookieConnection;
public String displayName;
+ public net.kyori.adventure.text.Component adventure$displayName; // Paper
diff --git a/net/minecraft/world/level/NaturalSpawner.java b/net/minecraft/world/level/NaturalSpawner.java
index c710e08ab48075ce7854e56826adb8f0364b025b..14a2514a408a66a83f7b5fb43b4c4dc8f23fd5f4 100644
--- a/net/minecraft/world/level/NaturalSpawner.java
diff --git a/paper-server/patches/features/0030-Optimise-collision-checking-in-player-move-packet-ha.patch b/paper-server/patches/features/0030-Optimise-collision-checking-in-player-move-packet-ha.patch
index 73a0f055bd..c02eb1f83a 100644
--- a/paper-server/patches/features/0030-Optimise-collision-checking-in-player-move-packet-ha.patch
+++ b/paper-server/patches/features/0030-Optimise-collision-checking-in-player-move-packet-ha.patch
@@ -6,10 +6,10 @@ Subject: [PATCH] Optimise collision checking in player move packet handling
Move collision logic to just the hasNewCollision call instead of getCubes + hasNewCollision
diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
-index c9a0df5e5617e62703787942d067883ea537618b..aeb43902a09ef9c1b137964065780be3e87648f4 100644
+index ff2f83dc1cbf10baccd07e65c894dff1d2f9a3cf..63d01c3753b865532a222b5b7408c8857c064d55 100644
--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
-@@ -592,6 +592,7 @@ public class ServerGamePacketListenerImpl
+@@ -594,6 +594,7 @@ public class ServerGamePacketListenerImpl
}
rootVehicle.move(MoverType.PLAYER, new Vec3(d3, d4, d5));
@@ -17,7 +17,7 @@ index c9a0df5e5617e62703787942d067883ea537618b..aeb43902a09ef9c1b137964065780be3
double verticalDelta = d4;
d3 = d - rootVehicle.getX();
d4 = d1 - rootVehicle.getY();
-@@ -603,12 +604,21 @@ public class ServerGamePacketListenerImpl
+@@ -605,12 +606,21 @@ public class ServerGamePacketListenerImpl
d7 = d3 * d3 + d4 * d4 + d5 * d5;
boolean flag1 = false;
if (d7 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot
@@ -42,7 +42,7 @@ index c9a0df5e5617e62703787942d067883ea537618b..aeb43902a09ef9c1b137964065780be3
rootVehicle.absSnapTo(x, y, z, f, f1);
this.send(ClientboundMoveVehiclePacket.fromEntity(rootVehicle));
rootVehicle.removeLatestMovementRecording();
-@@ -687,9 +697,32 @@ public class ServerGamePacketListenerImpl
+@@ -689,9 +699,32 @@ public class ServerGamePacketListenerImpl
}
private boolean noBlocksAround(Entity entity) {
@@ -78,7 +78,7 @@ index c9a0df5e5617e62703787942d067883ea537618b..aeb43902a09ef9c1b137964065780be3
}
@Override
-@@ -1465,7 +1498,7 @@ public class ServerGamePacketListenerImpl
+@@ -1467,7 +1500,7 @@ public class ServerGamePacketListenerImpl
}
}
@@ -87,7 +87,7 @@ index c9a0df5e5617e62703787942d067883ea537618b..aeb43902a09ef9c1b137964065780be3
d3 = d - this.lastGoodX; // Paper - diff on change, used for checking large move vectors above
d4 = d1 - this.lastGoodY; // Paper - diff on change, used for checking large move vectors above
d5 = d2 - this.lastGoodZ; // Paper - diff on change, used for checking large move vectors above
-@@ -1504,6 +1537,7 @@ public class ServerGamePacketListenerImpl
+@@ -1506,6 +1539,7 @@ public class ServerGamePacketListenerImpl
boolean flag1 = this.player.verticalCollisionBelow;
this.player.move(MoverType.PLAYER, new Vec3(d3, d4, d5));
this.player.onGround = packet.isOnGround(); // CraftBukkit - SPIGOT-5810, SPIGOT-5835, SPIGOT-6828: reset by this.player.move
@@ -95,7 +95,7 @@ index c9a0df5e5617e62703787942d067883ea537618b..aeb43902a09ef9c1b137964065780be3
// Paper start - prevent position desync
if (this.awaitingPositionFromClient != null) {
return; // ... thanks Mojang for letting move calls teleport across dimensions.
-@@ -1536,7 +1570,17 @@ public class ServerGamePacketListenerImpl
+@@ -1538,7 +1572,17 @@ public class ServerGamePacketListenerImpl
}
// Paper start - Add fail move event
@@ -114,7 +114,7 @@ index c9a0df5e5617e62703787942d067883ea537618b..aeb43902a09ef9c1b137964065780be3
if (!allowMovement) {
io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.CLIPPED_INTO_BLOCK,
toX, toY, toZ, toYaw, toPitch, false);
-@@ -1672,7 +1716,7 @@ public class ServerGamePacketListenerImpl
+@@ -1674,7 +1718,7 @@ public class ServerGamePacketListenerImpl
private boolean updateAwaitingTeleport() {
if (this.awaitingPositionFromClient != null) {
@@ -123,7 +123,7 @@ index c9a0df5e5617e62703787942d067883ea537618b..aeb43902a09ef9c1b137964065780be3
this.awaitingTeleportTime = this.tickCount;
this.teleport(
this.awaitingPositionFromClient.x,
-@@ -1691,6 +1735,33 @@ public class ServerGamePacketListenerImpl
+@@ -1693,6 +1737,33 @@ public class ServerGamePacketListenerImpl
}
}
diff --git a/paper-server/patches/features/0032-Improve-keepalive-ping-system.patch b/paper-server/patches/features/0032-Improve-keepalive-ping-system.patch
index a0955e50f0..0d8ff77121 100644
--- a/paper-server/patches/features/0032-Improve-keepalive-ping-system.patch
+++ b/paper-server/patches/features/0032-Improve-keepalive-ping-system.patch
@@ -11,15 +11,16 @@ keepalives in case multiple end up in flight.
Additionally, replace the latency calculation with a true
average over the last 5 seconds of keepalive transactions.
-diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java
-index 53f038e1b5e7a13a08a0c925c8bd3f8a40868195..f3eca351021c37b64315872d075bd0a84aeee267 100644
---- a/net/minecraft/server/level/ServerPlayer.java
-+++ b/net/minecraft/server/level/ServerPlayer.java
-@@ -461,6 +461,70 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
- return this.viewDistanceHolder;
- }
- // Paper end - rewrite chunk system
-+ // Paper start - improve keepalives
+diff --git a/io/papermc/paper/util/KeepAlive.java b/io/papermc/paper/util/KeepAlive.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..4a2520f554c2ee74faf86d7c93baccf0f391a6b3
+--- /dev/null
++++ b/io/papermc/paper/util/KeepAlive.java
+@@ -0,0 +1,67 @@
++package io.papermc.paper.util;
++
++public class KeepAlive {
++
+ public long lastKeepAliveTx = System.nanoTime();
+ public static final record KeepAliveResponse(long txTimeNS, long rxTimeNS) {
+ public long latencyNS() {
@@ -82,15 +83,27 @@ index 53f038e1b5e7a13a08a0c925c8bd3f8a40868195..f3eca351021c37b64315872d075bd0a8
+ return ret;
+ }
+ }
-+ // Paper end - improve keepalives
++}
+diff --git a/net/minecraft/server/network/CommonListenerCookie.java b/net/minecraft/server/network/CommonListenerCookie.java
+index b94988d6dd98bfb7d3d2f08c2adaa96d7c7a8b55..7e61c8222d567feb8c2b988699e1cfe83bf4be31 100644
+--- a/net/minecraft/server/network/CommonListenerCookie.java
++++ b/net/minecraft/server/network/CommonListenerCookie.java
+@@ -3,8 +3,8 @@ package net.minecraft.server.network;
+ import com.mojang.authlib.GameProfile;
+ import net.minecraft.server.level.ClientInformation;
- public ServerPlayer(MinecraftServer server, ServerLevel level, GameProfile gameProfile, ClientInformation clientInformation) {
- super(level, gameProfile);
+-public record CommonListenerCookie(GameProfile gameProfile, int latency, ClientInformation clientInformation, boolean transferred, @org.jetbrains.annotations.Nullable String brandName) { // Paper
++public record CommonListenerCookie(GameProfile gameProfile, int latency, ClientInformation clientInformation, boolean transferred, @org.jetbrains.annotations.Nullable String brandName, io.papermc.paper.util.KeepAlive keepAlive) { // Paper
+ public static CommonListenerCookie createInitial(GameProfile gameProfile, boolean transferred) {
+- return new CommonListenerCookie(gameProfile, 0, ClientInformation.createDefault(), transferred, null); // Paper
++ return new CommonListenerCookie(gameProfile, 0, ClientInformation.createDefault(), transferred, null, new io.papermc.paper.util.KeepAlive()); // Paper
+ }
+ }
diff --git a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
-index 85e01c3a1536b41a0301a5a6506e058ff9633a4a..43f70a5561d6cc62aaeba6d1e39598ecb382e369 100644
+index fae1523404b7afa2b904faad9242273b3c406aac..16962ccab91d0941e428a2e53aa37e9ca975f62f 100644
--- a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
-@@ -38,12 +38,12 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
+@@ -38,12 +38,13 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
protected final MinecraftServer server;
public final Connection connection; // Paper
private final boolean transferred;
@@ -104,19 +117,26 @@ index 85e01c3a1536b41a0301a5a6506e058ff9633a4a..43f70a5561d6cc62aaeba6d1e39598ec
private boolean closed = false;
- private int latency;
+ private volatile int latency; // Paper - improve keepalives - make volatile
++ private final io.papermc.paper.util.KeepAlive keepAlive; // Paper - improve keepalives
private volatile boolean suspendFlushingOnServerThread = false;
// CraftBukkit start
- protected final net.minecraft.server.level.ServerPlayer player;
-@@ -57,7 +57,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
- public ServerCommonPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie, net.minecraft.server.level.ServerPlayer player) { // CraftBukkit
+ protected final org.bukkit.craftbukkit.CraftServer cserver;
+@@ -57,12 +58,13 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
+ public ServerCommonPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie) {
this.server = server;
this.connection = connection;
- this.keepAliveTime = Util.getMillis();
+ //this.keepAliveTime = Util.getMillis(); // Paper - improve keepalives
this.latency = cookie.latency();
this.transferred = cookie.transferred();
- // CraftBukkit start - add fields and methods
-@@ -120,13 +120,41 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
+ // Paper start
+ this.playerBrand = cookie.brandName();
+ this.cserver = server.server;
++ this.keepAlive = cookie.keepAlive();
+ // Paper end
+ }
+
+@@ -89,13 +91,41 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
@Override
public void handleKeepAlive(ServerboundKeepAlivePacket packet) {
@@ -128,27 +148,27 @@ index 85e01c3a1536b41a0301a5a6506e058ff9633a4a..43f70a5561d6cc62aaeba6d1e39598ec
- this.disconnectAsync(TIMEOUT_DISCONNECTION_MESSAGE, org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); // Paper - add proper async disconnect
+ // Paper start - improve keepalives
+ long now = System.nanoTime();
-+ net.minecraft.server.level.ServerPlayer.PendingKeepAlive pending = this.player.pendingKeepAlives.peek();
++ io.papermc.paper.util.KeepAlive.PendingKeepAlive pending = this.keepAlive.pendingKeepAlives.peek();
+ if (pending != null && pending.challengeId() == packet.getId()) {
-+ this.player.pendingKeepAlives.remove(pending);
++ this.keepAlive.pendingKeepAlives.remove(pending);
+
-+ net.minecraft.server.level.ServerPlayer.KeepAliveResponse response = new net.minecraft.server.level.ServerPlayer.KeepAliveResponse(pending.txTimeNS(), now);
++ io.papermc.paper.util.KeepAlive.KeepAliveResponse response = new io.papermc.paper.util.KeepAlive.KeepAliveResponse(pending.txTimeNS(), now);
+
-+ this.player.pingCalculator1m.update(response);
-+ this.player.pingCalculator5s.update(response);
++ this.keepAlive.pingCalculator1m.update(response);
++ this.keepAlive.pingCalculator5s.update(response);
+
-+ this.latency = this.player.pingCalculator5s.getAvgLatencyMS();
++ this.latency = this.keepAlive.pingCalculator5s.getAvgLatencyMS();
+ return;
+ }
+
-+ for (java.util.Iterator itr = this.player.pendingKeepAlives.iterator(); itr.hasNext();) {
-+ net.minecraft.server.level.ServerPlayer.PendingKeepAlive ka = itr.next();
++ for (java.util.Iterator itr = this.keepAlive.pendingKeepAlives.iterator(); itr.hasNext();) {
++ io.papermc.paper.util.KeepAlive.PendingKeepAlive ka = itr.next();
+ if (ka.challengeId() == packet.getId()) {
+ itr.remove();
+
+ if (!this.processedDisconnect) {
-+ LOGGER.info("Disconnecting " + this.player.getScoreboardName() + " for sending keepalive response (" + packet.getId() + ") out-of-order!");
-+ this.disconnectAsync(TIMEOUT_DISCONNECTION_MESSAGE, org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT);
++ LOGGER.info("Disconnecting {} for sending keepalive response ({}) out-of-order!", this.playerProfile().getName(), packet.getId());
++ this.disconnectAsync(TIMEOUT_DISCONNECTION_MESSAGE, io.papermc.paper.connection.DisconnectionReason.TIMEOUT);
+ return;
+ }
+ break;
@@ -156,15 +176,15 @@ index 85e01c3a1536b41a0301a5a6506e058ff9633a4a..43f70a5561d6cc62aaeba6d1e39598ec
}
+
+ if (!this.processedDisconnect) {
-+ LOGGER.info("Disconnecting " + this.player.getScoreboardName() + " for sending keepalive response (" + packet.getId() + ") without matching challenge!");
-+ this.disconnectAsync(TIMEOUT_DISCONNECTION_MESSAGE, org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT);
++ LOGGER.info("Disconnecting {} for sending keepalive response ({}) without matching challenge!", this.playerProfile().getName(), packet.getId());
++ this.disconnectAsync(TIMEOUT_DISCONNECTION_MESSAGE, io.papermc.paper.connection.DisconnectionReason.TIMEOUT);
+ return;
+ }
+ // Paper end - improve keepalives
}
@Override
-@@ -247,20 +275,23 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
+@@ -148,20 +178,23 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
protected void keepConnectionAlive() {
Profiler.get().push("keepAlive");
long millis = Util.getMillis();
@@ -186,19 +206,29 @@ index 85e01c3a1536b41a0301a5a6506e058ff9633a4a..43f70a5561d6cc62aaeba6d1e39598ec
+ if (this.checkIfClosed(millis) && !this.processedDisconnect) {
+ long currTime = System.nanoTime();
+
-+ if ((currTime - this.player.lastKeepAliveTx) >= java.util.concurrent.TimeUnit.SECONDS.toNanos(1L)) {
-+ this.player.lastKeepAliveTx = currTime;
++ if ((currTime - this.keepAlive.lastKeepAliveTx) >= java.util.concurrent.TimeUnit.SECONDS.toNanos(1L)) {
++ this.keepAlive.lastKeepAliveTx = currTime;
+
-+ net.minecraft.server.level.ServerPlayer.PendingKeepAlive pka = new net.minecraft.server.level.ServerPlayer.PendingKeepAlive(currTime, millis);
-+ this.player.pendingKeepAlives.add(pka);
++ io.papermc.paper.util.KeepAlive.PendingKeepAlive pka = new io.papermc.paper.util.KeepAlive.PendingKeepAlive(currTime, millis);
++ this.keepAlive.pendingKeepAlives.add(pka);
+ this.send(new ClientboundKeepAlivePacket(pka.challengeId()));
+ }
+
-+ net.minecraft.server.level.ServerPlayer.PendingKeepAlive oldest = this.player.pendingKeepAlives.peek();
++ io.papermc.paper.util.KeepAlive.PendingKeepAlive oldest = this.keepAlive.pendingKeepAlives.peek();
+ if (oldest != null && (currTime - oldest.txTimeNS()) > java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(KEEPALIVE_LIMIT)) {
-+ LOGGER.warn(this.player.getScoreboardName() + " was kicked due to keepalive timeout!");
-+ this.disconnect(TIMEOUT_DISCONNECTION_MESSAGE, org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT);
++ LOGGER.info("{} was kicked due to keepalive timeout!", this.playerProfile().getName());
++ this.disconnect(TIMEOUT_DISCONNECTION_MESSAGE, io.papermc.paper.connection.DisconnectionReason.TIMEOUT); // Paper - kick event cause
+ // Paper end - improve keepalives
}
}
+@@ -341,6 +374,6 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
+ }
+
+ protected CommonListenerCookie createCookie(ClientInformation clientInformation) {
+- return new CommonListenerCookie(this.playerProfile(), this.latency, clientInformation, this.transferred, this.playerBrand); // Paper
++ return new CommonListenerCookie(this.playerProfile(), this.latency, clientInformation, this.transferred, this.playerBrand, this.keepAlive); // Paper
+ }
+-}
++}
+\ No newline at end of file
diff --git a/paper-server/patches/sources/net/minecraft/network/Connection.java.patch b/paper-server/patches/sources/net/minecraft/network/Connection.java.patch
index 773b494465..ef3704f1e0 100644
--- a/paper-server/patches/sources/net/minecraft/network/Connection.java.patch
+++ b/paper-server/patches/sources/net/minecraft/network/Connection.java.patch
@@ -29,7 +29,7 @@
@Nullable
private volatile PacketListener disconnectListener;
@Nullable
-@@ -106,6 +_,40 @@
+@@ -106,6 +_,41 @@
private volatile DisconnectionDetails delayedDisconnect;
@Nullable
BandwidthDebugMonitor bandwidthDebugMonitor;
@@ -44,8 +44,7 @@
+ if (this.packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl impl) {
+ return impl.player;
+ } else if (this.packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl impl) {
-+ org.bukkit.craftbukkit.entity.CraftPlayer player = impl.getCraftPlayer();
-+ return player == null ? null : player.getHandle();
++ return null;
+ }
+ return null;
+ }
@@ -67,6 +66,8 @@
+ }
+ // Paper end - packet limiter
+ @Nullable public SocketAddress haProxyAddress; // Paper - Add API to get player's proxy address
++ public @Nullable java.util.Optional legacySavedLoginEventResultOverride; // Paper - playerloginevent
++ public @Nullable net.minecraft.server.level.ServerPlayer savedPlayerForLoginEventLegacy; // Paper - playerloginevent
public Connection(PacketFlow receiving) {
this.receiving = receiving;
diff --git a/paper-server/patches/sources/net/minecraft/network/DisconnectionDetails.java.patch b/paper-server/patches/sources/net/minecraft/network/DisconnectionDetails.java.patch
new file mode 100644
index 0000000000..434d042dcd
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/network/DisconnectionDetails.java.patch
@@ -0,0 +1,17 @@
+--- a/net/minecraft/network/DisconnectionDetails.java
++++ b/net/minecraft/network/DisconnectionDetails.java
+@@ -5,7 +_,13 @@
+ import java.util.Optional;
+ import net.minecraft.network.chat.Component;
+
+-public record DisconnectionDetails(Component reason, Optional report, Optional bugReportLink) {
++// Paper start - Configuration API: add support for enhanced disconnection causes
++public record DisconnectionDetails(Component reason, Optional report, Optional bugReportLink, Optional quitMessage, Optional disconnectionReason) {
++ public DisconnectionDetails(Component reason, Optional report, Optional bugReportLink) {
++ this(reason, report, bugReportLink, Optional.empty(), Optional.empty());
++ }
++// Paper end - Configuration API: add support for enhanced disconnection causes
++
+ public DisconnectionDetails(Component reason) {
+ this(reason, Optional.empty(), Optional.empty());
+ }
diff --git a/paper-server/patches/sources/net/minecraft/server/level/ServerPlayer.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ServerPlayer.java.patch
index 382b39f8c0..0103d1fb0f 100644
--- a/paper-server/patches/sources/net/minecraft/server/level/ServerPlayer.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/level/ServerPlayer.java.patch
@@ -57,7 +57,7 @@
@Override
public void dataChanged(AbstractContainerMenu containerMenu, int dataSlotIndex, int value) {
}
-@@ -354,9 +_,43 @@
+@@ -354,9 +_,41 @@
public void sendSystemMessage(Component component) {
ServerPlayer.this.sendSystemMessage(component);
}
@@ -79,7 +79,6 @@
+ public @Nullable net.minecraft.network.protocol.game.ClientboundSetHealthPacket queuedHealthUpdatePacket;
+ // Paper end - cancellable death event
+ // CraftBukkit start
-+ public org.bukkit.craftbukkit.entity.CraftPlayer.TransferCookieConnection transferCookieConnection;
+ public String displayName;
+ public net.kyori.adventure.text.Component adventure$displayName; // Paper
+ public @Nullable Component listName;
@@ -96,7 +95,6 @@
+ // CraftBukkit end
+ public boolean isRealPlayer; // Paper
+ public @Nullable com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper - PlayerNaturallySpawnCreaturesEvent
-+ public @Nullable String clientBrandName = null; // Paper - Brand support
+ public @Nullable org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - Add API for quit reason; there are a lot of changes to do if we change all methods leading to the event
public ServerPlayer(MinecraftServer server, ServerLevel level, GameProfile gameProfile, ClientInformation clientInformation) {
diff --git a/paper-server/patches/sources/net/minecraft/server/network/CommonListenerCookie.java.patch b/paper-server/patches/sources/net/minecraft/server/network/CommonListenerCookie.java.patch
new file mode 100644
index 0000000000..38dc666c1a
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/server/network/CommonListenerCookie.java.patch
@@ -0,0 +1,13 @@
+--- a/net/minecraft/server/network/CommonListenerCookie.java
++++ b/net/minecraft/server/network/CommonListenerCookie.java
+@@ -3,8 +_,8 @@
+ import com.mojang.authlib.GameProfile;
+ import net.minecraft.server.level.ClientInformation;
+
+-public record CommonListenerCookie(GameProfile gameProfile, int latency, ClientInformation clientInformation, boolean transferred) {
++public record CommonListenerCookie(GameProfile gameProfile, int latency, ClientInformation clientInformation, boolean transferred, @org.jetbrains.annotations.Nullable String brandName) { // Paper
+ public static CommonListenerCookie createInitial(GameProfile gameProfile, boolean transferred) {
+- return new CommonListenerCookie(gameProfile, 0, ClientInformation.createDefault(), transferred);
++ return new CommonListenerCookie(gameProfile, 0, ClientInformation.createDefault(), transferred, null); // Paper
+ }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch b/paper-server/patches/sources/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch
index dac8b4dcd5..a3d4b912c9 100644
--- a/paper-server/patches/sources/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch
@@ -1,14 +1,6 @@
--- a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
-@@ -29,14 +_,14 @@
- import net.minecraft.util.profiling.Profiler;
- import org.slf4j.Logger;
-
--public abstract class ServerCommonPacketListenerImpl implements ServerCommonPacketListener {
-+public abstract class ServerCommonPacketListenerImpl implements ServerCommonPacketListener, org.bukkit.craftbukkit.entity.CraftPlayer.TransferCookieConnection { // CraftBukkit
- private static final Logger LOGGER = LogUtils.getLogger();
- public static final int LATENCY_CHECK_INTERVAL = 15000;
- private static final int CLOSED_LISTENER_TIMEOUT = 15000;
+@@ -36,7 +_,7 @@
private static final Component TIMEOUT_DISCONNECTION_MESSAGE = Component.translatable("disconnect.timeout");
static final Component DISCONNECT_UNEXPECTED_QUERY = Component.translatable("multiplayer.disconnect.unexpected_query_response");
protected final MinecraftServer server;
@@ -17,73 +9,32 @@
private final boolean transferred;
private long keepAliveTime;
private boolean keepAlivePending;
-@@ -45,14 +_,51 @@
+@@ -45,6 +_,14 @@
private boolean closed = false;
private int latency;
private volatile boolean suspendFlushingOnServerThread = false;
+ // CraftBukkit start
-+ protected final net.minecraft.server.level.ServerPlayer player;
+ protected final org.bukkit.craftbukkit.CraftServer cserver;
+ public boolean processedDisconnect;
+ // CraftBukkit end
+ public final java.util.Map packCallbacks = new java.util.concurrent.ConcurrentHashMap<>(); // Paper - adventure resource pack callbacks
+ private static final long KEEPALIVE_LIMIT = Long.getLong("paper.playerconnection.keepalive", 30) * 1000; // Paper - provide property to set keepalive limit
+ protected static final net.minecraft.resources.ResourceLocation MINECRAFT_BRAND = net.minecraft.resources.ResourceLocation.withDefaultNamespace("brand"); // Paper - Brand support
++ public @Nullable String playerBrand; // Paper
-- public ServerCommonPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie) {
-+ public ServerCommonPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie, net.minecraft.server.level.ServerPlayer player) { // CraftBukkit
+ public ServerCommonPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie) {
this.server = server;
- this.connection = connection;
+@@ -52,6 +_,10 @@
this.keepAliveTime = Util.getMillis();
this.latency = cookie.latency();
this.transferred = cookie.transferred();
-- }
-+ // CraftBukkit start - add fields and methods
-+ this.player = player;
-+ this.player.transferCookieConnection = this;
++ // Paper start
++ this.playerBrand = cookie.brandName();
+ this.cserver = server.server;
-+ }
-+
-+ public org.bukkit.craftbukkit.entity.CraftPlayer getCraftPlayer() {
-+ return this.player.getBukkitEntity();
-+ }
-+
-+ @Override
-+ public boolean isTransferred() {
-+ return this.transferred;
-+ }
-+
-+ @Override
-+ public net.minecraft.network.ConnectionProtocol getProtocol() {
-+ return this.protocol();
-+ }
-+
-+ @Override
-+ public void sendPacket(Packet> packet) {
-+ this.send(packet);
-+ }
-+
-+ @Override
-+ public void kickPlayer(Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) { // Paper - kick event causes
-+ this.disconnect(reason, cause); // Paper - kick event causes
-+ }
-+ // CraftBukkit end
++ // Paper end
+ }
private void close() {
- if (!this.closed) {
-@@ -63,6 +_,12 @@
-
- @Override
- public void onDisconnect(DisconnectionDetails details) {
-+ // Paper start - Fix kick event leave message not being sent
-+ this.onDisconnect(details, null);
-+ }
-+
-+ public void onDisconnect(DisconnectionDetails details, @Nullable net.kyori.adventure.text.Component quitMessage) {
-+ // Paper end - Fix kick event leave message not being sent
- if (this.isSingleplayerOwner()) {
- LOGGER.info("Stopping singleplayer server as player logged out");
- this.server.halt(false);
@@ -82,7 +_,7 @@
this.latency = (this.latency * 3 + i) / 4;
this.keepAlivePending = false;
@@ -93,88 +44,13 @@
}
}
-@@ -90,9 +_,73 @@
- public void handlePong(ServerboundPongPacket packet) {
- }
-
-+ public static final net.minecraft.resources.ResourceLocation CUSTOM_REGISTER = net.minecraft.resources.ResourceLocation.withDefaultNamespace("register"); // CraftBukkit
-+ private static final net.minecraft.resources.ResourceLocation CUSTOM_UNREGISTER = net.minecraft.resources.ResourceLocation.withDefaultNamespace("unregister"); // CraftBukkit
-+
- @Override
- public void handleCustomPayload(ServerboundCustomPayloadPacket packet) {
-- }
-+ // Paper start
-+ if (packet.payload() instanceof net.minecraft.network.protocol.common.custom.BrandPayload(String brand)) {
-+ this.player.clientBrandName = brand;
-+ }
-+
-+ if (!(packet.payload() instanceof final net.minecraft.network.protocol.common.custom.DiscardedPayload discardedPayload)) {
-+ return;
-+ }
-+
-+ PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
-+
-+ final net.minecraft.resources.ResourceLocation identifier = packet.payload().type().id();
-+ final byte[] data = discardedPayload.data();
-+ try {
-+ final boolean registerChannel = ServerCommonPacketListenerImpl.CUSTOM_REGISTER.equals(identifier);
-+ if (registerChannel || ServerCommonPacketListenerImpl.CUSTOM_UNREGISTER.equals(identifier)) {
-+ // Strings separated by zeros instead of length prefixes
-+ int startIndex = 0;
-+ for (int i = 0; i < data.length; i++) {
-+ final byte b = data[i];
-+ if (b != 0) {
-+ continue;
-+ }
-+
-+ readChannelIdentifier(data, startIndex, i, registerChannel);
-+ startIndex = i + 1;
-+ }
-+
-+ // Read the last one
-+ readChannelIdentifier(data, startIndex, data.length, registerChannel);
-+ return;
-+ }
-+
-+ if (identifier.equals(MINECRAFT_BRAND)) {
-+ this.player.clientBrandName = new net.minecraft.network.FriendlyByteBuf(io.netty.buffer.Unpooled.wrappedBuffer(data)).readUtf(256);
-+ }
-+
-+ this.cserver.getMessenger().dispatchIncomingMessage(this.player.getBukkitEntity(), identifier.toString(), data);
-+ } catch (final Exception e) {
-+ ServerGamePacketListenerImpl.LOGGER.error("Couldn't handle custom payload on channel {}", identifier, e);
-+ this.disconnect(Component.literal("Invalid custom payload payload!"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PAYLOAD); // Paper - kick event cause
-+ }
-+ }
-+
-+ private void readChannelIdentifier(final byte[] data, final int from, final int to, final boolean register) {
-+ final int length = to - from;
-+ if (length == 0) {
-+ return;
-+ }
-+
-+ final String channel = new String(data, from, length, java.nio.charset.StandardCharsets.US_ASCII);
-+ if (register) {
-+ this.getCraftPlayer().addChannel(channel);
-+ } else {
-+ this.getCraftPlayer().removeChannel(channel);
-+ }
-+ }
-+
-+ public final boolean isDisconnected() {
-+ return (!this.player.joining && !this.connection.isConnected()) || this.processedDisconnect; // Paper - Fix duplication bugs
-+ }
-+ // Paper end
-
- @Override
- public void handleCustomClickAction(ServerboundCustomClickActionPacket packet) {
-@@ -105,21 +_,50 @@
+@@ -105,21 +_,46 @@
PacketUtils.ensureRunningOnSameThread(packet, this, this.server);
if (packet.action() == ServerboundResourcePackPacket.Action.DECLINED && this.server.isResourcePackRequired()) {
LOGGER.info("Disconnecting {} due to resource pack {} rejection", this.playerProfile().getName(), packet.id());
- this.disconnect(Component.translatable("multiplayer.requiredTexturePrompt.disconnect"));
- }
-+ this.disconnect(Component.translatable("multiplayer.requiredTexturePrompt.disconnect"), org.bukkit.event.player.PlayerKickEvent.Cause.RESOURCE_PACK_REJECTION); // Paper - kick event cause
++ this.disconnect(Component.translatable("multiplayer.requiredTexturePrompt.disconnect"), io.papermc.paper.connection.DisconnectionReason.RESOURCE_PACK_REJECTION); // Paper - kick event cause
+ }
+ // Paper start - adventure pack callbacks
+ // call the callbacks before the previously-existing event so the event has final say
@@ -185,26 +61,22 @@
+ callback = this.packCallbacks.get(packet.id());
+ }
+ if (callback != null) {
-+ callback.packEventReceived(packet.id(), net.kyori.adventure.resource.ResourcePackStatus.valueOf(packet.action().name()), this.getCraftPlayer());
++ net.kyori.adventure.audience.Audience audience = switch (this) {
++ case ServerGamePacketListenerImpl serverGamePacketListener -> serverGamePacketListener.getCraftPlayer();
++ case ServerConfigurationPacketListenerImpl configurationPacketListener -> configurationPacketListener.paperConnection.getAudience();
++ default -> throw new IllegalStateException("Unexpected value: " + this);
++ };
++
++ callback.packEventReceived(packet.id(), net.kyori.adventure.resource.ResourcePackStatus.valueOf(packet.action().name()), audience);
+ }
+ // Paper end
-+ // Paper start - store last pack status
-+ org.bukkit.event.player.PlayerResourcePackStatusEvent.Status packStatus = org.bukkit.event.player.PlayerResourcePackStatusEvent.Status.values()[packet.action().ordinal()];
-+ this.player.getBukkitEntity().resourcePackStatus = packStatus;
-+ this.cserver.getPluginManager().callEvent(new org.bukkit.event.player.PlayerResourcePackStatusEvent(this.getCraftPlayer(), packet.id(), packStatus)); // CraftBukkit
-+ // Paper end - store last pack status
}
@Override
public void handleCookieResponse(ServerboundCookieResponsePacket packet) {
- this.disconnect(DISCONNECT_UNEXPECTED_QUERY);
-+ // CraftBukkit start
-+ PacketUtils.ensureRunningOnSameThread(packet, this, this.server);
-+ if (this.player.getBukkitEntity().handleCookieResponse(packet)) {
-+ return;
-+ }
-+ // CraftBukkit end
-+ this.disconnect(DISCONNECT_UNEXPECTED_QUERY, org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_COOKIE); // Paper - kick event cause
++ if (this.getCookieConnection().handleCookieResponse(packet)) return; // Paper
++ this.disconnect(DISCONNECT_UNEXPECTED_QUERY, io.papermc.paper.connection.DisconnectionReason.INVALID_COOKIE); // Paper - kick event cause
}
protected void keepConnectionAlive() {
@@ -229,7 +101,7 @@
if (this.closed) {
if (time - this.closedListenerTime >= 15000L) {
- this.disconnect(TIMEOUT_DISCONNECTION_MESSAGE);
-+ this.disconnect(TIMEOUT_DISCONNECTION_MESSAGE, org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause
++ this.disconnect(TIMEOUT_DISCONNECTION_MESSAGE, io.papermc.paper.connection.DisconnectionReason.TIMEOUT); // Paper - kick event cause
}
return false;
@@ -240,43 +112,34 @@
+ // CraftBukkit start
+ if (packet == null || this.processedDisconnect) { // Spigot
+ return;
-+ } else if (packet instanceof net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket defaultSpawnPositionPacket) {
-+ this.player.compassTarget = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(defaultSpawnPositionPacket.getPos(), this.getCraftPlayer().getWorld());
++ } else if (packet instanceof net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket defaultSpawnPositionPacket && this instanceof ServerGamePacketListenerImpl serverGamePacketListener) {
++ serverGamePacketListener.player.compassTarget = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(defaultSpawnPositionPacket.getPos(), serverGamePacketListener.getCraftPlayer().getWorld());
+ }
+ // CraftBukkit end
if (packet.isTerminal()) {
this.close();
}
-@@ -173,19 +_,115 @@
+@@ -173,19 +_,113 @@
}
}
-+ // Paper start - adventure
-+ public void disconnect(final net.kyori.adventure.text.Component reason) {
-+ this.disconnect(reason, org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN);
-+ }
-+
-+ public void disconnect(final net.kyori.adventure.text.Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
-+ this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(reason), cause);
-+ }
-+ // Paper end - adventure
-+
+ @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper - kick event causes
public void disconnect(Component reason) {
- this.disconnect(new DisconnectionDetails(reason));
-- }
--
-- public void disconnect(DisconnectionDetails disconnectionDetails) {
+ // Paper start - kick event causes
-+ this.disconnect(reason, org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN);
++ this.disconnect(reason, io.papermc.paper.connection.DisconnectionReason.UNKNOWN);
+ }
+
-+ public void disconnect(Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
-+ this.disconnect(new DisconnectionDetails(reason), cause);
++ public void disconnect(Component reason, io.papermc.paper.connection.DisconnectionReason cause) {
++ this.disconnect(new DisconnectionDetails(reason, java.util.Optional.empty(), java.util.Optional.empty(), java.util.Optional.empty(), java.util.Optional.of(cause)));
+ // Paper end - kick event causes
-+ }
-+
-+ public void disconnect(DisconnectionDetails disconnectionDetails, org.bukkit.event.player.PlayerKickEvent.Cause cause) { // Paper - kick event cause
+ }
+
+ public void disconnect(DisconnectionDetails disconnectionDetails) {
+- this.connection
+- .send(
+- new ClientboundDisconnectPacket(disconnectionDetails.reason()),
+- PacketSendListener.thenRun(() -> this.connection.disconnect(disconnectionDetails))
+ // CraftBukkit start - fire PlayerKickEvent
+ if (this.processedDisconnect) {
+ return;
@@ -285,7 +148,7 @@
+ org.bukkit.craftbukkit.util.Waitable waitable = new org.bukkit.craftbukkit.util.Waitable() {
+ @Override
+ protected Object evaluate() {
-+ ServerCommonPacketListenerImpl.this.disconnect(disconnectionDetails, cause); // Paper - kick event causes
++ ServerCommonPacketListenerImpl.this.disconnect(disconnectionDetails);
+ return null;
+ }
+ };
@@ -302,71 +165,86 @@
+ return;
+ }
+
-+ net.kyori.adventure.text.Component leaveMessage = net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? this.player.getBukkitEntity().displayName() : net.kyori.adventure.text.Component.text(this.player.getScoreboardName())); // Paper - Adventure
++ Component reason;
++ Component leaveMessage;
++ if (this instanceof ServerGamePacketListenerImpl serverGamePacketListener) {
++ net.kyori.adventure.text.Component rawLeaveMessage = net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? serverGamePacketListener.player.getBukkitEntity().displayName() : net.kyori.adventure.text.Component.text(serverGamePacketListener.player.getScoreboardName())); // Paper - Adventure
+
-+ org.bukkit.event.player.PlayerKickEvent event = new org.bukkit.event.player.PlayerKickEvent(this.player.getBukkitEntity(), io.papermc.paper.adventure.PaperAdventure.asAdventure(disconnectionDetails.reason()), leaveMessage, cause); // Paper - adventure & kick event causes
++ net.minecraft.server.level.ServerPlayer player = serverGamePacketListener.player;
++ org.bukkit.event.player.PlayerKickEvent.Cause cause = disconnectionDetails.disconnectionReason().orElseThrow().game().orElse(org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN);
++ org.bukkit.event.player.PlayerKickEvent event = new org.bukkit.event.player.PlayerKickEvent(
++ player.getBukkitEntity(),
++ io.papermc.paper.adventure.PaperAdventure.asAdventure(disconnectionDetails.reason()),
++ rawLeaveMessage, cause
+
-+ if (this.cserver.getServer().isRunning()) {
-+ this.cserver.getPluginManager().callEvent(event);
-+ }
+ );
+
-+ if (event.isCancelled()) {
-+ // Do not kick the player
-+ return;
-+ }
++ if (this.cserver.getServer().isRunning()) {
++ this.cserver.getPluginManager().callEvent(event);
++ }
+
-+ // Log kick to console *after* event was processed.
-+ switch (cause) {
-+ case FLYING_PLAYER -> LOGGER.warn("{} was kicked for floating too long!", this.player.getName().getString());
-+ case FLYING_VEHICLE -> LOGGER.warn("{} was kicked for floating a vehicle too long!", this.player.getName().getString());
++ if (event.isCancelled()) {
++ // Do not kick the player
++ return;
++ }
++
++ reason = io.papermc.paper.adventure.PaperAdventure.asVanilla(event.reason());
++ leaveMessage = io.papermc.paper.adventure.PaperAdventure.asVanilla(event.leaveMessage());
++ serverGamePacketListener.player.quitReason = org.bukkit.event.player.PlayerQuitEvent.QuitReason.KICKED; // Paper - Add API for quit reason
++ // Log kick to console *after* event was processed.
++ switch (cause) {
++ case FLYING_PLAYER -> LOGGER.warn("{} was kicked for floating too long!", player.getName().getString());
++ case FLYING_VEHICLE -> LOGGER.warn("{} was kicked for floating a vehicle too long!", player.getName().getString());
++ }
++ } else {
++ // TODO: Add event for config event
++ reason = disconnectionDetails.reason();
++ leaveMessage = null;
+ }
+
+ // Send the possibly modified leave message
-+ this.disconnect0(new DisconnectionDetails(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.reason()), disconnectionDetails.report(), disconnectionDetails.bugReportLink()), event.leaveMessage()); // Paper - Adventure & use kick event leave message
++ this.disconnect0(new DisconnectionDetails(reason, disconnectionDetails.report(), disconnectionDetails.bugReportLink(), java.util.Optional.ofNullable(leaveMessage), disconnectionDetails.disconnectionReason()));
+ }
+
-+ private void disconnect0(DisconnectionDetails disconnectionDetails, @Nullable net.kyori.adventure.text.Component leaveMessage) { // Paper - use kick event leave message
-+ // CraftBukkit end
-+ this.player.quitReason = org.bukkit.event.player.PlayerQuitEvent.QuitReason.KICKED; // Paper - Add API for quit reason
- this.connection
- .send(
- new ClientboundDisconnectPacket(disconnectionDetails.reason()),
- PacketSendListener.thenRun(() -> this.connection.disconnect(disconnectionDetails))
- );
-- this.connection.setReadOnly();
++ private void disconnect0(DisconnectionDetails disconnectionDetails) {
++ this.connection
++ .send(
++ new ClientboundDisconnectPacket(disconnectionDetails.reason()),
++ PacketSendListener.thenRun(() -> this.connection.disconnect(disconnectionDetails))
++ );
++ this.onDisconnect(disconnectionDetails);
+ this.connection.setReadOnly();
- this.server.executeBlocking(this.connection::handleDisconnection);
- }
-+ this.onDisconnect(disconnectionDetails, leaveMessage); // CraftBukkit - fire quit instantly // Paper - use kick event leave message
-+ this.connection.setReadOnly();
+ // CraftBukkit - Don't wait
+ this.server.scheduleOnMain(this.connection::handleDisconnection); // Paper
+ }
+
+ // Paper start - add proper async disconnect
-+ public void disconnectAsync(net.kyori.adventure.text.Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
-+ this.disconnectAsync(io.papermc.paper.adventure.PaperAdventure.asVanilla(reason), cause);
++ public final void disconnectAsync(Component component, io.papermc.paper.connection.DisconnectionReason reason) {
++ this.disconnectAsync(new DisconnectionDetails(component, java.util.Optional.empty(), java.util.Optional.empty(), java.util.Optional.empty(), java.util.Optional.of(reason)));
+ }
+
-+ public void disconnectAsync(Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
-+ this.disconnectAsync(new DisconnectionDetails(reason), cause);
++ public final void disconnectAsync(Component component) {
++ this.disconnectAsync(new DisconnectionDetails(component));
+ }
+
-+ public void disconnectAsync(DisconnectionDetails disconnectionInfo, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
-+ if (this.cserver.isPrimaryThread()) {
-+ this.disconnect(disconnectionInfo, cause);
-+ return;
-+ }
++ public abstract void disconnectAsync(DisconnectionDetails disconnectionInfo);
+
-+ this.connection.setReadOnly();
-+ this.server.scheduleOnMain(() -> {
-+ ServerCommonPacketListenerImpl.this.disconnect(disconnectionInfo, cause);
-+ if (ServerCommonPacketListenerImpl.this.player.quitReason == null) {
-+ // cancelled
-+ ServerCommonPacketListenerImpl.this.connection.enableAutoRead();
-+ }
-+ });
++ public abstract io.papermc.paper.connection.ReadablePlayerCookieConnectionImpl getCookieConnection();
++
++ public boolean isTransferred() {
++ return this.transferred;
+ }
+ // Paper end - add proper async disconnect
protected boolean isSingleplayerOwner() {
return this.server.isSingleplayerOwner(this.playerProfile());
+@@ -203,6 +_,6 @@
+ }
+
+ protected CommonListenerCookie createCookie(ClientInformation clientInformation) {
+- return new CommonListenerCookie(this.playerProfile(), this.latency, clientInformation, this.transferred);
++ return new CommonListenerCookie(this.playerProfile(), this.latency, clientInformation, this.transferred, this.playerBrand); // Paper
+ }
+ }
diff --git a/paper-server/patches/sources/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch b/paper-server/patches/sources/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch
index 12373ae87d..d14ee1bbae 100644
--- a/paper-server/patches/sources/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch
@@ -1,18 +1,19 @@
--- a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java
-@@ -48,8 +_,10 @@
+@@ -47,11 +_,13 @@
+ public ClientInformation clientInformation;
@Nullable
private SynchronizeRegistriesTask synchronizeRegistriesTask;
++ public io.papermc.paper.connection.PaperPlayerConfigurationConnection paperConnection; // Paper
-- public ServerConfigurationPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie) {
-- super(server, connection, cookie);
-+ // CraftBukkit start
-+ public ServerConfigurationPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie, ServerPlayer player) {
-+ super(server, connection, cookie, player);
-+ // CraftBukkit end
+ public ServerConfigurationPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie) {
+ super(server, connection, cookie);
this.gameProfile = cookie.gameProfile();
this.clientInformation = cookie.clientInformation();
++ this.paperConnection = new io.papermc.paper.connection.PaperPlayerConfigurationConnection(this); // Paper
}
+
+ @Override
@@ -61,6 +_,11 @@
@Override
@@ -25,20 +26,32 @@
LOGGER.info("{} lost connection: {}", this.gameProfile, details.reason().getString());
super.onDisconnect(details);
}
-@@ -73,6 +_,12 @@
+@@ -71,10 +_,15 @@
+ }
+
public void startConfiguration() {
++ new io.papermc.paper.event.connection.configuration.PlayerConnectionInitialConfigureEvent(this.paperConnection).callEvent(); // Paper
this.send(new ClientboundCustomPayloadPacket(new BrandPayload(this.server.getServerModName())));
ServerLinks serverLinks = this.server.serverLinks();
-+ // CraftBukkit start
-+ org.bukkit.craftbukkit.CraftServerLinks wrapper = new org.bukkit.craftbukkit.CraftServerLinks(serverLinks);
-+ org.bukkit.event.player.PlayerLinksSendEvent event = new org.bukkit.event.player.PlayerLinksSendEvent(this.player.getBukkitEntity(), wrapper);
-+ this.cserver.getPluginManager().callEvent(event);
-+ serverLinks = wrapper.getServerLinks();
-+ // CraftBukkit end
if (!serverLinks.isEmpty()) {
- this.send(new ClientboundServerLinksPacket(serverLinks.untrust()));
+- this.send(new ClientboundServerLinksPacket(serverLinks.untrust()));
++ // Paper start
++ org.bukkit.craftbukkit.CraftServerLinks links = new org.bukkit.craftbukkit.CraftServerLinks(serverLinks);
++ new org.bukkit.event.player.PlayerLinksSendEvent(this.paperConnection, links).callEvent();
++ this.send(new ClientboundServerLinksPacket(links.getServerLinks().untrust()));
++ // Paper end
}
-@@ -105,6 +_,7 @@
+
+ LayeredRegistryAccess layeredRegistryAccess = this.server.registries();
+@@ -87,6 +_,7 @@
+ this.synchronizeRegistriesTask = new SynchronizeRegistriesTask(list, layeredRegistryAccess);
+ this.configurationTasks.add(this.synchronizeRegistriesTask);
+ this.addOptionalTasks();
++ this.configurationTasks.add(new io.papermc.paper.connection.PaperConfigurationTask(this)); // Paper
+ this.configurationTasks.add(new JoinWorldTask());
+ this.startNextTask();
+ }
+@@ -105,12 +_,13 @@
@Override
public void handleClientInformation(ServerboundClientInformationPacket packet) {
this.clientInformation = packet.information();
@@ -46,20 +59,35 @@
}
@Override
-@@ -139,16 +_,22 @@
+ public void handleResourcePackResponse(ServerboundResourcePackPacket packet) {
+ super.handleResourcePackResponse(packet);
+- if (packet.action().isTerminal()) {
++ if (packet.action().isTerminal() && packet.id().equals(this.server.getServerResourcePack().map(MinecraftServer.ServerResourcePackInfo::id).orElse(null))) { // Paper - Ignore resource pack requests that are not vanilla
+ this.finishCurrentTask(ServerResourcePackConfigurationTask.TYPE);
+ }
+ }
+@@ -139,16 +_,30 @@
return;
}
- Component component = playerList.canPlayerLogin(this.connection.getRemoteAddress(), this.gameProfile);
-+ Component component = null; // CraftBukkit - login checks already completed
++ Component component = org.bukkit.craftbukkit.event.CraftEventFactory.handleLoginResult(playerList.canPlayerLogin(this.connection.getRemoteAddress(), this.gameProfile), this.paperConnection, this.connection, this.gameProfile, this.server, false); // Paper - Login event logic
if (component != null) {
this.disconnect(component);
return;
}
- ServerPlayer serverPlayer = new ServerPlayer(this.server, this.server.overworld(), this.gameProfile, this.clientInformation);
-+ ServerPlayer serverPlayer = this.player; // Paper
-+ this.player.updateOptions(this.clientInformation); // Paper - Of course, we reuse the player
++ // Paper start
++ ServerPlayer serverPlayer;
++ if (this.connection.savedPlayerForLoginEventLegacy != null) {
++ serverPlayer = this.connection.savedPlayerForLoginEventLegacy;
++ // WE have to do this because the player isnt updated properly
++ serverPlayer.updateOptionsNoEvents(this.clientInformation);
++ } else {
++ serverPlayer = new ServerPlayer(this.server, this.server.overworld(), this.gameProfile, this.clientInformation);
++ }
++ // Paper end
playerList.placeNewPlayer(this.connection, serverPlayer, this.createCookie(this.clientInformation));
} catch (Exception var5) {
LOGGER.error("Couldn't place player in world", (Throwable)var5);
@@ -71,3 +99,35 @@
this.connection.send(new ClientboundDisconnectPacket(DISCONNECT_REASON_INVALID_DATA));
this.connection.disconnect(DISCONNECT_REASON_INVALID_DATA);
}
+@@ -180,4 +_,31 @@
+ this.startNextTask();
+ }
+ }
++
++ // Paper start
++ @Override
++ public void disconnectAsync(final net.minecraft.network.DisconnectionDetails disconnectionInfo) {
++ if (this.cserver.isPrimaryThread()) {
++ this.disconnect(disconnectionInfo);
++ return;
++ }
++
++ this.connection.setReadOnly();
++ this.server.scheduleOnMain(() -> {
++ this.disconnect(disconnectionInfo); // Currently you cannot cancel disconnect during the config stage
++ });
++ }
++
++ @Override
++ public io.papermc.paper.connection.ReadablePlayerCookieConnectionImpl getCookieConnection() {
++ return this.paperConnection;
++ }
++
++ @Override
++ public void handleCustomPayload(net.minecraft.network.protocol.common.ServerboundCustomPayloadPacket packet) {
++ if (packet.payload() instanceof net.minecraft.network.protocol.common.custom.BrandPayload(String brand)) {
++ this.playerBrand = brand;
++ }
++ }
++ // Paper end
+ }
diff --git a/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch b/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch
index 70b6b96e54..cd9988cb2b 100644
--- a/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch
@@ -49,7 +49,7 @@
private double firstGoodX;
private double firstGoodY;
private double firstGoodZ;
-@@ -252,23 +_,42 @@
+@@ -252,14 +_,33 @@
private int receivedMovePacketCount;
private int knownMovePacketCount;
private boolean receivedMovementThisTick;
@@ -79,18 +79,18 @@
private boolean waitingForSwitchToConfig;
+ private static final int MAX_SIGN_LINE_LENGTH = Integer.getInteger("Paper.maxSignLength", 80); // Paper - Limit client sign length
+ private final io.papermc.paper.event.packet.ClientTickEndEvent tickEndEvent; // Paper - add client tick end event
++ public final io.papermc.paper.connection.PaperPlayerGameConnection playerGameConnection; // Paper
public ServerGamePacketListenerImpl(MinecraftServer server, Connection connection, ServerPlayer player, CommonListenerCookie cookie) {
-- super(server, connection, cookie);
-+ super(server, connection, cookie, player); // CraftBukkit
- this.chunkSender = new PlayerChunkSender(connection.isMemoryConnection());
- this.player = player;
+ super(server, connection, cookie);
+@@ -268,7 +_,9 @@
player.connection = this;
player.getTextFilter().join();
this.signedMessageDecoder = SignedMessageChain.Decoder.unsigned(player.getUUID(), server::enforceSecureProfile);
- this.chatMessageChain = new FutureChain(server);
+ this.chatMessageChain = new FutureChain(server.chatExecutor); // CraftBukkit - async chat
+ this.tickEndEvent = new io.papermc.paper.event.packet.ClientTickEndEvent(player.getBukkitEntity()); // Paper - add client tick end event
++ this.playerGameConnection = new io.papermc.paper.connection.PaperPlayerGameConnection(this); // Paper
}
@Override
@@ -1334,17 +1334,10 @@
return;
}
}
-@@ -1347,24 +_,54 @@
+@@ -1347,24 +_,50 @@
@Override
public void onDisconnect(DisconnectionDetails details) {
-+ // Paper start - Fix kick event leave message not being sent
-+ this.onDisconnect(details, null);
-+ }
-+
-+ @Override
-+ public void onDisconnect(DisconnectionDetails details, @Nullable net.kyori.adventure.text.Component quitMessage) {
-+ // Paper end - Fix kick event leave message not being sent
+ // CraftBukkit start - Rarely it would send a disconnect line twice
+ if (this.processedDisconnect) {
+ return;
@@ -1354,9 +1347,11 @@
+ // CraftBukkit end
LOGGER.info("{} lost connection: {}", this.player.getName().getString(), details.reason().getString());
- this.removePlayerFromWorld();
-- super.onDisconnect(details);
-+ this.removePlayerFromWorld(quitMessage); // Paper - Fix kick event leave message not being sent
-+ super.onDisconnect(details, quitMessage); // Paper - Fix kick event leave message not being sent
++ // Paper start - Fix kick event leave message not being sent
++ final net.kyori.adventure.text.Component quitMessage = io.papermc.paper.adventure.PaperAdventure.asAdventure(details.quitMessage().orElse(null));
++ this.removePlayerFromWorld(quitMessage);
++ // Paper end - Fix kick event leave message not being sent
+ super.onDisconnect(details);
}
private void removePlayerFromWorld() {
@@ -2566,16 +2561,22 @@
}
}
}
-@@ -2008,7 +_,7 @@
+@@ -2005,11 +_,13 @@
+ if (!this.waitingForSwitchToConfig) {
+ throw new IllegalStateException("Client acknowledged config, but none was requested");
+ } else {
++ final ServerConfigurationPacketListenerImpl listener = new ServerConfigurationPacketListenerImpl(this.server, this.connection, this.createCookie(this.player.clientInformation())); // Paper
this.connection
.setupInboundProtocol(
ConfigurationProtocols.SERVERBOUND,
- new ServerConfigurationPacketListenerImpl(this.server, this.connection, this.createCookie(this.player.clientInformation()))
-+ new ServerConfigurationPacketListenerImpl(this.server, this.connection, this.createCookie(this.player.clientInformation()), this.player) // CraftBukkit
++ listener // Paper
);
++ new io.papermc.paper.event.connection.configuration.PlayerConnectionReconfigureEvent(listener.paperConnection).callEvent(); // Paper
}
}
-@@ -2027,27 +_,32 @@
+
+@@ -2027,27 +_,27 @@
private void resetPlayerChatState(RemoteChatSession chatSession) {
this.chatSession = chatSession;
@@ -2595,11 +2596,10 @@
+ });
}
);
- }
-
- @Override
- public void handleCustomPayload(ServerboundCustomPayloadPacket packet) {
-+ super.handleCustomPayload(packet); // CraftBukkit - handled in super
+- }
+-
+- @Override
+- public void handleCustomPayload(ServerboundCustomPayloadPacket packet) {
}
@Override
@@ -2609,7 +2609,7 @@
if (!this.receivedMovementThisTick) {
this.player.setKnownMovement(Vec3.ZERO);
}
-@@ -2078,4 +_,29 @@
+@@ -2078,4 +_,157 @@
interface EntityInteraction {
InteractionResult run(ServerPlayer player, Entity entity, InteractionHand hand);
}
@@ -2638,4 +2638,132 @@
+ }
+ }
+ // Paper end - Implement click callbacks with custom click action
++
++ // Paper start - add utilities
++ public org.bukkit.craftbukkit.entity.CraftPlayer getCraftPlayer() {
++ return this.player == null ? null : this.player.getBukkitEntity();
++ }
++
++ @Override
++ public void disconnectAsync(final net.minecraft.network.DisconnectionDetails disconnectionInfo) {
++ if (this.cserver.isPrimaryThread()) {
++ this.disconnect(disconnectionInfo);
++ return;
++ }
++
++ this.connection.setReadOnly();
++ this.server.scheduleOnMain(() -> {
++ this.disconnect(disconnectionInfo);
++ if (this.player.quitReason == null) {
++ // cancelled
++ this.connection.enableAutoRead();
++ }
++ });
++ }
++
++ // Paper start
++ public static final ResourceLocation CUSTOM_REGISTER = ResourceLocation.withDefaultNamespace("register"); // CraftBukkit
++ private static final ResourceLocation CUSTOM_UNREGISTER = ResourceLocation.withDefaultNamespace("unregister"); // CraftBukkit
++
++ @Override
++ public void handleCustomPayload(ServerboundCustomPayloadPacket packet) {
++ // Paper start
++ if (packet.payload() instanceof net.minecraft.network.protocol.common.custom.BrandPayload(String brand)) {
++ this.playerBrand = brand;
++ }
++
++ if (!(packet.payload() instanceof final net.minecraft.network.protocol.common.custom.DiscardedPayload discardedPayload)) {
++ return;
++ }
++
++ PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
++
++ final net.minecraft.resources.ResourceLocation identifier = packet.payload().type().id();
++ final byte[] data = discardedPayload.data();
++ try {
++ final boolean registerChannel = CUSTOM_REGISTER.equals(identifier);
++ if (registerChannel || CUSTOM_UNREGISTER.equals(identifier)) {
++ // Strings separated by zeros instead of length prefixes
++ int startIndex = 0;
++ for (int i = 0; i < data.length; i++) {
++ final byte b = data[i];
++ if (b != 0) {
++ continue;
++ }
++
++ readChannelIdentifier(data, startIndex, i, registerChannel);
++ startIndex = i + 1;
++ }
++
++ // Read the last one
++ readChannelIdentifier(data, startIndex, data.length, registerChannel);
++ return;
++ }
++
++ if (identifier.equals(MINECRAFT_BRAND)) {
++ this.player.connection.playerBrand = new net.minecraft.network.FriendlyByteBuf(io.netty.buffer.Unpooled.wrappedBuffer(data)).readUtf(256);
++ }
++
++ this.cserver.getMessenger().dispatchIncomingMessage(this.player.getBukkitEntity(), identifier.toString(), data);
++ } catch (final Exception e) {
++ ServerGamePacketListenerImpl.LOGGER.error("Couldn't handle custom payload on channel {}", identifier, e);
++ this.disconnect(Component.literal("Invalid custom payload payload!"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PAYLOAD); // Paper - kick event cause
++ }
++ }
++
++ private void readChannelIdentifier(final byte[] data, final int from, final int to, final boolean register) {
++ final int length = to - from;
++ if (length == 0) {
++ return;
++ }
++
++ final String channel = new String(data, from, length, java.nio.charset.StandardCharsets.US_ASCII);
++ if (register) {
++ this.getCraftPlayer().addChannel(channel);
++ } else {
++ this.getCraftPlayer().removeChannel(channel);
++ }
++ }
++
++ public final boolean isDisconnected() {
++ return (!this.player.joining && !this.connection.isConnected()) || this.processedDisconnect; // Paper - Fix duplication bugs
++ }
++
++ @Override
++ public void handleResourcePackResponse(final net.minecraft.network.protocol.common.ServerboundResourcePackPacket packet) {
++ super.handleResourcePackResponse(packet);
++ // Paper start - store last pack status
++ org.bukkit.event.player.PlayerResourcePackStatusEvent.Status packStatus = org.bukkit.event.player.PlayerResourcePackStatusEvent.Status.values()[packet.action().ordinal()];
++ this.player.getBukkitEntity().resourcePackStatus = packStatus;
++ this.cserver.getPluginManager().callEvent(new org.bukkit.event.player.PlayerResourcePackStatusEvent(this.getCraftPlayer(), packet.id(), packStatus)); // CraftBukkit
++ // Paper end - store last pack status
++ }
++
++ // Paper start - adventure
++ public void disconnect(final net.kyori.adventure.text.Component reason) {
++ this.disconnect(reason, org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN);
++ }
++
++ public void disconnect(final net.kyori.adventure.text.Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
++ this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(reason), io.papermc.paper.connection.DisconnectionReason.game(cause));
++ }
++
++ public void disconnect(final Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
++ this.disconnect(reason, io.papermc.paper.connection.DisconnectionReason.game(cause));
++ }
++
++ public void disconnectAsync(net.kyori.adventure.text.Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
++ this.disconnectAsync(io.papermc.paper.adventure.PaperAdventure.asVanilla(reason), cause);
++ }
++
++ public void disconnectAsync(Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
++ this.disconnectAsync(reason, io.papermc.paper.connection.DisconnectionReason.game(cause));
++ }
++
++ @Override
++ public io.papermc.paper.connection.ReadablePlayerCookieConnectionImpl getCookieConnection() {
++ return this.playerGameConnection;
++ }
++
++ // Paper end
}
diff --git a/paper-server/patches/sources/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch b/paper-server/patches/sources/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch
index d85b10aa39..9a11f8e87d 100644
--- a/paper-server/patches/sources/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch
@@ -1,85 +1,72 @@
--- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
-@@ -44,9 +_,17 @@
+@@ -44,9 +_,16 @@
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
--public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, TickablePacketListener {
+// CraftBukkit start
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.craftbukkit.util.Waitable;
+import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
+import org.bukkit.event.player.PlayerPreLoginEvent;
+
-+public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, TickablePacketListener, CraftPlayer.TransferCookieConnection {
-+ // CraftBukkit end
+ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, TickablePacketListener {
private static final AtomicInteger UNIQUE_THREAD_ID = new AtomicInteger(0);
static final Logger LOGGER = LogUtils.getLogger();
+ private static final java.util.concurrent.ExecutorService authenticatorPool = java.util.concurrent.Executors.newCachedThreadPool(new com.google.common.util.concurrent.ThreadFactoryBuilder().setNameFormat("User Authenticator #%d").setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(LOGGER)).build()); // Paper - Cache authenticator threads
private static final int MAX_TICKS_BEFORE_LOGIN = 600;
private final byte[] challenge;
final MinecraftServer server;
-@@ -59,6 +_,9 @@
+@@ -54,21 +_,36 @@
+ public volatile ServerLoginPacketListenerImpl.State state = ServerLoginPacketListenerImpl.State.HELLO;
+ private int tick;
+ @Nullable
+- String requestedUsername;
++ public String requestedUsername; // Paper
+ @Nullable
public GameProfile authenticatedProfile;
private final String serverId = "";
- private final boolean transferred;
-+ private net.minecraft.server.level.ServerPlayer player; // CraftBukkit
+- private final boolean transferred;
++ public final boolean transferred; // Paper
+ public boolean iKnowThisMayNotBeTheBestIdeaButPleaseDisableUsernameValidation = false; // Paper - username validation overriding
+ private int velocityLoginMessageId = -1; // Paper - Add Velocity IP Forwarding Support
++ public @Nullable java.util.UUID requestedUuid; // Paper
++ private final io.papermc.paper.connection.PaperPlayerLoginConnection paperLoginConnection; // Paper - Config API
++ private volatile boolean disconnecting = false; // Paper - Fix disconnect still ticking login
public ServerLoginPacketListenerImpl(MinecraftServer server, Connection connection, boolean transferred) {
this.server = server;
-@@ -67,11 +_,48 @@
+ this.connection = connection;
+ this.challenge = Ints.toByteArray(RandomSource.create().nextInt());
this.transferred = transferred;
++ this.paperLoginConnection = new io.papermc.paper.connection.PaperPlayerLoginConnection(this); // Paper
}
-+ // CraftBukkit start
-+ @Override
-+ public boolean isTransferred() {
-+ return this.transferred;
-+ }
-+
-+ @Override
-+ public net.minecraft.network.ConnectionProtocol getProtocol() {
-+ return net.minecraft.network.ConnectionProtocol.LOGIN;
-+ }
-+
-+ @Override
-+ public void sendPacket(net.minecraft.network.protocol.Packet> packet) {
-+ this.connection.send(packet);
-+ }
-+
-+ @Override
-+ public void kickPlayer(Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) { // Paper - kick event causes - during login, no event can be called.
-+ this.disconnect(reason);
-+ }
-+ // CraftBukkit end
-+
@Override
public void tick() {
-+ // Paper start - Do not allow logins while the server is shutting down
-+ if (!this.server.isRunning()) {
-+ this.disconnect(org.bukkit.craftbukkit.util.CraftChatMessage.fromString(org.spigotmc.SpigotConfig.restartMessage)[0]);
++ if (this.disconnecting) return; // Paper - Fix disconnect being called multiple times due to not awaiting for disconnect
++ // Paper start - login cookie API
++ // Don't block the connection
++ if (this.paperLoginConnection.isAwaitingCookies()) {
++ this.tickTimeout(); // Ensure we tick timeout logic
+ return;
+ }
-+ // Paper end - Do not allow logins while the server is shutting down
++ // Paper end - login cookie API
+
if (this.state == ServerLoginPacketListenerImpl.State.VERIFYING) {
-+ if (this.connection.isConnected()) { // Paper - prevent logins to be processed even though disconnect was called
this.verifyLoginAndFinishConnectionSetup(Objects.requireNonNull(this.authenticatedProfile));
-- }
-+ } // Paper - prevent logins to be processed even though disconnect was called
-+ }
-+
-+ // CraftBukkit start
-+ if (this.state == ServerLoginPacketListenerImpl.State.WAITING_FOR_COOKIES && !this.player.getBukkitEntity().isAwaitingCookies()) {
-+ this.postCookies(this.authenticatedProfile);
-+ }
-+ // CraftBukkit end
+ }
+@@ -78,11 +_,23 @@
+ this.finishLoginAndWaitForClient(this.authenticatedProfile);
+ }
- if (this.state == ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT
- && !this.isPlayerAlreadyInWorld(Objects.requireNonNull(this.authenticatedProfile))) {
-@@ -83,6 +_,13 @@
++ // Paper start - login cookie API
++ this.tickTimeout();
++ }
++ public void tickTimeout() {
++ // Paper end - login cookie API
+ if (this.tick++ == 600) {
+ this.disconnect(Component.translatable("multiplayer.disconnect.slow_login"));
}
}
@@ -93,7 +80,15 @@
@Override
public boolean isAcceptingMessages() {
return this.connection.isConnected();
-@@ -115,7 +_,13 @@
+@@ -93,6 +_,7 @@
+ LOGGER.info("Disconnecting {}: {}", this.getUserName(), reason.getString());
+ this.connection.send(new ClientboundLoginDisconnectPacket(reason));
+ this.connection.disconnect(reason);
++ this.disconnecting = true; // Paper - Fix disconnect still ticking login
+ } catch (Exception var3) {
+ LOGGER.error("Error whilst disconnecting player", (Throwable)var3);
+ }
+@@ -115,7 +_,14 @@
@Override
public void handleHello(ServerboundHelloPacket packet) {
Validate.validState(this.state == ServerLoginPacketListenerImpl.State.HELLO, "Unexpected hello packet");
@@ -104,6 +99,7 @@
+ && !this.iKnowThisMayNotBeTheBestIdeaButPleaseDisableUsernameValidation) {
+ Validate.validState(StringUtil.isReasonablePlayerName(packet.name()), "Invalid characters in username");
+ }
++ this.requestedUuid = packet.profileId();
+ // Paper end - Validate usernames
this.requestedUsername = packet.name();
GameProfile singleplayerProfile = this.server.getSingleplayerProfile();
@@ -142,42 +138,15 @@
}
}
}
-@@ -137,9 +_,23 @@
+@@ -137,7 +_,7 @@
private void verifyLoginAndFinishConnectionSetup(GameProfile profile) {
PlayerList playerList = this.server.getPlayerList();
- Component component = playerList.canPlayerLogin(this.connection.getRemoteAddress(), profile);
-- if (component != null) {
-- this.disconnect(component);
-+ // CraftBukkit start - fire PlayerLoginEvent
-+ this.player = playerList.canPlayerLogin(this, profile); // CraftBukkit
-+ if (this.player != null) {
-+ if (this.player.getBukkitEntity().isAwaitingCookies()) {
-+ this.state = ServerLoginPacketListenerImpl.State.WAITING_FOR_COOKIES;
-+ } else {
-+ this.postCookies(profile);
-+ }
-+ }
-+ }
-+
-+ private void postCookies(GameProfile profile) {
-+ PlayerList playerList = this.server.getPlayerList();
-+
-+ if (this.player == null) {
-+ // this.disconnect(component);
-+ // CraftBukkit end
++ Component component = org.bukkit.craftbukkit.event.CraftEventFactory.handleLoginResult(playerList.canPlayerLogin(this.connection.getRemoteAddress(), profile), this.paperLoginConnection, this.connection, profile, this.server, true); // Paper
+ if (component != null) {
+ this.disconnect(component);
} else {
- if (this.server.getCompressionThreshold() >= 0 && !this.connection.isMemoryConnection()) {
- this.connection
-@@ -149,7 +_,7 @@
- );
- }
-
-- boolean flag = playerList.disconnectAllPlayersWithProfile(profile);
-+ boolean flag = playerList.disconnectAllPlayersWithProfile(profile, this.player); // CraftBukkit - add player reference
- if (flag) {
- this.state = ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT;
- } else {
@@ -184,7 +_,8 @@
throw new IllegalStateException("Protocol error", var7);
}
@@ -226,7 +195,7 @@
}
}
-@@ -222,24 +_,120 @@
+@@ -222,18 +_,113 @@
? ((InetSocketAddress)remoteAddress).getAddress()
: null;
}
@@ -254,7 +223,7 @@
+ // Paper start - Add more fields to AsyncPlayerPreLoginEvent
+ final InetAddress rawAddress = ((InetSocketAddress) this.connection.channel.remoteAddress()).getAddress();
+ com.destroystokyo.paper.profile.PlayerProfile profile = com.destroystokyo.paper.profile.CraftPlayerProfile.asBukkitMirror(gameprofile); // Paper - setPlayerProfileAPI
-+ AsyncPlayerPreLoginEvent asyncEvent = new AsyncPlayerPreLoginEvent(playerName, address, rawAddress, uniqueId, this.transferred, profile, this.connection.hostname);
++ AsyncPlayerPreLoginEvent asyncEvent = new AsyncPlayerPreLoginEvent(playerName, address, rawAddress, uniqueId, this.transferred, profile, this.connection.hostname, this.paperLoginConnection); // Paper
+ server.getPluginManager().callEvent(asyncEvent);
+ profile = asyncEvent.getPlayerProfile();
+ profile.complete(true); // Paper - setPlayerProfileAPI
@@ -344,24 +313,11 @@
Validate.validState(this.state == ServerLoginPacketListenerImpl.State.PROTOCOL_SWITCHING, "Unexpected login acknowledgement packet");
this.connection.setupOutboundProtocol(ConfigurationProtocols.CLIENTBOUND);
CommonListenerCookie commonListenerCookie = CommonListenerCookie.createInitial(Objects.requireNonNull(this.authenticatedProfile), this.transferred);
- ServerConfigurationPacketListenerImpl serverConfigurationPacketListenerImpl = new ServerConfigurationPacketListenerImpl(
-- this.server, this.connection, commonListenerCookie
-+ this.server, this.connection, commonListenerCookie, this.player // CraftBukkit
- );
-+
- this.connection.setupInboundProtocol(ConfigurationProtocols.SERVERBOUND, serverConfigurationPacketListenerImpl);
- serverConfigurationPacketListenerImpl.startConfiguration();
- this.state = ServerLoginPacketListenerImpl.State.ACCEPTED;
-@@ -252,8 +_,36 @@
+@@ -252,8 +_,31 @@
@Override
public void handleCookieResponse(ServerboundCookieResponsePacket packet) {
-+ // CraftBukkit start
-+ net.minecraft.network.protocol.PacketUtils.ensureRunningOnSameThread(packet, this, this.server);
-+ if (this.player != null && this.player.getBukkitEntity().handleCookieResponse(packet)) {
-+ return;
-+ }
-+ // CraftBukkit end
++ if (this.paperLoginConnection.handleCookieResponse(packet)) return; // Paper
this.disconnect(ServerCommonPacketListenerImpl.DISCONNECT_UNEXPECTED_QUERY);
}
+
@@ -389,11 +345,3 @@
public static enum State {
HELLO,
-@@ -261,6 +_,7 @@
- AUTHENTICATING,
- NEGOTIATING,
- VERIFYING,
-+ WAITING_FOR_COOKIES, // CraftBukkit
- WAITING_FOR_DUPE_DISCONNECT,
- PROTOCOL_SWITCHING,
- ACCEPTED;
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
index 35cfaca434..53789f75a2 100644
--- a/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch
+++ b/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch
@@ -339,7 +339,7 @@
}
@Override
-@@ -319,56 +_,156 @@
+@@ -319,56 +_,138 @@
}
protected void save(ServerPlayer player) {
@@ -446,6 +446,12 @@
- }
-
- this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID())));
+- }
+-
+- @Nullable
+- public Component canPlayerLogin(SocketAddress socketAddress, GameProfile gameProfile) {
+- if (this.bans.isBanned(gameProfile)) {
+- UserBanListEntry userBanListEntry = this.bans.get(gameProfile);
+ // CraftBukkit start
+ // this.stats.remove(uuid);
+ // this.advancements.remove(uuid);
@@ -468,140 +474,76 @@
+ 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);
-+ // CraftBukkit start - Whole method, SocketAddress to LoginListener, added hostname to signature, return EntityPlayer
-+ public @Nullable ServerPlayer canPlayerLogin(net.minecraft.server.network.ServerLoginPacketListenerImpl loginlistener, GameProfile gameProfile) {
-+ // if (this.bans.isBanned(gameProfile)) {
-+ // UserBanListEntry userBanListEntry = this.bans.get(gameProfile);
-+ // Moved from disconnectAllPlayersWithProfile
-+ UUID uuid = gameProfile.getId();
-+ List list = Lists.newArrayList();
++ }
+
-+ for (net.minecraft.server.level.ServerPlayer serverPlayer : this.players) {
-+ if (serverPlayer.getUUID().equals(uuid) || (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() && serverPlayer.getGameProfile().getName().equalsIgnoreCase(gameProfile.getName()))) { // Paper - validate usernames
-+ list.add(serverPlayer);
-+ }
++ // Paper start - PlayerLoginEvent
++ public record LoginResult(@Nullable Component message, @org.jetbrains.annotations.NotNull org.bukkit.event.player.PlayerLoginEvent.Result result) {
++ public static LoginResult ALLOW = new net.minecraft.server.players.PlayerList.LoginResult(null, org.bukkit.event.player.PlayerLoginEvent.Result.ALLOWED);
++
++ public boolean isAllowed() {
++ return this == ALLOW;
+ }
-+
-+ for (final net.minecraft.server.level.ServerPlayer serverPlayer : list) {
-+ this.save(serverPlayer); // CraftBukkit - Force the player's inventory to be saved
-+ serverPlayer.connection.disconnect(DUPLICATE_LOGIN_DISCONNECT_MESSAGE, 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, net.minecraft.server.level.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 end - PlayerLoginEvent
++ public LoginResult canPlayerLogin(SocketAddress socketAddress, GameProfile gameProfile) { // Paper - PlayerLoginEvent
++ LoginResult whitelistEventResult; // Paper
+ // Paper start - Fix MC-158900
+ UserBanListEntry userBanListEntry;
+ if (this.bans.isBanned(gameProfile) && (userBanListEntry = this.bans.get(gameProfile)) != null) {
-+ // Paper end - Fix MC-158900
++ // Paper end - Fix MC-158900
MutableComponent mutableComponent = Component.translatable("multiplayer.disconnect.banned.reason", userBanListEntry.getReason());
if (userBanListEntry.getExpires() != null) {
mutableComponent.append(
-@@ -376,10 +_,12 @@
+@@ -376,9 +_,11 @@
);
}
- 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
++ return new LoginResult(mutableComponent, org.bukkit.event.player.PlayerLoginEvent.Result.KICK_BANNED); // Paper - PlayerLoginEvent
++ // Paper start - whitelist event
++ } else if ((whitelistEventResult = this.isWhiteListedLogin(gameProfile)).result == org.bukkit.event.player.PlayerLoginEvent.Result.KICK_WHITELIST) {
++ return whitelistEventResult;
++ // Paper end
+ } else if (this.ipBans.isBanned(socketAddress)) {
IpBanListEntry ipBanListEntry = this.ipBans.get(socketAddress);
MutableComponent mutableComponent = Component.translatable("multiplayer.disconnect.banned_ip.reason", ipBanListEntry.getReason());
- if (ipBanListEntry.getExpires() != null) {
-@@ -388,65 +_,124 @@
+@@ -388,11 +_,9 @@
);
}
- return mutableComponent;
-+ // return mutableComponent;
-+ event.disallow(org.bukkit.event.player.PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.PaperAdventure.asAdventure(mutableComponent)); // Paper - Adventure
++ return new LoginResult(mutableComponent, org.bukkit.event.player.PlayerLoginEvent.Result.KICK_BANNED); // Paper - PlayerLoginEvent
} else {
- return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile)
- ? Component.translatable("multiplayer.disconnect.server_full")
- : null;
-- }
-- }
--
-- public boolean disconnectAllPlayersWithProfile(GameProfile gameProfile) {
-- UUID id = gameProfile.getId();
-- Set set = Sets.newIdentityHashSet();
--
-- for (ServerPlayer serverPlayer : this.players) {
++ return this.canBypassFullServerLogin(gameProfile, new LoginResult(Component.translatable("multiplayer.disconnect.server_full"), org.bukkit.event.player.PlayerLoginEvent.Result.KICK_FULL)); // Paper - PlayerServerFullCheckEvent
+ }
+ }
+
+@@ -401,7 +_,7 @@
+ 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
++ if (serverPlayer.getUUID().equals(id) || (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() && serverPlayer.getGameProfile().getName().equalsIgnoreCase(gameProfile.getName()))) { // Paper - validate usernames
+ set.add(serverPlayer);
}
}
--
-- ServerPlayer serverPlayer1 = this.playersByUUID.get(gameProfile.getId());
-- if (serverPlayer1 != null) {
-- set.add(serverPlayer1);
-- }
--
-- for (ServerPlayer serverPlayer2 : set) {
+@@ -412,41 +_,87 @@
+ }
+
+ for (ServerPlayer serverPlayer2 : set) {
- serverPlayer2.connection.disconnect(DUPLICATE_LOGIN_DISCONNECT_MESSAGE);
-- }
--
-- return !set.isEmpty();
-- }
--
++ serverPlayer2.connection.disconnect(DUPLICATE_LOGIN_DISCONNECT_MESSAGE, io.papermc.paper.connection.DisconnectionReason.DUPLICATE_LOGIN_MESSAGE); // Paper - disconnect API
+ }
+
+ 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;
-+ }
-+
-+ 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, @Nullable org.bukkit.event.player.PlayerRespawnEvent.RespawnReason eventReason) {
+ return this.respawn(player, keepInventory, reason, eventReason, null);
@@ -836,41 +778,57 @@
if (player.connection != null) {
byte b;
if (permLevel <= 0) {
-@@ -597,11 +_,33 @@
+@@ -597,9 +_,48 @@
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;
++ // Paper start - whitelist verify event / login event
++ public LoginResult canBypassFullServerLogin(final GameProfile profile, final LoginResult currentResult) {
++ final boolean shouldKick = this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(profile);
++ final io.papermc.paper.event.player.PlayerServerFullCheckEvent fullCheckEvent = new io.papermc.paper.event.player.PlayerServerFullCheckEvent(
++ com.destroystokyo.paper.profile.CraftPlayerProfile.asBukkitMirror(profile),
++ io.papermc.paper.adventure.PaperAdventure.asAdventure(currentResult.message),
++ shouldKick
++ );
++
++ fullCheckEvent.callEvent();
++ if (fullCheckEvent.isAllowed()) {
++ return net.minecraft.server.players.PlayerList.LoginResult.ALLOW;
++ } else {
++ return new net.minecraft.server.players.PlayerList.LoginResult(
++ io.papermc.paper.adventure.PaperAdventure.asVanilla(fullCheckEvent.kickMessage()), currentResult.result
++ );
++ }
++ }
++
++ public LoginResult isWhiteListedLogin(GameProfile profile) {
++ boolean isOp = this.ops.contains(profile);
++ boolean isWhitelisted = !this.doWhiteList || isOp || this.whitelist.contains(profile);
+
+ 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);
++ final com.destroystokyo.paper.event.profile.ProfileWhitelistVerifyEvent event
++ = new com.destroystokyo.paper.event.profile.ProfileWhitelistVerifyEvent(com.destroystokyo.paper.profile.CraftPlayerProfile.asBukkitMirror(profile), 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 new net.minecraft.server.players.PlayerList.LoginResult(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.kickMessage() == null ? configuredMessage : event.kickMessage()), org.bukkit.event.player.PlayerLoginEvent.Result.KICK_WHITELIST);
+ }
-+ return true;
-+ // Paper end - ProfileWhitelistVerifyEvent
++
++ return net.minecraft.server.players.PlayerList.LoginResult.ALLOW;
++ }
++ // Paper end
++
++ @io.papermc.paper.annotation.DoNotUse // Paper
+ public boolean isWhiteListed(GameProfile profile) {
+ return !this.doWhiteList || this.ops.contains(profile) || this.whitelist.contains(profile);
}
-
- public boolean isOp(GameProfile profile) {
@@ -612,21 +_,17 @@
@Nullable
diff --git a/paper-server/src/main/java/io/papermc/paper/connection/DisconnectionReason.java b/paper-server/src/main/java/io/papermc/paper/connection/DisconnectionReason.java
new file mode 100644
index 0000000000..64fac9b463
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/connection/DisconnectionReason.java
@@ -0,0 +1,28 @@
+package io.papermc.paper.connection;
+
+import java.util.Optional;
+import org.bukkit.event.player.PlayerKickEvent;
+
+// This class is used for when there may be a want for a common disconnect event for the configuration stage.
+public interface DisconnectionReason {
+
+ DisconnectionReason UNKNOWN = game(PlayerKickEvent.Cause.UNKNOWN);
+ DisconnectionReason TIMEOUT = game(PlayerKickEvent.Cause.TIMEOUT);
+ DisconnectionReason RESOURCE_PACK_REJECTION = game(PlayerKickEvent.Cause.RESOURCE_PACK_REJECTION);
+ DisconnectionReason INVALID_COOKIE = game(PlayerKickEvent.Cause.INVALID_COOKIE);
+ DisconnectionReason DUPLICATE_LOGIN_MESSAGE = game(PlayerKickEvent.Cause.DUPLICATE_LOGIN);
+
+ Optional game();
+
+ static DisconnectionReason game(PlayerKickEvent.Cause cause) {
+ return new GameEntry(cause);
+ }
+
+ record GameEntry(PlayerKickEvent.Cause gameQuitEvent) implements DisconnectionReason {
+
+ @Override
+ public Optional game() {
+ return Optional.ofNullable(this.gameQuitEvent);
+ }
+ }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/connection/HorriblePlayerLoginEventHack.java b/paper-server/src/main/java/io/papermc/paper/connection/HorriblePlayerLoginEventHack.java
new file mode 100644
index 0000000000..1e39d21a63
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/connection/HorriblePlayerLoginEventHack.java
@@ -0,0 +1,99 @@
+package io.papermc.paper.connection;
+
+import com.mojang.authlib.GameProfile;
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.adventure.PaperAdventure;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import io.papermc.paper.util.StackWalkerUtil;
+import net.minecraft.network.Connection;
+import net.minecraft.network.chat.Component;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ClientInformation;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.players.PlayerList;
+import net.minecraft.world.level.Level;
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.event.player.PlayerLoginEvent;
+import org.bukkit.plugin.RegisteredListener;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+
+// TODO: DO we even need this? Who mutates the player here? it does nothing anyways.
+// TODO: Make sure player settings are migrated too
+@NullMarked
+public class HorriblePlayerLoginEventHack {
+
+ private static final Logger LOGGER = LogUtils.getClassLogger();
+ private static boolean nagged = false;
+
+ public static void warnReenterConfiguration() {
+ LOGGER.warn("""
+
+ ============================================================
+ WARNING: {} Attempted to use PlayerGameConnection#reenterConfiguration()
+
+ This method currently requires that all plugins installed on the server
+ are not listening to the PlayerLoginEvent.
+
+ Please look in your logs for the Plugins listening to this event.
+ ============================================================""", StackWalkerUtil.getFirstPluginCaller().getName());
+ }
+
+ public static @Nullable Component execute(final Connection connection, MinecraftServer server, GameProfile profile, PlayerList.LoginResult result) {
+ if (PlayerLoginEvent.getHandlerList().getRegisteredListeners().length == 0) {
+ return result.message();
+ }
+
+ if (!nagged) {
+ List plugins = new ArrayList<>();
+ for (final RegisteredListener listener : PlayerLoginEvent.getHandlerList().getRegisteredListeners()) {
+ plugins.add(listener.getPlugin().getName());
+ }
+
+ LOGGER.warn("""
+
+ ============================================================
+ WARNING: Legacy PlayerLoginEvent usage detected!
+
+ This event forces an alternative player loading path that is
+ deprecated and will be removed in a future release.
+ For more information, see: https://forums.papermc.io/threads/1-21-7.1635
+
+ Please notify the following plugin developers: {}
+ ============================================================""", plugins.toString());
+ nagged = true;
+ }
+ // We need to account for the fact that during the config stage we now call this event to mimic the old behavior.
+ // However, we want to make sure that we still properly hold the same player reference
+ ServerPlayer player;
+ if (connection.savedPlayerForLoginEventLegacy != null) {
+ player = connection.savedPlayerForLoginEventLegacy;
+ } else {
+ ServerPlayer serverPlayer = new ServerPlayer(server, server.getLevel(Level.OVERWORLD), profile, ClientInformation.createDefault());
+ connection.savedPlayerForLoginEventLegacy = serverPlayer;
+ player = serverPlayer;
+ }
+
+ CraftPlayer horribleBukkitPlayer = player.getBukkitEntity();
+ PlayerLoginEvent event = new PlayerLoginEvent(horribleBukkitPlayer, connection.hostname, ((java.net.InetSocketAddress) connection.getRemoteAddress()).getAddress(), ((java.net.InetSocketAddress) connection.channel.remoteAddress()).getAddress());
+ event.disallow(result.result(), PaperAdventure.asAdventure(result.message()));
+ event.callEvent();
+
+ Component finalResult;
+ if (event.getResult() == PlayerLoginEvent.Result.ALLOWED) {
+ finalResult = null;
+ } else {
+ finalResult = PaperAdventure.asVanilla(event.kickMessage());
+ }
+
+ // If the event changed the result, save this result. We need to save this for config
+ if (event.getResult() != result.result()) {
+ connection.legacySavedLoginEventResultOverride = Optional.ofNullable(finalResult);
+ }
+
+ return finalResult;
+ }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/connection/PaperCommonConnection.java b/paper-server/src/main/java/io/papermc/paper/connection/PaperCommonConnection.java
new file mode 100644
index 0000000000..8fc168da1d
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/connection/PaperCommonConnection.java
@@ -0,0 +1,114 @@
+package io.papermc.paper.connection;
+
+import com.destroystokyo.paper.ClientOption;
+import com.destroystokyo.paper.PaperSkinParts;
+import com.google.common.base.Preconditions;
+import io.papermc.paper.adventure.PaperAdventure;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.Map;
+import net.kyori.adventure.text.Component;
+import net.minecraft.network.protocol.common.ClientboundCustomReportDetailsPacket;
+import net.minecraft.network.protocol.common.ClientboundServerLinksPacket;
+import net.minecraft.network.protocol.common.ClientboundStoreCookiePacket;
+import net.minecraft.network.protocol.common.ClientboundTransferPacket;
+import net.minecraft.server.level.ClientInformation;
+import net.minecraft.server.network.ServerCommonPacketListenerImpl;
+import org.bukkit.NamespacedKey;
+import org.bukkit.ServerLinks;
+import org.bukkit.craftbukkit.CraftServerLinks;
+import org.bukkit.craftbukkit.util.CraftNamespacedKey;
+import org.jspecify.annotations.Nullable;
+
+public abstract class PaperCommonConnection extends ReadablePlayerCookieConnectionImpl implements PlayerCommonConnection {
+
+ protected final T handle;
+
+ public PaperCommonConnection(final T serverConfigurationPacketListenerImpl) {
+ super(serverConfigurationPacketListenerImpl.connection);
+ this.handle = serverConfigurationPacketListenerImpl;
+ }
+
+ @Override
+ public void sendReportDetails(final Map details) {
+ this.handle.send(new ClientboundCustomReportDetailsPacket(details));
+ }
+
+ @Override
+ public void sendLinks(final ServerLinks links) {
+ this.handle.send(new ClientboundServerLinksPacket(((CraftServerLinks) links).getServerLinks().untrust()));
+ }
+
+ @Override
+ public void transfer(final String host, final int port) {
+ this.handle.send(new ClientboundTransferPacket(host, port));
+ }
+
+ @Override
+ public T getClientOption(ClientOption type) {
+ ClientInformation information = this.getClientInformation();
+
+ if (ClientOption.SKIN_PARTS == type) {
+ return type.getType().cast(new PaperSkinParts(information.modelCustomisation()));
+ } else if (ClientOption.CHAT_COLORS_ENABLED == type) {
+ return type.getType().cast(information.chatColors());
+ } else if (ClientOption.CHAT_VISIBILITY == type) {
+ return type.getType().cast(ClientOption.ChatVisibility.valueOf(information.chatVisibility().name()));
+ } else if (ClientOption.LOCALE == type) {
+ return type.getType().cast(information.language());
+ } else if (ClientOption.MAIN_HAND == type) {
+ return type.getType().cast(information.mainHand());
+ } else if (ClientOption.VIEW_DISTANCE == type) {
+ return type.getType().cast(information.viewDistance());
+ } else if (ClientOption.TEXT_FILTERING_ENABLED == type) {
+ return type.getType().cast(information.textFilteringEnabled());
+ } else if (ClientOption.ALLOW_SERVER_LISTINGS == type) {
+ return type.getType().cast(information.allowsListing());
+ } else if (ClientOption.PARTICLE_VISIBILITY == type) {
+ return type.getType().cast(ClientOption.ParticleVisibility.valueOf(information.particleStatus().name()));
+ }
+ throw new RuntimeException("Unknown settings type");
+ }
+
+ @Override
+ public void disconnect(final Component component) {
+ this.handle.disconnect(PaperAdventure.asVanilla(component), DisconnectionReason.UNKNOWN);
+ }
+
+ @Override
+ public boolean isTransferred() {
+ return this.handle.isTransferred();
+ }
+
+ @Override
+ public SocketAddress getAddress() {
+ return this.handle.connection.getRemoteAddress();
+ }
+
+ @Override
+ public InetSocketAddress getClientAddress() {
+ return (InetSocketAddress) this.handle.connection.channel.remoteAddress();
+ }
+
+ @Override
+ public @Nullable InetSocketAddress getVirtualHost() {
+ return this.handle.connection.virtualHost;
+ }
+
+ @Override
+ public @Nullable InetSocketAddress getHAProxyAddress() {
+ return this.handle.connection.haProxyAddress instanceof final InetSocketAddress inetSocketAddress ? inetSocketAddress : null;
+ }
+
+ @Override
+ public void storeCookie(final NamespacedKey key, final byte[] value) {
+ Preconditions.checkArgument(key != null, "Cookie key cannot be null");
+ Preconditions.checkArgument(value != null, "Cookie value cannot be null");
+ Preconditions.checkArgument(value.length <= 5120, "Cookie value too large, must be smaller than 5120 bytes");
+ Preconditions.checkState(this.canStoreCookie(), "Can only store cookie in CONFIGURATION or PLAY protocol.");
+
+ this.handle.send(new ClientboundStoreCookiePacket(CraftNamespacedKey.toMinecraft(key), value));
+ }
+
+ public abstract ClientInformation getClientInformation();
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/connection/PaperConfigurationTask.java b/paper-server/src/main/java/io/papermc/paper/connection/PaperConfigurationTask.java
new file mode 100644
index 0000000000..439ae39cc4
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/connection/PaperConfigurationTask.java
@@ -0,0 +1,46 @@
+package io.papermc.paper.connection;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.event.connection.configuration.AsyncPlayerConnectionConfigureEvent;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Consumer;
+import net.minecraft.DefaultUncaughtExceptionHandler;
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.server.network.ConfigurationTask;
+import net.minecraft.server.network.ServerConfigurationPacketListenerImpl;
+import org.slf4j.Logger;
+
+public class PaperConfigurationTask implements ConfigurationTask {
+ private static final Logger LOGGER = LogUtils.getClassLogger();
+
+ private static final ExecutorService CONFIGURATION_POOL = Executors.newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat("Configuration Thread #%d")
+ .setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(LOGGER)).build());
+
+ public static final ConfigurationTask.Type TYPE = new ConfigurationTask.Type("paper_event_handling");
+
+ private final ServerConfigurationPacketListenerImpl packetListener;
+
+ public PaperConfigurationTask(ServerConfigurationPacketListenerImpl packetListener) {
+ this.packetListener = packetListener;
+ }
+
+ @Override
+ public void start(final Consumer> task) {
+ if (AsyncPlayerConnectionConfigureEvent.getHandlerList().getRegisteredListeners().length == 0) {
+ this.packetListener.finishCurrentTask(TYPE);
+ return;
+ }
+ CONFIGURATION_POOL.execute(() -> {
+ AsyncPlayerConnectionConfigureEvent event = new AsyncPlayerConnectionConfigureEvent(this.packetListener.paperConnection);
+ event.callEvent();
+ this.packetListener.finishCurrentTask(TYPE);
+ });
+ }
+
+ @Override
+ public Type type() {
+ return TYPE;
+ }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/connection/PaperPlayerConfigurationConnection.java b/paper-server/src/main/java/io/papermc/paper/connection/PaperPlayerConfigurationConnection.java
new file mode 100644
index 0000000000..b71fbf68de
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/connection/PaperPlayerConfigurationConnection.java
@@ -0,0 +1,104 @@
+package io.papermc.paper.connection;
+
+import com.destroystokyo.paper.ClientOption;
+import com.destroystokyo.paper.PaperSkinParts;
+import com.destroystokyo.paper.profile.CraftPlayerProfile;
+import com.destroystokyo.paper.profile.PlayerProfile;
+import io.papermc.paper.adventure.PaperAdventure;
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.identity.Identity;
+import net.kyori.adventure.pointer.Pointers;
+import net.kyori.adventure.resource.ResourcePackCallback;
+import net.kyori.adventure.resource.ResourcePackInfo;
+import net.kyori.adventure.resource.ResourcePackRequest;
+import net.minecraft.network.protocol.common.ClientboundResourcePackPopPacket;
+import net.minecraft.network.protocol.common.ClientboundResourcePackPushPacket;
+import net.minecraft.network.protocol.configuration.ClientboundResetChatPacket;
+import net.minecraft.server.level.ClientInformation;
+import net.minecraft.server.network.ConfigurationTask;
+import net.minecraft.server.network.ServerConfigurationPacketListenerImpl;
+import org.jspecify.annotations.Nullable;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public class PaperPlayerConfigurationConnection extends PaperCommonConnection implements PlayerConfigurationConnection, Audience {
+
+ private @Nullable Pointers adventurePointers;
+
+ public PaperPlayerConfigurationConnection(final ServerConfigurationPacketListenerImpl packetListener) {
+ super(packetListener);
+ }
+
+ @Override
+ public ClientInformation getClientInformation() {
+ return this.handle.clientInformation;
+ }
+
+ @Override
+ public void sendResourcePacks(ResourcePackRequest request) {
+ final List packs = new java.util.ArrayList<>(request.packs().size());
+ if (request.replace()) {
+ this.clearResourcePacks();
+ }
+ final net.minecraft.network.chat.Component prompt = PaperAdventure.asVanilla(request.prompt());
+ for (final java.util.Iterator iter = request.packs().iterator(); iter.hasNext(); ) {
+ final ResourcePackInfo pack = iter.next();
+ packs.add(new ClientboundResourcePackPushPacket(pack.id(), pack.uri().toASCIIString(), pack.hash(), request.required(), iter.hasNext() ? Optional.empty() : Optional.ofNullable(prompt)));
+ if (request.callback() != ResourcePackCallback.noOp()) {
+ this.handle.packCallbacks.put(pack.id(), request.callback()); // just override if there is a previously existing callback
+ }
+ }
+ packs.forEach(this.handle::send);
+ }
+
+ @Override
+ public void removeResourcePacks(UUID id, UUID... others) {
+ net.kyori.adventure.util.MonkeyBars.nonEmptyArrayToList(pack -> new ClientboundResourcePackPopPacket(Optional.of(pack)), id, others).forEach(this.handle::send);
+ }
+
+ @Override
+ public void clearResourcePacks() {
+ this.handle.send(new ClientboundResourcePackPopPacket(Optional.empty()));
+ }
+
+ @Override
+ public Pointers pointers() {
+ if (this.adventurePointers == null) {
+ this.adventurePointers = Pointers.builder()
+ .withDynamic(Identity.NAME, () -> this.handle.getOwner().getName())
+ .withDynamic(Identity.UUID, () -> this.handle.getOwner().getId())
+ .build();
+ }
+
+ return this.adventurePointers;
+ }
+
+ @Override
+ public Audience getAudience() {
+ return this;
+ }
+
+ @Override
+ public PlayerProfile getProfile() {
+ return CraftPlayerProfile.asBukkitCopy(this.handle.getOwner());
+ }
+
+ @Override
+ public void clearChat() {
+ this.handle.send(ClientboundResetChatPacket.INSTANCE);
+ }
+
+ @Override
+ public void completeReconfiguration() {
+ ConfigurationTask task = this.handle.currentTask;
+ if (task != null) {
+ // This means that the player is going through the normal configuration process, or is already returning to the game phase.
+ // Be safe and just ignore, as many plugins may call this.
+ return;
+ }
+
+ this.handle.returnToWorld();
+ }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/connection/PaperPlayerGameConnection.java b/paper-server/src/main/java/io/papermc/paper/connection/PaperPlayerGameConnection.java
new file mode 100644
index 0000000000..452d7229b1
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/connection/PaperPlayerGameConnection.java
@@ -0,0 +1,31 @@
+package io.papermc.paper.connection;
+
+import net.minecraft.server.level.ClientInformation;
+import net.minecraft.server.network.ServerGamePacketListenerImpl;
+import org.bukkit.entity.Player;
+
+public class PaperPlayerGameConnection extends PaperCommonConnection implements PlayerGameConnection {
+
+ public PaperPlayerGameConnection(final ServerGamePacketListenerImpl serverConfigurationPacketListenerImpl) {
+ super(serverConfigurationPacketListenerImpl);
+ }
+
+ @Override
+ public ClientInformation getClientInformation() {
+ return this.handle.player.clientInformation();
+ }
+
+ @Override
+ public void reenterConfiguration() {
+ if (this.handle.connection.savedPlayerForLoginEventLegacy != null) {
+ HorriblePlayerLoginEventHack.warnReenterConfiguration();
+ return;
+ }
+ this.handle.switchToConfig();
+ }
+
+ @Override
+ public Player getPlayer() {
+ return this.handle.getCraftPlayer();
+ }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/connection/PaperPlayerLoginConnection.java b/paper-server/src/main/java/io/papermc/paper/connection/PaperPlayerLoginConnection.java
new file mode 100644
index 0000000000..7269a08f77
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/connection/PaperPlayerLoginConnection.java
@@ -0,0 +1,64 @@
+package io.papermc.paper.connection;
+
+import com.destroystokyo.paper.profile.CraftPlayerProfile;
+import com.destroystokyo.paper.profile.PlayerProfile;
+import io.papermc.paper.adventure.PaperAdventure;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import net.kyori.adventure.text.Component;
+import net.minecraft.server.network.ServerLoginPacketListenerImpl;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+@NullMarked
+public class PaperPlayerLoginConnection extends ReadablePlayerCookieConnectionImpl implements PlayerLoginConnection {
+
+ private final ServerLoginPacketListenerImpl packetListener;
+
+ public PaperPlayerLoginConnection(final ServerLoginPacketListenerImpl packetListener) {
+ super(packetListener.connection);
+ this.packetListener = packetListener;
+ }
+
+ @Override
+ public @Nullable PlayerProfile getAuthenticatedProfile() {
+ return this.packetListener.authenticatedProfile == null ? null : CraftPlayerProfile.asBukkitCopy(this.packetListener.authenticatedProfile);
+ }
+
+ @Override
+ public @Nullable PlayerProfile getUnsafeProfile() {
+ // This can possibly error as requestedUsername can have wacky stuff. But its fine I doubt mojang will support this
+ // much longer.
+ return new CraftPlayerProfile(this.packetListener.requestedUuid, this.packetListener.requestedUsername);
+ }
+
+ @Override
+ public SocketAddress getAddress() {
+ return this.packetListener.connection.getRemoteAddress();
+ }
+
+ @Override
+ public InetSocketAddress getClientAddress() {
+ return (InetSocketAddress) this.packetListener.connection.channel.remoteAddress();
+ }
+
+ @Override
+ public @org.jspecify.annotations.Nullable InetSocketAddress getVirtualHost() {
+ return this.packetListener.connection.virtualHost;
+ }
+
+ @Override
+ public @org.jspecify.annotations.Nullable InetSocketAddress getHAProxyAddress() {
+ return this.packetListener.connection.haProxyAddress instanceof final InetSocketAddress inetSocketAddress ? inetSocketAddress : null;
+ }
+
+ @Override
+ public boolean isTransferred() {
+ return this.packetListener.transferred;
+ }
+
+ @Override
+ public void disconnect(final Component component) {
+ this.packetListener.disconnect(PaperAdventure.asVanilla(component));
+ }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/connection/ReadablePlayerCookieConnectionImpl.java b/paper-server/src/main/java/io/papermc/paper/connection/ReadablePlayerCookieConnectionImpl.java
new file mode 100644
index 0000000000..a537efef8a
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/connection/ReadablePlayerCookieConnectionImpl.java
@@ -0,0 +1,60 @@
+package io.papermc.paper.connection;
+
+import com.google.common.base.Preconditions;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import net.minecraft.network.Connection;
+import net.minecraft.network.protocol.cookie.ClientboundCookieRequestPacket;
+import net.minecraft.network.protocol.cookie.ServerboundCookieResponsePacket;
+import net.minecraft.resources.ResourceLocation;
+import org.bukkit.NamespacedKey;
+import org.bukkit.craftbukkit.util.CraftNamespacedKey;
+import org.jspecify.annotations.NullMarked;
+
+@NullMarked
+public abstract class ReadablePlayerCookieConnectionImpl implements ReadablePlayerCookieConnection {
+
+ // Because we support async cookies, order is not promised.
+ private final Map requestedCookies = new ConcurrentHashMap<>();
+ private final Connection connection;
+
+ public ReadablePlayerCookieConnectionImpl(final Connection connection) {
+ this.connection = connection;
+ }
+
+ @Override
+ public CompletableFuture retrieveCookie(final NamespacedKey key) {
+ Preconditions.checkArgument(key != null, "Cookie key cannot be null");
+
+ CompletableFuture future = new CompletableFuture<>();
+ ResourceLocation resourceLocation = CraftNamespacedKey.toMinecraft(key);
+ this.requestedCookies.put(resourceLocation, new CookieFuture(resourceLocation, future));
+
+ this.connection.send(new ClientboundCookieRequestPacket(resourceLocation));
+
+ return future;
+ }
+
+ public boolean canStoreCookie() {
+ return true;
+ }
+
+ public boolean handleCookieResponse(ServerboundCookieResponsePacket packet) {
+ CookieFuture future = this.requestedCookies.get(packet.key());
+ if (future != null) {
+ future.future().complete(packet.payload());
+ this.requestedCookies.remove(packet.key());
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean isAwaitingCookies() {
+ return !this.requestedCookies.isEmpty();
+ }
+
+ public record CookieFuture(ResourceLocation key, CompletableFuture future) {
+ }
+}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
index 3e18819ae2..3ea6d207b2 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
@@ -9,6 +9,7 @@ import com.mojang.authlib.GameProfile;
import com.mojang.datafixers.util.Pair;
import io.papermc.paper.FeatureHooks;
import io.papermc.paper.configuration.GlobalConfiguration;
+import io.papermc.paper.connection.PlayerGameConnection;
import io.papermc.paper.entity.LookAnchor;
import io.papermc.paper.entity.PaperPlayerGiveResult;
import io.papermc.paper.entity.PlayerGiveResult;
@@ -331,75 +332,24 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
return this.getHandle().connection.connection.haProxyAddress instanceof final InetSocketAddress inetSocketAddress ? inetSocketAddress : null;
}
// Paper end - Add API to get player's proxy address
-
- public interface TransferCookieConnection {
-
- boolean isTransferred();
-
- ConnectionProtocol getProtocol();
-
- void sendPacket(Packet> packet);
-
- void kickPlayer(Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause); // Paper - kick event causes
- }
-
- public record CookieFuture(ResourceLocation key, CompletableFuture future) {
-
- }
- private final Queue requestedCookies = new LinkedList<>();
-
- public boolean isAwaitingCookies() {
- return !this.requestedCookies.isEmpty();
- }
-
- public boolean handleCookieResponse(ServerboundCookieResponsePacket response) {
- CookieFuture future = this.requestedCookies.peek();
- if (future != null) {
- if (future.key.equals(response.key())) {
- Preconditions.checkState(future == this.requestedCookies.poll(), "requestedCookies queue mismatch");
-
- future.future().complete(response.payload());
- return true;
- }
- }
-
- return false;
- }
-
@Override
public boolean isTransferred() {
- return this.getHandle().transferCookieConnection.isTransferred();
+ return this.getHandle().connection.playerGameConnection.isTransferred();
}
@Override
public CompletableFuture retrieveCookie(final NamespacedKey key) {
- Preconditions.checkArgument(key != null, "Cookie key cannot be null");
-
- CompletableFuture future = new CompletableFuture<>();
- ResourceLocation nms = CraftNamespacedKey.toMinecraft(key);
- this.requestedCookies.add(new CookieFuture(nms, future));
-
- this.getHandle().transferCookieConnection.sendPacket(new ClientboundCookieRequestPacket(nms));
-
- return future;
+ return this.getHandle().connection.playerGameConnection.retrieveCookie(key);
}
@Override
public void storeCookie(NamespacedKey key, byte[] value) {
- Preconditions.checkArgument(key != null, "Cookie key cannot be null");
- Preconditions.checkArgument(value != null, "Cookie value cannot be null");
- Preconditions.checkArgument(value.length <= 5120, "Cookie value too large, must be smaller than 5120 bytes");
- Preconditions.checkState(this.getHandle().transferCookieConnection.getProtocol() == ConnectionProtocol.CONFIGURATION || this.getHandle().transferCookieConnection.getProtocol() == ConnectionProtocol.PLAY, "Can only store cookie in CONFIGURATION or PLAY protocol.");
-
- this.getHandle().transferCookieConnection.sendPacket(new ClientboundStoreCookiePacket(CraftNamespacedKey.toMinecraft(key), value));
+ this.getHandle().connection.playerGameConnection.storeCookie(key, value);
}
@Override
public void transfer(String host, int port) {
- Preconditions.checkArgument(host != null, "Host cannot be null");
- Preconditions.checkState(this.getHandle().transferCookieConnection.getProtocol() == ConnectionProtocol.CONFIGURATION || this.getHandle().transferCookieConnection.getProtocol() == ConnectionProtocol.PLAY, "Can only transfer in CONFIGURATION or PLAY protocol.");
-
- this.getHandle().transferCookieConnection.sendPacket(new ClientboundTransferPacket(host, port));
+ this.getHandle().connection.playerGameConnection.transfer(host, port);
}
// Paper start - Implement NetworkClient
@@ -699,7 +649,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
@Override
public void kickPlayer(String message) {
org.spigotmc.AsyncCatcher.catchOp("player kick"); // Spigot
- this.getHandle().transferCookieConnection.kickPlayer(CraftChatMessage.fromStringOrEmpty(message, true), org.bukkit.event.player.PlayerKickEvent.Cause.PLUGIN); // Paper - kick event cause
+ this.getHandle().connection.disconnect(CraftChatMessage.fromStringOrEmpty(message, true), org.bukkit.event.player.PlayerKickEvent.Cause.PLUGIN); // Paper - kick event cause
}
private static final net.kyori.adventure.text.Component DEFAULT_KICK_COMPONENT = net.kyori.adventure.text.Component.translatable("multiplayer.disconnect.kicked");
@@ -3389,7 +3339,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
// Paper start - brand support
@Override
public String getClientBrandName() {
- return getHandle().clientBrandName;
+ return getHandle().connection.playerBrand;
}
// Paper end
@@ -3569,4 +3519,9 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
public void setDeathScreenScore(final int score) {
getHandle().setScore(score);
}
+
+ @Override
+ public PlayerGameConnection getConnection() {
+ return this.getHandle().connection.playerGameConnection;
+ }
}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
index c5daefa565..1d11251009 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
@@ -4,6 +4,7 @@ import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
+import com.mojang.authlib.GameProfile;
import com.mojang.datafixers.util.Either;
import java.util.ArrayList;
import java.util.Collections;
@@ -13,12 +14,20 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import io.papermc.paper.adventure.PaperAdventure;
+import io.papermc.paper.connection.HorriblePlayerLoginEventHack;
+import io.papermc.paper.connection.PlayerConnection;
+import io.papermc.paper.event.connection.PlayerConnectionValidateLoginEvent;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
+import net.minecraft.network.Connection;
+import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.ServerboundContainerClosePacket;
import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.players.PlayerList;
import net.minecraft.tags.DamageTypeTags;
import net.minecraft.util.Unit;
import net.minecraft.world.Container;
@@ -225,6 +234,7 @@ import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerItemBreakEvent;
import org.bukkit.event.player.PlayerItemMendEvent;
import org.bukkit.event.player.PlayerLevelChangeEvent;
+import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerRecipeBookClickEvent;
import org.bukkit.event.player.PlayerRecipeBookSettingsChangeEvent;
import org.bukkit.event.player.PlayerRecipeDiscoverEvent;
@@ -2085,4 +2095,27 @@ public class CraftEventFactory {
return event;
}
+
+ @SuppressWarnings("OptionalAssignedToNull")
+ public static Component handleLoginResult(PlayerList.LoginResult result, PlayerConnection paperConnection, Connection connection, GameProfile profile, MinecraftServer server, boolean loginPhase) {
+ PlayerConnectionValidateLoginEvent event = new PlayerConnectionValidateLoginEvent(
+ paperConnection, result.isAllowed() ? null : PaperAdventure.asAdventure(result.message())
+ );
+ event.callEvent();
+
+ Component disconnectReason = PaperAdventure.asVanilla(event.getKickMessage());
+
+ // For the login event it normally was never fired during configuration phase. In order to make this deprecation less
+ // breaky we will cache result and use it next time.
+ if (loginPhase) {
+ disconnectReason = HorriblePlayerLoginEventHack.execute(connection, server, profile,
+ disconnectReason == null ? PlayerList.LoginResult.ALLOW : new PlayerList.LoginResult(disconnectReason, disconnectReason == null ? PlayerLoginEvent.Result.KICK_OTHER : result.result())
+ );
+ } else if (connection.legacySavedLoginEventResultOverride != null) {
+ // If the override is set, use it.
+ disconnectReason = connection.legacySavedLoginEventResultOverride.orElse(null);
+ }
+
+ return disconnectReason;
+ }
}