--- a/net/minecraft/network/Connection.java
+++ b/net/minecraft/network/Connection.java
@@ -45,6 +45,7 @@
 import net.minecraft.SharedConstants;
 import net.minecraft.Util;
 import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.MutableComponent;
 import net.minecraft.network.protocol.BundlerInfo;
 import net.minecraft.network.protocol.Packet;
 import net.minecraft.network.protocol.PacketFlow;
@@ -67,27 +68,39 @@
 import org.slf4j.MarkerFactory;
 
 public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
+
     private static final float AVERAGE_PACKETS_SMOOTHING = 0.75F;
     private static final Logger LOGGER = LogUtils.getLogger();
     public static final Marker ROOT_MARKER = MarkerFactory.getMarker("NETWORK");
-    public static final Marker PACKET_MARKER = Util.make(MarkerFactory.getMarker("NETWORK_PACKETS"), marker -> marker.add(ROOT_MARKER));
-    public static final Marker PACKET_RECEIVED_MARKER = Util.make(MarkerFactory.getMarker("PACKET_RECEIVED"), marker -> marker.add(PACKET_MARKER));
-    public static final Marker PACKET_SENT_MARKER = Util.make(MarkerFactory.getMarker("PACKET_SENT"), marker -> marker.add(PACKET_MARKER));
-    public static final Supplier<NioEventLoopGroup> NETWORK_WORKER_GROUP = Suppliers.memoize(
-        () -> new NioEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Client IO #%d").setDaemon(true).build())
-    );
-    public static final Supplier<EpollEventLoopGroup> NETWORK_EPOLL_WORKER_GROUP = Suppliers.memoize(
-        () -> new EpollEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Epoll Client IO #%d").setDaemon(true).build())
-    );
-    public static final Supplier<DefaultEventLoopGroup> LOCAL_WORKER_GROUP = Suppliers.memoize(
-        () -> new DefaultEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Netty Local Client IO #%d").setDaemon(true).build())
-    );
+    public static final Marker PACKET_MARKER = (Marker) Util.make(MarkerFactory.getMarker("NETWORK_PACKETS"), (marker) -> {
+        marker.add(Connection.ROOT_MARKER);
+    });
+    public static final Marker PACKET_RECEIVED_MARKER = (Marker) Util.make(MarkerFactory.getMarker("PACKET_RECEIVED"), (marker) -> {
+        marker.add(Connection.PACKET_MARKER);
+    });
+    public static final Marker PACKET_SENT_MARKER = (Marker) Util.make(MarkerFactory.getMarker("PACKET_SENT"), (marker) -> {
+        marker.add(Connection.PACKET_MARKER);
+    });
+    public static final Supplier<NioEventLoopGroup> NETWORK_WORKER_GROUP = Suppliers.memoize(() -> {
+        return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
+    });
+    public static final Supplier<EpollEventLoopGroup> NETWORK_EPOLL_WORKER_GROUP = Suppliers.memoize(() -> {
+        return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
+    });
+    public static final Supplier<DefaultEventLoopGroup> LOCAL_WORKER_GROUP = Suppliers.memoize(() -> {
+        return new DefaultEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Local Client IO #%d").setDaemon(true).setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(LOGGER)).build()); // Paper
+    });
     private static final ProtocolInfo<ServerHandshakePacketListener> INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND;
     private final PacketFlow receiving;
     private volatile boolean sendLoginDisconnect = true;
-    private final Queue<Consumer<Connection>> pendingActions = Queues.newConcurrentLinkedQueue();
+    private final Queue<WrappedConsumer> pendingActions = Queues.newConcurrentLinkedQueue(); // Paper
     public Channel channel;
     public SocketAddress address;
+    // Spigot Start
+    public java.util.UUID spoofedUUID;
+    public com.mojang.authlib.properties.Property[] spoofedProfile;
+    public boolean preparing = true;
+    // Spigot End
     @Nullable
     private volatile PacketListener disconnectListener;
     @Nullable
@@ -106,93 +119,218 @@
     private volatile Component delayedDisconnect;
     @Nullable
     BandwidthDebugMonitor bandwidthDebugMonitor;
+    public String hostname = ""; // CraftBukkit - add field
+    // Paper start - NetworkClient implementation
+    public int protocolVersion;
+    public java.net.InetSocketAddress virtualHost;
+    private static boolean enableExplicitFlush = Boolean.getBoolean("paper.explicit-flush"); // Paper - Disable explicit network manager flushing
+    // Paper end
+    // Paper start - Optimize network
+    public boolean isPending = true;
+    public boolean queueImmunity;
+    // Paper end - Optimize network
+
+    // Paper start - add utility methods
+    public final net.minecraft.server.level.ServerPlayer getPlayer() {
+        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;
+    }
+    // Paper end - add utility methods
+    // Paper start - packet limiter
+    protected final Object PACKET_LIMIT_LOCK = new Object();
+    protected final @Nullable io.papermc.paper.util.IntervalledCounter allPacketCounts = io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.allPackets.isEnabled() ? new io.papermc.paper.util.IntervalledCounter(
+        (long)(io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.allPackets.interval() * 1.0e9)
+    ) : null;
+    protected final java.util.Map<Class<? extends net.minecraft.network.protocol.Packet<?>>, io.papermc.paper.util.IntervalledCounter> packetSpecificLimits = new java.util.HashMap<>();
+
+    private boolean stopReadingPackets;
+    private void killForPacketSpam() {
+        this.sendPacket(new ClientboundDisconnectPacket(io.papermc.paper.adventure.PaperAdventure.asVanilla(io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.kickMessage)), PacketSendListener.thenRun(() -> {
+            this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.kickMessage));
+        }), true);
+        this.setReadOnly();
+        this.stopReadingPackets = true;
+    }
+    // Paper end - packet limiter
+    @Nullable public SocketAddress haProxyAddress; // Paper - Add API to get player's proxy address
 
     public Connection(PacketFlow side) {
         this.receiving = side;
     }
 
-    public void channelActive(ChannelHandlerContext channelHandlerContext) throws Exception {
-        super.channelActive(channelHandlerContext);
-        this.channel = channelHandlerContext.channel();
+    public void channelActive(ChannelHandlerContext channelhandlercontext) throws Exception {
+        super.channelActive(channelhandlercontext);
+        this.channel = channelhandlercontext.channel();
         this.address = this.channel.remoteAddress();
+        // Spigot Start
+        this.preparing = false;
+        // Spigot End
         if (this.delayedDisconnect != null) {
             this.disconnect(this.delayedDisconnect);
         }
+
     }
 
-    public void channelInactive(ChannelHandlerContext channelHandlerContext) {
+    public void channelInactive(ChannelHandlerContext channelhandlercontext) {
         this.disconnect(Component.translatable("disconnect.endOfStream"));
     }
 
-    public void exceptionCaught(ChannelHandlerContext channelHandlerContext, Throwable throwable) {
+    public void exceptionCaught(ChannelHandlerContext channelhandlercontext, Throwable throwable) {
+        // Paper start - Handle large packets disconnecting client
+        if (throwable instanceof io.netty.handler.codec.EncoderException && throwable.getCause() instanceof PacketEncoder.PacketTooLargeException packetTooLargeException) {
+            final Packet<?> packet = packetTooLargeException.getPacket();
+            if (packet.packetTooLarge(this)) {
+                ProtocolSwapHandler.handleOutboundTerminalPacket(channelhandlercontext, packet);
+                return;
+            } else if (packet.isSkippable()) {
+                Connection.LOGGER.debug("Skipping packet due to errors", throwable.getCause());
+                ProtocolSwapHandler.handleOutboundTerminalPacket(channelhandlercontext, packet);
+                return;
+            } else {
+                throwable = throwable.getCause();
+            }
+        }
+        // Paper end - Handle large packets disconnecting client
         if (throwable instanceof SkipPacketException) {
-            LOGGER.debug("Skipping packet due to errors", throwable.getCause());
+            Connection.LOGGER.debug("Skipping packet due to errors", throwable.getCause());
         } else {
-            boolean bl = !this.handlingFault;
+            boolean flag = !this.handlingFault;
+
             this.handlingFault = true;
             if (this.channel.isOpen()) {
+                net.minecraft.server.level.ServerPlayer player = this.getPlayer(); // Paper - Add API for quit reason
                 if (throwable instanceof TimeoutException) {
-                    LOGGER.debug("Timeout", throwable);
+                    Connection.LOGGER.debug("Timeout", throwable);
+                    if (player != null) player.quitReason = org.bukkit.event.player.PlayerQuitEvent.QuitReason.TIMED_OUT; // Paper - Add API for quit reason
                     this.disconnect(Component.translatable("disconnect.timeout"));
                 } else {
-                    Component component = Component.translatable("disconnect.genericReason", "Internal Exception: " + throwable);
-                    if (bl) {
-                        LOGGER.debug("Failed to sent packet", throwable);
-                        if (this.getSending() == PacketFlow.CLIENTBOUND) {
-                            Packet<?> packet = (Packet<?>)(this.sendLoginDisconnect
-                                ? new ClientboundLoginDisconnectPacket(component)
-                                : new ClientboundDisconnectPacket(component));
-                            this.send(packet, PacketSendListener.thenRun(() -> this.disconnect(component)));
+                    MutableComponent ichatmutablecomponent = Component.translatable("disconnect.genericReason", "Internal Exception: " + String.valueOf(throwable));
+
+                    if (player != null) player.quitReason = org.bukkit.event.player.PlayerQuitEvent.QuitReason.ERRONEOUS_STATE; // Paper - Add API for quit reason
+                    if (flag) {
+                        Connection.LOGGER.debug("Failed to sent packet", throwable);
+                        boolean doesDisconnectExist = this.packetListener.protocol() != ConnectionProtocol.STATUS && this.packetListener.protocol() != ConnectionProtocol.HANDSHAKING; // Paper
+                        if (this.getSending() == PacketFlow.CLIENTBOUND && doesDisconnectExist) { // Paper
+                            Packet<?> packet = this.sendLoginDisconnect ? new ClientboundLoginDisconnectPacket(ichatmutablecomponent) : new ClientboundDisconnectPacket(ichatmutablecomponent);
+
+                            this.send((Packet) packet, PacketSendListener.thenRun(() -> {
+                                this.disconnect(ichatmutablecomponent);
+                            }));
                         } else {
-                            this.disconnect(component);
+                            this.disconnect(ichatmutablecomponent);
                         }
 
                         this.setReadOnly();
                     } else {
-                        LOGGER.debug("Double fault", throwable);
-                        this.disconnect(component);
+                        Connection.LOGGER.debug("Double fault", throwable);
+                        this.disconnect(ichatmutablecomponent);
                     }
                 }
+
             }
         }
+        if (net.minecraft.server.MinecraftServer.getServer().isDebugging()) io.papermc.paper.util.TraceUtil.printStackTrace(throwable); // Spigot // Paper
     }
 
-    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Packet<?> packet) {
+    protected void channelRead0(ChannelHandlerContext channelhandlercontext, Packet<?> packet) {
         if (this.channel.isOpen()) {
-            PacketListener packetListener = this.packetListener;
-            if (packetListener == null) {
+            PacketListener packetlistener = this.packetListener;
+
+            if (packetlistener == null) {
                 throw new IllegalStateException("Received a packet before the packet listener was initialized");
             } else {
-                if (packetListener.shouldHandleMessage(packet)) {
+                // Paper start - packet limiter
+                if (this.stopReadingPackets) {
+                    return;
+                }
+                if (this.allPacketCounts != null ||
+                    io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.overrides.containsKey(packet.getClass())) {
+                    long time = System.nanoTime();
+                    synchronized (PACKET_LIMIT_LOCK) {
+                        if (this.allPacketCounts != null) {
+                            this.allPacketCounts.updateAndAdd(1, time);
+                            if (this.allPacketCounts.getRate() >= io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.allPackets.maxPacketRate()) {
+                                this.killForPacketSpam();
+                                return;
+                            }
+                        }
+
+                        for (Class<?> check = packet.getClass(); check != Object.class; check = check.getSuperclass()) {
+                            io.papermc.paper.configuration.GlobalConfiguration.PacketLimiter.PacketLimit packetSpecificLimit =
+                                io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.overrides.get(check);
+                            if (packetSpecificLimit == null || !packetSpecificLimit.isEnabled()) {
+                                continue;
+                            }
+                            io.papermc.paper.util.IntervalledCounter counter = this.packetSpecificLimits.computeIfAbsent((Class)check, (clazz) -> {
+                                return new io.papermc.paper.util.IntervalledCounter((long)(packetSpecificLimit.interval() * 1.0e9));
+                            });
+                            counter.updateAndAdd(1, time);
+                            if (counter.getRate() >= packetSpecificLimit.maxPacketRate()) {
+                                switch (packetSpecificLimit.action()) {
+                                    case DROP:
+                                        return;
+                                    case KICK:
+                                        String deobfedPacketName = io.papermc.paper.util.ObfHelper.INSTANCE.deobfClassName(check.getName());
+
+                                        String playerName;
+                                        if (this.packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl impl) {
+                                            playerName = impl.getOwner().getName();
+                                        } else {
+                                            playerName = this.getLoggableAddress(net.minecraft.server.MinecraftServer.getServer().logIPs());
+                                        }
+
+                                        Connection.LOGGER.warn("{} kicked for packet spamming: {}", playerName, deobfedPacketName.substring(deobfedPacketName.lastIndexOf(".") + 1));
+                                        this.killForPacketSpam();
+                                        return;
+                                }
+                            }
+                        }
+                    }
+                }
+                // Paper end - packet limiter
+                if (packetlistener.shouldHandleMessage(packet)) {
                     try {
-                        genericsFtw(packet, packetListener);
-                    } catch (RunningOnDifferentThreadException var5) {
-                    } catch (RejectedExecutionException var6) {
+                        Connection.genericsFtw(packet, packetlistener);
+                    } catch (RunningOnDifferentThreadException cancelledpackethandleexception) {
+                        ;
+                    } catch (io.papermc.paper.util.ServerStopRejectedExecutionException ignored) { // Paper - do not prematurely disconnect players on stop
+                    } catch (RejectedExecutionException rejectedexecutionexception) {
                         this.disconnect(Component.translatable("multiplayer.disconnect.server_shutdown"));
-                    } catch (ClassCastException var7) {
-                        LOGGER.error("Received {} that couldn't be processed", packet.getClass(), var7);
+                    } catch (ClassCastException classcastexception) {
+                        Connection.LOGGER.error("Received {} that couldn't be processed", packet.getClass(), classcastexception);
                         this.disconnect(Component.translatable("multiplayer.disconnect.invalid_packet"));
                     }
 
-                    this.receivedPackets++;
+                    ++this.receivedPackets;
                 }
+
             }
         }
     }
 
     private static <T extends PacketListener> void genericsFtw(Packet<T> packet, PacketListener listener) {
-        packet.handle((T)listener);
+        packet.handle((T) listener); // CraftBukkit - decompile error
     }
 
     private void validateListener(ProtocolInfo<?> state, PacketListener listener) {
-        Validate.notNull(listener, "packetListener");
-        PacketFlow packetFlow = listener.flow();
-        if (packetFlow != this.receiving) {
-            throw new IllegalStateException("Trying to set listener for wrong side: connection is " + this.receiving + ", but listener is " + packetFlow);
+        Validate.notNull(listener, "packetListener", new Object[0]);
+        PacketFlow enumprotocoldirection = listener.flow();
+        String s;
+
+        if (enumprotocoldirection != this.receiving) {
+            s = String.valueOf(this.receiving);
+            throw new IllegalStateException("Trying to set listener for wrong side: connection is " + s + ", but listener is " + String.valueOf(enumprotocoldirection));
         } else {
-            ConnectionProtocol connectionProtocol = listener.protocol();
-            if (state.id() != connectionProtocol) {
-                throw new IllegalStateException("Listener protocol (" + connectionProtocol + ") does not match requested one " + state);
+            ConnectionProtocol enumprotocol = listener.protocol();
+
+            if (state.id() != enumprotocol) {
+                s = String.valueOf(enumprotocol);
+                throw new IllegalStateException("Listener protocol (" + s + ") does not match requested one " + String.valueOf(state));
             }
         }
     }
@@ -200,11 +338,11 @@
     private static void syncAfterConfigurationChange(ChannelFuture future) {
         try {
             future.syncUninterruptibly();
-        } catch (Exception var2) {
-            if (var2 instanceof ClosedChannelException) {
-                LOGGER.info("Connection closed during protocol change");
+        } catch (Exception exception) {
+            if (exception instanceof ClosedChannelException) {
+                Connection.LOGGER.info("Connection closed during protocol change");
             } else {
-                throw var2;
+                throw exception;
             }
         }
     }
@@ -212,45 +350,52 @@
     public <T extends PacketListener> void setupInboundProtocol(ProtocolInfo<T> state, T packetListener) {
         this.validateListener(state, packetListener);
         if (state.flow() != this.getReceiving()) {
-            throw new IllegalStateException("Invalid inbound protocol: " + state.id());
+            throw new IllegalStateException("Invalid inbound protocol: " + String.valueOf(state.id()));
         } else {
             this.packetListener = packetListener;
             this.disconnectListener = null;
-            UnconfiguredPipelineHandler.InboundConfigurationTask inboundConfigurationTask = UnconfiguredPipelineHandler.setupInboundProtocol(state);
-            BundlerInfo bundlerInfo = state.bundlerInfo();
-            if (bundlerInfo != null) {
-                PacketBundlePacker packetBundlePacker = new PacketBundlePacker(bundlerInfo);
-                inboundConfigurationTask = inboundConfigurationTask.andThen(context -> context.pipeline().addAfter("decoder", "bundler", packetBundlePacker));
+            UnconfiguredPipelineHandler.InboundConfigurationTask unconfiguredpipelinehandler_b = UnconfiguredPipelineHandler.setupInboundProtocol(state);
+            BundlerInfo bundlerinfo = state.bundlerInfo();
+
+            if (bundlerinfo != null) {
+                PacketBundlePacker packetbundlepacker = new PacketBundlePacker(bundlerinfo);
+
+                unconfiguredpipelinehandler_b = unconfiguredpipelinehandler_b.andThen((channelhandlercontext) -> {
+                    channelhandlercontext.pipeline().addAfter("decoder", "bundler", packetbundlepacker);
+                });
             }
 
-            syncAfterConfigurationChange(this.channel.writeAndFlush(inboundConfigurationTask));
+            Connection.syncAfterConfigurationChange(this.channel.writeAndFlush(unconfiguredpipelinehandler_b));
         }
     }
 
     public void setupOutboundProtocol(ProtocolInfo<?> newState) {
         if (newState.flow() != this.getSending()) {
-            throw new IllegalStateException("Invalid outbound protocol: " + newState.id());
+            throw new IllegalStateException("Invalid outbound protocol: " + String.valueOf(newState.id()));
         } else {
-            UnconfiguredPipelineHandler.OutboundConfigurationTask outboundConfigurationTask = UnconfiguredPipelineHandler.setupOutboundProtocol(newState);
-            BundlerInfo bundlerInfo = newState.bundlerInfo();
-            if (bundlerInfo != null) {
-                PacketBundleUnpacker packetBundleUnpacker = new PacketBundleUnpacker(bundlerInfo);
-                outboundConfigurationTask = outboundConfigurationTask.andThen(
-                    context -> context.pipeline().addAfter("encoder", "unbundler", packetBundleUnpacker)
-                );
+            UnconfiguredPipelineHandler.OutboundConfigurationTask unconfiguredpipelinehandler_d = UnconfiguredPipelineHandler.setupOutboundProtocol(newState);
+            BundlerInfo bundlerinfo = newState.bundlerInfo();
+
+            if (bundlerinfo != null) {
+                PacketBundleUnpacker packetbundleunpacker = new PacketBundleUnpacker(bundlerinfo);
+
+                unconfiguredpipelinehandler_d = unconfiguredpipelinehandler_d.andThen((channelhandlercontext) -> {
+                    channelhandlercontext.pipeline().addAfter("encoder", "unbundler", packetbundleunpacker);
+                });
             }
 
-            boolean bl = newState.id() == ConnectionProtocol.LOGIN;
-            syncAfterConfigurationChange(this.channel.writeAndFlush(outboundConfigurationTask.andThen(context -> this.sendLoginDisconnect = bl)));
+            boolean flag = newState.id() == ConnectionProtocol.LOGIN;
+
+            Connection.syncAfterConfigurationChange(this.channel.writeAndFlush(unconfiguredpipelinehandler_d.andThen((channelhandlercontext) -> {
+                this.sendLoginDisconnect = flag;
+            })));
         }
     }
 
     public void setListenerForServerboundHandshake(PacketListener packetListener) {
         if (this.packetListener != null) {
             throw new IllegalStateException("Listener already set");
-        } else if (this.receiving == PacketFlow.SERVERBOUND
-            && packetListener.flow() == PacketFlow.SERVERBOUND
-            && packetListener.protocol() == INITIAL_PROTOCOL.id()) {
+        } else if (this.receiving == PacketFlow.SERVERBOUND && packetListener.flow() == PacketFlow.SERVERBOUND && packetListener.protocol() == Connection.INITIAL_PROTOCOL.id()) {
             this.packetListener = packetListener;
         } else {
             throw new IllegalStateException("Invalid initial listener");
@@ -265,31 +410,25 @@
         this.initiateServerboundConnection(address, port, LoginProtocols.SERVERBOUND, LoginProtocols.CLIENTBOUND, listener, ClientIntent.LOGIN);
     }
 
-    public <S extends ServerboundPacketListener, C extends ClientboundPacketListener> void initiateServerboundPlayConnection(
-        String address, int port, ProtocolInfo<S> outboundState, ProtocolInfo<C> inboundState, C prePlayStateListener, boolean transfer
-    ) {
-        this.initiateServerboundConnection(
-            address, port, outboundState, inboundState, prePlayStateListener, transfer ? ClientIntent.TRANSFER : ClientIntent.LOGIN
-        );
+    public <S extends ServerboundPacketListener, C extends ClientboundPacketListener> void initiateServerboundPlayConnection(String address, int port, ProtocolInfo<S> outboundState, ProtocolInfo<C> inboundState, C prePlayStateListener, boolean transfer) {
+        this.initiateServerboundConnection(address, port, outboundState, inboundState, prePlayStateListener, transfer ? ClientIntent.TRANSFER : ClientIntent.LOGIN);
     }
 
-    private <S extends ServerboundPacketListener, C extends ClientboundPacketListener> void initiateServerboundConnection(
-        String address, int port, ProtocolInfo<S> outboundState, ProtocolInfo<C> inboundState, C prePlayStateListener, ClientIntent intent
-    ) {
+    private <S extends ServerboundPacketListener, C extends ClientboundPacketListener> void initiateServerboundConnection(String address, int port, ProtocolInfo<S> outboundState, ProtocolInfo<C> inboundState, C prePlayStateListener, ClientIntent intent) {
         if (outboundState.id() != inboundState.id()) {
             throw new IllegalStateException("Mismatched initial protocols");
         } else {
             this.disconnectListener = prePlayStateListener;
-            this.runOnceConnected(connection -> {
+            this.runOnceConnected((networkmanager) -> {
                 this.setupInboundProtocol(inboundState, prePlayStateListener);
-                connection.sendPacket(new ClientIntentionPacket(SharedConstants.getCurrentVersion().getProtocolVersion(), address, port, intent), null, true);
+                networkmanager.sendPacket(new ClientIntentionPacket(SharedConstants.getCurrentVersion().getProtocolVersion(), address, port, intent), (PacketSendListener) null, true);
                 this.setupOutboundProtocol(outboundState);
             });
         }
     }
 
     public void send(Packet<?> packet) {
-        this.send(packet, null);
+        this.send(packet, (PacketSendListener) null);
     }
 
     public void send(Packet<?> packet, @Nullable PacketSendListener callbacks) {
@@ -297,11 +436,38 @@
     }
 
     public void send(Packet<?> packet, @Nullable PacketSendListener callbacks, boolean flush) {
-        if (this.isConnected()) {
-            this.flushQueue();
+        // Paper start - Optimize network: Handle oversized packets better
+        final boolean connected = this.isConnected();
+        if (!connected && !this.preparing) {
+            return;
+        }
+
+        packet.onPacketDispatch(this.getPlayer());
+        if (connected && (InnerUtil.canSendImmediate(this, packet)
+            || (io.papermc.paper.util.MCUtil.isMainThread() && packet.isReady() && this.pendingActions.isEmpty()
+            && (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty())))) {
             this.sendPacket(packet, callbacks, flush);
         } else {
-            this.pendingActions.add(connection -> connection.sendPacket(packet, callbacks, flush));
+            // Write the packets to the queue, then flush - antixray hooks there already
+            final java.util.List<Packet<?>> extraPackets = InnerUtil.buildExtraPackets(packet);
+            final boolean hasExtraPackets = extraPackets != null && !extraPackets.isEmpty();
+            if (!hasExtraPackets) {
+                this.pendingActions.add(new PacketSendAction(packet, callbacks, flush));
+            } else {
+                final java.util.List<PacketSendAction> actions = new java.util.ArrayList<>(1 + extraPackets.size());
+                actions.add(new PacketSendAction(packet, null, false)); // Delay the future listener until the end of the extra packets
+
+                for (int i = 0, len = extraPackets.size(); i < len;) {
+                    final Packet<?> extraPacket = extraPackets.get(i);
+                    final boolean end = ++i == len;
+                    actions.add(new PacketSendAction(extraPacket, end ? callbacks : null, end)); // Append listener to the end
+                }
+
+                this.pendingActions.addAll(actions);
+            }
+
+            this.flushQueue();
+            // Paper end - Optimize network
         }
     }
 
@@ -310,69 +476,162 @@
             this.flushQueue();
             task.accept(this);
         } else {
-            this.pendingActions.add(task);
+            this.pendingActions.add(new WrappedConsumer(task)); // Paper - Optimize network
         }
+
     }
 
     private void sendPacket(Packet<?> packet, @Nullable PacketSendListener callbacks, boolean flush) {
-        this.sentPackets++;
+        ++this.sentPackets;
         if (this.channel.eventLoop().inEventLoop()) {
             this.doSendPacket(packet, callbacks, flush);
         } else {
-            this.channel.eventLoop().execute(() -> this.doSendPacket(packet, callbacks, flush));
+            this.channel.eventLoop().execute(() -> {
+                this.doSendPacket(packet, callbacks, flush);
+            });
         }
+
     }
 
     private void doSendPacket(Packet<?> packet, @Nullable PacketSendListener callbacks, boolean flush) {
-        ChannelFuture channelFuture = flush ? this.channel.writeAndFlush(packet) : this.channel.write(packet);
+        // Paper start - Optimize network
+        final net.minecraft.server.level.ServerPlayer player = this.getPlayer();
+        if (!this.isConnected()) {
+            packet.onPacketDispatchFinish(player, null);
+            return;
+        }
+        try {
+        // Paper end - Optimize network
+        ChannelFuture channelfuture = flush ? this.channel.writeAndFlush(packet) : this.channel.write(packet);
+
         if (callbacks != null) {
-            channelFuture.addListener(future -> {
+            channelfuture.addListener((future) -> {
                 if (future.isSuccess()) {
                     callbacks.onSuccess();
                 } else {
-                    Packet<?> packetx = callbacks.onFailure();
-                    if (packetx != null) {
-                        ChannelFuture channelFuturex = this.channel.writeAndFlush(packetx);
-                        channelFuturex.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+                    Packet<?> packet1 = callbacks.onFailure();
+
+                    if (packet1 != null) {
+                        ChannelFuture channelfuture1 = this.channel.writeAndFlush(packet1);
+
+                        channelfuture1.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
                     }
                 }
+
             });
         }
 
-        channelFuture.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+        // Paper start - Optimize network
+        if (packet.hasFinishListener()) {
+            channelfuture.addListener((ChannelFutureListener) channelFuture -> packet.onPacketDispatchFinish(player, channelFuture));
+        }
+        channelfuture.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+        } catch (final Exception e) {
+            LOGGER.error("NetworkException: {}", player, e);
+            this.disconnect(Component.translatable("disconnect.genericReason", "Internal Exception: " + e.getMessage()));
+            packet.onPacketDispatchFinish(player, null);
+        }
+        // Paper end - Optimize network
     }
 
     public void flushChannel() {
         if (this.isConnected()) {
             this.flush();
         } else {
-            this.pendingActions.add(Connection::flush);
+            this.pendingActions.add(new WrappedConsumer(Connection::flush)); // Paper - Optimize network
         }
+
     }
 
     private void flush() {
         if (this.channel.eventLoop().inEventLoop()) {
             this.channel.flush();
         } else {
-            this.channel.eventLoop().execute(() -> this.channel.flush());
+            this.channel.eventLoop().execute(() -> {
+                this.channel.flush();
+            });
         }
+
     }
 
-    private void flushQueue() {
-        if (this.channel != null && this.channel.isOpen()) {
+    // Paper start - Optimize network: Rewrite this to be safer if ran off main thread
+    private boolean flushQueue() {
+        if (!this.isConnected()) {
+            return true;
+        }
+        if (io.papermc.paper.util.MCUtil.isMainThread()) {
+            return this.processQueue();
+        } else if (this.isPending) {
+            // Should only happen during login/status stages
             synchronized (this.pendingActions) {
-                Consumer<Connection> consumer;
-                while ((consumer = this.pendingActions.poll()) != null) {
-                    consumer.accept(this);
+                return this.processQueue();
+            }
+        }
+        return false;
+    }
+
+    private boolean processQueue() {
+        if (this.pendingActions.isEmpty()) {
+            return true;
+        }
+
+        // If we are on main, we are safe here in that nothing else should be processing queue off main anymore
+        // But if we are not on main due to login/status, the parent is synchronized on packetQueue
+        final java.util.Iterator<WrappedConsumer> iterator = this.pendingActions.iterator();
+        while (iterator.hasNext()) {
+            final WrappedConsumer queued = iterator.next(); // poll -> peek
+
+            // Fix NPE (Spigot bug caused by handleDisconnection())
+            if (queued == null) {
+                return true;
+            }
+
+            if (queued.isConsumed()) {
+                continue;
+            }
+
+            if (queued instanceof PacketSendAction packetSendAction) {
+                final Packet<?> packet = packetSendAction.packet;
+                if (!packet.isReady()) {
+                    return false;
                 }
             }
+
+            iterator.remove();
+            if (queued.tryMarkConsumed()) {
+                queued.accept(this);
+            }
         }
+        return true;
     }
+    // Paper end - Optimize network
 
+    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
+    private static int currTick; // Paper - Buffer joins to world
     public void tick() {
         this.flushQueue();
-        if (this.packetListener instanceof TickablePacketListener tickablePacketListener) {
-            tickablePacketListener.tick();
+        // Paper start - Buffer joins to world
+        if (Connection.currTick != net.minecraft.server.MinecraftServer.currentTick) {
+            Connection.currTick = net.minecraft.server.MinecraftServer.currentTick;
+            Connection.joinAttemptsThisTick = 0;
+        }
+        // Paper end - Buffer joins to world
+        PacketListener packetlistener = this.packetListener;
+
+        if (packetlistener instanceof TickablePacketListener tickablepacketlistener) {
+            // Paper start - Buffer joins to world
+            if (!(this.packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener)
+                || loginPacketListener.state != net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING
+                || Connection.joinAttemptsThisTick++ < MAX_PER_TICK) {
+            // Paper start - detailed watchdog information
+            net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener);
+            try {
+            tickablepacketlistener.tick();
+            } finally {
+                net.minecraft.network.protocol.PacketUtils.packetProcessing.pop();
+            } // Paper end - detailed watchdog information
+            } // Paper end - Buffer joins to world
         }
 
         if (!this.isConnected() && !this.disconnectionHandled) {
@@ -380,7 +639,7 @@
         }
 
         if (this.channel != null) {
-            this.channel.flush();
+            if (enableExplicitFlush) this.channel.eventLoop().execute(() -> this.channel.flush()); // Paper - Disable explicit network manager flushing; we don't need to explicit flush here, but allow opt in incase issues are found to a better version
         }
 
         if (this.tickCount++ % 20 == 0) {
@@ -390,11 +649,12 @@
         if (this.bandwidthDebugMonitor != null) {
             this.bandwidthDebugMonitor.tick();
         }
+
     }
 
     protected void tickSecond() {
-        this.averageSentPackets = Mth.lerp(0.75F, (float)this.sentPackets, this.averageSentPackets);
-        this.averageReceivedPackets = Mth.lerp(0.75F, (float)this.receivedPackets, this.averageReceivedPackets);
+        this.averageSentPackets = Mth.lerp(0.75F, (float) this.sentPackets, this.averageSentPackets);
+        this.averageReceivedPackets = Mth.lerp(0.75F, (float) this.receivedPackets, this.averageReceivedPackets);
         this.sentPackets = 0;
         this.receivedPackets = 0;
     }
@@ -404,22 +664,23 @@
     }
 
     public String getLoggableAddress(boolean logIps) {
-        if (this.address == null) {
-            return "local";
-        } else {
-            return logIps ? this.address.toString() : "IP hidden";
-        }
+        return this.address == null ? "local" : (logIps ? this.address.toString() : "IP hidden");
     }
 
     public void disconnect(Component disconnectReason) {
+        // Spigot Start
+        this.preparing = false;
+        this.clearPacketQueue(); // Paper - Optimize network
+        // Spigot End
         if (this.channel == null) {
             this.delayedDisconnect = disconnectReason;
         }
 
         if (this.isConnected()) {
-            this.channel.close().awaitUninterruptibly();
+            this.channel.close(); // We can't wait as this may be called from an event loop.
             this.disconnectedReason = disconnectReason;
         }
+
     }
 
     public boolean isMemoryConnection() {
@@ -435,39 +696,44 @@
     }
 
     public static Connection connectToServer(InetSocketAddress address, boolean useEpoll, @Nullable LocalSampleLogger packetSizeLog) {
-        Connection connection = new Connection(PacketFlow.CLIENTBOUND);
+        Connection networkmanager = new Connection(PacketFlow.CLIENTBOUND);
+
         if (packetSizeLog != null) {
-            connection.setBandwidthLogger(packetSizeLog);
+            networkmanager.setBandwidthLogger(packetSizeLog);
         }
 
-        ChannelFuture channelFuture = connect(address, useEpoll, connection);
-        channelFuture.syncUninterruptibly();
-        return connection;
+        ChannelFuture channelfuture = Connection.connect(address, useEpoll, networkmanager);
+
+        channelfuture.syncUninterruptibly();
+        return networkmanager;
     }
 
-    public static ChannelFuture connect(InetSocketAddress address, boolean useEpoll, Connection connection) {
-        Class<? extends SocketChannel> class_;
-        EventLoopGroup eventLoopGroup;
+    public static ChannelFuture connect(InetSocketAddress address, boolean useEpoll, final Connection connection) {
+        Class oclass;
+        EventLoopGroup eventloopgroup;
+
         if (Epoll.isAvailable() && useEpoll) {
-            class_ = EpollSocketChannel.class;
-            eventLoopGroup = NETWORK_EPOLL_WORKER_GROUP.get();
+            oclass = EpollSocketChannel.class;
+            eventloopgroup = (EventLoopGroup) Connection.NETWORK_EPOLL_WORKER_GROUP.get();
         } else {
-            class_ = NioSocketChannel.class;
-            eventLoopGroup = NETWORK_WORKER_GROUP.get();
+            oclass = NioSocketChannel.class;
+            eventloopgroup = (EventLoopGroup) Connection.NETWORK_WORKER_GROUP.get();
         }
 
-        return new Bootstrap().group(eventLoopGroup).handler(new ChannelInitializer<Channel>() {
+        return ((Bootstrap) ((Bootstrap) ((Bootstrap) (new Bootstrap()).group(eventloopgroup)).handler(new ChannelInitializer<Channel>() {
             protected void initChannel(Channel channel) {
                 try {
                     channel.config().setOption(ChannelOption.TCP_NODELAY, true);
-                } catch (ChannelException var3) {
+                } catch (ChannelException channelexception) {
+                    ;
                 }
 
-                ChannelPipeline channelPipeline = channel.pipeline().addLast("timeout", new ReadTimeoutHandler(30));
-                Connection.configureSerialization(channelPipeline, PacketFlow.CLIENTBOUND, false, connection.bandwidthDebugMonitor);
-                connection.configurePacketHandler(channelPipeline);
+                ChannelPipeline channelpipeline = channel.pipeline().addLast("timeout", new ReadTimeoutHandler(30));
+
+                Connection.configureSerialization(channelpipeline, PacketFlow.CLIENTBOUND, false, connection.bandwidthDebugMonitor);
+                connection.configurePacketHandler(channelpipeline);
             }
-        }).channel(class_).connect(address.getAddress(), address.getPort());
+        })).channel(oclass)).connect(address.getAddress(), address.getPort());
     }
 
     private static String outboundHandlerName(boolean sendingSide) {
@@ -479,57 +745,69 @@
     }
 
     public void configurePacketHandler(ChannelPipeline pipeline) {
-        pipeline.addLast("hackfix", new ChannelOutboundHandlerAdapter() {
-            public void write(ChannelHandlerContext channelHandlerContext, Object object, ChannelPromise channelPromise) throws Exception {
-                super.write(channelHandlerContext, object, channelPromise);
+        pipeline.addLast("hackfix", new ChannelOutboundHandlerAdapter() { // CraftBukkit - decompile error
+            public void write(ChannelHandlerContext channelhandlercontext, Object object, ChannelPromise channelpromise) throws Exception {
+                super.write(channelhandlercontext, object, channelpromise);
             }
         }).addLast("packet_handler", this);
     }
 
     public static void configureSerialization(ChannelPipeline pipeline, PacketFlow side, boolean local, @Nullable BandwidthDebugMonitor packetSizeLogger) {
-        PacketFlow packetFlow = side.getOpposite();
-        boolean bl = side == PacketFlow.SERVERBOUND;
-        boolean bl2 = packetFlow == PacketFlow.SERVERBOUND;
-        pipeline.addLast("splitter", createFrameDecoder(packetSizeLogger, local))
-            .addLast(new FlowControlHandler())
-            .addLast(inboundHandlerName(bl), (ChannelHandler)(bl ? new PacketDecoder<>(INITIAL_PROTOCOL) : new UnconfiguredPipelineHandler.Inbound()))
-            .addLast("prepender", createFrameEncoder(local))
-            .addLast(outboundHandlerName(bl2), (ChannelHandler)(bl2 ? new PacketEncoder<>(INITIAL_PROTOCOL) : new UnconfiguredPipelineHandler.Outbound()));
+        PacketFlow enumprotocoldirection1 = side.getOpposite();
+        boolean flag1 = side == PacketFlow.SERVERBOUND;
+        boolean flag2 = enumprotocoldirection1 == PacketFlow.SERVERBOUND;
+
+        pipeline.addLast("splitter", Connection.createFrameDecoder(packetSizeLogger, local)).addLast(new ChannelHandler[]{new FlowControlHandler()}).addLast(Connection.inboundHandlerName(flag1), (ChannelHandler) (flag1 ? new PacketDecoder<>(Connection.INITIAL_PROTOCOL) : new UnconfiguredPipelineHandler.Inbound())).addLast("prepender", Connection.createFrameEncoder(local)).addLast(Connection.outboundHandlerName(flag2), (ChannelHandler) (flag2 ? new PacketEncoder<>(Connection.INITIAL_PROTOCOL) : new UnconfiguredPipelineHandler.Outbound()));
     }
 
     private static ChannelOutboundHandler createFrameEncoder(boolean local) {
-        return (ChannelOutboundHandler)(local ? new NoOpFrameEncoder() : new Varint21LengthFieldPrepender());
+        return (ChannelOutboundHandler) (local ? new NoOpFrameEncoder() : new Varint21LengthFieldPrepender());
     }
 
     private static ChannelInboundHandler createFrameDecoder(@Nullable BandwidthDebugMonitor packetSizeLogger, boolean local) {
-        if (!local) {
-            return new Varint21FrameDecoder(packetSizeLogger);
-        } else {
-            return (ChannelInboundHandler)(packetSizeLogger != null ? new MonitorFrameDecoder(packetSizeLogger) : new NoOpFrameDecoder());
-        }
+        return (ChannelInboundHandler) (!local ? new Varint21FrameDecoder(packetSizeLogger) : (packetSizeLogger != null ? new MonitorFrameDecoder(packetSizeLogger) : new NoOpFrameDecoder()));
     }
 
     public static void configureInMemoryPipeline(ChannelPipeline pipeline, PacketFlow side) {
-        configureSerialization(pipeline, side, true, null);
+        Connection.configureSerialization(pipeline, side, true, (BandwidthDebugMonitor) null);
     }
 
     public static Connection connectToLocalServer(SocketAddress address) {
-        final Connection connection = new Connection(PacketFlow.CLIENTBOUND);
-        new Bootstrap().group(LOCAL_WORKER_GROUP.get()).handler(new ChannelInitializer<Channel>() {
+        final Connection networkmanager = new Connection(PacketFlow.CLIENTBOUND);
+
+        ((Bootstrap) ((Bootstrap) ((Bootstrap) (new Bootstrap()).group((EventLoopGroup) Connection.LOCAL_WORKER_GROUP.get())).handler(new ChannelInitializer<Channel>() {
             protected void initChannel(Channel channel) {
-                ChannelPipeline channelPipeline = channel.pipeline();
-                Connection.configureInMemoryPipeline(channelPipeline, PacketFlow.CLIENTBOUND);
-                connection.configurePacketHandler(channelPipeline);
+                ChannelPipeline channelpipeline = channel.pipeline();
+
+                Connection.configureInMemoryPipeline(channelpipeline, PacketFlow.CLIENTBOUND);
+                networkmanager.configurePacketHandler(channelpipeline);
             }
-        }).channel(LocalChannel.class).connect(address).syncUninterruptibly();
-        return connection;
+        })).channel(LocalChannel.class)).connect(address).syncUninterruptibly();
+        return networkmanager;
     }
 
-    public void setEncryptionKey(Cipher decryptionCipher, Cipher encryptionCipher) {
-        this.encrypted = true;
-        this.channel.pipeline().addBefore("splitter", "decrypt", new CipherDecoder(decryptionCipher));
-        this.channel.pipeline().addBefore("prepender", "encrypt", new CipherEncoder(encryptionCipher));
+    // Paper start - Use Velocity cipher
+//    public void setEncryptionKey(Cipher decryptionCipher, Cipher encryptionCipher) {
+//        this.encrypted = true;
+//        this.channel.pipeline().addBefore("splitter", "decrypt", new CipherDecoder(decryptionCipher));
+//        this.channel.pipeline().addBefore("prepender", "encrypt", new CipherEncoder(encryptionCipher));
+//    }
+
+    public void setupEncryption(javax.crypto.SecretKey key) throws net.minecraft.util.CryptException {
+        if (!this.encrypted) {
+            try {
+                com.velocitypowered.natives.encryption.VelocityCipher decryption = com.velocitypowered.natives.util.Natives.cipher.get().forDecryption(key);
+                com.velocitypowered.natives.encryption.VelocityCipher encryption = com.velocitypowered.natives.util.Natives.cipher.get().forEncryption(key);
+
+                this.encrypted = true;
+                this.channel.pipeline().addBefore("splitter", "decrypt", new CipherDecoder(decryption));
+                this.channel.pipeline().addBefore("prepender", "encrypt", new CipherEncoder(encryption));
+            } catch (java.security.GeneralSecurityException e) {
+                throw new net.minecraft.util.CryptException(e);
+            }
+        }
     }
+    // Paper end - Use Velocity cipher
 
     public boolean isEncrypted() {
         return this.encrypted;
@@ -557,21 +835,31 @@
         if (this.channel != null) {
             this.channel.config().setAutoRead(false);
         }
+
     }
 
     public void setupCompression(int compressionThreshold, boolean rejectsBadPackets) {
         if (compressionThreshold >= 0) {
-            if (this.channel.pipeline().get("decompress") instanceof CompressionDecoder compressionDecoder) {
-                compressionDecoder.setThreshold(compressionThreshold, rejectsBadPackets);
+            com.velocitypowered.natives.compression.VelocityCompressor compressor = com.velocitypowered.natives.util.Natives.compress.get().create(io.papermc.paper.configuration.GlobalConfiguration.get().misc.compressionLevel.or(-1)); // Paper - Use Velocity cipher
+            ChannelHandler channelhandler = this.channel.pipeline().get("decompress");
+
+            if (channelhandler instanceof CompressionDecoder) {
+                CompressionDecoder packetdecompressor = (CompressionDecoder) channelhandler;
+
+                packetdecompressor.setThreshold(compressor, compressionThreshold, rejectsBadPackets); // Paper - Use Velocity cipher
             } else {
                 this.channel.pipeline().addAfter("splitter", "decompress", new CompressionDecoder(compressionThreshold, rejectsBadPackets));
             }
 
-            if (this.channel.pipeline().get("compress") instanceof CompressionEncoder compressionEncoder) {
-                compressionEncoder.setThreshold(compressionThreshold);
+            channelhandler = this.channel.pipeline().get("compress");
+            if (channelhandler instanceof CompressionEncoder) {
+                CompressionEncoder packetcompressor = (CompressionEncoder) channelhandler;
+
+                packetcompressor.setThreshold(compressionThreshold);
             } else {
-                this.channel.pipeline().addAfter("prepender", "compress", new CompressionEncoder(compressionThreshold));
+                this.channel.pipeline().addAfter("prepender", "compress", new CompressionEncoder(compressor, compressionThreshold)); // Paper - Use Velocity cipher
             }
+            this.channel.pipeline().fireUserEventTriggered(io.papermc.paper.network.ConnectionEvent.COMPRESSION_THRESHOLD_SET); // Paper - Add Channel initialization listeners
         } else {
             if (this.channel.pipeline().get("decompress") instanceof CompressionDecoder) {
                 this.channel.pipeline().remove("decompress");
@@ -580,23 +868,49 @@
             if (this.channel.pipeline().get("compress") instanceof CompressionEncoder) {
                 this.channel.pipeline().remove("compress");
             }
+            this.channel.pipeline().fireUserEventTriggered(io.papermc.paper.network.ConnectionEvent.COMPRESSION_DISABLED); // Paper - Add Channel initialization listeners
         }
+
     }
 
     public void handleDisconnection() {
         if (this.channel != null && !this.channel.isOpen()) {
             if (this.disconnectionHandled) {
-                LOGGER.warn("handleDisconnection() called twice");
+                // Connection.LOGGER.warn("handleDisconnection() called twice"); // Paper - Don't log useless message
             } else {
                 this.disconnectionHandled = true;
-                PacketListener packetListener = this.getPacketListener();
-                PacketListener packetListener2 = packetListener != null ? packetListener : this.disconnectListener;
-                if (packetListener2 != null) {
-                    Component component = Objects.requireNonNullElseGet(
-                        this.getDisconnectedReason(), () -> Component.translatable("multiplayer.disconnect.generic")
-                    );
-                    packetListener2.onDisconnect(component);
+                PacketListener packetlistener = this.getPacketListener();
+                PacketListener packetlistener1 = packetlistener != null ? packetlistener : this.disconnectListener;
+
+                if (packetlistener1 != null) {
+                    Component ichatbasecomponent = (Component) Objects.requireNonNullElseGet(this.getDisconnectedReason(), () -> {
+                        return Component.translatable("multiplayer.disconnect.generic");
+                    });
+
+                    packetlistener1.onDisconnect(ichatbasecomponent);
                 }
+                this.clearPacketQueue(); // Paper - Optimize network
+                // Paper start - Add PlayerConnectionCloseEvent
+                final PacketListener packetListener = this.getPacketListener();
+                if (packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) {
+                    /* Player was logged in, either game listener or configuration listener */
+                    final com.mojang.authlib.GameProfile profile = commonPacketListener.getOwner();
+                    new com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent(profile.getId(),
+                        profile.getName(), ((InetSocketAddress) this.address).getAddress(), false).callEvent();
+                } else if (packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginListener) {
+                    /* Player is login stage */
+                    switch (loginListener.state) {
+                        case VERIFYING:
+                        case WAITING_FOR_DUPE_DISCONNECT:
+                        case PROTOCOL_SWITCHING:
+                        case ACCEPTED:
+                            final com.mojang.authlib.GameProfile profile = loginListener.authenticatedProfile; /* Should be non-null at this stage */
+                            new com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent(profile.getId(), profile.getName(),
+                                ((InetSocketAddress) this.address).getAddress(), false).callEvent();
+                    }
+                }
+                // Paper end - Add PlayerConnectionCloseEvent
+
             }
         }
     }
@@ -612,4 +926,93 @@
     public void setBandwidthLogger(LocalSampleLogger log) {
         this.bandwidthDebugMonitor = new BandwidthDebugMonitor(log);
     }
+
+    // Paper start - Optimize network
+    public void clearPacketQueue() {
+        final net.minecraft.server.level.ServerPlayer player = getPlayer();
+        for (final Consumer<Connection> queuedAction : this.pendingActions) {
+            if (queuedAction instanceof PacketSendAction packetSendAction) {
+                final Packet<?> packet = packetSendAction.packet;
+                if (packet.hasFinishListener()) {
+                    packet.onPacketDispatchFinish(player, null);
+                }
+            }
+        }
+        this.pendingActions.clear();
+    }
+
+    private static class InnerUtil { // Attempt to hide these methods from ProtocolLib, so it doesn't accidently pick them up.
+
+        @Nullable
+        private static java.util.List<Packet<?>> buildExtraPackets(final Packet<?> packet) {
+            final java.util.List<Packet<?>> extra = packet.getExtraPackets();
+            if (extra == null || extra.isEmpty()) {
+                return null;
+            }
+
+            final java.util.List<Packet<?>> ret = new java.util.ArrayList<>(1 + extra.size());
+            buildExtraPackets0(extra, ret);
+            return ret;
+        }
+
+        private static void buildExtraPackets0(final java.util.List<Packet<?>> extraPackets, final java.util.List<Packet<?>> into) {
+            for (final Packet<?> extra : extraPackets) {
+                into.add(extra);
+                final java.util.List<Packet<?>> extraExtra = extra.getExtraPackets();
+                if (extraExtra != null && !extraExtra.isEmpty()) {
+                    buildExtraPackets0(extraExtra, into);
+                }
+            }
+        }
+
+        private static boolean canSendImmediate(final Connection networkManager, final net.minecraft.network.protocol.Packet<?> packet) {
+            return networkManager.isPending || networkManager.packetListener.protocol() != ConnectionProtocol.PLAY ||
+                packet instanceof net.minecraft.network.protocol.common.ClientboundKeepAlivePacket ||
+                packet instanceof net.minecraft.network.protocol.game.ClientboundPlayerChatPacket ||
+                packet instanceof net.minecraft.network.protocol.game.ClientboundSystemChatPacket ||
+                packet instanceof net.minecraft.network.protocol.game.ClientboundCommandSuggestionsPacket ||
+                packet instanceof net.minecraft.network.protocol.game.ClientboundSetTitleTextPacket ||
+                packet instanceof net.minecraft.network.protocol.game.ClientboundSetSubtitleTextPacket ||
+                packet instanceof net.minecraft.network.protocol.game.ClientboundSetActionBarTextPacket ||
+                packet instanceof net.minecraft.network.protocol.game.ClientboundSetTitlesAnimationPacket ||
+                packet instanceof net.minecraft.network.protocol.game.ClientboundClearTitlesPacket ||
+                packet instanceof net.minecraft.network.protocol.game.ClientboundSoundPacket ||
+                packet instanceof net.minecraft.network.protocol.game.ClientboundSoundEntityPacket ||
+                packet instanceof net.minecraft.network.protocol.game.ClientboundStopSoundPacket ||
+                packet instanceof net.minecraft.network.protocol.game.ClientboundLevelParticlesPacket ||
+                packet instanceof net.minecraft.network.protocol.game.ClientboundBossEventPacket;
+        }
+    }
+
+    private static class WrappedConsumer implements Consumer<Connection> {
+        private final Consumer<Connection> delegate;
+        private final java.util.concurrent.atomic.AtomicBoolean consumed = new java.util.concurrent.atomic.AtomicBoolean(false);
+
+        private WrappedConsumer(final Consumer<Connection> delegate) {
+            this.delegate = delegate;
+        }
+
+        @Override
+        public void accept(final Connection connection) {
+            this.delegate.accept(connection);
+        }
+
+        public boolean tryMarkConsumed() {
+            return consumed.compareAndSet(false, true);
+        }
+
+        public boolean isConsumed() {
+            return consumed.get();
+        }
+    }
+
+    private static final class PacketSendAction extends WrappedConsumer {
+        private final Packet<?> packet;
+
+        private PacketSendAction(final Packet<?> packet, @Nullable final PacketSendListener packetSendListener, final boolean flush) {
+            super(connection -> connection.sendPacket(packet, packetSendListener, flush));
+            this.packet = packet;
+        }
+    }
+    // Paper end - Optimize network
 }
