From 0640e356a649e38044bb71daeb05edbf0d131de5 Mon Sep 17 00:00:00 2001 From: VeVeVeVel <147647046+VeVeVeVel@users.noreply.github.com> Date: Sun, 14 Jun 2026 21:11:53 +0900 Subject: [PATCH] Paper: Disconnect State Fixes --- .../0336-Paper-Disconnect-State-Fixes.patch | 283 ++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 leaf-server/minecraft-patches/features/0336-Paper-Disconnect-State-Fixes.patch diff --git a/leaf-server/minecraft-patches/features/0336-Paper-Disconnect-State-Fixes.patch b/leaf-server/minecraft-patches/features/0336-Paper-Disconnect-State-Fixes.patch new file mode 100644 index 0000000000..5c05fb258b --- /dev/null +++ b/leaf-server/minecraft-patches/features/0336-Paper-Disconnect-State-Fixes.patch @@ -0,0 +1,283 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> +Date: Sun, 14 Jun 2026 20:49:01 +0900 +Subject: [PATCH] Paper: Disconnect State Fixes + +Original license: GPL-3.0-only +Original project: https://github.com/PaperMC/Paper +Paper commit: https://github.com/PaperMC/Paper/commit/8021488ef48a4aae5246cd3d2f97b8573bffdad9 + +Backported from Paper 26.1.2 + +diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java +index 4e16552680f86cf1589f862cb38c0f43df344e6a..c33bd4338d7164acd566c6fbcd33ddbad44f334a 100644 +--- a/net/minecraft/network/Connection.java ++++ b/net/minecraft/network/Connection.java +@@ -90,6 +90,7 @@ public class Connection extends SimpleChannelInboundHandler> { + // Paper start - NetworkClient implementation + public int protocolVersion; + public java.net.InetSocketAddress virtualHost; ++ public boolean handleConnectionDisconnectOnNextTick = false; // Paper - Force kill connection ticking + private static boolean enableExplicitFlush = Boolean.getBoolean("paper.explicit-flush"); // Paper - Disable explicit network manager flushing + // Paper end + // Paper start - add utility methods +@@ -521,7 +522,8 @@ public class Connection extends SimpleChannelInboundHandler> { + } + } catch (final Exception e) { + LOGGER.error("NetworkException: {}", player, e); +- this.disconnect(Component.translatable("disconnect.genericReason", "Internal Exception: " + e.getMessage())); ++ Component reason = Component.translatable("disconnect.genericReason", "Internal Exception: " + e.getMessage()); ++ this.send(new ClientboundDisconnectPacket(reason), PacketSendListener.thenRun(() -> this.disconnect(reason))); + packet.onPacketDispatchFinish(player, null); + } + // Paper end - Optimize network +@@ -672,7 +674,7 @@ public class Connection extends SimpleChannelInboundHandler> { + } + + if (this.isConnected()) { +- this.channel.close(); // We can't wait as this may be called from an event loop. ++ this.channel.close().awaitUninterruptibly(); + this.disconnectionDetails = disconnectionDetails; + } + } +diff --git a/net/minecraft/network/PacketProcessor.java b/net/minecraft/network/PacketProcessor.java +index d501168e40f951ee37efcbc464c3ab1307051060..eda0baf2c7736a1fe2694a72677c407f606b6b92 100644 +--- a/net/minecraft/network/PacketProcessor.java ++++ b/net/minecraft/network/PacketProcessor.java +@@ -93,7 +93,6 @@ public class PacketProcessor implements AutoCloseable { + public void handle() { + packetProcessing.push(this.listener); // Paper - detailed watchdog information + try { // Paper - detailed watchdog information +- if (this.listener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl serverCommonPacketListener && serverCommonPacketListener.processedDisconnect) return; // Paper - Don't handle sync packets for kicked players + if (this.listener.shouldHandleMessage(this.packet)) { + try { + this.packet.handle(this.listener); +diff --git a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java +index 59a5c6bd8da82c293bba739fba43dc12d28fa8a4..160e86ff0462c847a36e9e32b7c87483a53cff37 100644 +--- a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java +@@ -50,7 +50,6 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + private volatile boolean suspendFlushingOnServerThread = false; + // CraftBukkit start + public 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 + // Gale start - Purpur - send multiple keep-alive packets +@@ -145,20 +144,14 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + if (ka.challengeId() == packet.getId()) { + itr.remove(); + +- if (!this.processedDisconnect) { +- LOGGER.info("Disconnecting {} for sending keepalive response ({}) out-of-order!", this.playerProfile().name(), packet.getId()); +- this.disconnectAsync(TIMEOUT_DISCONNECTION_MESSAGE, io.papermc.paper.connection.DisconnectionReason.TIMEOUT); +- return; +- } +- break; ++ LOGGER.info("Disconnecting {} for sending keepalive response ({}) out-of-order!", this.playerProfile().name(), packet.getId()); ++ this.disconnectAsync(TIMEOUT_DISCONNECTION_MESSAGE, io.papermc.paper.connection.DisconnectionReason.TIMEOUT); ++ return; + } + } + +- if (!this.processedDisconnect) { +- LOGGER.info("Disconnecting {} for sending keepalive response ({}) without matching challenge!", this.playerProfile().name(), packet.getId()); +- this.disconnectAsync(TIMEOUT_DISCONNECTION_MESSAGE, io.papermc.paper.connection.DisconnectionReason.TIMEOUT); +- return; +- } ++ LOGGER.info("Disconnecting {} for sending keepalive response ({}) without matching challenge!", this.playerProfile().name(), packet.getId()); ++ this.disconnectAsync(TIMEOUT_DISCONNECTION_MESSAGE, io.papermc.paper.connection.DisconnectionReason.TIMEOUT); + // Paper end - improve keepalives + } + +@@ -314,11 +307,11 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + // Paper start - improve keepalives + // Gale start - Purpur - send multiple keep-alive packets + if (org.galemc.gale.configuration.GaleGlobalConfiguration.get().misc.keepalive.sendMultiple) { +- if (this.checkIfClosed(millis) && !this.processedDisconnect) { ++ if (this.checkIfClosed(millis)) { + long currTime = System.nanoTime(); + if ((currTime - this.keepAlive.lastKeepAliveTx) >= java.util.concurrent.TimeUnit.SECONDS.toNanos(1L)) { // 1 second + this.keepAlive.lastKeepAliveTx = currTime; +- if (this.keepAlivePending && !this.processedDisconnect && keepAlives.size() * 1000L >= KEEPALIVE_LIMIT) { ++ if (this.keepAlivePending && keepAlives.size() * 1000L >= KEEPALIVE_LIMIT) { + LOGGER.info("{} was kicked due to keepalive timeout!", this.playerProfile().name()); + this.disconnect(TIMEOUT_DISCONNECTION_MESSAGE, io.papermc.paper.connection.DisconnectionReason.TIMEOUT); + } else if (this.checkIfClosed(millis)) { +@@ -330,7 +323,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + } + } else { + // Gale end - Purpur - send multiple keep-alive packets +- if (this.checkIfClosed(millis) && !this.processedDisconnect) { ++ if (this.checkIfClosed(millis)) { + long currTime = System.nanoTime(); + + if ((currTime - this.keepAlive.lastKeepAliveTx) >= java.util.concurrent.TimeUnit.SECONDS.toNanos(1L)) { +@@ -400,7 +393,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + } + // Leaf end - Mirai - Configurable chat message signatures + // CraftBukkit start +- if (packet == null || this.processedDisconnect) { // Spigot ++ if (packet == null) { + return; + } 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.respawnData().pos(), serverGamePacketListener.getPlayer().level()); +@@ -435,7 +428,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + + public void disconnect(DisconnectionDetails disconnectionDetails) { + // CraftBukkit start - fire PlayerKickEvent +- if (this.processedDisconnect) { ++ if (!this.connection.isConnected()) { + return; + } + if (!this.cserver.isPrimaryThread()) { +@@ -506,10 +499,8 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + new ClientboundDisconnectPacket(disconnectionDetails.reason()), + PacketSendListener.thenRun(() -> this.connection.disconnect(disconnectionDetails)) + ); +- this.onDisconnect(disconnectionDetails); + this.connection.setReadOnly(); +- // CraftBukkit - Don't wait +- this.server.scheduleOnMain(this.connection::handleDisconnection); // Paper ++ this.connection.handleConnectionDisconnectOnNextTick = true; // Paper - Force kill connection ticking. Let this close the connection + } + + // Paper start - add proper async disconnect +diff --git a/net/minecraft/server/network/ServerConnectionListener.java b/net/minecraft/server/network/ServerConnectionListener.java +index 33e1cdb00d648c992af03a976899253141314b27..4aaaea148b16e3ea19691163ba0194c71d1fee11 100644 +--- a/net/minecraft/server/network/ServerConnectionListener.java ++++ b/net/minecraft/server/network/ServerConnectionListener.java +@@ -226,6 +226,13 @@ public class ServerConnectionListener { + Connection connection = iterator.next(); + if (!connection.isConnecting()) { + if (connection.isConnected()) { ++ // Paper start - Force kill connection ticking ++ // also call this multiple times, doesnt matter ++ // See MC-307764 ++ if (connection.handleConnectionDisconnectOnNextTick) { ++ continue; ++ } ++ // Paper end - Force kill connection ticking + try { + connection.tick(); + } catch (Exception var7) { +diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index c0c0244b13c88676e2d8c27b53f52ebc52b03f81..e57a0742289181e717121be4d49210fa85123f0b 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -360,7 +360,6 @@ public class ServerGamePacketListenerImpl + + @Override + public void tick() { +- if (this.isDisconnected()) return; // Paper + if (this.ackBlockChangesUpTo > -1) { + this.send(new ClientboundBlockChangedAckPacket(this.ackBlockChangesUpTo)); + this.ackBlockChangesUpTo = -1; +@@ -2319,13 +2318,6 @@ public class ServerGamePacketListenerImpl + + @Override + public void onDisconnect(DisconnectionDetails details) { +- // CraftBukkit start - Rarely it would send a disconnect line twice +- if (this.processedDisconnect) { +- return; +- } else { +- this.processedDisconnect = true; +- } +- // CraftBukkit end + LOGGER.info("{} lost connection: {}", this.player.getPlainTextName(), details.reason().getString()); + // Paper start - Fix kick event leave message not being sent + final net.kyori.adventure.text.Component quitMessage = details.quitMessage().map(io.papermc.paper.adventure.PaperAdventure::asAdventure).orElse(null); +@@ -3915,7 +3907,7 @@ public class ServerGamePacketListenerImpl + } + + public final boolean isDisconnected() { +- return (!this.player.joining && !this.connection.isConnected()) || this.processedDisconnect; // Paper - Fix duplication bugs ++ return (!this.player.joining && !this.connection.isConnected()); + } + + @Override +diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +index 875fb1360350c3b182758fa64d9ddecd6db4b3c3..bb4c722000f6e2d5b834e72b844bc79445f6ee1d 100644 +--- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +@@ -71,7 +71,6 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + private int velocityLoginMessageId = -1; // Paper - Add Velocity IP Forwarding Support + public java.util.@Nullable 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; +@@ -84,7 +83,6 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + + @Override + public void tick() { +- 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()) { +@@ -111,7 +109,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + int slowLoginTimeout = org.galemc.gale.configuration.GaleGlobalConfiguration.get().misc.premiumAccountSlowLoginTimeout; + if (this.tick++ >= (slowLoginTimeout < 1 ? MAX_TICKS_BEFORE_LOGIN : slowLoginTimeout)) { + // Gale end - make slow login timeout configurable +- this.disconnect(Component.translatable("multiplayer.disconnect.slow_login")); ++ this.disconnectAsync(Component.translatable("multiplayer.disconnect.slow_login")); // Paper + } + } + +@@ -127,6 +125,26 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + return this.connection.isConnected(); + } + ++ // Paper start ++ public void disconnectAsync(Component reason) { ++ try { ++ // Gale start - Pufferfish - do not log disconnections with null id ++ if (!org.galemc.gale.configuration.GaleGlobalConfiguration.get().logToConsole.nullIdDisconnections && this.authenticatedProfile != null && this.authenticatedProfile.id() == null) { ++ var reasonString = reason.getString(); ++ ++ if ("Disconnected".equals(reasonString) || Component.translatable("multiplayer.disconnect.generic").getString().equals(reasonString)) { ++ return; ++ } ++ } ++ // Gale end - Pufferfish - do not log disconnections with null id ++ LOGGER.info("Disconnecting {}: {}", this.getUserName(), reason.getString()); ++ this.connection.send(new ClientboundLoginDisconnectPacket(reason), PacketSendListener.thenRun(() -> this.connection.disconnect(reason))); ++ this.connection.handleConnectionDisconnectOnNextTick = true; ++ } catch (Exception var3) { ++ LOGGER.error("Error whilst disconnecting player", (Throwable)var3); ++ } ++ } ++ // Paper end + public void disconnect(Component reason) { + try { + // Gale start - Pufferfish - do not log disconnections with null id +@@ -141,7 +159,6 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + 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); + } +@@ -241,7 +258,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + PlayerList playerList = this.server.getPlayerList(); + Component component = org.bukkit.craftbukkit.event.CraftEventFactory.handleLoginResult(playerList.canPlayerLogin(this.connection.getRemoteAddress(), new NameAndId(profile)), this.paperLoginConnection, this.connection, profile, this.server, true); // Paper + if (component != null) { +- this.disconnect(component); ++ this.disconnectAsync(component); // Paper + } else { + if (this.server.getCompressionThreshold() >= 0 && !this.connection.isMemoryConnection()) { + this.connection +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index 284940e73feb577c28848fa5cce2c541e3c5d02b..ccf2a7d2fbff4bfc9b6e4606059b4b2863071a08 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -623,7 +623,6 @@ public abstract class PlayerList { + this.cserver.getPluginManager().callEvent(playerQuitEvent); + player.getBukkitEntity().disconnect(); + +- if (this.server.isSameThread()) player.doTick(); // SPIGOT-924 // Paper - Improved watchdog support; don't tick during emergency shutdowns + // CraftBukkit end + + // Paper start - Configurable player collision; Remove from collideRule team if needed