--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
@@ -2,19 +2,21 @@
 
 import com.google.common.collect.Lists;
 import com.google.common.primitives.Floats;
-import com.mojang.brigadier.CommandDispatcher;
 import com.mojang.brigadier.ParseResults;
 import com.mojang.brigadier.StringReader;
 import com.mojang.brigadier.exceptions.CommandSyntaxException;
 import com.mojang.datafixers.util.Pair;
 import com.mojang.logging.LogUtils;
-import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
 import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ObjectIterator;
 import java.time.Instant;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.CancellationException;
@@ -58,11 +60,13 @@
 import net.minecraft.network.chat.LastSeenMessagesValidator;
 import net.minecraft.network.chat.MessageSignature;
 import net.minecraft.network.chat.MessageSigner;
+import net.minecraft.network.chat.MutableComponent;
 import net.minecraft.network.chat.PlayerChatMessage;
 import net.minecraft.network.chat.PreviewableCommand;
 import net.minecraft.network.chat.SignedMessageChain;
 import net.minecraft.network.protocol.Packet;
 import net.minecraft.network.protocol.PacketUtils;
+import net.minecraft.network.protocol.game.ClientboundAddEntityPacket;
 import net.minecraft.network.protocol.game.ClientboundBlockChangedAckPacket;
 import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket;
 import net.minecraft.network.protocol.game.ClientboundChatPreviewPacket;
@@ -73,6 +77,10 @@
 import net.minecraft.network.protocol.game.ClientboundMoveVehiclePacket;
 import net.minecraft.network.protocol.game.ClientboundPlayerPositionPacket;
 import net.minecraft.network.protocol.game.ClientboundSetCarriedItemPacket;
+import net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket;
+import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket;
+import net.minecraft.network.protocol.game.ClientboundSetEntityLinkPacket;
+import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket;
 import net.minecraft.network.protocol.game.ClientboundSystemChatPacket;
 import net.minecraft.network.protocol.game.ClientboundTagQueryPacket;
 import net.minecraft.network.protocol.game.ServerGamePacketListener;
@@ -129,6 +137,7 @@
 import net.minecraft.server.MinecraftServer;
 import net.minecraft.server.level.ServerLevel;
 import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.stats.ServerRecipeBook;
 import net.minecraft.util.FutureChain;
 import net.minecraft.util.Mth;
 import net.minecraft.util.StringUtil;
@@ -138,26 +147,23 @@
 import net.minecraft.world.entity.Entity;
 import net.minecraft.world.entity.ExperienceOrb;
 import net.minecraft.world.entity.HasCustomInventoryScreen;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.Mob;
 import net.minecraft.world.entity.MoverType;
 import net.minecraft.world.entity.PlayerRideableJumping;
-import net.minecraft.world.entity.item.ItemEntity;
 import net.minecraft.world.entity.player.ChatVisiblity;
 import net.minecraft.world.entity.player.Inventory;
-import net.minecraft.world.entity.player.Player;
 import net.minecraft.world.entity.player.ProfilePublicKey;
 import net.minecraft.world.entity.projectile.AbstractArrow;
 import net.minecraft.world.entity.vehicle.Boat;
-import net.minecraft.world.inventory.AbstractContainerMenu;
-import net.minecraft.world.inventory.AnvilMenu;
-import net.minecraft.world.inventory.BeaconMenu;
-import net.minecraft.world.inventory.MerchantMenu;
-import net.minecraft.world.inventory.RecipeBookMenu;
 import net.minecraft.world.item.BlockItem;
 import net.minecraft.world.item.BucketItem;
 import net.minecraft.world.item.Item;
 import net.minecraft.world.item.ItemStack;
 import net.minecraft.world.item.Items;
+import net.minecraft.world.item.crafting.Recipe;
 import net.minecraft.world.level.BaseCommandBlock;
+import net.minecraft.world.level.ClipContext;
 import net.minecraft.world.level.GameRules;
 import net.minecraft.world.level.GameType;
 import net.minecraft.world.level.Level;
@@ -173,6 +179,7 @@
 import net.minecraft.world.level.block.state.BlockState;
 import net.minecraft.world.phys.AABB;
 import net.minecraft.world.phys.BlockHitResult;
+import net.minecraft.world.phys.HitResult;
 import net.minecraft.world.phys.Vec3;
 import net.minecraft.world.phys.shapes.BooleanOp;
 import net.minecraft.world.phys.shapes.Shapes;
@@ -180,7 +187,66 @@
 import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
 
+// CraftBukkit start
+import io.papermc.paper.adventure.ChatProcessor; // Paper
+import io.papermc.paper.adventure.PaperAdventure; // Paper
+import java.util.Arrays;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicInteger;
+import net.minecraft.network.chat.OutgoingPlayerChatMessage;
+import net.minecraft.world.entity.animal.Bucketable;
+import net.minecraft.world.entity.animal.allay.Allay;
+import net.minecraft.world.entity.item.ItemEntity;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import net.minecraft.world.inventory.AnvilMenu;
+import net.minecraft.world.inventory.BeaconMenu;
+import net.minecraft.world.inventory.MerchantMenu;
+import net.minecraft.world.inventory.RecipeBookMenu;
+import net.minecraft.world.inventory.Slot;
+import org.bukkit.Location;
+import org.bukkit.craftbukkit.v1_19_R1.entity.CraftPlayer;
+import org.bukkit.craftbukkit.v1_19_R1.event.CraftEventFactory;
+import org.bukkit.craftbukkit.v1_19_R1.inventory.CraftItemStack;
+import org.bukkit.craftbukkit.v1_19_R1.util.CraftChatMessage;
+import org.bukkit.craftbukkit.v1_19_R1.util.CraftMagicNumbers;
+import org.bukkit.craftbukkit.v1_19_R1.util.LazyPlayerSet;
+import org.bukkit.craftbukkit.v1_19_R1.util.Waitable;
+import org.bukkit.entity.Player;
+import org.bukkit.event.Event;
+import org.bukkit.event.block.Action;
+import org.bukkit.event.block.SignChangeEvent;
+import org.bukkit.event.inventory.ClickType;
+import org.bukkit.event.inventory.CraftItemEvent;
+import org.bukkit.event.inventory.InventoryAction;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryCloseEvent; // Paper
+import org.bukkit.event.inventory.InventoryCreativeEvent;
+import org.bukkit.event.inventory.InventoryType.SlotType;
+import org.bukkit.event.inventory.SmithItemEvent;
+import org.bukkit.event.player.AsyncPlayerChatEvent;
+import org.bukkit.event.player.PlayerAnimationEvent;
+import org.bukkit.event.player.PlayerAnimationType;
+import org.bukkit.event.player.PlayerChatEvent;
+import org.bukkit.event.player.PlayerCommandPreprocessEvent;
+import org.bukkit.event.player.PlayerInteractAtEntityEvent;
+import org.bukkit.event.player.PlayerInteractEntityEvent;
+import org.bukkit.event.player.PlayerItemHeldEvent;
+import org.bukkit.event.player.PlayerKickEvent;
+import org.bukkit.event.player.PlayerMoveEvent;
+import org.bukkit.event.player.PlayerResourcePackStatusEvent;
+import org.bukkit.event.player.PlayerSwapHandItemsEvent;
+import org.bukkit.event.player.PlayerTeleportEvent;
+import org.bukkit.event.player.PlayerToggleFlightEvent;
+import org.bukkit.event.player.PlayerToggleSneakEvent;
+import org.bukkit.event.player.PlayerToggleSprintEvent;
+import org.bukkit.inventory.CraftingInventory;
+import org.bukkit.inventory.EquipmentSlot;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.SmithingInventory;
+// CraftBukkit end
+
 public class ServerGamePacketListenerImpl implements ServerPlayerConnection, TickablePacketListener, ServerGamePacketListener {
+
     static final Logger LOGGER = LogUtils.getLogger();
     private static final int LATENCY_CHECK_INTERVAL = 15000;
     public static final double MAX_INTERACTION_DISTANCE = Mth.square(6.0D);
@@ -191,10 +257,14 @@
     public ServerPlayer player;
     private int tickCount;
     private int ackBlockChangesUpTo = -1;
-    private long keepAliveTime;
+    private long keepAliveTime = Util.getMillis();
     private boolean keepAlivePending;
     private long keepAliveChallenge;
-    private int chatSpamTickCount;
+    // CraftBukkit start - multithreaded fields
+    private final AtomicInteger chatSpamTickCount = new AtomicInteger();
+    private final java.util.concurrent.atomic.AtomicInteger tabSpamLimiter = new java.util.concurrent.atomic.AtomicInteger(); // Paper - configurable tab spam limits
+    private final java.util.concurrent.atomic.AtomicInteger recipeSpamPackets =  new java.util.concurrent.atomic.AtomicInteger(); // Paper - auto recipe limit
+    // CraftBukkit end
     private int dropSpamTickCount;
     private double firstGoodX;
     private double firstGoodY;
@@ -222,12 +292,18 @@
     private int knownMovePacketCount;
     private final ChatPreviewCache chatPreviewCache = new ChatPreviewCache();
     private final ChatPreviewThrottler chatPreviewThrottler = new ChatPreviewThrottler();
-    private final AtomicReference<Instant> lastChatTimeStamp = new AtomicReference<>(Instant.EPOCH);
+    private final AtomicReference<Instant> lastChatTimeStamp;
     private final SignedMessageChain.Decoder signedMessageDecoder;
-    private final LastSeenMessagesValidator lastSeenMessagesValidator = new LastSeenMessagesValidator();
+    private final LastSeenMessagesValidator lastSeenMessagesValidator;
     private final FutureChain chatMessageChain;
+    private static final long KEEPALIVE_LIMIT = Long.getLong("paper.playerconnection.keepalive", 30) * 1000; // Paper - provide property to set keepalive limit
+    private static final int MAX_SIGN_LINE_LENGTH = Integer.getInteger("Paper.maxSignLength", 80); // Paper
+
+    private String clientBrandName = null; // Paper - Brand name
 
     public ServerGamePacketListenerImpl(MinecraftServer server, Connection connection, ServerPlayer player) {
+        this.lastChatTimeStamp = new AtomicReference(Instant.EPOCH);
+        this.lastSeenMessagesValidator = new LastSeenMessagesValidator();
         this.server = server;
         this.connection = connection;
         connection.setListener(this);
@@ -235,18 +311,48 @@
         player.connection = this;
         this.keepAliveTime = Util.getMillis();
         player.getTextFilter().join();
-        ProfilePublicKey profilePublicKey = player.getProfilePublicKey();
-        if (profilePublicKey != null) {
+        ProfilePublicKey profilepublickey = player.getProfilePublicKey();
+
+        if (profilepublickey != null) {
             this.signedMessageDecoder = (new SignedMessageChain()).decoder();
         } else {
             this.signedMessageDecoder = SignedMessageChain.Decoder.UNSIGNED;
         }
 
-        this.chatMessageChain = new FutureChain(server);
+        this.chatMessageChain = new FutureChain(server.chatExecutor); // CraftBukkit - async chat
+
+        // CraftBukkit start - add fields and methods
+        this.cserver = server.server;
+    }
+
+    private final org.bukkit.craftbukkit.v1_19_R1.CraftServer cserver;
+    public boolean processedDisconnect;
+    private int lastTick = MinecraftServer.currentTick;
+    private int allowedPlayerTicks = 1;
+    private int lastDropTick = MinecraftServer.currentTick;
+    private int lastBookTick  = MinecraftServer.currentTick;
+    private int dropCount = 0;
+
+    // Get position of last block hit for BlockDamageLevel.STOPPED
+    private double lastPosX = Double.MAX_VALUE;
+    private double lastPosY = Double.MAX_VALUE;
+    private double lastPosZ = Double.MAX_VALUE;
+    private float lastPitch = Float.MAX_VALUE;
+    private float lastYaw = Float.MAX_VALUE;
+    private boolean justTeleported = false;
+    private boolean hasMoved; // Spigot
+
+    public CraftPlayer getCraftPlayer() {
+        return (this.player == null) ? null : (CraftPlayer) this.player.getBukkitEntity();
     }
+    // CraftBukkit end
 
     @Override
     public void tick() {
+        // Paper start - login async
+        // Don't tick if not valid (dead), otherwise we load chunks below
+        if (this.player.valid) {
+        // Paper end
         if (this.ackBlockChangesUpTo > -1) {
             this.send(new ClientboundBlockChangedAckPacket(this.ackBlockChangesUpTo));
             this.ackBlockChangesUpTo = -1;
@@ -262,8 +368,8 @@
         this.knownMovePacketCount = this.receivedMovePacketCount;
         if (this.clientIsFloating && !this.player.isSleeping() && !this.player.isPassenger()) {
             if (++this.aboveGroundTickCount > 80) {
-                LOGGER.warn("{} was kicked for floating too long!", (Object)this.player.getName().getString());
-                this.disconnect(Component.translatable("multiplayer.disconnect.flying"));
+                ServerGamePacketListenerImpl.LOGGER.warn("{} was kicked for floating too long!", this.player.getName().getString());
+                this.disconnect(io.papermc.paper.configuration.GlobalConfiguration.get().messages.kick.flyingPlayer, org.bukkit.event.player.PlayerKickEvent.Cause.FLYING_PLAYER); // Paper - use configurable kick message & kick event cause
                 return;
             }
         } else {
@@ -281,8 +387,8 @@
             this.vehicleLastGoodZ = this.lastVehicle.getZ();
             if (this.clientVehicleIsFloating && this.player.getRootVehicle().getControllingPassenger() == this.player) {
                 if (++this.aboveGroundVehicleTickCount > 80) {
-                    LOGGER.warn("{} was kicked for floating a vehicle too long!", (Object)this.player.getName().getString());
-                    this.disconnect(Component.translatable("multiplayer.disconnect.flying"));
+                    ServerGamePacketListenerImpl.LOGGER.warn("{} was kicked for floating a vehicle too long!", this.player.getName().getString());
+                    this.disconnect(io.papermc.paper.configuration.GlobalConfiguration.get().messages.kick.flyingVehicle, org.bukkit.event.player.PlayerKickEvent.Cause.FLYING_VEHICLE); // Paper - use configurable kick message & kick event cause
                     return;
                 }
             } else {
@@ -293,32 +399,48 @@
             this.lastVehicle = null;
             this.clientVehicleIsFloating = false;
             this.aboveGroundVehicleTickCount = 0;
-        }
+        }} // Paper - end if (valid)
 
         this.server.getProfiler().push("keepAlive");
-        long l = Util.getMillis();
-        if (l - this.keepAliveTime >= 15000L) {
-            if (this.keepAlivePending) {
-                this.disconnect(Component.translatable("disconnect.timeout"));
-            } else {
+        // Paper Start - give clients a longer time to respond to pings as per pre 1.12.2 timings
+        // This should effectively place the keepalive handling back to "as it was" before 1.12.2
+        long currentTime = Util.getMillis();
+        long elapsedTime = currentTime - this.keepAliveTime;
+
+        if (this.keepAlivePending) {
+            if (!this.processedDisconnect && elapsedTime >= KEEPALIVE_LIMIT) { // check keepalive limit, don't fire if already disconnected
+                ServerGamePacketListenerImpl.LOGGER.warn("{} was kicked due to keepalive timeout!", this.player.getScoreboardName()); // more info
+                this.disconnect(Component.translatable("disconnect.timeout", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause
+            }
+        } else {
+            if (elapsedTime >= 15000L) { // 15 seconds
                 this.keepAlivePending = true;
-                this.keepAliveTime = l;
-                this.keepAliveChallenge = l;
+                this.keepAliveTime = currentTime;
+                this.keepAliveChallenge = currentTime;
                 this.send(new ClientboundKeepAlivePacket(this.keepAliveChallenge));
             }
         }
+        // Paper end
 
         this.server.getProfiler().pop();
+        // CraftBukkit start
+        for (int spam; (spam = this.chatSpamTickCount.get()) > 0 && !this.chatSpamTickCount.compareAndSet(spam, spam - 1); ) ;
+        if (tabSpamLimiter.get() > 0) tabSpamLimiter.getAndDecrement(); // Paper - split to seperate variable
+        if (recipeSpamPackets.get() > 0) recipeSpamPackets.getAndDecrement(); // Paper
+        /* Use thread-safe field access instead
         if (this.chatSpamTickCount > 0) {
             --this.chatSpamTickCount;
         }
+        */
+        // CraftBukkit end
 
         if (this.dropSpamTickCount > 0) {
             --this.dropSpamTickCount;
         }
 
-        if (this.player.getLastActionTime() > 0L && this.server.getPlayerIdleTimeout() > 0 && Util.getMillis() - this.player.getLastActionTime() > (long)(this.server.getPlayerIdleTimeout() * 1000 * 60)) {
-            this.disconnect(Component.translatable("multiplayer.disconnect.idling"));
+        if (this.player.getLastActionTime() > 0L && this.server.getPlayerIdleTimeout() > 0 && Util.getMillis() - this.player.getLastActionTime() > (long) (this.server.getPlayerIdleTimeout() * 1000 * 60) && !this.player.wonGame) { // Paper - Prevent AFK kick while watching end credits.
+            this.player.resetLastActionTime(); // CraftBukkit - SPIGOT-854
+            this.disconnect(Component.translatable("multiplayer.disconnect.idling"), org.bukkit.event.player.PlayerKickEvent.Cause.IDLING); // Paper - kick event cause
         }
 
         this.chatPreviewThrottler.tick();
@@ -342,21 +464,89 @@
         return this.server.isSingleplayerOwner(this.player.getGameProfile());
     }
 
-    public void disconnect(Component reason) {
-        this.connection.send(new ClientboundDisconnectPacket(reason), PacketSendListener.thenRun(() -> {
-            this.connection.disconnect(reason);
+    @io.papermc.paper.annotation.DoNotUse // Paper
+    public void disconnect(String s) {
+        // Paper start
+        this.disconnect(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(s), org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN);
+    }
+
+    public void disconnect(String s, PlayerKickEvent.Cause cause) {
+        this.disconnect(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(s), cause);
+    }
+
+    @io.papermc.paper.annotation.DoNotUse // Paper
+    public void disconnect(final Component reason) {
+        this.disconnect(PaperAdventure.asAdventure(reason), org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN);
+    }
+
+    public void disconnect(final Component reason, PlayerKickEvent.Cause cause) {
+        this.disconnect(PaperAdventure.asAdventure(reason), cause);
+    }
+
+    public void disconnect(net.kyori.adventure.text.Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
+        // Paper end
+        // CraftBukkit start - fire PlayerKickEvent
+        if (this.processedDisconnect) {
+            return;
+        }
+        if (!this.cserver.isPrimaryThread()) {
+            Waitable waitable = new Waitable() {
+                @Override
+                protected Object evaluate() {
+                    ServerGamePacketListenerImpl.this.disconnect(reason, cause); // Paper - adventure
+                    return null;
+                }
+            };
+
+            this.server.processQueue.add(waitable);
+
+            try {
+                waitable.get();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            } catch (ExecutionException e) {
+                throw new RuntimeException(e);
+            }
+            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
+
+        PlayerKickEvent event = new PlayerKickEvent(this.player.getBukkitEntity(), reason, leaveMessage, cause); // Paper - Adventure & kick event reason
+
+        if (this.cserver.getServer().isRunning()) {
+            this.cserver.getPluginManager().callEvent(event);
+        }
+
+        if (event.isCancelled()) {
+            // Do not kick the player
+            return;
+        }
+        // Send the possibly modified leave message
+        final Component ichatbasecomponent = PaperAdventure.asVanilla(event.reason()); // Paper - Adventure
+        // CraftBukkit end
+
+        this.player.quitReason = org.bukkit.event.player.PlayerQuitEvent.QuitReason.KICKED; // Paper
+        this.connection.send(new ClientboundDisconnectPacket(ichatbasecomponent), PacketSendListener.thenRun(() -> {
+            this.connection.disconnect(ichatbasecomponent);
         }));
+        this.onDisconnect(ichatbasecomponent, event.leaveMessage()); // CraftBukkit - fire quit instantly // Paper - use kick event leave message
         this.connection.setReadOnly();
-        this.server.executeBlocking(this.connection::handleDisconnection);
+        MinecraftServer minecraftserver = this.server;
+        Connection networkmanager = this.connection;
+
+        Objects.requireNonNull(this.connection);
+        // CraftBukkit - Don't wait
+        minecraftserver.scheduleOnMain(networkmanager::handleDisconnection); // Paper
     }
 
     private <T, R> CompletableFuture<R> filterTextPacket(T text, BiFunction<TextFilter, T, CompletableFuture<R>> filterer) {
-        return filterer.apply(this.player.getTextFilter(), text).thenApply((filtered) -> {
+        return ((CompletableFuture) filterer.apply(this.player.getTextFilter(), text)).thenApply((object) -> {
             if (!this.getConnection().isConnected()) {
-                LOGGER.debug("Ignoring packet due to disconnection");
+                ServerGamePacketListenerImpl.LOGGER.debug("Ignoring packet due to disconnection");
                 throw new CancellationException("disconnected");
             } else {
-                return filtered;
+                return object;
             }
         });
     }
@@ -390,62 +580,203 @@
     @Override
     public void handleMoveVehicle(ServerboundMoveVehiclePacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
-        if (containsInvalidValues(packet.getX(), packet.getY(), packet.getZ(), packet.getYRot(), packet.getXRot())) {
-            this.disconnect(Component.translatable("multiplayer.disconnect.invalid_vehicle_movement"));
+        if (ServerGamePacketListenerImpl.containsInvalidValues(packet.getX(), packet.getY(), packet.getZ(), packet.getYRot(), packet.getXRot())) {
+            this.disconnect(Component.translatable("multiplayer.disconnect.invalid_vehicle_movement"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_VEHICLE_MOVEMENT); // Paper - kick event cause
         } else {
             Entity entity = this.player.getRootVehicle();
+
+            // Paper start
+            if (this.awaitingPositionFromClient != null || this.player.isImmobile() || entity.isRemoved()) {
+                return;
+            }
+            // Paper end
+
             if (entity != this.player && entity.getControllingPassenger() == this.player && entity == this.lastVehicle) {
-                ServerLevel serverLevel = this.player.getLevel();
-                double d = entity.getX();
-                double e = entity.getY();
-                double f = entity.getZ();
-                double g = clampHorizontal(packet.getX());
-                double h = clampVertical(packet.getY());
-                double i = clampHorizontal(packet.getZ());
-                float j = Mth.wrapDegrees(packet.getYRot());
-                float k = Mth.wrapDegrees(packet.getXRot());
-                double l = g - this.vehicleFirstGoodX;
-                double m = h - this.vehicleFirstGoodY;
-                double n = i - this.vehicleFirstGoodZ;
-                double o = entity.getDeltaMovement().lengthSqr();
-                double p = l * l + m * m + n * n;
-                if (p - o > 100.0D && !this.isSingleplayerOwner()) {
-                    LOGGER.warn("{} (vehicle of {}) moved too quickly! {},{},{}", entity.getName().getString(), this.player.getName().getString(), l, m, n);
+                ServerLevel worldserver = this.player.getLevel();
+                double d0 = entity.getX();final double fromX = d0; // Paper - OBFHELPER
+                double d1 = entity.getY();final double fromY = d1; // Paper - OBFHELPER
+                double d2 = entity.getZ();final double fromZ = d2; // Paper - OBFHELPER
+                double d3 = ServerGamePacketListenerImpl.clampHorizontal(packet.getX()); final double toX = d3; // Paper - OBFHELPER
+                double d4 = ServerGamePacketListenerImpl.clampVertical(packet.getY()); final double toY = d4; // Paper - OBFHELPER
+                double d5 = ServerGamePacketListenerImpl.clampHorizontal(packet.getZ()); final double toZ = d5; // Paper - OBFHELPER
+                float f = Mth.wrapDegrees(packet.getYRot());
+                float f1 = Mth.wrapDegrees(packet.getXRot());
+                double d6 = d3 - this.vehicleFirstGoodX;
+                double d7 = d4 - this.vehicleFirstGoodY;
+                double d8 = d5 - this.vehicleFirstGoodZ;
+                double d9 = entity.getDeltaMovement().lengthSqr();
+                // Paper start - fix large move vectors killing the server
+                double currDeltaX = toX - fromX;
+                double currDeltaY = toY - fromY;
+                double currDeltaZ = toZ - fromZ;
+                double d10 = Math.max(d6 * d6 + d7 * d7 + d8 * d8, (currDeltaX * currDeltaX + currDeltaY * currDeltaY + currDeltaZ * currDeltaZ) - 1);
+                // Paper end - fix large move vectors killing the server
+
+                // Paper start - fix large move vectors killing the server
+                double otherFieldX = d3 - this.vehicleLastGoodX;
+                double otherFieldY = d4 - this.vehicleLastGoodY - 1.0E-6D;
+                double otherFieldZ = d5 - this.vehicleLastGoodZ;
+                d10 = Math.max(d10, (otherFieldX * otherFieldX + otherFieldY * otherFieldY + otherFieldZ * otherFieldZ) - 1);
+                // Paper end - fix large move vectors killing the server
+
+                // CraftBukkit start - handle custom speeds and skipped ticks
+                this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick;
+                this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1);
+                this.lastTick = (int) (System.currentTimeMillis() / 50);
+
+                ++this.receivedMovePacketCount;
+                int i = this.receivedMovePacketCount - this.knownMovePacketCount;
+                if (i > Math.max(this.allowedPlayerTicks, 5)) {
+                    ServerGamePacketListenerImpl.LOGGER.debug(this.player.getScoreboardName() + " is sending move packets too frequently (" + i + " packets since last tick)");
+                    i = 1;
+                }
+
+                if (d10 > 0) {
+                    this.allowedPlayerTicks -= 1;
+                } else {
+                    this.allowedPlayerTicks = 20;
+                }
+                double speed;
+                if (this.player.getAbilities().flying) {
+                    speed = this.player.getAbilities().flyingSpeed * 20f;
+                } else {
+                    speed = this.player.getAbilities().walkingSpeed * 10f;
+                }
+                speed *= 2f; // TODO: Get the speed of the vehicle instead of the player
+
+                // Paper start - Prevent moving into unloaded chunks
+                if (player.level.paperConfig().chunks.preventMovingIntoUnloadedChunks && (
+                    !worldserver.areChunksLoadedForMove(this.player.getBoundingBox().expandTowards(new Vec3(toX, toY, toZ).subtract(this.player.position()))) ||
+                        !worldserver.areChunksLoadedForMove(entity.getBoundingBox().expandTowards(new Vec3(toX, toY, toZ).subtract(entity.position())))
+                    )) {
                     this.connection.send(new ClientboundMoveVehiclePacket(entity));
                     return;
                 }
+                // Paper end
 
-                boolean bl = serverLevel.noCollision(entity, entity.getBoundingBox().deflate(0.0625D));
-                l = g - this.vehicleLastGoodX;
-                m = h - this.vehicleLastGoodY - 1.0E-6D;
-                n = i - this.vehicleLastGoodZ;
-                boolean bl2 = entity.verticalCollisionBelow;
-                entity.move(MoverType.PLAYER, new Vec3(l, m, n));
-                l = g - entity.getX();
-                m = h - entity.getY();
-                if (m > -0.5D || m < 0.5D) {
-                    m = 0.0D;
+                if (d10 - d9 > Math.max(100.0D, Math.pow((double) (org.spigotmc.SpigotConfig.movedTooQuicklyMultiplier * (float) i * speed), 2)) && !this.isSingleplayerOwner()) {
+                // CraftBukkit end
+                    ServerGamePacketListenerImpl.LOGGER.warn("{} (vehicle of {}) moved too quickly! {},{},{}", new Object[]{entity.getName().getString(), this.player.getName().getString(), d6, d7, d8});
+                    this.connection.send(new ClientboundMoveVehiclePacket(entity));
+                    return;
                 }
 
-                n = i - entity.getZ();
-                p = l * l + m * m + n * n;
-                boolean bl3 = false;
-                if (p > 0.0625D) {
-                    bl3 = true;
-                    LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", entity.getName().getString(), this.player.getName().getString(), Math.sqrt(p));
+                AABB oldBox = entity.getBoundingBox(); // Paper - copy from player movement packet
+
+                d6 = d3 - this.vehicleLastGoodX; // Paper - diff on change, used for checking large move vectors above
+                d7 = d4 - this.vehicleLastGoodY - 1.0E-6D; // Paper - diff on change, used for checking large move vectors above
+                d8 = d5 - this.vehicleLastGoodZ; // Paper - diff on change, used for checking large move vectors above
+                boolean flag1 = entity.verticalCollisionBelow;
+
+                entity.move(MoverType.PLAYER, new Vec3(d6, d7, d8));
+                boolean didCollide = toX != entity.getX() || toY != entity.getY() || toZ != entity.getZ(); // Paper - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be...
+                double d11 = d7;
+
+                d6 = d3 - entity.getX();
+                d7 = d4 - entity.getY();
+                if (d7 > -0.5D || d7 < 0.5D) {
+                    d7 = 0.0D;
                 }
 
-                entity.absMoveTo(g, h, i, j, k);
-                boolean bl4 = serverLevel.noCollision(entity, entity.getBoundingBox().deflate(0.0625D));
-                if (bl && (bl3 || !bl4)) {
-                    entity.absMoveTo(d, e, f, j, k);
+                d8 = d5 - entity.getZ();
+                d10 = d6 * d6 + d7 * d7 + d8 * d8;
+                boolean flag2 = false;
+
+                if (d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot
+                    flag2 = true; // Paper - diff on change, this should be moved wrongly
+                    ServerGamePacketListenerImpl.LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", new Object[]{entity.getName().getString(), this.player.getName().getString(), Math.sqrt(d10)});
+                }
+                Location curPos = this.getCraftPlayer().getLocation(); // Spigot
+
+                entity.absMoveTo(d3, d4, d5, f, f1);
+                this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit
+
+                // Paper start - optimise out extra getCubes
+                boolean teleportBack = flag2; // violating this is always a fail
+                if (!teleportBack) {
+                    // note: only call after setLocation, or else getBoundingBox is wrong
+                    AABB newBox = entity.getBoundingBox();
+                    if (didCollide || !oldBox.equals(newBox)) {
+                        teleportBack = this.hasNewCollision(worldserver, entity, oldBox, newBox);
+                    } // else: no collision at all detected, why do we care?
+                }
+                if (teleportBack) { // Paper end - optimise out extra getCubes
+                    entity.absMoveTo(d0, d1, d2, f, f1);
+                    this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit
                     this.connection.send(new ClientboundMoveVehiclePacket(entity));
                     return;
                 }
 
+                // CraftBukkit start - fire PlayerMoveEvent
+                Player player = this.getCraftPlayer();
+                // Spigot Start
+                if ( !this.hasMoved )
+                {
+                    this.lastPosX = curPos.getX();
+                    this.lastPosY = curPos.getY();
+                    this.lastPosZ = curPos.getZ();
+                    this.lastYaw = curPos.getYaw();
+                    this.lastPitch = curPos.getPitch();
+                    this.hasMoved = true;
+                }
+                // Spigot End
+                Location from = new Location(player.getWorld(), this.lastPosX, this.lastPosY, this.lastPosZ, this.lastYaw, this.lastPitch); // Get the Players previous Event location.
+                Location to = player.getLocation().clone(); // Start off the To location as the Players current location.
+
+                // If the packet contains movement information then we update the To location with the correct XYZ.
+                to.setX(packet.getX());
+                to.setY(packet.getY());
+                to.setZ(packet.getZ());
+
+
+                // If the packet contains look information then we update the To location with the correct Yaw & Pitch.
+                to.setYaw(packet.getYRot());
+                to.setPitch(packet.getXRot());
+
+                // Prevent 40 event-calls for less than a single pixel of movement >.>
+                double delta = Math.pow(this.lastPosX - to.getX(), 2) + Math.pow(this.lastPosY - to.getY(), 2) + Math.pow(this.lastPosZ - to.getZ(), 2);
+                float deltaAngle = Math.abs(this.lastYaw - to.getYaw()) + Math.abs(this.lastPitch - to.getPitch());
+
+                if ((delta > 1f / 256 || deltaAngle > 10f) && !this.player.isImmobile()) {
+                    this.lastPosX = to.getX();
+                    this.lastPosY = to.getY();
+                    this.lastPosZ = to.getZ();
+                    this.lastYaw = to.getYaw();
+                    this.lastPitch = to.getPitch();
+
+                    // Skip the first time we do this
+                    if (true) { // Spigot - don't skip any move events
+                        Location oldTo = to.clone();
+                        PlayerMoveEvent event = new PlayerMoveEvent(player, from, to);
+                        this.cserver.getPluginManager().callEvent(event);
+
+                        // If the event is cancelled we move the player back to their old location.
+                        if (event.isCancelled()) {
+                            this.teleport(from);
+                            return;
+                        }
+
+                        // If a Plugin has changed the To destination then we teleport the Player
+                        // there to avoid any 'Moved wrongly' or 'Moved too quickly' errors.
+                        // We only do this if the Event was not cancelled.
+                        if (!oldTo.equals(event.getTo()) && !event.isCancelled()) {
+                            this.player.getBukkitEntity().teleport(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN);
+                            return;
+                        }
+
+                        // Check to see if the Players Location has some how changed during the call of the event.
+                        // This can happen due to a plugin teleporting the player instead of using .setTo()
+                        if (!from.equals(this.getCraftPlayer().getLocation()) && this.justTeleported) {
+                            this.justTeleported = false;
+                            return;
+                        }
+                    }
+                }
+                // CraftBukkit end
+
                 this.player.getLevel().getChunkSource().move(this.player);
-                this.player.checkMovementStatistics(this.player.getX() - d, this.player.getY() - e, this.player.getZ() - f);
-                this.clientVehicleIsFloating = m >= -0.03125D && !bl2 && !this.server.isFlightAllowed() && !entity.isNoGravity() && this.noBlocksAround(entity);
+                this.player.checkMovementStatistics(this.player.getX() - d0, this.player.getY() - d1, this.player.getZ() - d2);
+                this.clientVehicleIsFloating = d11 >= -0.03125D && !flag1 && !this.server.isFlightAllowed() && !entity.isNoGravity() && this.noBlocksAround(entity);
                 this.vehicleLastGoodX = entity.getX();
                 this.vehicleLastGoodY = entity.getY();
                 this.vehicleLastGoodZ = entity.getZ();
@@ -455,7 +786,32 @@
     }
 
     private boolean noBlocksAround(Entity entity) {
-        return entity.level.getBlockStates(entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D)).allMatch(BlockBehaviour.BlockStateBase::isAir);
+        // Paper start - stop using streams, this is already a known fixed problem in Entity#move
+        AABB box = entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D);
+        int minX = Mth.floor(box.minX);
+        int minY = Mth.floor(box.minY);
+        int minZ = Mth.floor(box.minZ);
+        int maxX = Mth.floor(box.maxX);
+        int maxY = Mth.floor(box.maxY);
+        int maxZ = Mth.floor(box.maxZ);
+
+        Level world = entity.level;
+        BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
+
+        for (int y = minY; y <= maxY; ++y) {
+            for (int z = minZ; z <= maxZ; ++z) {
+                for (int x = minX; x <= maxX; ++x) {
+                    pos.set(x, y, z);
+                    BlockState type = world.getBlockStateIfLoaded(pos);
+                    if (type != null && !type.isAir()) {
+                        return false;
+                    }
+                }
+            }
+        }
+
+        return true;
+        // Paper end - stop using streams, this is already a known fixed problem in Entity#move
     }
 
     @Override
@@ -463,11 +819,11 @@
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
         if (packet.getId() == this.awaitingTeleport) {
             if (this.awaitingPositionFromClient == null) {
-                this.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_movement"));
+                this.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_movement"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PLAYER_MOVEMENT); // Paper - kick event cause
                 return;
             }
 
-            this.player.absMoveTo(this.awaitingPositionFromClient.x, this.awaitingPositionFromClient.y, this.awaitingPositionFromClient.z, this.player.getYRot(), this.player.getXRot());
+            this.player.moveTo(this.awaitingPositionFromClient.x, this.awaitingPositionFromClient.y, this.awaitingPositionFromClient.z, this.player.getYRot(), this.player.getXRot()); // Paper - use proper moveTo for teleportation
             this.lastGoodX = this.awaitingPositionFromClient.x;
             this.lastGoodY = this.awaitingPositionFromClient.y;
             this.lastGoodZ = this.awaitingPositionFromClient.z;
@@ -476,6 +832,7 @@
             }
 
             this.awaitingPositionFromClient = null;
+            this.player.getLevel().getChunkSource().move(this.player); // CraftBukkit
         }
 
     }
@@ -483,7 +840,11 @@
     @Override
     public void handleRecipeBookSeenRecipePacket(ServerboundRecipeBookSeenRecipePacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
-        this.server.getRecipeManager().byKey(packet.getRecipe()).ifPresent(this.player.getRecipeBook()::removeHighlight);
+        Optional<? extends Recipe<?>> optional = this.server.getRecipeManager().byKey(packet.getRecipe()); // CraftBukkit - decompile error
+        ServerRecipeBook recipebookserver = this.player.getRecipeBook();
+
+        Objects.requireNonNull(recipebookserver);
+        optional.ifPresent(recipebookserver::removeHighlight);
     }
 
     @Override
@@ -496,8 +857,9 @@
     public void handleSeenAdvancements(ServerboundSeenAdvancementsPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
         if (packet.getAction() == ServerboundSeenAdvancementsPacket.Action.OPENED_TAB) {
-            ResourceLocation resourceLocation = packet.getTab();
-            Advancement advancement = this.server.getAdvancements().getAdvancement(resourceLocation);
+            ResourceLocation minecraftkey = packet.getTab();
+            Advancement advancement = this.server.getAdvancements().getAdvancement(minecraftkey);
+
             if (advancement != null) {
                 this.player.getAdvancements().setSelectedTab(advancement);
             }
@@ -505,18 +867,80 @@
 
     }
 
+    // Paper start
+    private static final java.util.concurrent.ExecutorService TAB_COMPLETE_EXECUTOR = java.util.concurrent.Executors.newFixedThreadPool(4,
+        new com.google.common.util.concurrent.ThreadFactoryBuilder().setDaemon(true).setNameFormat("Async Tab Complete Thread - #%d").setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(net.minecraft.server.MinecraftServer.LOGGER)).build());
+    // Paper end
     @Override
     public void handleCustomCommandSuggestions(ServerboundCommandSuggestionPacket packet) {
-        PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
-        StringReader stringReader = new StringReader(packet.getCommand());
-        if (stringReader.canRead() && stringReader.peek() == '/') {
-            stringReader.skip();
+        // PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); // Paper - run this async
+        // CraftBukkit start
+        if (this.chatSpamTickCount.addAndGet(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamIncrement) > io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamLimit && !this.server.getPlayerList().isOp(this.player.getGameProfile())) { // Paper start - split and make configurable
+            server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause
+            return;
         }
+        // Paper start
+        String str = packet.getCommand(); int index = -1;
+        if (str.length() > 64 && ((index = str.indexOf(' ')) == -1 || index >= 64)) {
+            server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause
+            return;
+        }
+        // Paper end
+        // CraftBukkit end
+        // Paper start - Don't suggest if tab-complete is disabled
+        if (org.spigotmc.SpigotConfig.tabComplete < 0) {
+            return;
+        }
+        // Paper end - Don't suggest if tab-complete is disabled
+        // Paper start - async tab completion
+        TAB_COMPLETE_EXECUTOR.execute(() -> {
+        StringReader stringreader = new StringReader(packet.getCommand());
 
-        ParseResults<CommandSourceStack> parseResults = this.server.getCommands().getDispatcher().parse(stringReader, this.player.createCommandSourceStack());
-        this.server.getCommands().getDispatcher().getCompletionSuggestions(parseResults).thenAccept((suggestions) -> {
-            this.connection.send(new ClientboundCommandSuggestionsPacket(packet.getId(), suggestions));
+        if (stringreader.canRead() && stringreader.peek() == '/') {
+            stringreader.skip();
+        }
+            final String command = packet.getCommand();
+            final com.destroystokyo.paper.event.server.AsyncTabCompleteEvent event = new com.destroystokyo.paper.event.server.AsyncTabCompleteEvent(this.getCraftPlayer(), command, true, null);
+            event.callEvent();
+            final java.util.List<com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion> completions = event.isCancelled() ? com.google.common.collect.ImmutableList.of() : event.completions();
+            // If the event isn't handled, we can assume that we have no completions, and so we'll ask the server
+            if (!event.isHandled()) {
+                if (!event.isCancelled()) {
+
+                    this.server.scheduleOnMain(() -> { // This needs to be on main
+                        ParseResults<CommandSourceStack> parseresults = this.server.getCommands().getDispatcher().parse(stringreader, this.player.createCommandSourceStack());
+
+                        this.server.getCommands().getDispatcher().getCompletionSuggestions(parseresults).thenAccept((suggestions) -> {
+                            // Paper start - Brigadier API
+                            com.destroystokyo.paper.event.brigadier.AsyncPlayerSendSuggestionsEvent suggestEvent = new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendSuggestionsEvent(this.getCraftPlayer(), suggestions, command);
+                            suggestEvent.setCancelled(suggestions.isEmpty());
+                            if (!suggestEvent.callEvent()) return;
+                            this.connection.send(new ClientboundCommandSuggestionsPacket(packet.getId(), suggestEvent.getSuggestions()));
+                            // Paper end - Brigadier API
+                        });
+                    });
+                }
+            } else if (!completions.isEmpty()) {
+                final com.mojang.brigadier.suggestion.SuggestionsBuilder builder0 = new com.mojang.brigadier.suggestion.SuggestionsBuilder(command, stringreader.getTotalLength());
+                final com.mojang.brigadier.suggestion.SuggestionsBuilder builder = builder0.createOffset(builder0.getInput().lastIndexOf(' ') + 1);
+                completions.forEach(completion -> {
+                    final Integer intSuggestion = com.google.common.primitives.Ints.tryParse(completion.suggestion());
+                    if (intSuggestion != null) {
+                        builder.suggest(intSuggestion, PaperAdventure.asVanilla(completion.tooltip()));
+                    } else {
+                        builder.suggest(completion.suggestion(), PaperAdventure.asVanilla(completion.tooltip()));
+                    }
+                });
+                // Paper start - Brigadier API
+                com.mojang.brigadier.suggestion.Suggestions suggestions = builder.buildFuture().join();
+                com.destroystokyo.paper.event.brigadier.AsyncPlayerSendSuggestionsEvent suggestEvent = new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendSuggestionsEvent(this.getCraftPlayer(), suggestions, command);
+                suggestEvent.setCancelled(suggestions.isEmpty());
+                if (!suggestEvent.callEvent()) return;
+                this.connection.send(new ClientboundCommandSuggestionsPacket(packet.getId(), suggestEvent.getSuggestions()));
+                // Paper end - Brigadier API
+            }
         });
+        // Paper end - async tab completion
     }
 
     @Override
@@ -524,58 +948,62 @@
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
         if (!this.server.isCommandBlockEnabled()) {
             this.player.sendSystemMessage(Component.translatable("advMode.notEnabled"));
-        } else if (!this.player.canUseGameMasterBlocks()) {
+        } else if (!this.player.canUseGameMasterBlocks() && (!this.player.isCreative() || !this.player.getBukkitEntity().hasPermission("minecraft.commandblock"))) { // Paper - command block permission
             this.player.sendSystemMessage(Component.translatable("advMode.notAllowed"));
         } else {
-            BaseCommandBlock baseCommandBlock = null;
-            CommandBlockEntity commandBlockEntity = null;
-            BlockPos blockPos = packet.getPos();
-            BlockEntity blockEntity = this.player.level.getBlockEntity(blockPos);
-            if (blockEntity instanceof CommandBlockEntity) {
-                commandBlockEntity = (CommandBlockEntity)blockEntity;
-                baseCommandBlock = commandBlockEntity.getCommandBlock();
+            BaseCommandBlock commandblocklistenerabstract = null;
+            CommandBlockEntity tileentitycommand = null;
+            BlockPos blockposition = packet.getPos();
+            BlockEntity tileentity = this.player.level.getBlockEntity(blockposition);
+
+            if (tileentity instanceof CommandBlockEntity) {
+                tileentitycommand = (CommandBlockEntity) tileentity;
+                commandblocklistenerabstract = tileentitycommand.getCommandBlock();
             }
 
-            String string = packet.getCommand();
-            boolean bl = packet.isTrackOutput();
-            if (baseCommandBlock != null) {
-                CommandBlockEntity.Mode mode = commandBlockEntity.getMode();
-                BlockState blockState = this.player.level.getBlockState(blockPos);
-                Direction direction = blockState.getValue(CommandBlock.FACING);
-                BlockState blockState2;
+            String s = packet.getCommand();
+            boolean flag = packet.isTrackOutput();
+
+            if (commandblocklistenerabstract != null) {
+                CommandBlockEntity.Mode tileentitycommand_type = tileentitycommand.getMode();
+                BlockState iblockdata = this.player.level.getBlockState(blockposition);
+                Direction enumdirection = (Direction) iblockdata.getValue(CommandBlock.FACING);
+                BlockState iblockdata1;
+
                 switch (packet.getMode()) {
                     case SEQUENCE:
-                        blockState2 = Blocks.CHAIN_COMMAND_BLOCK.defaultBlockState();
+                        iblockdata1 = Blocks.CHAIN_COMMAND_BLOCK.defaultBlockState();
                         break;
                     case AUTO:
-                        blockState2 = Blocks.REPEATING_COMMAND_BLOCK.defaultBlockState();
+                        iblockdata1 = Blocks.REPEATING_COMMAND_BLOCK.defaultBlockState();
                         break;
                     case REDSTONE:
                     default:
-                        blockState2 = Blocks.COMMAND_BLOCK.defaultBlockState();
+                        iblockdata1 = Blocks.COMMAND_BLOCK.defaultBlockState();
                 }
 
-                BlockState blockState5 = blockState2.setValue(CommandBlock.FACING, direction).setValue(CommandBlock.CONDITIONAL, Boolean.valueOf(packet.isConditional()));
-                if (blockState5 != blockState) {
-                    this.player.level.setBlock(blockPos, blockState5, 2);
-                    blockEntity.setBlockState(blockState5);
-                    this.player.level.getChunkAt(blockPos).setBlockEntity(blockEntity);
+                BlockState iblockdata2 = (BlockState) ((BlockState) iblockdata1.setValue(CommandBlock.FACING, enumdirection)).setValue(CommandBlock.CONDITIONAL, packet.isConditional());
+
+                if (iblockdata2 != iblockdata) {
+                    this.player.level.setBlock(blockposition, iblockdata2, 2);
+                    tileentity.setBlockState(iblockdata2);
+                    this.player.level.getChunkAt(blockposition).setBlockEntity(tileentity);
                 }
 
-                baseCommandBlock.setCommand(string);
-                baseCommandBlock.setTrackOutput(bl);
-                if (!bl) {
-                    baseCommandBlock.setLastOutput((Component)null);
+                commandblocklistenerabstract.setCommand(s);
+                commandblocklistenerabstract.setTrackOutput(flag);
+                if (!flag) {
+                    commandblocklistenerabstract.setLastOutput((Component) null);
                 }
 
-                commandBlockEntity.setAutomatic(packet.isAutomatic());
-                if (mode != packet.getMode()) {
-                    commandBlockEntity.onModeSwitch();
+                tileentitycommand.setAutomatic(packet.isAutomatic());
+                if (tileentitycommand_type != packet.getMode()) {
+                    tileentitycommand.onModeSwitch();
                 }
 
-                baseCommandBlock.onUpdated();
-                if (!StringUtil.isNullOrEmpty(string)) {
-                    this.player.sendSystemMessage(Component.translatable("advMode.setCommand.success", string));
+                commandblocklistenerabstract.onUpdated();
+                if (!StringUtil.isNullOrEmpty(s)) {
+                    this.player.sendSystemMessage(Component.translatable("advMode.setCommand.success", s));
                 }
             }
 
@@ -587,18 +1015,19 @@
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
         if (!this.server.isCommandBlockEnabled()) {
             this.player.sendSystemMessage(Component.translatable("advMode.notEnabled"));
-        } else if (!this.player.canUseGameMasterBlocks()) {
+        } else if (!this.player.canUseGameMasterBlocks() && (!this.player.isCreative() || !this.player.getBukkitEntity().hasPermission("minecraft.commandblock"))) { // Paper - command block permission
             this.player.sendSystemMessage(Component.translatable("advMode.notAllowed"));
         } else {
-            BaseCommandBlock baseCommandBlock = packet.getCommandBlock(this.player.level);
-            if (baseCommandBlock != null) {
-                baseCommandBlock.setCommand(packet.getCommand());
-                baseCommandBlock.setTrackOutput(packet.isTrackOutput());
+            BaseCommandBlock commandblocklistenerabstract = packet.getCommandBlock(this.player.level);
+
+            if (commandblocklistenerabstract != null) {
+                commandblocklistenerabstract.setCommand(packet.getCommand());
+                commandblocklistenerabstract.setTrackOutput(packet.isTrackOutput());
                 if (!packet.isTrackOutput()) {
-                    baseCommandBlock.setLastOutput((Component)null);
+                    commandblocklistenerabstract.setLastOutput((Component) null);
                 }
 
-                baseCommandBlock.onUpdated();
+                commandblocklistenerabstract.onUpdated();
                 this.player.sendSystemMessage(Component.translatable("advMode.setCommand.success", packet.getCommand()));
             }
 
@@ -608,7 +1037,14 @@
     @Override
     public void handlePickItem(ServerboundPickItemPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
-        this.player.getInventory().pickSlot(packet.getSlot());
+        // Paper start - validate pick item position
+        if (!(packet.getSlot() >= 0 && packet.getSlot() < this.player.getInventory().items.size())) {
+            ServerGamePacketListenerImpl.LOGGER.warn("{} tried to set an invalid carried item", this.player.getName().getString());
+            this.disconnect("Invalid hotbar selection (Hacking?)", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause
+            return;
+        }
+        this.player.getInventory().pickSlot(packet.getSlot()); // Paper - Diff above if changed
+        // Paper end
         this.player.connection.send(new ClientboundContainerSetSlotPacket(-2, 0, this.player.getInventory().selected, this.player.getInventory().getItem(this.player.getInventory().selected)));
         this.player.connection.send(new ClientboundContainerSetSlotPacket(-2, 0, packet.getSlot(), this.player.getInventory().getItem(packet.getSlot())));
         this.player.connection.send(new ClientboundSetCarriedItemPacket(this.player.getInventory().selected));
@@ -617,16 +1053,20 @@
     @Override
     public void handleRenameItem(ServerboundRenameItemPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
-        AbstractContainerMenu string = this.player.containerMenu;
-        if (string instanceof AnvilMenu anvilMenu) {
-            if (!anvilMenu.stillValid(this.player)) {
-                LOGGER.debug("Player {} interacted with invalid menu {}", this.player, anvilMenu);
+        AbstractContainerMenu container = this.player.containerMenu;
+
+        if (container instanceof AnvilMenu) {
+            AnvilMenu containeranvil = (AnvilMenu) container;
+
+            if (!containeranvil.stillValid(this.player)) {
+                ServerGamePacketListenerImpl.LOGGER.debug("Player {} interacted with invalid menu {}", this.player, containeranvil);
                 return;
             }
 
-            String string = SharedConstants.filterText(packet.getName());
-            if (string.length() <= 50) {
-                anvilMenu.setItemName(string);
+            String s = SharedConstants.filterText(packet.getName());
+
+            if (s.length() <= 50) {
+                containeranvil.setItemName(s);
             }
         }
 
@@ -635,14 +1075,17 @@
     @Override
     public void handleSetBeaconPacket(ServerboundSetBeaconPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
-        AbstractContainerMenu var3 = this.player.containerMenu;
-        if (var3 instanceof BeaconMenu beaconMenu) {
+        AbstractContainerMenu container = this.player.containerMenu;
+
+        if (container instanceof BeaconMenu) {
+            BeaconMenu containerbeacon = (BeaconMenu) container;
+
             if (!this.player.containerMenu.stillValid(this.player)) {
-                LOGGER.debug("Player {} interacted with invalid menu {}", this.player, this.player.containerMenu);
+                ServerGamePacketListenerImpl.LOGGER.debug("Player {} interacted with invalid menu {}", this.player, this.player.containerMenu);
                 return;
             }
 
-            beaconMenu.updateEffects(packet.getPrimary(), packet.getSecondary());
+            containerbeacon.updateEffects(packet.getPrimary(), packet.getSecondary());
         }
 
     }
@@ -651,42 +1094,45 @@
     public void handleSetStructureBlock(ServerboundSetStructureBlockPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
         if (this.player.canUseGameMasterBlocks()) {
-            BlockPos blockPos = packet.getPos();
-            BlockState blockState = this.player.level.getBlockState(blockPos);
-            BlockEntity blockEntity = this.player.level.getBlockEntity(blockPos);
-            if (blockEntity instanceof StructureBlockEntity) {
-                StructureBlockEntity structureBlockEntity = (StructureBlockEntity)blockEntity;
-                structureBlockEntity.setMode(packet.getMode());
-                structureBlockEntity.setStructureName(packet.getName());
-                structureBlockEntity.setStructurePos(packet.getOffset());
-                structureBlockEntity.setStructureSize(packet.getSize());
-                structureBlockEntity.setMirror(packet.getMirror());
-                structureBlockEntity.setRotation(packet.getRotation());
-                structureBlockEntity.setMetaData(packet.getData());
-                structureBlockEntity.setIgnoreEntities(packet.isIgnoreEntities());
-                structureBlockEntity.setShowAir(packet.isShowAir());
-                structureBlockEntity.setShowBoundingBox(packet.isShowBoundingBox());
-                structureBlockEntity.setIntegrity(packet.getIntegrity());
-                structureBlockEntity.setSeed(packet.getSeed());
-                if (structureBlockEntity.hasStructureName()) {
-                    String string = structureBlockEntity.getStructureName();
+            BlockPos blockposition = packet.getPos();
+            BlockState iblockdata = this.player.level.getBlockState(blockposition);
+            BlockEntity tileentity = this.player.level.getBlockEntity(blockposition);
+
+            if (tileentity instanceof StructureBlockEntity) {
+                StructureBlockEntity tileentitystructure = (StructureBlockEntity) tileentity;
+
+                tileentitystructure.setMode(packet.getMode());
+                tileentitystructure.setStructureName(packet.getName());
+                tileentitystructure.setStructurePos(packet.getOffset());
+                tileentitystructure.setStructureSize(packet.getSize());
+                tileentitystructure.setMirror(packet.getMirror());
+                tileentitystructure.setRotation(packet.getRotation());
+                tileentitystructure.setMetaData(packet.getData());
+                tileentitystructure.setIgnoreEntities(packet.isIgnoreEntities());
+                tileentitystructure.setShowAir(packet.isShowAir());
+                tileentitystructure.setShowBoundingBox(packet.isShowBoundingBox());
+                tileentitystructure.setIntegrity(packet.getIntegrity());
+                tileentitystructure.setSeed(packet.getSeed());
+                if (tileentitystructure.hasStructureName()) {
+                    String s = tileentitystructure.getStructureName();
+
                     if (packet.getUpdateType() == StructureBlockEntity.UpdateType.SAVE_AREA) {
-                        if (structureBlockEntity.saveStructure()) {
-                            this.player.displayClientMessage(Component.translatable("structure_block.save_success", string), false);
+                        if (tileentitystructure.saveStructure()) {
+                            this.player.displayClientMessage(Component.translatable("structure_block.save_success", s), false);
                         } else {
-                            this.player.displayClientMessage(Component.translatable("structure_block.save_failure", string), false);
+                            this.player.displayClientMessage(Component.translatable("structure_block.save_failure", s), false);
                         }
                     } else if (packet.getUpdateType() == StructureBlockEntity.UpdateType.LOAD_AREA) {
-                        if (!structureBlockEntity.isStructureLoadable()) {
-                            this.player.displayClientMessage(Component.translatable("structure_block.load_not_found", string), false);
-                        } else if (structureBlockEntity.loadStructure(this.player.getLevel())) {
-                            this.player.displayClientMessage(Component.translatable("structure_block.load_success", string), false);
+                        if (!tileentitystructure.isStructureLoadable()) {
+                            this.player.displayClientMessage(Component.translatable("structure_block.load_not_found", s), false);
+                        } else if (tileentitystructure.loadStructure(this.player.getLevel())) {
+                            this.player.displayClientMessage(Component.translatable("structure_block.load_success", s), false);
                         } else {
-                            this.player.displayClientMessage(Component.translatable("structure_block.load_prepare", string), false);
+                            this.player.displayClientMessage(Component.translatable("structure_block.load_prepare", s), false);
                         }
                     } else if (packet.getUpdateType() == StructureBlockEntity.UpdateType.SCAN_AREA) {
-                        if (structureBlockEntity.detectSize()) {
-                            this.player.displayClientMessage(Component.translatable("structure_block.size_success", string), false);
+                        if (tileentitystructure.detectSize()) {
+                            this.player.displayClientMessage(Component.translatable("structure_block.size_success", s), false);
                         } else {
                             this.player.displayClientMessage(Component.translatable("structure_block.size_failure"), false);
                         }
@@ -695,8 +1141,8 @@
                     this.player.displayClientMessage(Component.translatable("structure_block.invalid_structure_name", packet.getName()), false);
                 }
 
-                structureBlockEntity.setChanged();
-                this.player.level.sendBlockUpdated(blockPos, blockState, blockState, 3);
+                tileentitystructure.setChanged();
+                this.player.level.sendBlockUpdated(blockposition, iblockdata, iblockdata, 3);
             }
 
         }
@@ -706,18 +1152,20 @@
     public void handleSetJigsawBlock(ServerboundSetJigsawBlockPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
         if (this.player.canUseGameMasterBlocks()) {
-            BlockPos blockPos = packet.getPos();
-            BlockState blockState = this.player.level.getBlockState(blockPos);
-            BlockEntity blockEntity = this.player.level.getBlockEntity(blockPos);
-            if (blockEntity instanceof JigsawBlockEntity) {
-                JigsawBlockEntity jigsawBlockEntity = (JigsawBlockEntity)blockEntity;
-                jigsawBlockEntity.setName(packet.getName());
-                jigsawBlockEntity.setTarget(packet.getTarget());
-                jigsawBlockEntity.setPool(ResourceKey.create(Registry.TEMPLATE_POOL_REGISTRY, packet.getPool()));
-                jigsawBlockEntity.setFinalState(packet.getFinalState());
-                jigsawBlockEntity.setJoint(packet.getJoint());
-                jigsawBlockEntity.setChanged();
-                this.player.level.sendBlockUpdated(blockPos, blockState, blockState, 3);
+            BlockPos blockposition = packet.getPos();
+            BlockState iblockdata = this.player.level.getBlockState(blockposition);
+            BlockEntity tileentity = this.player.level.getBlockEntity(blockposition);
+
+            if (tileentity instanceof JigsawBlockEntity) {
+                JigsawBlockEntity tileentityjigsaw = (JigsawBlockEntity) tileentity;
+
+                tileentityjigsaw.setName(packet.getName());
+                tileentityjigsaw.setTarget(packet.getTarget());
+                tileentityjigsaw.setPool(ResourceKey.create(Registry.TEMPLATE_POOL_REGISTRY, packet.getPool()));
+                tileentityjigsaw.setFinalState(packet.getFinalState());
+                tileentityjigsaw.setJoint(packet.getJoint());
+                tileentityjigsaw.setChanged();
+                this.player.level.sendBlockUpdated(blockposition, iblockdata, iblockdata, 3);
             }
 
         }
@@ -727,11 +1175,13 @@
     public void handleJigsawGenerate(ServerboundJigsawGeneratePacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
         if (this.player.canUseGameMasterBlocks()) {
-            BlockPos blockPos = packet.getPos();
-            BlockEntity blockEntity = this.player.level.getBlockEntity(blockPos);
-            if (blockEntity instanceof JigsawBlockEntity) {
-                JigsawBlockEntity jigsawBlockEntity = (JigsawBlockEntity)blockEntity;
-                jigsawBlockEntity.generate(this.player.getLevel(), packet.levels(), packet.keepJigsaws());
+            BlockPos blockposition = packet.getPos();
+            BlockEntity tileentity = this.player.level.getBlockEntity(blockposition);
+
+            if (tileentity instanceof JigsawBlockEntity) {
+                JigsawBlockEntity tileentityjigsaw = (JigsawBlockEntity) tileentity;
+
+                tileentityjigsaw.generate(this.player.getLevel(), packet.levels(), packet.keepJigsaws());
             }
 
         }
@@ -741,92 +1191,164 @@
     public void handleSelectTrade(ServerboundSelectTradePacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
         int i = packet.getItem();
-        AbstractContainerMenu var4 = this.player.containerMenu;
-        if (var4 instanceof MerchantMenu merchantMenu) {
-            if (!merchantMenu.stillValid(this.player)) {
-                LOGGER.debug("Player {} interacted with invalid menu {}", this.player, merchantMenu);
+        AbstractContainerMenu container = this.player.containerMenu;
+
+        if (container instanceof MerchantMenu) {
+            MerchantMenu containermerchant = (MerchantMenu) container;
+            // CraftBukkit start
+            final org.bukkit.event.inventory.TradeSelectEvent tradeSelectEvent = CraftEventFactory.callTradeSelectEvent(this.player, i, containermerchant);
+            if (tradeSelectEvent.isCancelled()) {
+                this.player.getBukkitEntity().updateInventory();
                 return;
             }
+            // CraftBukkit end
 
-            merchantMenu.setSelectionHint(i);
-            merchantMenu.tryMoveItems(i);
+            if (!containermerchant.stillValid(this.player)) {
+                ServerGamePacketListenerImpl.LOGGER.debug("Player {} interacted with invalid menu {}", this.player, containermerchant);
+                return;
+            }
+
+            containermerchant.setSelectionHint(i);
+            containermerchant.tryMoveItems(i);
         }
 
     }
 
     @Override
     public void handleEditBook(ServerboundEditBookPacket packet) {
+        // Paper start
+        if (!this.cserver.isPrimaryThread()) {
+            List<String> pageList = packet.getPages();
+            long byteTotal = 0;
+            int maxBookPageSize = io.papermc.paper.configuration.GlobalConfiguration.get().itemValidation.bookSize.pageMax;
+            double multiplier = Math.max(0.3D, Math.min(1D, io.papermc.paper.configuration.GlobalConfiguration.get().itemValidation.bookSize.totalMultiplier));
+            long byteAllowed = maxBookPageSize;
+            for (String testString : pageList) {
+                int byteLength = testString.getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
+                if (byteLength > 256 * 4) {
+                    ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send a book with with a page too large!");
+                    server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause
+                    return;
+                }
+                byteTotal += byteLength;
+                int length = testString.length();
+                int multibytes = 0;
+                if (byteLength != length) {
+                    for (char c : testString.toCharArray()) {
+                        if (c > 127) {
+                            multibytes++;
+                        }
+                    }
+                }
+                byteAllowed += (maxBookPageSize * Math.min(1, Math.max(0.1D, (double) length / 255D))) * multiplier;
+
+                if (multibytes > 1) {
+                    // penalize MB
+                    byteAllowed -= multibytes;
+                }
+            }
+
+            if (byteTotal > byteAllowed) {
+                ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send too large of a book. Book Size: " + byteTotal + " - Allowed:  "+ byteAllowed + " - Pages: " + pageList.size());
+                server.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause
+                return;
+            }
+        }
+        // Paper end
+        // CraftBukkit start
+        if (this.lastBookTick + 20 > MinecraftServer.currentTick) {
+            server.scheduleOnMain(() -> this.disconnect("Book edited too quickly!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause // Paper - Also ensure this is called on main
+            return;
+        }
+        this.lastBookTick = MinecraftServer.currentTick;
+        // CraftBukkit end
         int i = packet.getSlot();
+
         if (Inventory.isHotbarSlot(i) || i == 40) {
             List<String> list = Lists.newArrayList();
             Optional<String> optional = packet.getTitle();
+
+            Objects.requireNonNull(list);
             optional.ifPresent(list::add);
-            packet.getPages().stream().limit(100L).forEach(list::add);
-            Consumer<List<FilteredText>> consumer = optional.isPresent() ? (texts) -> {
-                this.signBook(texts.get(0), texts.subList(1, texts.size()), i);
-            } : (texts) -> {
-                this.updateBookContents(texts, i);
+            Stream<String> stream = packet.getPages().stream().limit(100L); // CraftBukkit - decompile error
+
+            Objects.requireNonNull(list);
+            stream.forEach(list::add);
+            Consumer<List<FilteredText>> consumer = optional.isPresent() ? (list1) -> {
+                this.signBook((FilteredText) list1.get(0), list1.subList(1, list1.size()), i);
+            } : (list1) -> {
+                this.updateBookContents(list1, i);
             };
-            this.filterTextPacket(list).thenAcceptAsync(consumer, this.server);
+
+            this.filterTextPacket((List) list).thenAcceptAsync(consumer, this.server);
         }
     }
 
     private void updateBookContents(List<FilteredText> pages, int slotId) {
-        ItemStack itemStack = this.player.getInventory().getItem(slotId);
-        if (itemStack.is(Items.WRITABLE_BOOK)) {
-            this.updateBookPages(pages, UnaryOperator.identity(), itemStack);
+        ItemStack itemstack = this.player.getInventory().getItem(slotId);
+
+        if (itemstack.is(Items.WRITABLE_BOOK)) {
+            this.updateBookPages(pages, UnaryOperator.identity(), itemstack.copy(), slotId, itemstack); // CraftBukkit
         }
     }
 
     private void signBook(FilteredText title, List<FilteredText> pages, int slotId) {
-        ItemStack itemStack = this.player.getInventory().getItem(slotId);
-        if (itemStack.is(Items.WRITABLE_BOOK)) {
-            ItemStack itemStack2 = new ItemStack(Items.WRITTEN_BOOK);
-            CompoundTag compoundTag = itemStack.getTag();
-            if (compoundTag != null) {
-                itemStack2.setTag(compoundTag.copy());
+        ItemStack itemstack = this.player.getInventory().getItem(slotId);
+
+        if (itemstack.is(Items.WRITABLE_BOOK)) {
+            ItemStack itemstack1 = new ItemStack(Items.WRITTEN_BOOK);
+            CompoundTag nbttagcompound = itemstack.getTag();
+
+            if (nbttagcompound != null) {
+                itemstack1.setTag(nbttagcompound.copy());
             }
 
-            itemStack2.addTagElement("author", StringTag.valueOf(this.player.getName().getString()));
+            itemstack1.addTagElement("author", StringTag.valueOf(this.player.getName().getString()));
             if (this.player.isTextFilteringEnabled()) {
-                itemStack2.addTagElement("title", StringTag.valueOf(title.filteredOrEmpty()));
+                itemstack1.addTagElement("title", StringTag.valueOf(title.filteredOrEmpty()));
             } else {
-                itemStack2.addTagElement("filtered_title", StringTag.valueOf(title.filteredOrEmpty()));
-                itemStack2.addTagElement("title", StringTag.valueOf(title.raw()));
+                itemstack1.addTagElement("filtered_title", StringTag.valueOf(title.filteredOrEmpty()));
+                itemstack1.addTagElement("title", StringTag.valueOf(title.raw()));
             }
 
-            this.updateBookPages(pages, (text) -> {
-                return Component.Serializer.toJson(Component.literal(text));
-            }, itemStack2);
-            this.player.getInventory().setItem(slotId, itemStack2);
+            this.updateBookPages(pages, (s) -> {
+                return Component.Serializer.toJson(Component.literal(s));
+            }, itemstack1, slotId, itemstack); // CraftBukkit
+            this.player.getInventory().setItem(slotId, itemstack); // CraftBukkit - event factory updates the hand book
         }
     }
 
-    private void updateBookPages(List<FilteredText> messages, UnaryOperator<String> postProcessor, ItemStack book) {
-        ListTag listTag = new ListTag();
+    public void updateBookPages(List<FilteredText> list, UnaryOperator<String> unaryoperator, ItemStack itemstack, int slot, ItemStack handItem) { // CraftBukkit
+        ListTag nbttaglist = new ListTag();
+
         if (this.player.isTextFilteringEnabled()) {
-            messages.stream().map((message) -> {
-                return StringTag.valueOf(postProcessor.apply(message.filteredOrEmpty()));
-            }).forEach(listTag::add);
+            Stream<StringTag> stream = list.stream().map((filteredtext) -> { // CraftBukkit - decompile error
+                return StringTag.valueOf((String) unaryoperator.apply(filteredtext.filteredOrEmpty()));
+            });
+
+            Objects.requireNonNull(nbttaglist);
+            stream.forEach(nbttaglist::add);
         } else {
-            CompoundTag compoundTag = new CompoundTag();
+            CompoundTag nbttagcompound = new CompoundTag();
             int i = 0;
 
-            for(int j = messages.size(); i < j; ++i) {
-                FilteredText filteredText = messages.get(i);
-                String string = filteredText.raw();
-                listTag.add(StringTag.valueOf(postProcessor.apply(string)));
-                if (filteredText.isFiltered()) {
-                    compoundTag.putString(String.valueOf(i), postProcessor.apply(filteredText.filteredOrEmpty()));
+            for (int j = list.size(); i < j; ++i) {
+                FilteredText filteredtext = (FilteredText) list.get(i);
+                String s = filteredtext.raw();
+
+                nbttaglist.add(StringTag.valueOf((String) unaryoperator.apply(s)));
+                if (filteredtext.isFiltered()) {
+                    nbttagcompound.putString(String.valueOf(i), (String) unaryoperator.apply(filteredtext.filteredOrEmpty()));
                 }
             }
 
-            if (!compoundTag.isEmpty()) {
-                book.addTagElement("filtered_pages", compoundTag);
+            if (!nbttagcompound.isEmpty()) {
+                itemstack.addTagElement("filtered_pages", nbttagcompound);
             }
         }
 
-        book.addTagElement("pages", listTag);
+        itemstack.addTagElement("pages", nbttaglist);
+        this.player.getInventory().setItem(slot, CraftEventFactory.handleEditBookEvent(player, slot, handItem, itemstack)); // CraftBukkit // Paper - Don't ignore result (see other callsite for handleEditBookEvent)
     }
 
     @Override
@@ -834,9 +1356,11 @@
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
         if (this.player.hasPermissions(2)) {
             Entity entity = this.player.getLevel().getEntity(packet.getEntityId());
+
             if (entity != null) {
-                CompoundTag compoundTag = entity.saveWithoutId(new CompoundTag());
-                this.player.connection.send(new ClientboundTagQueryPacket(packet.getTransactionId(), compoundTag));
+                CompoundTag nbttagcompound = entity.saveWithoutId(new CompoundTag());
+
+                this.player.connection.send(new ClientboundTagQueryPacket(packet.getTransactionId(), nbttagcompound));
             }
 
         }
@@ -846,113 +1370,277 @@
     public void handleBlockEntityTagQuery(ServerboundBlockEntityTagQuery packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
         if (this.player.hasPermissions(2)) {
-            BlockEntity blockEntity = this.player.getLevel().getBlockEntity(packet.getPos());
-            CompoundTag compoundTag = blockEntity != null ? blockEntity.saveWithoutMetadata() : null;
-            this.player.connection.send(new ClientboundTagQueryPacket(packet.getTransactionId(), compoundTag));
+            BlockEntity tileentity = this.player.getLevel().getBlockEntity(packet.getPos());
+            CompoundTag nbttagcompound = tileentity != null ? tileentity.saveWithoutMetadata() : null;
+
+            this.player.connection.send(new ClientboundTagQueryPacket(packet.getTransactionId(), nbttagcompound));
         }
     }
 
     @Override
     public void handleMovePlayer(ServerboundMovePlayerPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
-        if (containsInvalidValues(packet.getX(0.0D), packet.getY(0.0D), packet.getZ(0.0D), packet.getYRot(0.0F), packet.getXRot(0.0F))) {
-            this.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_movement"));
+        if (ServerGamePacketListenerImpl.containsInvalidValues(packet.getX(0.0D), packet.getY(0.0D), packet.getZ(0.0D), packet.getYRot(0.0F), packet.getXRot(0.0F))) {
+            this.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_movement"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PLAYER_MOVEMENT); // Paper - kick event cause
         } else {
-            ServerLevel serverLevel = this.player.getLevel();
-            if (!this.player.wonGame) {
+            ServerLevel worldserver = this.player.getLevel();
+
+            if (!this.player.wonGame && !this.player.isImmobile()) { // CraftBukkit
                 if (this.tickCount == 0) {
                     this.resetPosition();
                 }
 
                 if (this.awaitingPositionFromClient != null) {
-                    if (this.tickCount - this.awaitingTeleportTime > 20) {
+                    if (false && this.tickCount - this.awaitingTeleportTime > 20) { // Paper - this will greatly screw with clients with > 1000ms RTT
                         this.awaitingTeleportTime = this.tickCount;
                         this.teleport(this.awaitingPositionFromClient.x, this.awaitingPositionFromClient.y, this.awaitingPositionFromClient.z, this.player.getYRot(), this.player.getXRot());
                     }
-
+                    this.allowedPlayerTicks = 20; // CraftBukkit
                 } else {
                     this.awaitingTeleportTime = this.tickCount;
-                    double d = clampHorizontal(packet.getX(this.player.getX()));
-                    double e = clampVertical(packet.getY(this.player.getY()));
-                    double f = clampHorizontal(packet.getZ(this.player.getZ()));
-                    float g = Mth.wrapDegrees(packet.getYRot(this.player.getYRot()));
-                    float h = Mth.wrapDegrees(packet.getXRot(this.player.getXRot()));
+                    double d0 = ServerGamePacketListenerImpl.clampHorizontal(packet.getX(this.player.getX())); final double toX = d0; // Paper - OBFHELPER
+                    double d1 = ServerGamePacketListenerImpl.clampVertical(packet.getY(this.player.getY())); final double toY = d1;
+                    double d2 = ServerGamePacketListenerImpl.clampHorizontal(packet.getZ(this.player.getZ())); final double toZ = d2; // Paper - OBFHELPER
+                    float f = Mth.wrapDegrees(packet.getYRot(this.player.getYRot()));
+                    float f1 = Mth.wrapDegrees(packet.getXRot(this.player.getXRot()));
+
                     if (this.player.isPassenger()) {
-                        this.player.absMoveTo(this.player.getX(), this.player.getY(), this.player.getZ(), g, h);
+                        this.player.absMoveTo(this.player.getX(), this.player.getY(), this.player.getZ(), f, f1);
                         this.player.getLevel().getChunkSource().move(this.player);
+                        this.allowedPlayerTicks = 20; // CraftBukkit
                     } else {
-                        double i = this.player.getX();
-                        double j = this.player.getY();
-                        double k = this.player.getZ();
-                        double l = this.player.getY();
-                        double m = d - this.firstGoodX;
-                        double n = e - this.firstGoodY;
-                        double o = f - this.firstGoodZ;
-                        double p = this.player.getDeltaMovement().lengthSqr();
-                        double q = m * m + n * n + o * o;
+                        // CraftBukkit - Make sure the move is valid but then reset it for plugins to modify
+                        double prevX = this.player.getX();
+                        double prevY = this.player.getY();
+                        double prevZ = this.player.getZ();
+                        float prevYaw = this.player.getYRot();
+                        float prevPitch = this.player.getXRot();
+                        // CraftBukkit end
+                        double d3 = this.player.getX();
+                        double d4 = this.player.getY();
+                        double d5 = this.player.getZ();
+                        double d6 = this.player.getY();
+                        double d7 = d0 - this.firstGoodX;
+                        double d8 = d1 - this.firstGoodY;
+                        double d9 = d2 - this.firstGoodZ;
+                        double d10 = this.player.getDeltaMovement().lengthSqr();
+                        // Paper start - fix large move vectors killing the server
+                        double currDeltaX = toX - prevX;
+                        double currDeltaY = toY - prevY;
+                        double currDeltaZ = toZ - prevZ;
+                        double d11 = Math.max(d7 * d7 + d8 * d8 + d9 * d9, (currDeltaX * currDeltaX + currDeltaY * currDeltaY + currDeltaZ * currDeltaZ) - 1);
+                        // Paper end - fix large move vectors killing the server
+                        // Paper start - fix large move vectors killing the server
+                        double otherFieldX = d0 - this.lastGoodX;
+                        double otherFieldY = d1 - this.lastGoodY;
+                        double otherFieldZ = d2 - this.lastGoodZ;
+                        d11 = Math.max(d11, (otherFieldX * otherFieldX + otherFieldY * otherFieldY + otherFieldZ * otherFieldZ) - 1);
+                        // Paper end - fix large move vectors killing the server
+
                         if (this.player.isSleeping()) {
-                            if (q > 1.0D) {
-                                this.teleport(this.player.getX(), this.player.getY(), this.player.getZ(), g, h);
+                            if (d11 > 1.0D) {
+                                this.teleport(this.player.getX(), this.player.getY(), this.player.getZ(), f, f1);
                             }
 
                         } else {
                             ++this.receivedMovePacketCount;
-                            int r = this.receivedMovePacketCount - this.knownMovePacketCount;
-                            if (r > 5) {
-                                LOGGER.debug("{} is sending move packets too frequently ({} packets since last tick)", this.player.getName().getString(), r);
-                                r = 1;
+                            int i = this.receivedMovePacketCount - this.knownMovePacketCount;
+
+                            // CraftBukkit start - handle custom speeds and skipped ticks
+                            this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick;
+                            this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1);
+                            this.lastTick = (int) (System.currentTimeMillis() / 50);
+
+                            if (i > Math.max(this.allowedPlayerTicks, 5)) {
+                                ServerGamePacketListenerImpl.LOGGER.debug("{} is sending move packets too frequently ({} packets since last tick)", this.player.getName().getString(), i);
+                                i = 1;
+                            }
+
+                            if (packet.hasRot || d11 > 0) {
+                                this.allowedPlayerTicks -= 1;
+                            } else {
+                                this.allowedPlayerTicks = 20;
+                            }
+                            double speed;
+                            if (this.player.getAbilities().flying) {
+                                speed = this.player.getAbilities().flyingSpeed * 20f;
+                            } else {
+                                speed = this.player.getAbilities().walkingSpeed * 10f;
+                            }
+                            // Paper start - Prevent moving into unloaded chunks
+                            if (player.level.paperConfig().chunks.preventMovingIntoUnloadedChunks && (this.player.getX() != toX || this.player.getZ() != toZ) && !worldserver.areChunksLoadedForMove(this.player.getBoundingBox().expandTowards(new Vec3(toX, toY, toZ).subtract(this.player.position())))) {
+                                this.internalTeleport(this.player.getX(), this.player.getY(), this.player.getZ(), this.player.getYRot(), this.player.getXRot(), Collections.emptySet(), true);
+                                return;
                             }
+                            // Paper end
 
                             if (!this.player.isChangingDimension() && (!this.player.getLevel().getGameRules().getBoolean(GameRules.RULE_DISABLE_ELYTRA_MOVEMENT_CHECK) || !this.player.isFallFlying())) {
-                                float s = this.player.isFallFlying() ? 300.0F : 100.0F;
-                                if (q - p > (double)(s * (float)r) && !this.isSingleplayerOwner()) {
-                                    LOGGER.warn("{} moved too quickly! {},{},{}", this.player.getName().getString(), m, n, o);
+                                float f2 = this.player.isFallFlying() ? 300.0F : 100.0F;
+
+                                if (d11 - d10 > Math.max(f2, Math.pow((double) (org.spigotmc.SpigotConfig.movedTooQuicklyMultiplier * (float) i * speed), 2)) && !this.isSingleplayerOwner()) {
+                                // CraftBukkit end
+                                    ServerGamePacketListenerImpl.LOGGER.warn("{} moved too quickly! {},{},{}", new Object[]{this.player.getName().getString(), d7, d8, d9});
                                     this.teleport(this.player.getX(), this.player.getY(), this.player.getZ(), this.player.getYRot(), this.player.getXRot());
                                     return;
                                 }
                             }
 
-                            AABB aABB = this.player.getBoundingBox();
-                            m = d - this.lastGoodX;
-                            n = e - this.lastGoodY;
-                            o = f - this.lastGoodZ;
-                            boolean bl = n > 0.0D;
-                            if (this.player.isOnGround() && !packet.isOnGround() && bl) {
-                                this.player.jumpFromGround();
+                            AABB axisalignedbb = this.player.getBoundingBox(); // Paper - diff on change, should be old AABB
+
+                            d7 = d0 - this.lastGoodX; // Paper - diff on change, used for checking large move vectors above
+                            d8 = d1 - this.lastGoodY; // Paper - diff on change, used for checking large move vectors above
+                            d9 = d2 - this.lastGoodZ; // Paper - diff on change, used for checking large move vectors above
+                            boolean flag = d8 > 0.0D;
+
+                            if (this.player.isOnGround() && !packet.isOnGround() && flag) {
+                                // Paper start - Add player jump event
+                                Player player = this.getCraftPlayer();
+                                Location from = new Location(player.getWorld(), lastPosX, lastPosY, lastPosZ, lastYaw, lastPitch); // Get the Players previous Event location.
+                                Location to = player.getLocation().clone(); // Start off the To location as the Players current location.
+
+                                // If the packet contains movement information then we update the To location with the correct XYZ.
+                                if (packet.hasPos) {
+                                    to.setX(packet.x);
+                                    to.setY(packet.y);
+                                    to.setZ(packet.z);
+                                }
+
+                                // If the packet contains look information then we update the To location with the correct Yaw & Pitch.
+                                if (packet.hasRot) {
+                                    to.setYaw(packet.yRot);
+                                    to.setPitch(packet.xRot);
+                                }
+
+                                com.destroystokyo.paper.event.player.PlayerJumpEvent event = new com.destroystokyo.paper.event.player.PlayerJumpEvent(player, from, to);
+
+                                if (event.callEvent()) {
+                                    this.player.jumpFromGround();
+                                } else {
+                                    from = event.getFrom();
+                                    this.internalTeleport(from.getX(), from.getY(), from.getZ(), from.getYaw(), from.getPitch(), Collections.emptySet(), false);
+                                    return;
+                                }
+                                // Paper end
                             }
 
-                            boolean bl2 = this.player.verticalCollisionBelow;
-                            this.player.move(MoverType.PLAYER, new Vec3(m, n, o));
-                            m = d - this.player.getX();
-                            n = e - this.player.getY();
-                            if (n > -0.5D || n < 0.5D) {
-                                n = 0.0D;
+                            boolean flag1 = this.player.verticalCollisionBelow;
+
+                            this.player.move(MoverType.PLAYER, new Vec3(d7, d8, d9));
+                            boolean didCollide = toX != this.player.getX() || toY != this.player.getY() || toZ != this.player.getZ(); // Paper - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be...
+                            this.player.onGround = packet.isOnGround(); // CraftBukkit - SPIGOT-5810, SPIGOT-5835, SPIGOT-6828: reset by this.player.move
+                            // Paper start - prevent position desync
+                            if (this.awaitingPositionFromClient != null) {
+                                return; // ... thanks Mojang for letting move calls teleport across dimensions.
                             }
+                            // Paper end - prevent position desync
+                            double d12 = d8;
 
-                            o = f - this.player.getZ();
-                            q = m * m + n * n + o * o;
-                            boolean bl3 = false;
-                            if (!this.player.isChangingDimension() && q > 0.0625D && !this.player.isSleeping() && !this.player.gameMode.isCreative() && this.player.gameMode.getGameModeForPlayer() != GameType.SPECTATOR) {
-                                bl3 = true;
-                                LOGGER.warn("{} moved wrongly!", (Object)this.player.getName().getString());
+                            d7 = d0 - this.player.getX();
+                            d8 = d1 - this.player.getY();
+                            if (d8 > -0.5D || d8 < 0.5D) {
+                                d8 = 0.0D;
                             }
 
-                            this.player.absMoveTo(d, e, f, g, h);
-                            if (this.player.noPhysics || this.player.isSleeping() || (!bl3 || !serverLevel.noCollision(this.player, aABB)) && !this.isPlayerCollidingWithAnythingNew(serverLevel, aABB)) {
-                                this.clientIsFloating = n >= -0.03125D && !bl2 && this.player.gameMode.getGameModeForPlayer() != GameType.SPECTATOR && !this.server.isFlightAllowed() && !this.player.getAbilities().mayfly && !this.player.hasEffect(MobEffects.LEVITATION) && !this.player.isFallFlying() && !this.player.isAutoSpinAttack() && this.noBlocksAround(this.player);
+                            d9 = d2 - this.player.getZ();
+                            d11 = d7 * d7 + d8 * d8 + d9 * d9;
+                            boolean flag2 = false;
+
+                            if (!this.player.isChangingDimension() && d11 > org.spigotmc.SpigotConfig.movedWronglyThreshold && !this.player.isSleeping() && !this.player.gameMode.isCreative() && this.player.gameMode.getGameModeForPlayer() != GameType.SPECTATOR) { // Spigot
+                                flag2 = true; // Paper - diff on change, this should be moved wrongly
+                                ServerGamePacketListenerImpl.LOGGER.warn("{} moved wrongly!", this.player.getName().getString());
+                            }
+
+                            this.player.absMoveTo(d0, d1, d2, f, f1);
+                            // Paper start - optimise out extra getCubes
+                            // Original for reference:
+                            // boolean teleportBack = flag2 && worldserver.getCubes(this.player, axisalignedbb) || (didCollide && this.a((IWorldReader) worldserver, axisalignedbb));
+                            boolean teleportBack = flag2; // violating this is always a fail
+                            if (!this.player.noPhysics && !this.player.isSleeping() && !teleportBack) {
+                                AABB newBox = this.player.getBoundingBox();
+                                if (didCollide || !axisalignedbb.equals(newBox)) {
+                                    // note: only call after setLocation, or else getBoundingBox is wrong
+                                    teleportBack = this.hasNewCollision(worldserver, this.player, axisalignedbb, newBox);
+                                } // else: no collision at all detected, why do we care?
+                            }
+                            if (!this.player.noPhysics && !this.player.isSleeping() && teleportBack) { // Paper end - optimise out extra getCubes
+                                this.internalTeleport(d3, d4, d5, f, f1, Collections.emptySet(), false); // CraftBukkit - SPIGOT-1807: Don't call teleport event, when the client thinks the player is falling, because the chunks are not loaded on the client yet.
+                                this.player.doCheckFallDamage(this.player.getY() - d6, packet.isOnGround()); // Paper - fix nothing mlg (sneaking and jumping before impact)
+                            } else {
+                                // CraftBukkit start - fire PlayerMoveEvent
+                                // Rest to old location first
+                                this.player.absMoveTo(prevX, prevY, prevZ, prevYaw, prevPitch);
+
+                                Player player = this.getCraftPlayer();
+                                Location from = new Location(player.getWorld(), this.lastPosX, this.lastPosY, this.lastPosZ, this.lastYaw, this.lastPitch); // Get the Players previous Event location.
+                                Location to = player.getLocation().clone(); // Start off the To location as the Players current location.
+
+                                // If the packet contains movement information then we update the To location with the correct XYZ.
+                                if (packet.hasPos) {
+                                    to.setX(packet.x);
+                                    to.setY(packet.y);
+                                    to.setZ(packet.z);
+                                }
+
+                                // If the packet contains look information then we update the To location with the correct Yaw & Pitch.
+                                if (packet.hasRot) {
+                                    to.setYaw(packet.yRot);
+                                    to.setPitch(packet.xRot);
+                                }
+
+                                // Prevent 40 event-calls for less than a single pixel of movement >.>
+                                double delta = Math.pow(this.lastPosX - to.getX(), 2) + Math.pow(this.lastPosY - to.getY(), 2) + Math.pow(this.lastPosZ - to.getZ(), 2);
+                                float deltaAngle = Math.abs(this.lastYaw - to.getYaw()) + Math.abs(this.lastPitch - to.getPitch());
+
+                                if ((delta > 1f / 256 || deltaAngle > 10f) && !this.player.isImmobile()) {
+                                    this.lastPosX = to.getX();
+                                    this.lastPosY = to.getY();
+                                    this.lastPosZ = to.getZ();
+                                    this.lastYaw = to.getYaw();
+                                    this.lastPitch = to.getPitch();
+
+                                    // Skip the first time we do this
+                                    if (from.getX() != Double.MAX_VALUE) {
+                                        Location oldTo = to.clone();
+                                        PlayerMoveEvent event = new PlayerMoveEvent(player, from, to);
+                                        this.cserver.getPluginManager().callEvent(event);
+
+                                        // If the event is cancelled we move the player back to their old location.
+                                        if (event.isCancelled()) {
+                                            this.teleport(from);
+                                            return;
+                                        }
+
+                                        // If a Plugin has changed the To destination then we teleport the Player
+                                        // there to avoid any 'Moved wrongly' or 'Moved too quickly' errors.
+                                        // We only do this if the Event was not cancelled.
+                                        if (!oldTo.equals(event.getTo()) && !event.isCancelled()) {
+                                            this.player.getBukkitEntity().teleport(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN);
+                                            return;
+                                        }
+
+                                        // Check to see if the Players Location has some how changed during the call of the event.
+                                        // This can happen due to a plugin teleporting the player instead of using .setTo()
+                                        if (!from.equals(this.getCraftPlayer().getLocation()) && this.justTeleported) {
+                                            this.justTeleported = false;
+                                            return;
+                                        }
+                                    }
+                                }
+                                this.player.absMoveTo(d0, d1, d2, f, f1); // Copied from above
+                                // CraftBukkit end
+
+                                this.clientIsFloating = d12 >= -0.03125D && !flag1 && this.player.gameMode.getGameModeForPlayer() != GameType.SPECTATOR && !this.server.isFlightAllowed() && !this.player.getAbilities().mayfly && !this.player.hasEffect(MobEffects.LEVITATION) && !this.player.isFallFlying() && !this.player.isAutoSpinAttack() && this.noBlocksAround(this.player);
                                 this.player.getLevel().getChunkSource().move(this.player);
-                                this.player.doCheckFallDamage(this.player.getY() - l, packet.isOnGround());
+                                this.player.doCheckFallDamage(this.player.getY() - d6, packet.isOnGround());
                                 this.player.setOnGround(packet.isOnGround());
-                                if (bl) {
+                                if (flag) {
                                     this.player.resetFallDistance();
                                 }
 
-                                this.player.checkMovementStatistics(this.player.getX() - i, this.player.getY() - j, this.player.getZ() - k);
+                                this.player.checkMovementStatistics(this.player.getX() - d3, this.player.getY() - d4, this.player.getZ() - d5);
                                 this.lastGoodX = this.player.getX();
                                 this.lastGoodY = this.player.getY();
                                 this.lastGoodZ = this.player.getZ();
-                            } else {
-                                this.teleport(i, j, k, g, h);
                             }
                         }
                     }
@@ -961,65 +1649,211 @@
         }
     }
 
-    private boolean isPlayerCollidingWithAnythingNew(LevelReader world, AABB box) {
-        Iterable<VoxelShape> iterable = world.getCollisions(this.player, this.player.getBoundingBox().deflate((double)1.0E-5F));
-        VoxelShape voxelShape = Shapes.create(box.deflate((double)1.0E-5F));
+    // Paper start - optimise out extra getCubes
+    private boolean hasNewCollision(final ServerLevel world, final Entity entity, final AABB oldBox, final AABB newBox) {
+        final List<AABB> collisions = io.papermc.paper.util.CachedLists.getTempCollisionList();
+        try {
+            io.papermc.paper.util.CollisionUtil.getCollisions(world, entity, newBox, collisions, false, true,
+                true, false, null, null);
 
-        for(VoxelShape voxelShape2 : iterable) {
-            if (!Shapes.joinIsNotEmpty(voxelShape2, voxelShape, BooleanOp.AND)) {
-                return true;
+            for (int i = 0, len = collisions.size(); i < len; ++i) {
+                final AABB box = collisions.get(i);
+                if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(box, oldBox)) {
+                    return true;
+                }
             }
+
+            return false;
+        } finally {
+            io.papermc.paper.util.CachedLists.returnTempCollisionList(collisions);
         }
+    }
+    // Paper end - optimise out extra getCubes
 
-        return false;
+    private boolean isPlayerCollidingWithAnythingNew(LevelReader world, AABB box) {
+        Iterable<VoxelShape> iterable = world.getCollisions(this.player, this.player.getBoundingBox().deflate(9.999999747378752E-6D));
+        VoxelShape voxelshape = Shapes.create(box.deflate(9.999999747378752E-6D));
+        Iterator iterator = iterable.iterator();
+
+        VoxelShape voxelshape1;
+
+        do {
+            if (!iterator.hasNext()) {
+                return false;
+            }
+
+            voxelshape1 = (VoxelShape) iterator.next();
+        } while (Shapes.joinIsNotEmpty(voxelshape1, voxelshape, BooleanOp.AND));
+
+        return true;
     }
 
+    // CraftBukkit start - Delegate to teleport(Location)
     public void dismount(double x, double y, double z, float yaw, float pitch) {
-        this.teleport(x, y, z, yaw, pitch, Collections.emptySet(), true);
+        this.dismount(x, y, z, yaw, pitch, PlayerTeleportEvent.TeleportCause.UNKNOWN);
+    }
+
+    public void dismount(double d0, double d1, double d2, float f, float f1, PlayerTeleportEvent.TeleportCause cause) {
+        this.teleport(d0, d1, d2, f, f1, Collections.emptySet(), true, cause);
     }
 
     public void teleport(double x, double y, double z, float yaw, float pitch) {
-        this.teleport(x, y, z, yaw, pitch, Collections.emptySet(), false);
+        this.teleport(x, y, z, yaw, pitch, PlayerTeleportEvent.TeleportCause.UNKNOWN);
+    }
+
+    public void teleport(double d0, double d1, double d2, float f, float f1, PlayerTeleportEvent.TeleportCause cause) {
+        this.teleport(d0, d1, d2, f, f1, Collections.emptySet(), false, cause);
     }
 
     public void teleport(double x, double y, double z, float yaw, float pitch, Set<ClientboundPlayerPositionPacket.RelativeArgument> flags) {
-        this.teleport(x, y, z, yaw, pitch, flags, false);
+        this.teleport(x, y, z, yaw, pitch, flags, PlayerTeleportEvent.TeleportCause.UNKNOWN);
     }
 
-    public void teleport(double x, double y, double z, float yaw, float pitch, Set<ClientboundPlayerPositionPacket.RelativeArgument> flags, boolean shouldDismount) {
-        double d = flags.contains(ClientboundPlayerPositionPacket.RelativeArgument.X) ? this.player.getX() : 0.0D;
-        double e = flags.contains(ClientboundPlayerPositionPacket.RelativeArgument.Y) ? this.player.getY() : 0.0D;
-        double f = flags.contains(ClientboundPlayerPositionPacket.RelativeArgument.Z) ? this.player.getZ() : 0.0D;
-        float g = flags.contains(ClientboundPlayerPositionPacket.RelativeArgument.Y_ROT) ? this.player.getYRot() : 0.0F;
-        float h = flags.contains(ClientboundPlayerPositionPacket.RelativeArgument.X_ROT) ? this.player.getXRot() : 0.0F;
-        this.awaitingPositionFromClient = new Vec3(x, y, z);
+    public void teleport(double d0, double d1, double d2, float f, float f1, Set<ClientboundPlayerPositionPacket.RelativeArgument> set, PlayerTeleportEvent.TeleportCause cause) {
+        this.teleport(d0, d1, d2, f, f1, set, false, cause);
+    }
+
+    public boolean teleport(double d0, double d1, double d2, float f, float f1, Set<ClientboundPlayerPositionPacket.RelativeArgument> set, boolean flag, PlayerTeleportEvent.TeleportCause cause) { // CraftBukkit - Return event status
+        Player player = this.getCraftPlayer();
+        Location from = player.getLocation();
+
+        double x = d0;
+        double y = d1;
+        double z = d2;
+        float yaw = f;
+        float pitch = f1;
+
+        Location to = new Location(this.getCraftPlayer().getWorld(), x, y, z, yaw, pitch);
+        // SPIGOT-5171: Triggered on join
+        if (from.equals(to)) {
+            this.internalTeleport(d0, d1, d2, f, f1, set, flag);
+            return false; // CraftBukkit - Return event status
+        }
+
+        // Paper start - Teleport API
+        Set<io.papermc.paper.entity.RelativeTeleportFlag> relativeFlags = java.util.EnumSet.noneOf(io.papermc.paper.entity.RelativeTeleportFlag.class);
+        for (net.minecraft.network.protocol.game.ClientboundPlayerPositionPacket.RelativeArgument relativeArgument : set) {
+            relativeFlags.add(org.bukkit.craftbukkit.v1_19_R1.entity.CraftPlayer.toApiRelativeFlag(relativeArgument));
+        }
+        PlayerTeleportEvent event = new PlayerTeleportEvent(player, from.clone(), to.clone(), cause, flag, java.util.Set.copyOf(relativeFlags));
+        // Paper end
+        this.cserver.getPluginManager().callEvent(event);
+
+        if (event.isCancelled() || !to.equals(event.getTo())) {
+            //set.clear(); // Can't relative teleport // Paper - Teleport API: Now you can!
+            to = event.isCancelled() ? event.getFrom() : event.getTo();
+            d0 = to.getX();
+            d1 = to.getY();
+            d2 = to.getZ();
+            f = to.getYaw();
+            f1 = to.getPitch();
+        }
+
+        this.internalTeleport(d0, d1, d2, f, f1, set, flag);
+        return event.isCancelled(); // CraftBukkit - Return event status
+    }
+
+    public void teleport(Location dest) {
+        this.internalTeleport(dest.getX(), dest.getY(), dest.getZ(), dest.getYaw(), dest.getPitch(), Collections.<ClientboundPlayerPositionPacket.RelativeArgument>emptySet(), true);
+    }
+
+    public void internalTeleport(double d0, double d1, double d2, float f, float f1, Set<ClientboundPlayerPositionPacket.RelativeArgument> set, boolean flag) {
+        org.spigotmc.AsyncCatcher.catchOp("teleport"); // Paper
+        // Paper start
+        if (player.isRemoved()) {
+            LOGGER.info("Attempt to teleport removed player {} restricted", player.getScoreboardName());
+            if (server.isDebugging()) io.papermc.paper.util.TraceUtil.dumpTraceForThread("Attempt to teleport removed player");
+            return;
+        }
+        // Paper end
+        // CraftBukkit start
+        if (Float.isNaN(f)) {
+            f = 0;
+        }
+        if (Float.isNaN(f1)) {
+            f1 = 0;
+        }
+
+        this.justTeleported = true;
+        // CraftBukkit end
+        double d3 = set.contains(ClientboundPlayerPositionPacket.RelativeArgument.X) ? this.player.getX() : 0.0D;
+        double d4 = set.contains(ClientboundPlayerPositionPacket.RelativeArgument.Y) ? this.player.getY() : 0.0D;
+        double d5 = set.contains(ClientboundPlayerPositionPacket.RelativeArgument.Z) ? this.player.getZ() : 0.0D;
+        float f2 = set.contains(ClientboundPlayerPositionPacket.RelativeArgument.Y_ROT) ? this.player.getYRot() : 0.0F;
+        float f3 = set.contains(ClientboundPlayerPositionPacket.RelativeArgument.X_ROT) ? this.player.getXRot() : 0.0F;
+
+        this.awaitingPositionFromClient = new Vec3(d0, d1, d2);
         if (++this.awaitingTeleport == Integer.MAX_VALUE) {
             this.awaitingTeleport = 0;
         }
 
+        // CraftBukkit start - update last location
+        this.lastPosX = this.awaitingPositionFromClient.x;
+        this.lastPosY = this.awaitingPositionFromClient.y;
+        this.lastPosZ = this.awaitingPositionFromClient.z;
+        this.lastYaw = f;
+        this.lastPitch = f1;
+        // CraftBukkit end
+
         this.awaitingTeleportTime = this.tickCount;
-        this.player.absMoveTo(x, y, z, yaw, pitch);
-        this.player.connection.send(new ClientboundPlayerPositionPacket(x - d, y - e, z - f, yaw - g, pitch - h, flags, this.awaitingTeleport, shouldDismount));
+        this.player.moveTo(d0, d1, d2, f, f1); // Paper - use proper moveTo for teleportation
+        this.player.connection.send(new ClientboundPlayerPositionPacket(d0 - d3, d1 - d4, d2 - d5, f - f2, f1 - f3, set, this.awaitingTeleport, flag));
     }
 
     @Override
     public void handlePlayerAction(ServerboundPlayerActionPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
-        BlockPos blockPos = packet.getPos();
+        if (this.player.isImmobile()) return; // CraftBukkit
+        BlockPos blockposition = packet.getPos();
+
         this.player.resetLastActionTime();
-        ServerboundPlayerActionPacket.Action action = packet.getAction();
-        switch (action) {
+        ServerboundPlayerActionPacket.Action packetplayinblockdig_enumplayerdigtype = packet.getAction();
+
+        switch (packetplayinblockdig_enumplayerdigtype) {
             case SWAP_ITEM_WITH_OFFHAND:
                 if (!this.player.isSpectator()) {
-                    ItemStack itemStack = this.player.getItemInHand(InteractionHand.OFF_HAND);
-                    this.player.setItemInHand(InteractionHand.OFF_HAND, this.player.getItemInHand(InteractionHand.MAIN_HAND));
-                    this.player.setItemInHand(InteractionHand.MAIN_HAND, itemStack);
+                    ItemStack itemstack = this.player.getItemInHand(InteractionHand.OFF_HAND);
+
+                    // CraftBukkit start - inspiration taken from DispenserRegistry (See SpigotCraft#394)
+                    CraftItemStack mainHand = CraftItemStack.asCraftMirror(itemstack);
+                    CraftItemStack offHand = CraftItemStack.asCraftMirror(this.player.getItemInHand(InteractionHand.MAIN_HAND));
+                    PlayerSwapHandItemsEvent swapItemsEvent = new PlayerSwapHandItemsEvent(this.getCraftPlayer(), mainHand.clone(), offHand.clone());
+                    this.cserver.getPluginManager().callEvent(swapItemsEvent);
+                    if (swapItemsEvent.isCancelled()) {
+                        return;
+                    }
+                    if (swapItemsEvent.getOffHandItem().equals(offHand)) {
+                        this.player.setItemInHand(InteractionHand.OFF_HAND, this.player.getItemInHand(InteractionHand.MAIN_HAND));
+                    } else {
+                        this.player.setItemInHand(InteractionHand.OFF_HAND, CraftItemStack.asNMSCopy(swapItemsEvent.getOffHandItem()));
+                    }
+                    if (swapItemsEvent.getMainHandItem().equals(mainHand)) {
+                        this.player.setItemInHand(InteractionHand.MAIN_HAND, itemstack);
+                    } else {
+                        this.player.setItemInHand(InteractionHand.MAIN_HAND, CraftItemStack.asNMSCopy(swapItemsEvent.getMainHandItem()));
+                    }
+                    // CraftBukkit end
                     this.player.stopUsingItem();
                 }
 
                 return;
             case DROP_ITEM:
                 if (!this.player.isSpectator()) {
+                    // limit how quickly items can be dropped
+                    // If the ticks aren't the same then the count starts from 0 and we update the lastDropTick.
+                    if (this.lastDropTick != MinecraftServer.currentTick) {
+                        this.dropCount = 0;
+                        this.lastDropTick = MinecraftServer.currentTick;
+                    } else {
+                        // Else we increment the drop count and check the amount.
+                        this.dropCount++;
+                        if (this.dropCount >= 20) {
+                            ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " dropped their items too quickly!");
+                            this.disconnect("You dropped your items too quickly (Hacking?)", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause
+                            return;
+                        }
+                    }
+                    // CraftBukkit end
                     this.player.drop(false);
                 }
 
@@ -1036,8 +1870,34 @@
             case START_DESTROY_BLOCK:
             case ABORT_DESTROY_BLOCK:
             case STOP_DESTROY_BLOCK:
-                this.player.gameMode.handleBlockBreakAction(blockPos, action, packet.getDirection(), this.player.level.getMaxBuildHeight(), packet.getSequence());
+                // Paper start - Don't allow digging in unloaded chunks
+                if (this.player.level.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4) == null) {
+                    this.player.connection.ackBlockChangesUpTo(packet.getSequence());
+                    return;
+                }
+                // Paper end - Don't allow digging in unloaded chunks
+                // Paper start - send block entities after prediction
+                this.player.gameMode.capturedBlockEntity = false;
+                this.player.gameMode.captureSentBlockEntities = true;
+                // Paper end - send block entities after prediction
+                this.player.gameMode.handleBlockBreakAction(blockposition, packetplayinblockdig_enumplayerdigtype, packet.getDirection(), this.player.level.getMaxBuildHeight(), packet.getSequence());
                 this.player.connection.ackBlockChangesUpTo(packet.getSequence());
+                // Paper start - send block entities after prediction
+                this.player.gameMode.captureSentBlockEntities = false;
+                // If a block entity was modified speedup the block change ack to avoid the block entity
+                // being overriden.
+                if (this.player.gameMode.capturedBlockEntity) {
+                    // manually tick
+                    this.send(new ClientboundBlockChangedAckPacket(this.ackBlockChangesUpTo));
+                    this.player.connection.ackBlockChangesUpTo = -1;
+
+                    this.player.gameMode.capturedBlockEntity = false;
+                    BlockEntity tileentity = this.player.level.getBlockEntity(blockposition);
+                    if (tileentity != null) {
+                        this.player.connection.send(tileentity.getUpdatePacket());
+                    }
+                }
+                // Paper end - send block entities after prediction
                 return;
             default:
                 throw new IllegalArgumentException("Invalid player action");
@@ -1049,47 +1909,83 @@
             return false;
         } else {
             Item item = stack.getItem();
+
             return (item instanceof BlockItem || item instanceof BucketItem) && !player.getCooldowns().isOnCooldown(item);
         }
     }
 
+    // Spigot start - limit place/interactions
+    private int limitedPackets;
+    private long lastLimitedPacket = -1;
+    private static final int THRESHOLD = io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.incomingPacketThreshold; // Paper - Configurable threshold
+
+    private boolean checkLimit(long timestamp) {
+        if (this.lastLimitedPacket != -1 && timestamp - this.lastLimitedPacket < THRESHOLD && this.limitedPackets++ >= 8) { // Paper - Use threshold, raise packet limit to 8
+            return false;
+        }
+
+        if (this.lastLimitedPacket == -1 || timestamp - this.lastLimitedPacket >= THRESHOLD) { // Paper
+            this.lastLimitedPacket = timestamp;
+            this.limitedPackets = 0;
+            return true;
+        }
+
+        return true;
+    }
+    // Spigot end
+
     @Override
     public void handleUseItemOn(ServerboundUseItemOnPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
+        if (this.player.isImmobile()) return; // CraftBukkit
+        if (!this.checkLimit(packet.timestamp)) return; // Spigot - check limit
         this.player.connection.ackBlockChangesUpTo(packet.getSequence());
-        ServerLevel serverLevel = this.player.getLevel();
-        InteractionHand interactionHand = packet.getHand();
-        ItemStack itemStack = this.player.getItemInHand(interactionHand);
-        BlockHitResult blockHitResult = packet.getHitResult();
-        Vec3 vec3 = blockHitResult.getLocation();
-        BlockPos blockPos = blockHitResult.getBlockPos();
-        Vec3 vec32 = Vec3.atCenterOf(blockPos);
-        if (!(this.player.getEyePosition().distanceToSqr(vec32) > MAX_INTERACTION_DISTANCE)) {
-            Vec3 vec33 = vec3.subtract(vec32);
-            double d = 1.0000001D;
-            if (Math.abs(vec33.x()) < 1.0000001D && Math.abs(vec33.y()) < 1.0000001D && Math.abs(vec33.z()) < 1.0000001D) {
-                Direction direction = blockHitResult.getDirection();
+        ServerLevel worldserver = this.player.getLevel();
+        InteractionHand enumhand = packet.getHand();
+        ItemStack itemstack = this.player.getItemInHand(enumhand);
+        BlockHitResult movingobjectpositionblock = packet.getHitResult();
+        Vec3 vec3d = movingobjectpositionblock.getLocation();
+        // Paper start - improve distance check
+        if (!Double.isFinite(vec3d.x) || !Double.isFinite(vec3d.y) || !Double.isFinite(vec3d.z)) {
+            return;
+        }
+        // Paper end
+        BlockPos blockposition = movingobjectpositionblock.getBlockPos();
+        Vec3 vec3d1 = Vec3.atCenterOf(blockposition);
+
+        if (this.player.getEyePosition().distanceToSqr(vec3d1) <= ServerGamePacketListenerImpl.MAX_INTERACTION_DISTANCE) {
+            Vec3 vec3d2 = vec3d.subtract(vec3d1);
+            double d0 = 1.0000001D;
+
+            if (Math.abs(vec3d2.x()) < 1.0000001D && Math.abs(vec3d2.y()) < 1.0000001D && Math.abs(vec3d2.z()) < 1.0000001D) {
+                Direction enumdirection = movingobjectpositionblock.getDirection();
+
                 this.player.resetLastActionTime();
                 int i = this.player.level.getMaxBuildHeight();
-                if (blockPos.getY() < i) {
-                    if (this.awaitingPositionFromClient == null && this.player.distanceToSqr((double)blockPos.getX() + 0.5D, (double)blockPos.getY() + 0.5D, (double)blockPos.getZ() + 0.5D) < 64.0D && serverLevel.mayInteract(this.player, blockPos)) {
-                        InteractionResult interactionResult = this.player.gameMode.useItemOn(this.player, serverLevel, itemStack, interactionHand, blockHitResult);
-                        if (direction == Direction.UP && !interactionResult.consumesAction() && blockPos.getY() >= i - 1 && wasBlockPlacementAttempt(this.player, itemStack)) {
-                            Component component = Component.translatable("build.tooHigh", i - 1).withStyle(ChatFormatting.RED);
-                            this.player.sendSystemMessage(component, true);
-                        } else if (interactionResult.shouldSwing()) {
-                            this.player.swing(interactionHand, true);
+
+                if (blockposition.getY() < i) {
+                    if (this.awaitingPositionFromClient == null && this.player.distanceToSqr((double) blockposition.getX() + 0.5D, (double) blockposition.getY() + 0.5D, (double) blockposition.getZ() + 0.5D) < 64.0D && (worldserver.mayInteract(this.player, blockposition)  || (worldserver.paperConfig().spawn.allowUsingSignsInsideSpawnProtection && worldserver.getBlockState(blockposition).getBlock() instanceof net.minecraft.world.level.block.SignBlock))) { // Paper - sign check
+                        this.player.stopUsingItem(); // CraftBukkit - SPIGOT-4706
+                        InteractionResult enuminteractionresult = this.player.gameMode.useItemOn(this.player, worldserver, itemstack, enumhand, movingobjectpositionblock);
+
+                        if (enumdirection == Direction.UP && !enuminteractionresult.consumesAction() && blockposition.getY() >= i - 1 && ServerGamePacketListenerImpl.wasBlockPlacementAttempt(this.player, itemstack)) {
+                            MutableComponent ichatmutablecomponent = Component.translatable("build.tooHigh", i - 1).withStyle(ChatFormatting.RED);
+
+                            this.player.sendSystemMessage(ichatmutablecomponent, true);
+                        } else if (enuminteractionresult.shouldSwing() && !this.player.gameMode.interactResult) { // Paper
+                            this.player.swing(enumhand, true);
                         }
                     }
                 } else {
-                    Component component2 = Component.translatable("build.tooHigh", i - 1).withStyle(ChatFormatting.RED);
-                    this.player.sendSystemMessage(component2, true);
+                    MutableComponent ichatmutablecomponent1 = Component.translatable("build.tooHigh", i - 1).withStyle(ChatFormatting.RED);
+
+                    this.player.sendSystemMessage(ichatmutablecomponent1, true);
                 }
 
-                this.player.connection.send(new ClientboundBlockUpdatePacket(serverLevel, blockPos));
-                this.player.connection.send(new ClientboundBlockUpdatePacket(serverLevel, blockPos.relative(direction)));
+                this.player.connection.send(new ClientboundBlockUpdatePacket(worldserver, blockposition));
+                this.player.connection.send(new ClientboundBlockUpdatePacket(worldserver, blockposition.relative(enumdirection)));
             } else {
-                LOGGER.warn("Rejecting UseItemOnPacket from {}: Location {} too far away from hit block {}.", this.player.getGameProfile().getName(), vec3, blockPos);
+                ServerGamePacketListenerImpl.LOGGER.warn("Rejecting UseItemOnPacket from {}: Location {} too far away from hit block {}.", new Object[]{this.player.getGameProfile().getName(), vec3d, blockposition});
             }
         }
     }
@@ -1097,15 +1993,62 @@
     @Override
     public void handleUseItem(ServerboundUseItemPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
+        if (this.player.isImmobile()) return; // CraftBukkit
+        if (!this.checkLimit(packet.timestamp)) return; // Spigot - check limit
         this.ackBlockChangesUpTo(packet.getSequence());
-        ServerLevel serverLevel = this.player.getLevel();
-        InteractionHand interactionHand = packet.getHand();
-        ItemStack itemStack = this.player.getItemInHand(interactionHand);
+        ServerLevel worldserver = this.player.getLevel();
+        InteractionHand enumhand = packet.getHand();
+        ItemStack itemstack = this.player.getItemInHand(enumhand);
+
         this.player.resetLastActionTime();
-        if (!itemStack.isEmpty()) {
-            InteractionResult interactionResult = this.player.gameMode.useItem(this.player, serverLevel, itemStack, interactionHand);
-            if (interactionResult.shouldSwing()) {
-                this.player.swing(interactionHand, true);
+        if (!itemstack.isEmpty()) {
+            // CraftBukkit start
+            // Raytrace to look for 'rogue armswings'
+            float f1 = this.player.getXRot();
+            float f2 = this.player.getYRot();
+            double d0 = this.player.getX();
+            double d1 = this.player.getY() + (double) this.player.getEyeHeight();
+            double d2 = this.player.getZ();
+            Vec3 vec3d = new Vec3(d0, d1, d2);
+
+            float f3 = Mth.cos(-f2 * 0.017453292F - 3.1415927F);
+            float f4 = Mth.sin(-f2 * 0.017453292F - 3.1415927F);
+            float f5 = -Mth.cos(-f1 * 0.017453292F);
+            float f6 = Mth.sin(-f1 * 0.017453292F);
+            float f7 = f4 * f5;
+            float f8 = f3 * f5;
+            double d3 = player.gameMode.getGameModeForPlayer()== GameType.CREATIVE ? 5.0D : 4.5D;
+            Vec3 vec3d1 = vec3d.add((double) f7 * d3, (double) f6 * d3, (double) f8 * d3);
+            HitResult movingobjectposition = this.player.level.clip(new ClipContext(vec3d, vec3d1, ClipContext.Block.OUTLINE, ClipContext.Fluid.NONE, this.player));
+
+            boolean cancelled;
+            if (movingobjectposition == null || movingobjectposition.getType() != HitResult.Type.BLOCK) {
+                org.bukkit.event.player.PlayerInteractEvent event = CraftEventFactory.callPlayerInteractEvent(this.player, Action.RIGHT_CLICK_AIR, itemstack, enumhand);
+                cancelled = event.useItemInHand() == Event.Result.DENY;
+            } else {
+                BlockHitResult movingobjectpositionblock = (BlockHitResult) movingobjectposition;
+                if (player.gameMode.firedInteract && player.gameMode.interactPosition.equals(movingobjectpositionblock.getBlockPos()) && player.gameMode.interactHand == enumhand && ItemStack.tagMatches(player.gameMode.interactItemStack, itemstack)) {
+                    cancelled = player.gameMode.interactResult;
+                } else {
+                    org.bukkit.event.player.PlayerInteractEvent event = CraftEventFactory.callPlayerInteractEvent(player, Action.RIGHT_CLICK_BLOCK, movingobjectpositionblock.getBlockPos(), movingobjectpositionblock.getDirection(), itemstack, true, enumhand);
+                    cancelled = event.useItemInHand() == Event.Result.DENY;
+                }
+                player.gameMode.firedInteract = false;
+            }
+
+            if (cancelled) {
+                this.player.getBukkitEntity().updateInventory(); // SPIGOT-2524
+                return;
+            }
+            itemstack = this.player.getItemInHand(enumhand); // Update in case it was changed in the event
+            if (itemstack.isEmpty()) {
+                return;
+            }
+            // CraftBukkit end
+            InteractionResult enuminteractionresult = this.player.gameMode.useItem(this.player, worldserver, itemstack, enumhand);
+
+            if (enuminteractionresult.shouldSwing()) {
+                this.player.swing(enumhand, true);
             }
 
         }
@@ -1115,10 +2058,14 @@
     public void handleTeleportToEntityPacket(ServerboundTeleportToEntityPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
         if (this.player.isSpectator()) {
-            for(ServerLevel serverLevel : this.server.getAllLevels()) {
-                Entity entity = packet.getEntity(serverLevel);
+            Iterator iterator = this.server.getAllLevels().iterator();
+
+            while (iterator.hasNext()) {
+                ServerLevel worldserver = (ServerLevel) iterator.next();
+                Entity entity = packet.getEntity(worldserver);
+
                 if (entity != null) {
-                    this.player.teleportTo(serverLevel, entity.getX(), entity.getY(), entity.getZ(), entity.getYRot(), entity.getXRot());
+                    this.player.teleportTo(worldserver, entity.getX(), entity.getY(), entity.getZ(), entity.getYRot(), entity.getXRot(), org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE); // CraftBukkit
                     return;
                 }
             }
@@ -1130,36 +2077,62 @@
     public void handleResourcePackResponse(ServerboundResourcePackPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
         if (packet.getAction() == ServerboundResourcePackPacket.Action.DECLINED && this.server.isResourcePackRequired()) {
-            LOGGER.info("Disconnecting {} due to resource pack rejection", (Object)this.player.getName());
-            this.disconnect(Component.translatable("multiplayer.requiredTexturePrompt.disconnect"));
+            ServerGamePacketListenerImpl.LOGGER.info("Disconnecting {} due to resource pack rejection", this.player.getGameProfile().getName()); // Paper - Don't print component in resource pack rejection message
+            this.disconnect(Component.translatable("multiplayer.requiredTexturePrompt.disconnect"), org.bukkit.event.player.PlayerKickEvent.Cause.RESOURCE_PACK_REJECTION); // Paper - add cause
         }
-
+        // Paper start
+        PlayerResourcePackStatusEvent.Status packStatus = PlayerResourcePackStatusEvent.Status.values()[packet.action.ordinal()];
+        player.getBukkitEntity().setResourcePackStatus(packStatus);
+        this.cserver.getPluginManager().callEvent(new PlayerResourcePackStatusEvent(this.getCraftPlayer(), packStatus)); // CraftBukkit
+        // Paper end
     }
 
     @Override
     public void handlePaddleBoat(ServerboundPaddleBoatPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
         Entity entity = this.player.getVehicle();
+
         if (entity instanceof Boat) {
-            ((Boat)entity).setPaddleState(packet.getLeft(), packet.getRight());
+            ((Boat) entity).setPaddleState(packet.getLeft(), packet.getRight());
         }
 
     }
 
     @Override
-    public void handlePong(ServerboundPongPacket packet) {
-    }
+    public void handlePong(ServerboundPongPacket packet) {}
 
     @Override
     public void onDisconnect(Component reason) {
-        LOGGER.info("{} lost connection: {}", this.player.getName().getString(), reason.getString());
+        // Paper start
+        this.onDisconnect(reason, null);
+    }
+    public void onDisconnect(Component reason, @Nullable net.kyori.adventure.text.Component quitMessage) {
+        // Paper end
+        // CraftBukkit start - Rarely it would send a disconnect line twice
+        if (this.processedDisconnect) {
+            return;
+        } else {
+            this.processedDisconnect = true;
+        }
+        // CraftBukkit end
+        ServerGamePacketListenerImpl.LOGGER.info("{} lost connection: {}", this.player.getName().getString(), reason.getString());
+        // CraftBukkit start - Replace vanilla quit message handling with our own.
+        /*
         this.server.invalidateStatus();
-        this.server.getPlayerList().broadcastSystemMessage(Component.translatable("multiplayer.player.left", this.player.getDisplayName()).withStyle(ChatFormatting.YELLOW), false);
+        this.server.getPlayerList().broadcastSystemMessage(IChatBaseComponent.translatable("multiplayer.player.left", this.player.getDisplayName()).withStyle(EnumChatFormat.YELLOW), false);
+        */
+
         this.player.disconnect();
-        this.server.getPlayerList().remove(this.player);
+        // Paper start - Adventure
+        quitMessage = quitMessage == null ? this.server.getPlayerList().remove(this.player) : this.server.getPlayerList().remove(this.player, quitMessage); // Paper - pass in quitMessage to fix kick message not being used
+        if ((quitMessage != null) && !quitMessage.equals(net.kyori.adventure.text.Component.empty())) {
+            this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(quitMessage), false);
+            // Paper end
+        }
+        // CraftBukkit end
         this.player.getTextFilter().leave();
         if (this.isSingleplayerOwner()) {
-            LOGGER.info("Stopping singleplayer server as player logged out");
+            ServerGamePacketListenerImpl.LOGGER.info("Stopping singleplayer server as player logged out");
             this.server.halt(false);
         }
 
@@ -1175,26 +2148,46 @@
 
     @Override
     public void send(Packet<?> packet) {
-        this.send(packet, (PacketSendListener)null);
+        this.send(packet, (PacketSendListener) null);
     }
 
     public void send(Packet<?> packet, @Nullable PacketSendListener callbacks) {
+        // CraftBukkit start
+        if (packet == null || this.processedDisconnect) { // Spigot
+            return;
+        } else if (packet instanceof ClientboundSetDefaultSpawnPositionPacket) {
+            ClientboundSetDefaultSpawnPositionPacket packet6 = (ClientboundSetDefaultSpawnPositionPacket) packet;
+            this.player.compassTarget = new Location(this.getCraftPlayer().getWorld(), packet6.pos.getX(), packet6.pos.getY(), packet6.pos.getZ());
+        }
+        // CraftBukkit end
+
         try {
             this.connection.send(packet, callbacks);
-        } catch (Throwable var6) {
-            CrashReport crashReport = CrashReport.forThrowable(var6, "Sending packet");
-            CrashReportCategory crashReportCategory = crashReport.addCategory("Packet being sent");
-            crashReportCategory.setDetail("Packet class", () -> {
+        } catch (Throwable throwable) {
+            CrashReport crashreport = CrashReport.forThrowable(throwable, "Sending packet");
+            CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Packet being sent");
+
+            crashreportsystemdetails.setDetail("Packet class", () -> {
                 return packet.getClass().getCanonicalName();
             });
-            throw new ReportedException(crashReport);
+            throw new ReportedException(crashreport);
         }
     }
 
     @Override
     public void handleSetCarriedItem(ServerboundSetCarriedItemPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
+        if (this.player.isImmobile()) return; // CraftBukkit
         if (packet.getSlot() >= 0 && packet.getSlot() < Inventory.getSelectionSize()) {
+            if (packet.getSlot() == this.player.getInventory().selected) { return; } // Paper - don't fire itemheldevent when there wasn't a slot change
+            PlayerItemHeldEvent event = new PlayerItemHeldEvent(this.getCraftPlayer(), this.player.getInventory().selected, packet.getSlot());
+            this.cserver.getPluginManager().callEvent(event);
+            if (event.isCancelled()) {
+                this.send(new ClientboundSetCarriedItemPacket(this.player.getInventory().selected));
+                this.player.resetLastActionTime();
+                return;
+            }
+            // CraftBukkit end
             if (this.player.getInventory().selected != packet.getSlot() && this.player.getUsedItemHand() == InteractionHand.MAIN_HAND) {
                 this.player.stopUsingItem();
             }
@@ -1202,30 +2195,42 @@
             this.player.getInventory().selected = packet.getSlot();
             this.player.resetLastActionTime();
         } else {
-            LOGGER.warn("{} tried to set an invalid carried item", (Object)this.player.getName().getString());
+            ServerGamePacketListenerImpl.LOGGER.warn("{} tried to set an invalid carried item", this.player.getName().getString());
+            this.disconnect("Invalid hotbar selection (Hacking?)", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // CraftBukkit // Paper - kick event cause
         }
     }
 
     @Override
     public void handleChat(ServerboundChatPacket packet) {
-        if (isChatMessageIllegal(packet.message())) {
-            this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"));
+        // CraftBukkit start - async chat
+        // SPIGOT-3638
+        if (this.server.isStopped()) {
+            return;
+        }
+        // CraftBukkit end
+        if (ServerGamePacketListenerImpl.isChatMessageIllegal(packet.message())) {
+            this.server.scheduleOnMain(() -> { // Paper - push to main for event firing
+            this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper - add cause
+            }); // Paper - push to main for event firing
         } else {
             if (this.tryHandleChat(packet.message(), packet.timeStamp(), packet.lastSeenMessages())) {
-                this.server.submit(() -> {
-                    PlayerChatMessage playerChatMessage = this.getSignedMessage(packet);
-                    if (this.verifyChatMessage(playerChatMessage)) {
+                // this.server.submit(() -> { // CraftBukkit - async chat
+                    PlayerChatMessage playerchatmessage = this.getSignedMessage(packet);
+
+                    if (this.verifyChatMessage(playerchatmessage)) {
                         this.chatMessageChain.append(() -> {
-                            CompletableFuture<FilteredText> completableFuture = this.filterTextPacket(playerChatMessage.signedContent().plain());
-                            CompletableFuture<PlayerChatMessage> completableFuture2 = this.server.getChatDecorator().decorate(this.player, playerChatMessage);
-                            return CompletableFuture.allOf(completableFuture, completableFuture2).thenAcceptAsync((void_) -> {
-                                FilterMask filterMask = completableFuture.join().mask();
-                                PlayerChatMessage playerChatMessage = completableFuture2.join().filter(filterMask);
-                                this.broadcastChatMessage(playerChatMessage);
-                            }, this.server);
+                            CompletableFuture<FilteredText> completablefuture = this.filterTextPacket(playerchatmessage.signedContent().plain());
+                            CompletableFuture<PlayerChatMessage> completablefuture1 = this.server.getChatDecorator().decorate(this.player, null, playerchatmessage); // Paper
+
+                            return CompletableFuture.allOf(completablefuture, completablefuture1).thenAcceptAsync((ovoid) -> {
+                                FilterMask filtermask = ((FilteredText) completablefuture.join()).mask();
+                                PlayerChatMessage playerchatmessage1 = ((PlayerChatMessage) completablefuture1.join()).filter(filtermask);
+
+                                this.broadcastChatMessage(playerchatmessage1);
+                            }, this.server.chatExecutor); // CraftBukkit - async chat
                         });
                     }
-                });
+                // }); // CraftBukkit - async chat
             }
 
         }
@@ -1233,13 +2238,15 @@
 
     @Override
     public void handleChatCommand(ServerboundChatCommandPacket packet) {
-        if (isChatMessageIllegal(packet.command())) {
-            this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"));
+        if (ServerGamePacketListenerImpl.isChatMessageIllegal(packet.command())) {
+            this.server.scheduleOnMain(() -> { // Paper - push to main for event firing
+            this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper
+            }); // Paper - push to main for event firing
         } else {
             if (this.tryHandleChat(packet.command(), packet.timeStamp(), packet.lastSeenMessages())) {
                 this.server.submit(() -> {
                     this.performChatCommand(packet);
-                    this.detectRateSpam();
+                    this.detectRateSpam("/" + packet.command()); // Spigot
                 });
             }
 
@@ -1247,63 +2254,102 @@
     }
 
     private void performChatCommand(ServerboundChatCommandPacket packet) {
-        ParseResults<CommandSourceStack> parseResults = this.parseCommand(packet.command());
-        Map<String, PlayerChatMessage> map = this.collectSignedArguments(packet, PreviewableCommand.of(parseResults));
+        // CraftBukkit start
+        String command = "/" + packet.command();
+        ServerGamePacketListenerImpl.LOGGER.info(this.player.getScoreboardName() + " issued server command: " + command);
 
-        for(PlayerChatMessage playerChatMessage : map.values()) {
-            if (!this.verifyChatMessage(playerChatMessage)) {
+        PlayerCommandPreprocessEvent event = new PlayerCommandPreprocessEvent(this.getCraftPlayer(), command, new LazyPlayerSet(this.server));
+        this.cserver.getPluginManager().callEvent(event);
+
+        command = event.getMessage().substring(1);
+
+        // Paper start - send message headers for cancelled or changed commands
+        ParseResults<CommandSourceStack> parseresults = this.parseCommand(packet.command());
+        Map<String, PlayerChatMessage> map = this.collectSignedArguments(packet, PreviewableCommand.of(parseresults));
+        if (event.isCancelled() || !packet.command().equals(command)) {
+            for (final PlayerChatMessage message : map.values()) {
+                player.server.getPlayerList().broadcastMessageHeader(message, Set.of());
+            }
+            if (event.isCancelled()) {
                 return;
             }
+
+            // Remove signatures if the command was changed and use the changed command source stack
+            map.clear();
+            parseresults = this.parseCommand(command);
         }
+        // Paper end
+        // CraftBukkit end
+        Iterator iterator = map.values().iterator();
+
+        PlayerChatMessage playerchatmessage;
+
+        do {
+            if (!iterator.hasNext()) {
+                CommandSigningContext.SignedArguments commandsigningcontext_a = new CommandSigningContext.SignedArguments(map);
+
+                parseresults = Commands.mapSource(parseresults, (commandlistenerwrapper) -> {
+                    return commandlistenerwrapper.withSigningContext(commandsigningcontext_a);
+                });
+                this.server.getCommands().performCommand(parseresults, command); // CraftBukkit
+                return;
+            }
+
+            playerchatmessage = (PlayerChatMessage) iterator.next();
+        } while (this.verifyChatMessage(playerchatmessage));
 
-        CommandSigningContext commandSigningContext = new CommandSigningContext.SignedArguments(map);
-        parseResults = Commands.mapSource(parseResults, (source) -> {
-            return source.withSigningContext(commandSigningContext);
-        });
-        this.server.getCommands().performCommand(parseResults, packet.command());
     }
 
     private Map<String, PlayerChatMessage> collectSignedArguments(ServerboundChatCommandPacket packet, PreviewableCommand<?> arguments) {
-        Component component = this.chatPreviewCache.pull(packet.command());
-        MessageSigner messageSigner = new MessageSigner(this.player.getUUID(), packet.timeStamp(), packet.salt());
-        LastSeenMessages lastSeenMessages = packet.lastSeenMessages().lastSeen();
-        Map<String, PlayerChatMessage> map = new Object2ObjectOpenHashMap<>();
-        SignedMessageChain.Decoder decoder = this.player.connection.signedMessageDecoder();
+        Component ichatbasecomponent = this.chatPreviewCache.pull(packet.command());
+        MessageSigner messagesigner = new MessageSigner(this.player.getUUID(), packet.timeStamp(), packet.salt());
+        LastSeenMessages lastseenmessages = packet.lastSeenMessages().lastSeen();
+        Map<String, PlayerChatMessage> map = new Object2ObjectOpenHashMap();
+        SignedMessageChain.Decoder signedmessagechain_a = this.player.connection.signedMessageDecoder();
+        Iterator iterator = ArgumentSignatures.collectPlainSignableArguments(arguments).iterator();
 
-        for(Pair<String, String> pair : ArgumentSignatures.collectPlainSignableArguments(arguments)) {
-            String string = pair.getFirst();
-            String string2 = pair.getSecond();
-            MessageSignature messageSignature = packet.argumentSignatures().get(string);
-            ChatMessageContent chatMessageContent;
-            if (packet.signedPreview() && component != null) {
-                chatMessageContent = new ChatMessageContent(string2, component);
+        while (iterator.hasNext()) {
+            Pair<String, String> pair = (Pair) iterator.next();
+            String s = (String) pair.getFirst();
+            String s1 = (String) pair.getSecond();
+            MessageSignature messagesignature = packet.argumentSignatures().get(s);
+            ChatMessageContent chatmessagecontent;
+
+            if (packet.signedPreview() && ichatbasecomponent != null) {
+                chatmessagecontent = new ChatMessageContent(s1, ichatbasecomponent);
             } else {
-                chatMessageContent = new ChatMessageContent(string2);
+                chatmessagecontent = new ChatMessageContent(s1);
             }
 
-            SignedMessageChain.Link link = new SignedMessageChain.Link(messageSignature);
-            map.put(string, decoder.unpack(link, messageSigner, chatMessageContent, lastSeenMessages));
+            SignedMessageChain.Link signedmessagechain_c = new SignedMessageChain.Link(messagesignature);
+
+            map.put(s, signedmessagechain_a.unpack(signedmessagechain_c, messagesigner, chatmessagecontent, lastseenmessages));
         }
 
         return map;
     }
 
     private ParseResults<CommandSourceStack> parseCommand(String command) {
-        CommandDispatcher<CommandSourceStack> commandDispatcher = this.server.getCommands().getDispatcher();
-        return commandDispatcher.parse(command, this.player.createCommandSourceStack());
+        com.mojang.brigadier.CommandDispatcher<CommandSourceStack> com_mojang_brigadier_commanddispatcher = this.server.getCommands().getDispatcher();
+
+        return com_mojang_brigadier_commanddispatcher.parse(command, this.player.createCommandSourceStack());
     }
 
     private boolean tryHandleChat(String message, Instant timestamp, LastSeenMessages.Update acknowledgment) {
         if (!this.updateChatOrder(timestamp)) {
-            LOGGER.warn("{} sent out-of-order chat: '{}'", this.player.getName().getString(), message);
-            this.disconnect(Component.translatable("multiplayer.disconnect.out_of_order_chat"));
+            ServerGamePacketListenerImpl.LOGGER.warn("{} sent out-of-order chat: '{}': {} > {}", this.player.getName().getString(), message, this.lastChatTimeStamp.get().getEpochSecond(), timestamp.getEpochSecond()); // Paper
+            this.server.scheduleOnMain(() -> { // Paper - push to main
+            this.disconnect(Component.translatable("multiplayer.disconnect.out_of_order_chat"), org.bukkit.event.player.PlayerKickEvent.Cause.OUT_OF_ORDER_CHAT); // Paper - kick event cause
+            }); // Paper - push to main
             return false;
-        } else if (this.player.getChatVisibility() == ChatVisiblity.HIDDEN) {
+        } else if (this.player.isRemoved() || this.player.getChatVisibility() == ChatVisiblity.HIDDEN) { // CraftBukkit - dead men tell no tales
             this.send(new ClientboundSystemChatPacket(Component.translatable("chat.disabled.options").withStyle(ChatFormatting.RED), false));
             return false;
         } else {
-            Set<LastSeenMessagesValidator.ErrorCondition> set;
-            synchronized(this.lastSeenMessagesValidator) {
+            LastSeenMessagesValidator lastseenmessagesvalidator = this.lastSeenMessagesValidator;
+            Set set;
+
+            synchronized (this.lastSeenMessagesValidator) {
                 set = this.lastSeenMessagesValidator.validateAndUpdate(acknowledgment);
             }
 
@@ -1318,19 +2364,20 @@
     }
 
     private boolean updateChatOrder(Instant timestamp) {
-        Instant instant;
+        Instant instant1;
+
         do {
-            instant = this.lastChatTimeStamp.get();
-            if (timestamp.isBefore(instant)) {
+            instant1 = (Instant) this.lastChatTimeStamp.get();
+            if (timestamp.isBefore(instant1)) {
                 return false;
             }
-        } while(!this.lastChatTimeStamp.compareAndSet(instant, timestamp));
+        } while (!this.lastChatTimeStamp.compareAndSet(instant1, timestamp));
 
         return true;
     }
 
-    private static boolean isChatMessageIllegal(String message) {
-        for(int i = 0; i < message.length(); ++i) {
+    public static boolean isChatMessageIllegal(String message) { // Paper - private -> public
+        for (int i = 0; i < message.length(); ++i) {
             if (!SharedConstants.isAllowedChatCharacter(message.charAt(i))) {
                 return true;
             }
@@ -1339,50 +2386,251 @@
         return false;
     }
 
+    // CraftBukkit start - add method
+    public void chat(String s, PlayerChatMessage original, boolean async) {
+        if (s.isEmpty() || this.player.getChatVisibility() == ChatVisiblity.HIDDEN) {
+            return;
+        }
+        OutgoingPlayerChatMessage outgoing = OutgoingPlayerChatMessage.create(original);
+
+        if (false && !async && s.startsWith("/")) { // Paper - don't handle commands in chat logic
+            this.handleCommand(s);
+        } else if (this.player.getChatVisibility() == ChatVisiblity.SYSTEM) {
+            // Do nothing, this is coming from a plugin
+        // Paper start
+        } else if (true) {
+            final ChatProcessor cp = new ChatProcessor(this.server, this.player, original, async);
+            cp.process();
+            // Paper end
+        } else if (false) { // Paper
+            Player player = this.getCraftPlayer();
+            AsyncPlayerChatEvent event = new AsyncPlayerChatEvent(async, player, s, new LazyPlayerSet(this.server));
+            String originalFormat = event.getFormat(), originalMessage = event.getMessage();
+            this.cserver.getPluginManager().callEvent(event);
+
+            if (PlayerChatEvent.getHandlerList().getRegisteredListeners().length != 0) {
+                // Evil plugins still listening to deprecated event
+                final PlayerChatEvent queueEvent = new PlayerChatEvent(player, event.getMessage(), event.getFormat(), event.getRecipients());
+                queueEvent.setCancelled(event.isCancelled());
+                Waitable waitable = new Waitable() {
+                    @Override
+                    protected Object evaluate() {
+                        org.bukkit.Bukkit.getPluginManager().callEvent(queueEvent);
+
+                        if (queueEvent.isCancelled()) {
+                            if (outgoing != null) {
+                                outgoing.sendHeadersToRemainingPlayers(ServerGamePacketListenerImpl.this.server.getPlayerList());
+                            }
+                            return null;
+                        }
+
+                        String message = String.format(queueEvent.getFormat(), queueEvent.getPlayer().getDisplayName(), queueEvent.getMessage());
+                        if (((LazyPlayerSet) queueEvent.getRecipients()).isLazy()) {
+                            if (!org.spigotmc.SpigotConfig.bungee && originalFormat.equals(queueEvent.getFormat()) && originalMessage.equals(queueEvent.getMessage()) && queueEvent.getPlayer().getName().equalsIgnoreCase(queueEvent.getPlayer().getDisplayName())) { // Spigot
+                                ServerGamePacketListenerImpl.this.server.getPlayerList().broadcastChatMessage(original, ServerGamePacketListenerImpl.this.player, ChatType.bind(ChatType.CHAT, (Entity) ServerGamePacketListenerImpl.this.player));
+                                return null;
+                            } else if (!org.spigotmc.SpigotConfig.bungee && CraftChatMessage.fromComponent(original.serverContent()).equals(message)) { // Spigot
+                                ServerGamePacketListenerImpl.this.server.getPlayerList().broadcastChatMessage(original, ServerGamePacketListenerImpl.this.player, ChatType.bind(ChatType.RAW, (Entity) ServerGamePacketListenerImpl.this.player));
+                                return null;
+                            }
+
+                            for (ServerPlayer recipient : ServerGamePacketListenerImpl.this.server.getPlayerList().players) {
+                                recipient.getBukkitEntity().sendMessage(ServerGamePacketListenerImpl.this.player.getUUID(), message);
+                            }
+                        } else {
+                            for (Player player : queueEvent.getRecipients()) {
+                                player.sendMessage(ServerGamePacketListenerImpl.this.player.getUUID(), message);
+                            }
+                        }
+                        ServerGamePacketListenerImpl.this.server.console.sendMessage(message);
+
+                        if (outgoing != null) {
+                            outgoing.sendHeadersToRemainingPlayers(ServerGamePacketListenerImpl.this.server.getPlayerList());
+                        }
+                        return null;
+                    }};
+                if (async) {
+                    server.processQueue.add(waitable);
+                } else {
+                    waitable.run();
+                }
+                try {
+                    waitable.get();
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt(); // This is proper habit for java. If we aren't handling it, pass it on!
+                } catch (ExecutionException e) {
+                    throw new RuntimeException("Exception processing chat event", e.getCause());
+                }
+            } else {
+                if (event.isCancelled()) {
+                    if (outgoing != null) {
+                        outgoing.sendHeadersToRemainingPlayers(ServerGamePacketListenerImpl.this.server.getPlayerList());
+                    }
+                    return;
+                }
+
+                s = String.format(event.getFormat(), event.getPlayer().getDisplayName(), event.getMessage());
+                if (((LazyPlayerSet) event.getRecipients()).isLazy()) {
+                    if (!org.spigotmc.SpigotConfig.bungee && originalFormat.equals(event.getFormat()) && originalMessage.equals(event.getMessage()) && event.getPlayer().getName().equalsIgnoreCase(event.getPlayer().getDisplayName())) { // Spigot
+                        ServerGamePacketListenerImpl.this.server.getPlayerList().broadcastChatMessage(original, ServerGamePacketListenerImpl.this.player, ChatType.bind(ChatType.CHAT, (Entity) ServerGamePacketListenerImpl.this.player));
+                        return;
+                    } else if (!org.spigotmc.SpigotConfig.bungee && CraftChatMessage.fromComponent(original.serverContent()).equals(s)) { // Spigot
+                        ServerGamePacketListenerImpl.this.server.getPlayerList().broadcastChatMessage(original, ServerGamePacketListenerImpl.this.player, ChatType.bind(ChatType.RAW, (Entity) ServerGamePacketListenerImpl.this.player));
+                        return;
+                    }
+
+                    for (ServerPlayer recipient : this.server.getPlayerList().players) {
+                        recipient.getBukkitEntity().sendMessage(ServerGamePacketListenerImpl.this.player.getUUID(), s);
+                    }
+                } else {
+                    for (Player recipient : event.getRecipients()) {
+                        recipient.sendMessage(ServerGamePacketListenerImpl.this.player.getUUID(), s);
+                    }
+                }
+                server.console.sendMessage(s);
+
+                if (outgoing != null) {
+                    outgoing.sendHeadersToRemainingPlayers(ServerGamePacketListenerImpl.this.server.getPlayerList());
+                }
+            }
+        }
+    }
+
+    public void handleCommand(String s) { // Paper - private -> public
+        // Paper Start
+        if (!org.spigotmc.AsyncCatcher.shuttingDown && !org.bukkit.Bukkit.isPrimaryThread()) {
+            LOGGER.error("Command Dispatched Async: " + s);
+            LOGGER.error("Please notify author of plugin causing this execution to fix this bug! see: http://bit.ly/1oSiM6C", new Throwable());
+            Waitable<Void> wait = new Waitable<>() {
+                @Override
+                protected Void evaluate() {
+                    ServerGamePacketListenerImpl.this.handleCommand(s);
+                    return null;
+                }
+            };
+            server.processQueue.add(wait);
+            try {
+                wait.get();
+                return;
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt(); // This is proper habit for java. If we aren't handling it, pass it on!
+            } catch (Exception e) {
+                throw new RuntimeException("Exception processing chat command", e.getCause());
+            }
+        }
+        // Paper End
+        co.aikar.timings.MinecraftTimings.playerCommandTimer.startTiming(); // Paper
+        if ( org.spigotmc.SpigotConfig.logCommands ) // Spigot
+        this.LOGGER.info(this.player.getScoreboardName() + " issued server command: " + s);
+
+        CraftPlayer player = this.getCraftPlayer();
+
+        PlayerCommandPreprocessEvent event = new PlayerCommandPreprocessEvent(player, s, new LazyPlayerSet(this.server));
+        this.cserver.getPluginManager().callEvent(event);
+
+        if (event.isCancelled()) {
+            co.aikar.timings.MinecraftTimings.playerCommandTimer.stopTiming(); // Paper
+            return;
+        }
+
+        try {
+            if (this.cserver.dispatchCommand(event.getPlayer(), event.getMessage().substring(1))) {
+                return;
+            }
+        } catch (org.bukkit.command.CommandException ex) {
+            player.sendMessage(org.bukkit.ChatColor.RED + "An internal error occurred while attempting to perform this command");
+            java.util.logging.Logger.getLogger(ServerGamePacketListenerImpl.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
+            return;
+        } finally {
+            co.aikar.timings.MinecraftTimings.playerCommandTimer.stopTiming(); // Paper
+        }
+    }
+    // CraftBukkit end
+
     private PlayerChatMessage getSignedMessage(ServerboundChatPacket packet) {
-        MessageSigner messageSigner = packet.getSigner(this.player);
-        SignedMessageChain.Link link = new SignedMessageChain.Link(packet.signature());
-        LastSeenMessages lastSeenMessages = packet.lastSeenMessages().lastSeen();
-        ChatMessageContent chatMessageContent = this.getSignedContent(packet);
-        return this.signedMessageDecoder.unpack(link, messageSigner, chatMessageContent, lastSeenMessages);
+        MessageSigner messagesigner = packet.getSigner(this.player);
+        SignedMessageChain.Link signedmessagechain_c = new SignedMessageChain.Link(packet.signature());
+        LastSeenMessages lastseenmessages = packet.lastSeenMessages().lastSeen();
+        ChatMessageContent chatmessagecontent = this.getSignedContent(packet);
+
+        return this.signedMessageDecoder.unpack(signedmessagechain_c, messagesigner, chatmessagecontent, lastseenmessages);
     }
 
     private ChatMessageContent getSignedContent(ServerboundChatPacket packet) {
-        Component component = this.chatPreviewCache.pull(packet.message());
-        return packet.signedPreview() && component != null ? new ChatMessageContent(packet.message(), component) : new ChatMessageContent(packet.message());
+        // Paper start
+        final net.minecraft.network.chat.ChatPreviewCache.Result result = this.chatPreviewCache.pullFull(packet.message());
+        Component ichatbasecomponent = result != null ? result.preview() : null;
+        // Paper end
+
+        return packet.signedPreview() && ichatbasecomponent != null ? new ChatMessageContent(packet.message(), ichatbasecomponent, result.decoratorResult()) : new ChatMessageContent(packet.message()); // Paper end
     }
 
     private void broadcastChatMessage(PlayerChatMessage message) {
-        this.server.getPlayerList().broadcastChatMessage(message, this.player, ChatType.bind(ChatType.CHAT, this.player));
-        this.detectRateSpam();
+        // CraftBukkit start
+        String s = message.signedContent().plain();
+        if (s.isEmpty()) {
+            ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send an empty message");
+        } else if (this.getCraftPlayer().isConversing()) {
+            OutgoingPlayerChatMessage outgoing = OutgoingPlayerChatMessage.create(message);
+            outgoing.sendHeadersToRemainingPlayers(this.server.getPlayerList());
+
+            final String conversationInput = s;
+            this.server.processQueue.add(new Runnable() {
+                @Override
+                public void run() {
+                    ServerGamePacketListenerImpl.this.getCraftPlayer().acceptConversationInput(conversationInput);
+                }
+            });
+        } else if (this.player.getChatVisibility() == ChatVisiblity.SYSTEM) { // Re-add "Command Only" flag check
+            this.send(new ClientboundSystemChatPacket(Component.translatable("chat.cannotSend").withStyle(ChatFormatting.RED), false));
+        } else {
+            this.chat(s, message, true);
+        }
+        // this.server.getPlayerList().broadcastChatMessage(playerchatmessage, this.player, ChatMessageType.bind(ChatMessageType.CHAT, (Entity) this.player));
+        // CraftBukkit end
+        this.detectRateSpam(s); // Spigot
     }
 
-    private boolean verifyChatMessage(PlayerChatMessage playerChatMessage) {
-        ChatSender chatSender = this.player.asChatSender();
-        ProfilePublicKey profilePublicKey = chatSender.profilePublicKey();
-        if (profilePublicKey != null) {
-            if (profilePublicKey.data().hasExpired()) {
+    private boolean verifyChatMessage(PlayerChatMessage playerchatmessage) {
+        ChatSender chatsender = this.player.asChatSender();
+        ProfilePublicKey profilepublickey = chatsender.profilePublicKey();
+
+        if (profilepublickey != null) {
+            if (profilepublickey.data().hasExpired()) {
                 this.player.sendSystemMessage(Component.translatable("chat.disabled.expiredProfileKey").withStyle(ChatFormatting.RED));
                 return false;
             }
 
-            if (!playerChatMessage.verify(chatSender)) {
-                this.disconnect(Component.translatable("multiplayer.disconnect.unsigned_chat"));
+            if (!playerchatmessage.verify(chatsender)) {
+                this.disconnect(Component.translatable("multiplayer.disconnect.unsigned_chat"), org.bukkit.event.player.PlayerKickEvent.Cause.UNSIGNED_CHAT); // Paper - kick event cause
                 return false;
             }
         }
 
-        if (playerChatMessage.hasExpiredServer(Instant.now())) {
-            LOGGER.warn("{} sent expired chat: '{}'. Is the client/server system time unsynchronized?", this.player.getName().getString(), playerChatMessage.signedContent().plain());
+        if (playerchatmessage.hasExpiredServer(Instant.now())) {
+            ServerGamePacketListenerImpl.LOGGER.warn("{} sent expired chat: '{}'. Is the client/server system time unsynchronized? c: {} s: {}", this.player.getName().getString(), playerchatmessage.signedContent().plain(), playerchatmessage.timeStamp().getEpochSecond(), Instant.now().getEpochSecond()); // Paper
         }
 
         return true;
     }
 
-    private void detectRateSpam() {
-        this.chatSpamTickCount += 20;
-        if (this.chatSpamTickCount > 200 && !this.server.getPlayerList().isOp(this.player.getGameProfile())) {
-            this.disconnect(Component.translatable("disconnect.spam"));
+    // Spigot start - spam exclusions
+    private void detectRateSpam(String s) {
+        // CraftBukkit start - replaced with thread safe throttle
+        boolean counted = true;
+        for ( String exclude : org.spigotmc.SpigotConfig.spamExclusions )
+        {
+            if ( exclude != null && s.startsWith( exclude ) )
+            {
+                counted = false;
+                break;
+            }
+        }
+        // Spigot end
+        // this.chatSpamTickCount += 20;
+        if (counted && this.chatSpamTickCount.addAndGet(20) > 200 && !this.server.getPlayerList().isOp(this.player.getGameProfile())) { // Paper - exclude from SpigotConfig.spamExclusions
+            // CraftBukkit end
+            this.disconnect(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - kick event cause
         }
 
     }
@@ -1392,9 +2640,10 @@
         if (this.handlesPreviewRequests()) {
             this.chatPreviewThrottler.schedule(() -> {
                 int i = packet.queryId();
-                String string = packet.query();
-                return this.queryPreview(string).thenAccept((decorated) -> {
-                    this.sendPreviewResponse(i, decorated);
+                String s = packet.query();
+
+                return this.queryPreview(s).thenAccept((ichatbasecomponent) -> {
+                    this.sendPreviewResponse(i, ichatbasecomponent);
                 });
             });
         }
@@ -1407,62 +2656,72 @@
 
     private void sendPreviewResponse(int queryId, Component preview) {
         this.send(new ClientboundChatPreviewPacket(queryId, preview), PacketSendListener.exceptionallySend(() -> {
-            return new ClientboundChatPreviewPacket(queryId, (Component)null);
+            return new ClientboundChatPreviewPacket(queryId, (Component) null);
         }));
     }
 
     private CompletableFuture<Component> queryPreview(String query) {
-        String string = StringUtils.normalizeSpace(query);
-        return string.startsWith("/") ? this.queryCommandPreview(string.substring(1)) : this.queryChatPreview(query);
+        String s1 = StringUtils.normalizeSpace(query);
+
+        return s1.startsWith("/") ? this.queryCommandPreview(s1.substring(1)) : this.queryChatPreview(query);
     }
 
     private CompletableFuture<Component> queryChatPreview(String query) {
-        Component component = Component.literal(query);
-        CompletableFuture<Component> completableFuture = this.server.getChatDecorator().decorate(this.player, component).thenApply((decorated) -> {
-            return !component.equals(decorated) ? decorated : null;
+        MutableComponent ichatmutablecomponent = Component.literal(query);
+        // Paper start
+        final CompletableFuture<net.minecraft.network.chat.ChatDecorator.Result> result = this.server.getChatDecorator().decorate(this.player, null, ichatmutablecomponent, true);
+        CompletableFuture<net.minecraft.network.chat.ChatDecorator.Result> completablefuture = result.thenApply((res) -> {
+            return !ichatmutablecomponent.equals(res.component()) ? res : null;
+            // Paper end
         });
-        completableFuture.thenAcceptAsync((decorated) -> {
-            this.chatPreviewCache.set(query, decorated);
+
+        completablefuture.thenAcceptAsync((ichatbasecomponent) -> {
+            if (ichatbasecomponent != null) this.chatPreviewCache.set(query, ichatbasecomponent); // Paper
         }, this.server);
-        return completableFuture;
+        return completablefuture.thenApply(net.minecraft.network.chat.ChatDecorator.Result::component); // paper
     }
 
     private CompletableFuture<Component> queryCommandPreview(String query) {
-        CommandSourceStack commandSourceStack = this.player.createCommandSourceStack();
-        ParseResults<CommandSourceStack> parseResults = this.server.getCommands().getDispatcher().parse(query, commandSourceStack);
-        CompletableFuture<Component> completableFuture = this.getPreviewedArgument(commandSourceStack, PreviewableCommand.of(parseResults));
-        completableFuture.thenAcceptAsync((decorated) -> {
-            this.chatPreviewCache.set(query, decorated);
+        CommandSourceStack commandlistenerwrapper = this.player.createCommandSourceStack();
+        ParseResults<CommandSourceStack> parseresults = this.server.getCommands().getDispatcher().parse(query, commandlistenerwrapper);
+        CompletableFuture<Component> completablefuture = this.getPreviewedArgument(commandlistenerwrapper, PreviewableCommand.of(parseresults));
+
+        completablefuture.thenAcceptAsync((ichatbasecomponent) -> {
+            if (ichatbasecomponent != null) this.chatPreviewCache.set(query, ichatbasecomponent); // Paper
         }, this.server);
-        return completableFuture;
+        return completablefuture;
     }
 
     private CompletableFuture<Component> getPreviewedArgument(CommandSourceStack source, PreviewableCommand<CommandSourceStack> arguments) {
         List<PreviewableCommand.Argument<CommandSourceStack>> list = arguments.arguments();
+
         if (list.isEmpty()) {
-            return CompletableFuture.completedFuture((Component)null);
+            return CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
         } else {
-            for(int i = list.size() - 1; i >= 0; --i) {
-                PreviewableCommand.Argument<CommandSourceStack> argument = list.get(i);
+            for (int i = list.size() - 1; i >= 0; --i) {
+                PreviewableCommand.Argument previewablecommand_a = (PreviewableCommand.Argument) list.get(i);
 
                 try {
-                    CompletableFuture<Component> completableFuture = argument.previewType().resolvePreview(source, argument.parsedValue());
-                    if (completableFuture != null) {
-                        return completableFuture;
+                    CompletableFuture<Component> completablefuture = previewablecommand_a.previewType().resolvePreview(source, previewablecommand_a.parsedValue());
+
+                    if (completablefuture != null) {
+                        return completablefuture;
                     }
-                } catch (CommandSyntaxException var7) {
-                    return CompletableFuture.completedFuture((Component)null);
+                } catch (CommandSyntaxException commandsyntaxexception) {
+                    return CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
                 }
             }
 
-            return CompletableFuture.completedFuture((Component)null);
+            return CompletableFuture.completedFuture(null); // CraftBukkit - decompile error
         }
     }
 
     @Override
     public void handleChatAck(ServerboundChatAckPacket packet) {
-        Set<LastSeenMessagesValidator.ErrorCondition> set;
-        synchronized(this.lastSeenMessagesValidator) {
+        LastSeenMessagesValidator lastseenmessagesvalidator = this.lastSeenMessagesValidator;
+        Set set;
+
+        synchronized (this.lastSeenMessagesValidator) {
             set = this.lastSeenMessagesValidator.validateAndUpdate(packet.lastSeenMessages());
         }
 
@@ -1473,24 +2732,79 @@
     }
 
     private void handleValidationFailure(Set<LastSeenMessagesValidator.ErrorCondition> reasons) {
-        LOGGER.warn("Failed to validate message from {}, reasons: {}", this.player.getName().getString(), reasons.stream().map(LastSeenMessagesValidator.ErrorCondition::message).collect(Collectors.joining(",")));
-        this.disconnect(Component.translatable("multiplayer.disconnect.chat_validation_failed"));
+        ServerGamePacketListenerImpl.LOGGER.warn("Failed to validate message from {}, reasons: {}", this.player.getName().getString(), reasons.stream().map(LastSeenMessagesValidator.ErrorCondition::message).collect(Collectors.joining(",")));
+        this.disconnect(Component.translatable("multiplayer.disconnect.chat_validation_failed"), org.bukkit.event.player.PlayerKickEvent.Cause.CHAT_VALIDATION_FAILED); // Paper - kick event cause
     }
 
     @Override
     public void handleAnimate(ServerboundSwingPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
+        if (this.player.isImmobile()) return; // CraftBukkit
         this.player.resetLastActionTime();
+        // CraftBukkit start - Raytrace to look for 'rogue armswings'
+        float f1 = this.player.getXRot();
+        float f2 = this.player.getYRot();
+        double d0 = this.player.getX();
+        double d1 = this.player.getY() + (double) this.player.getEyeHeight();
+        double d2 = this.player.getZ();
+        Location origin = new Location(this.player.level.getWorld(), d0, d1, d2, f2, f1);
+
+        double d3 = player.gameMode.getGameModeForPlayer() == GameType.CREATIVE ? 5.0D : 4.5D;
+        // SPIGOT-5607: Only call interact event if no block or entity is being clicked. Use bukkit ray trace method, because it handles blocks and entities at the same time
+        org.bukkit.util.RayTraceResult result = this.player.level.getWorld().rayTrace(origin, origin.getDirection(), d3, org.bukkit.FluidCollisionMode.NEVER, false, 0.1, entity -> entity != this.player.getBukkitEntity());
+
+        if (result == null || this.player.gameMode.getGameModeForPlayer() == GameType.ADVENTURE) { // Paper - call PlayerInteractEvent when left-clicking on a block in adventure mode
+            CraftEventFactory.callPlayerInteractEvent(this.player, Action.LEFT_CLICK_AIR, this.player.getInventory().getSelected(), InteractionHand.MAIN_HAND);
+        }
+
+        // Arm swing animation
+        io.papermc.paper.event.player.PlayerArmSwingEvent event = new io.papermc.paper.event.player.PlayerArmSwingEvent(this.getCraftPlayer(), packet.getHand() == InteractionHand.MAIN_HAND ? org.bukkit.inventory.EquipmentSlot.HAND : org.bukkit.inventory.EquipmentSlot.OFF_HAND); // Paper
+        this.cserver.getPluginManager().callEvent(event);
+
+        if (event.isCancelled()) return;
+        // CraftBukkit end
         this.player.swing(packet.getHand());
     }
 
     @Override
     public void handlePlayerCommand(ServerboundPlayerCommandPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
+        // CraftBukkit start
+        if (this.player.isRemoved()) return;
+        switch (packet.getAction()) {
+            case PRESS_SHIFT_KEY:
+            case RELEASE_SHIFT_KEY:
+                PlayerToggleSneakEvent event = new PlayerToggleSneakEvent(this.getCraftPlayer(), packet.getAction() == ServerboundPlayerCommandPacket.Action.PRESS_SHIFT_KEY);
+                this.cserver.getPluginManager().callEvent(event);
+
+                if (event.isCancelled()) {
+                    return;
+                }
+                break;
+            case START_SPRINTING:
+            case STOP_SPRINTING:
+                PlayerToggleSprintEvent e2 = new PlayerToggleSprintEvent(this.getCraftPlayer(), packet.getAction() == ServerboundPlayerCommandPacket.Action.START_SPRINTING);
+                this.cserver.getPluginManager().callEvent(e2);
+
+                if (e2.isCancelled()) {
+                    return;
+                }
+                break;
+        }
+        // CraftBukkit end
         this.player.resetLastActionTime();
+        PlayerRideableJumping ijumpable;
+
         switch (packet.getAction()) {
             case PRESS_SHIFT_KEY:
                 this.player.setShiftKeyDown(true);
+
+                // Paper start - Hang on!
+                if (this.player.level.paperConfig().entities.behavior.parrotsAreUnaffectedByPlayerMovement) {
+                    this.player.removeEntitiesOnShoulder();
+                }
+                // Paper end
+
                 break;
             case RELEASE_SHIFT_KEY:
                 this.player.setShiftKeyDown(false);
@@ -1509,23 +2823,27 @@
                 break;
             case START_RIDING_JUMP:
                 if (this.player.getVehicle() instanceof PlayerRideableJumping) {
-                    PlayerRideableJumping playerRideableJumping = (PlayerRideableJumping)this.player.getVehicle();
+                    ijumpable = (PlayerRideableJumping) this.player.getVehicle();
                     int i = packet.getData();
-                    if (playerRideableJumping.canJump() && i > 0) {
-                        playerRideableJumping.handleStartJump(i);
+
+                    if (ijumpable.canJump() && i > 0) {
+                        ijumpable.handleStartJump(i);
                     }
                 }
                 break;
             case STOP_RIDING_JUMP:
                 if (this.player.getVehicle() instanceof PlayerRideableJumping) {
-                    PlayerRideableJumping playerRideableJumping2 = (PlayerRideableJumping)this.player.getVehicle();
-                    playerRideableJumping2.handleStopJump();
+                    ijumpable = (PlayerRideableJumping) this.player.getVehicle();
+                    ijumpable.handleStopJump();
                 }
                 break;
             case OPEN_INVENTORY:
-                Entity i = this.player.getVehicle();
-                if (i instanceof HasCustomInventoryScreen hasCustomInventoryScreen) {
-                    hasCustomInventoryScreen.openCustomInventoryScreen(this.player);
+                Entity entity = this.player.getVehicle();
+
+                if (entity instanceof HasCustomInventoryScreen) {
+                    HasCustomInventoryScreen hascustominventoryscreen = (HasCustomInventoryScreen) entity;
+
+                    hascustominventoryscreen.openCustomInventoryScreen(this.player);
                 }
                 break;
             case START_FALL_FLYING:
@@ -1544,16 +2862,19 @@
     }
 
     public void addPendingMessage(PlayerChatMessage message) {
-        LastSeenMessages.Entry entry = message.toLastSeenEntry();
-        if (entry != null) {
+        LastSeenMessages.Entry lastseenmessages_a = message.toLastSeenEntry();
+
+        if (lastseenmessages_a != null) {
+            LastSeenMessagesValidator lastseenmessagesvalidator = this.lastSeenMessagesValidator;
             int i;
-            synchronized(this.lastSeenMessagesValidator) {
-                this.lastSeenMessagesValidator.addPending(entry);
+
+            synchronized (this.lastSeenMessagesValidator) {
+                this.lastSeenMessagesValidator.addPending(lastseenmessages_a);
                 i = this.lastSeenMessagesValidator.pendingMessagesCount();
             }
 
             if (i > 4096) {
-                this.disconnect(Component.translatable("multiplayer.disconnect.too_many_pending_chats"));
+                this.disconnect(Component.translatable("multiplayer.disconnect.too_many_pending_chats"), org.bukkit.event.player.PlayerKickEvent.Cause.TOO_MANY_PENDING_CHATS); // Paper - kick event cause
             }
 
         }
@@ -1562,24 +2883,81 @@
     @Override
     public void handleInteract(ServerboundInteractPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
-        ServerLevel serverLevel = this.player.getLevel();
-        final Entity entity = packet.getTarget(serverLevel);
+        if (this.player.isImmobile()) return; // CraftBukkit
+        ServerLevel worldserver = this.player.getLevel();
+        final Entity entity = packet.getTarget(worldserver);
+        // Spigot Start
+        if ( entity == this.player && !this.player.isSpectator() )
+        {
+            this.disconnect( "Cannot interact with self!", org.bukkit.event.player.PlayerKickEvent.Cause.SELF_INTERACTION ); // Paper - add cause
+            return;
+        }
+        // Spigot End
+
         this.player.resetLastActionTime();
         this.player.setShiftKeyDown(packet.isUsingSecondaryAction());
         if (entity != null) {
-            if (!serverLevel.getWorldBorder().isWithinBounds(entity.blockPosition())) {
+            if (!worldserver.getWorldBorder().isWithinBounds(entity.blockPosition())) {
                 return;
             }
 
-            if (entity.distanceToSqr(this.player.getEyePosition()) < MAX_INTERACTION_DISTANCE) {
+            if (entity.distanceToSqr(this.player.getEyePosition()) < ServerGamePacketListenerImpl.MAX_INTERACTION_DISTANCE) {
                 packet.dispatch(new ServerboundInteractPacket.Handler() {
-                    private void performInteraction(InteractionHand hand, ServerGamePacketListenerImpl.EntityInteraction action) {
-                        ItemStack itemStack = ServerGamePacketListenerImpl.this.player.getItemInHand(hand).copy();
-                        InteractionResult interactionResult = action.run(ServerGamePacketListenerImpl.this.player, entity, hand);
-                        if (interactionResult.consumesAction()) {
-                            CriteriaTriggers.PLAYER_INTERACTED_WITH_ENTITY.trigger(ServerGamePacketListenerImpl.this.player, itemStack, entity);
-                            if (interactionResult.shouldSwing()) {
-                                ServerGamePacketListenerImpl.this.player.swing(hand, true);
+                    private void performInteraction(InteractionHand enumhand, ServerGamePacketListenerImpl.EntityInteraction playerconnection_a, PlayerInteractEntityEvent event) { // CraftBukkit
+                        ItemStack itemstack = ServerGamePacketListenerImpl.this.player.getItemInHand(enumhand).copy();
+                        // CraftBukkit start
+                        ItemStack itemInHand = ServerGamePacketListenerImpl.this.player.getItemInHand(enumhand);
+                        boolean triggerLeashUpdate = itemInHand != null && itemInHand.getItem() == Items.LEAD && entity instanceof Mob;
+                        Item origItem = ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null ? null : ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem();
+
+                        ServerGamePacketListenerImpl.this.cserver.getPluginManager().callEvent(event);
+
+                        // Entity in bucket - SPIGOT-4048 and SPIGOT-6859
+                        if ((entity instanceof Bucketable && entity instanceof LivingEntity && origItem != null && origItem.asItem() == Items.WATER_BUCKET) && (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null || ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem() != origItem)) {
+                            ServerGamePacketListenerImpl.this.send(new ClientboundAddEntityPacket(entity));
+                            player.containerMenu.sendAllDataToRemote();
+                        }
+
+                        if (triggerLeashUpdate && (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null || ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem() != origItem)) {
+                            // Refresh the current leash state
+                            ServerGamePacketListenerImpl.this.send(new ClientboundSetEntityLinkPacket(entity, ((Mob) entity).getLeashHolder()));
+                        }
+
+                        if (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null || ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem() != origItem) {
+                            // Refresh the current entity metadata
+                            // Paper start - update entity for all players
+                            ClientboundSetEntityDataPacket entityDataPacket = new ClientboundSetEntityDataPacket(entity.getId(), entity.getEntityData(), true);
+                            if (entity.tracker != null) {
+                                entity.tracker.broadcast(entityDataPacket);
+                            } else {
+                                ServerGamePacketListenerImpl.this.send(entityDataPacket);
+                            }
+                            // Paper end
+                            // SPIGOT-7136 - Allays
+                            if (entity instanceof Allay allay) { // Paper
+                                ServerGamePacketListenerImpl.this.send(new ClientboundSetEquipmentPacket(entity.getId(), Arrays.stream(net.minecraft.world.entity.EquipmentSlot.values()).map((slot) -> Pair.of(slot, allay.stripMeta(allay.getItemBySlot(slot), true))).collect(Collectors.toList()))); // Paper - remove unnecessary item meta
+                                player.containerMenu.sendAllDataToRemote();
+                            }
+                        }
+
+                        if (event.isCancelled()) {
+                            ServerGamePacketListenerImpl.this.player.containerMenu.sendAllDataToRemote(); // Paper - Refresh player inventory
+                            return;
+                        }
+                        // CraftBukkit end
+
+                        InteractionResult enuminteractionresult = playerconnection_a.run(ServerGamePacketListenerImpl.this.player, entity, enumhand);
+
+                        // CraftBukkit start
+                        if (!itemInHand.isEmpty() && itemInHand.getCount() <= -1) {
+                            player.containerMenu.sendAllDataToRemote();
+                        }
+                        // CraftBukkit end
+
+                        if (enuminteractionresult.consumesAction()) {
+                            CriteriaTriggers.PLAYER_INTERACTED_WITH_ENTITY.trigger(ServerGamePacketListenerImpl.this.player, itemstack, entity);
+                            if (enuminteractionresult.shouldSwing()) {
+                                ServerGamePacketListenerImpl.this.player.swing(enumhand, true);
                             }
                         }
 
@@ -1587,41 +2965,78 @@
 
                     @Override
                     public void onInteraction(InteractionHand hand) {
-                        this.performInteraction(hand, Player::interactOn);
+                        this.performInteraction(hand, net.minecraft.world.entity.player.Player::interactOn, new PlayerInteractEntityEvent(ServerGamePacketListenerImpl.this.getCraftPlayer(), entity.getBukkitEntity(), (hand == InteractionHand.OFF_HAND) ? EquipmentSlot.OFF_HAND : EquipmentSlot.HAND));
                     }
 
                     @Override
                     public void onInteraction(InteractionHand hand, Vec3 pos) {
-                        this.performInteraction(hand, (player, entityx, handx) -> {
-                            return entityx.interactAt(player, pos, handx);
-                        });
+                        this.performInteraction(hand, (entityplayer, entity1, enumhand1) -> {
+                            return entity1.interactAt(entityplayer, pos, enumhand1);
+                        }, new PlayerInteractAtEntityEvent(ServerGamePacketListenerImpl.this.getCraftPlayer(), entity.getBukkitEntity(), new org.bukkit.util.Vector(pos.x, pos.y, pos.z), (hand == InteractionHand.OFF_HAND) ? EquipmentSlot.OFF_HAND : EquipmentSlot.HAND)); // CraftBukkit
                     }
 
                     @Override
                     public void onAttack() {
-                        if (!(entity instanceof ItemEntity) && !(entity instanceof ExperienceOrb) && !(entity instanceof AbstractArrow) && entity != ServerGamePacketListenerImpl.this.player) {
+                        // CraftBukkit start
+                        if (!(entity instanceof ItemEntity) && !(entity instanceof ExperienceOrb) && !(entity instanceof AbstractArrow) && (entity != ServerGamePacketListenerImpl.this.player || ServerGamePacketListenerImpl.this.player.isSpectator())) {
+                            ItemStack itemInHand = ServerGamePacketListenerImpl.this.player.getMainHandItem();
                             ServerGamePacketListenerImpl.this.player.attack(entity);
+
+                            if (!itemInHand.isEmpty() && itemInHand.getCount() <= -1) {
+                                player.containerMenu.sendAllDataToRemote();
+                            }
+                            // CraftBukkit end
                         } else {
-                            ServerGamePacketListenerImpl.this.disconnect(Component.translatable("multiplayer.disconnect.invalid_entity_attacked"));
-                            ServerGamePacketListenerImpl.LOGGER.warn("Player {} tried to attack an invalid entity", (Object)ServerGamePacketListenerImpl.this.player.getName().getString());
+                            ServerGamePacketListenerImpl.this.disconnect(Component.translatable("multiplayer.disconnect.invalid_entity_attacked"),  org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_ENTITY_ATTACKED); // Paper - add cause
+                            ServerGamePacketListenerImpl.LOGGER.warn("Player {} tried to attack an invalid entity", ServerGamePacketListenerImpl.this.player.getName().getString());
                         }
                     }
                 });
             }
         }
+        // Paper start - fire event
+        else {
+            packet.dispatch(new net.minecraft.network.protocol.game.ServerboundInteractPacket.Handler() {
+                @Override
+                public void onInteraction(net.minecraft.world.InteractionHand hand) {
+                    ServerGamePacketListenerImpl.this.callPlayerUseUnknownEntityEvent(packet, hand);
+                }
 
+                @Override
+                public void onInteraction(net.minecraft.world.InteractionHand hand, net.minecraft.world.phys.Vec3 pos) {
+                    ServerGamePacketListenerImpl.this.callPlayerUseUnknownEntityEvent(packet, hand);
+                }
+
+                @Override
+                public void onAttack() {
+                    ServerGamePacketListenerImpl.this.callPlayerUseUnknownEntityEvent(packet, net.minecraft.world.InteractionHand.MAIN_HAND);
+                }
+            });
+        }
+
+    }
+
+    private void callPlayerUseUnknownEntityEvent(ServerboundInteractPacket packet, InteractionHand hand) {
+        this.cserver.getPluginManager().callEvent(new com.destroystokyo.paper.event.player.PlayerUseUnknownEntityEvent(
+            this.getCraftPlayer(),
+            packet.getEntityId(),
+            packet.getActionType() == ServerboundInteractPacket.ActionType.ATTACK,
+            hand == InteractionHand.MAIN_HAND ? EquipmentSlot.HAND : EquipmentSlot.OFF_HAND
+        ));
     }
+    // Paper end
 
     @Override
     public void handleClientCommand(ServerboundClientCommandPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
         this.player.resetLastActionTime();
-        ServerboundClientCommandPacket.Action action = packet.getAction();
-        switch (action) {
+        ServerboundClientCommandPacket.Action packetplayinclientcommand_enumclientcommand = packet.getAction();
+
+        switch (packetplayinclientcommand_enumclientcommand) {
             case PERFORM_RESPAWN:
                 if (this.player.wonGame) {
                     this.player.wonGame = false;
-                    this.player = this.server.getPlayerList().respawn(this.player, true);
+                    this.player = this.server.getPlayerList().respawn(this.player, this.server.getLevel(this.player.getRespawnDimension()), true, null, true, org.bukkit.event.player.PlayerRespawnEvent.RespawnFlag.END_PORTAL); // Paper - add isEndCreditsRespawn argument
                     CriteriaTriggers.CHANGED_DIMENSION.trigger(this.player, Level.END, Level.OVERWORLD);
                 } else {
                     if (this.player.getHealth() > 0.0F) {
@@ -1630,8 +3045,8 @@
 
                     this.player = this.server.getPlayerList().respawn(this.player, false);
                     if (this.server.isHardcore()) {
-                        this.player.setGameMode(GameType.SPECTATOR);
-                        this.player.getLevel().getGameRules().getRule(GameRules.RULE_SPECTATORSGENERATECHUNKS).set(false, this.server);
+                        this.player.setGameMode(GameType.SPECTATOR, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.HARDCORE_DEATH, null); // Paper
+                        ((GameRules.BooleanValue) this.player.getLevel().getGameRules().getRule(GameRules.RULE_SPECTATORSGENERATECHUNKS)).set(false, this.player.getLevel()); // Paper
                     }
                 }
                 break;
@@ -1643,35 +3058,328 @@
 
     @Override
     public void handleContainerClose(ServerboundContainerClosePacket packet) {
+        // Paper start
+        handleContainerClose(packet, InventoryCloseEvent.Reason.PLAYER);
+    }
+    public void handleContainerClose(ServerboundContainerClosePacket packet, InventoryCloseEvent.Reason reason) {
+        // Paper end
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
+
+        if (this.player.isImmobile()) return; // CraftBukkit
+        CraftEventFactory.handleInventoryCloseEvent(this.player, reason); // CraftBukkit // Paper
+
         this.player.doCloseContainer();
     }
 
     @Override
     public void handleContainerClick(ServerboundContainerClickPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
+        if (this.player.isImmobile()) return; // CraftBukkit
         this.player.resetLastActionTime();
-        if (this.player.containerMenu.containerId == packet.getContainerId()) {
-            if (this.player.isSpectator()) {
+        if (this.player.containerMenu.containerId == packet.getContainerId() && this.player.containerMenu.stillValid(this.player)) { // CraftBukkit
+            boolean cancelled = this.player.isSpectator(); // CraftBukkit - see below if
+            if (false/*this.player.isSpectator()*/) { // CraftBukkit
                 this.player.containerMenu.sendAllDataToRemote();
             } else if (!this.player.containerMenu.stillValid(this.player)) {
-                LOGGER.debug("Player {} interacted with invalid menu {}", this.player, this.player.containerMenu);
+                ServerGamePacketListenerImpl.LOGGER.debug("Player {} interacted with invalid menu {}", this.player, this.player.containerMenu);
             } else {
                 int i = packet.getSlotNum();
+
                 if (!this.player.containerMenu.isValidSlotIndex(i)) {
-                    LOGGER.debug("Player {} clicked invalid slot index: {}, available slots: {}", this.player.getName(), i, this.player.containerMenu.slots.size());
+                    ServerGamePacketListenerImpl.LOGGER.debug("Player {} clicked invalid slot index: {}, available slots: {}", new Object[]{this.player.getName(), i, this.player.containerMenu.slots.size()});
                 } else {
-                    boolean bl = packet.getStateId() != this.player.containerMenu.getStateId();
+                    boolean flag = packet.getStateId() != this.player.containerMenu.getStateId();
+
                     this.player.containerMenu.suppressRemoteUpdates();
-                    this.player.containerMenu.clicked(i, packet.getButtonNum(), packet.getClickType(), this.player);
+                    // CraftBukkit start - Call InventoryClickEvent
+                    if (packet.getSlotNum() < -1 && packet.getSlotNum() != -999) {
+                        return;
+                    }
 
-                    for(Int2ObjectMap.Entry<ItemStack> entry : Int2ObjectMaps.fastIterable(packet.getChangedSlots())) {
-                        this.player.containerMenu.setRemoteSlotNoCopy(entry.getIntKey(), entry.getValue());
+                    InventoryView inventory = this.player.containerMenu.getBukkitView();
+                    SlotType type = inventory.getSlotType(packet.getSlotNum());
+
+                    InventoryClickEvent event;
+                    ClickType click = ClickType.UNKNOWN;
+                    InventoryAction action = InventoryAction.UNKNOWN;
+
+                    ItemStack itemstack = ItemStack.EMPTY;
+
+                    switch (packet.getClickType()) {
+                        case PICKUP:
+                            if (packet.getButtonNum() == 0) {
+                                click = ClickType.LEFT;
+                            } else if (packet.getButtonNum() == 1) {
+                                click = ClickType.RIGHT;
+                            }
+                            if (packet.getButtonNum() == 0 || packet.getButtonNum() == 1) {
+                                action = InventoryAction.NOTHING; // Don't want to repeat ourselves
+                                if (packet.getSlotNum() == -999) {
+                                    if (!player.containerMenu.getCarried().isEmpty()) {
+                                        action = packet.getButtonNum() == 0 ? InventoryAction.DROP_ALL_CURSOR : InventoryAction.DROP_ONE_CURSOR;
+                                    }
+                                } else if (packet.getSlotNum() < 0)  {
+                                    action = InventoryAction.NOTHING;
+                                } else {
+                                    Slot slot = this.player.containerMenu.getSlot(packet.getSlotNum());
+                                    if (slot != null) {
+                                        ItemStack clickedItem = slot.getItem();
+                                        ItemStack cursor = player.containerMenu.getCarried();
+                                        if (clickedItem.isEmpty()) {
+                                            if (!cursor.isEmpty()) {
+                                                action = packet.getButtonNum() == 0 ? InventoryAction.PLACE_ALL : InventoryAction.PLACE_ONE;
+                                            }
+                                        } else if (slot.mayPickup(player)) {
+                                            if (cursor.isEmpty()) {
+                                                action = packet.getButtonNum() == 0 ? InventoryAction.PICKUP_ALL : InventoryAction.PICKUP_HALF;
+                                            } else if (slot.mayPlace(cursor)) {
+                                                if (clickedItem.sameItem(cursor) && ItemStack.tagMatches(clickedItem, cursor)) {
+                                                    int toPlace = packet.getButtonNum() == 0 ? cursor.getCount() : 1;
+                                                    toPlace = Math.min(toPlace, clickedItem.getMaxStackSize() - clickedItem.getCount());
+                                                    toPlace = Math.min(toPlace, slot.container.getMaxStackSize() - clickedItem.getCount());
+                                                    if (toPlace == 1) {
+                                                        action = InventoryAction.PLACE_ONE;
+                                                    } else if (toPlace == cursor.getCount()) {
+                                                        action = InventoryAction.PLACE_ALL;
+                                                    } else if (toPlace < 0) {
+                                                        action = toPlace != -1 ? InventoryAction.PICKUP_SOME : InventoryAction.PICKUP_ONE; // this happens with oversized stacks
+                                                    } else if (toPlace != 0) {
+                                                        action = InventoryAction.PLACE_SOME;
+                                                    }
+                                                } else if (cursor.getCount() <= slot.getMaxStackSize()) {
+                                                    action = InventoryAction.SWAP_WITH_CURSOR;
+                                                }
+                                            } else if (cursor.getItem() == clickedItem.getItem() && ItemStack.tagMatches(cursor, clickedItem)) {
+                                                if (clickedItem.getCount() >= 0) {
+                                                    if (clickedItem.getCount() + cursor.getCount() <= cursor.getMaxStackSize()) {
+                                                        // As of 1.5, this is result slots only
+                                                        action = InventoryAction.PICKUP_ALL;
+                                                    }
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                            break;
+                        // TODO check on updates
+                        case QUICK_MOVE:
+                            if (packet.getButtonNum() == 0) {
+                                click = ClickType.SHIFT_LEFT;
+                            } else if (packet.getButtonNum() == 1) {
+                                click = ClickType.SHIFT_RIGHT;
+                            }
+                            if (packet.getButtonNum() == 0 || packet.getButtonNum() == 1) {
+                                if (packet.getSlotNum() < 0) {
+                                    action = InventoryAction.NOTHING;
+                                } else {
+                                    Slot slot = this.player.containerMenu.getSlot(packet.getSlotNum());
+                                    if (slot != null && slot.mayPickup(this.player) && slot.hasItem()) {
+                                        action = InventoryAction.MOVE_TO_OTHER_INVENTORY;
+                                    } else {
+                                        action = InventoryAction.NOTHING;
+                                    }
+                                }
+                            }
+                            break;
+                        case SWAP:
+                            if ((packet.getButtonNum() >= 0 && packet.getButtonNum() < 9) || packet.getButtonNum() == 40) {
+                                click = (packet.getButtonNum() == 40) ? ClickType.SWAP_OFFHAND : ClickType.NUMBER_KEY;
+                                Slot clickedSlot = this.player.containerMenu.getSlot(packet.getSlotNum());
+                                if (clickedSlot.mayPickup(player)) {
+                                    ItemStack hotbar = this.player.getInventory().getItem(packet.getButtonNum());
+                                    boolean canCleanSwap = hotbar.isEmpty() || (clickedSlot.container == this.player.getInventory() && clickedSlot.mayPlace(hotbar)); // the slot will accept the hotbar item
+                                    if (clickedSlot.hasItem()) {
+                                        if (canCleanSwap) {
+                                            action = InventoryAction.HOTBAR_SWAP;
+                                        } else {
+                                            action = InventoryAction.HOTBAR_MOVE_AND_READD;
+                                        }
+                                    } else if (!clickedSlot.hasItem() && !hotbar.isEmpty() && clickedSlot.mayPlace(hotbar)) {
+                                        action = InventoryAction.HOTBAR_SWAP;
+                                    } else {
+                                        action = InventoryAction.NOTHING;
+                                    }
+                                } else {
+                                    action = InventoryAction.NOTHING;
+                                }
+                            }
+                            break;
+                        case CLONE:
+                            if (packet.getButtonNum() == 2) {
+                                click = ClickType.MIDDLE;
+                                if (packet.getSlotNum() < 0) {
+                                    action = InventoryAction.NOTHING;
+                                } else {
+                                    Slot slot = this.player.containerMenu.getSlot(packet.getSlotNum());
+                                    if (slot != null && slot.hasItem() && this.player.getAbilities().instabuild && player.containerMenu.getCarried().isEmpty()) {
+                                        action = InventoryAction.CLONE_STACK;
+                                    } else {
+                                        action = InventoryAction.NOTHING;
+                                    }
+                                }
+                            } else {
+                                click = ClickType.UNKNOWN;
+                                action = InventoryAction.UNKNOWN;
+                            }
+                            break;
+                        case THROW:
+                            if (packet.getSlotNum() >= 0) {
+                                if (packet.getButtonNum() == 0) {
+                                    click = ClickType.DROP;
+                                    Slot slot = this.player.containerMenu.getSlot(packet.getSlotNum());
+                                    if (slot != null && slot.hasItem() && slot.mayPickup(player) && !slot.getItem().isEmpty() && slot.getItem().getItem() != Item.byBlock(Blocks.AIR)) {
+                                        action = InventoryAction.DROP_ONE_SLOT;
+                                    } else {
+                                        action = InventoryAction.NOTHING;
+                                    }
+                                } else if (packet.getButtonNum() == 1) {
+                                    click = ClickType.CONTROL_DROP;
+                                    Slot slot = this.player.containerMenu.getSlot(packet.getSlotNum());
+                                    if (slot != null && slot.hasItem() && slot.mayPickup(player) && !slot.getItem().isEmpty() && slot.getItem().getItem() != Item.byBlock(Blocks.AIR)) {
+                                        action = InventoryAction.DROP_ALL_SLOT;
+                                    } else {
+                                        action = InventoryAction.NOTHING;
+                                    }
+                                }
+                            } else {
+                                // Sane default (because this happens when they are holding nothing. Don't ask why.)
+                                click = ClickType.LEFT;
+                                if (packet.getButtonNum() == 1) {
+                                    click = ClickType.RIGHT;
+                                }
+                                action = InventoryAction.NOTHING;
+                            }
+                            break;
+                        case QUICK_CRAFT:
+                            this.player.containerMenu.clicked(packet.getSlotNum(), packet.getButtonNum(), packet.getClickType(), this.player);
+                            break;
+                        case PICKUP_ALL:
+                            click = ClickType.DOUBLE_CLICK;
+                            action = InventoryAction.NOTHING;
+                            if (packet.getSlotNum() >= 0 && !this.player.containerMenu.getCarried().isEmpty()) {
+                                ItemStack cursor = this.player.containerMenu.getCarried();
+                                action = InventoryAction.NOTHING;
+                                // Quick check for if we have any of the item
+                                if (inventory.getTopInventory().contains(CraftMagicNumbers.getMaterial(cursor.getItem())) || inventory.getBottomInventory().contains(CraftMagicNumbers.getMaterial(cursor.getItem()))) {
+                                    action = InventoryAction.COLLECT_TO_CURSOR;
+                                }
+                            }
+                            break;
+                        default:
+                            break;
+                    }
+
+                    if (packet.getClickType() != net.minecraft.world.inventory.ClickType.QUICK_CRAFT) {
+                        if (click == ClickType.NUMBER_KEY) {
+                            event = new InventoryClickEvent(inventory, type, packet.getSlotNum(), click, action, packet.getButtonNum());
+                        } else {
+                            event = new InventoryClickEvent(inventory, type, packet.getSlotNum(), click, action);
+                        }
+
+                        org.bukkit.inventory.Inventory top = inventory.getTopInventory();
+                        if (packet.getSlotNum() == 0 && top instanceof CraftingInventory) {
+                            org.bukkit.inventory.Recipe recipe = ((CraftingInventory) top).getRecipe();
+                            if (recipe != null) {
+                                if (click == ClickType.NUMBER_KEY) {
+                                    event = new CraftItemEvent(recipe, inventory, type, packet.getSlotNum(), click, action, packet.getButtonNum());
+                                } else {
+                                    event = new CraftItemEvent(recipe, inventory, type, packet.getSlotNum(), click, action);
+                                }
+                            }
+                        }
+
+                        if (packet.getSlotNum() == 2 && top instanceof SmithingInventory) {
+                            org.bukkit.inventory.ItemStack result = ((SmithingInventory) top).getResult();
+                            if (result != null) {
+                                if (click == ClickType.NUMBER_KEY) {
+                                    event = new SmithItemEvent(inventory, type, packet.getSlotNum(), click, action, packet.getButtonNum());
+                                } else {
+                                    event = new SmithItemEvent(inventory, type, packet.getSlotNum(), click, action);
+                                }
+                            }
+                        }
+
+                        event.setCancelled(cancelled);
+                        AbstractContainerMenu oldContainer = this.player.containerMenu; // SPIGOT-1224
+                        this.cserver.getPluginManager().callEvent(event);
+                        if (this.player.containerMenu != oldContainer) {
+                            return;
+                        }
+
+                        switch (event.getResult()) {
+                            case ALLOW:
+                            case DEFAULT:
+                                this.player.containerMenu.clicked(i, packet.getButtonNum(), packet.getClickType(), this.player);
+                                break;
+                            case DENY:
+                                /* Needs enum constructor in InventoryAction
+                                if (action.modifiesOtherSlots()) {
+
+                                } else {
+                                    if (action.modifiesCursor()) {
+                                        this.player.playerConnection.sendPacket(new Packet103SetSlot(-1, -1, this.player.inventory.getCarried()));
+                                    }
+                                    if (action.modifiesClicked()) {
+                                        this.player.playerConnection.sendPacket(new Packet103SetSlot(this.player.activeContainer.windowId, packet102windowclick.slot, this.player.activeContainer.getSlot(packet102windowclick.slot).getItem()));
+                                    }
+                                }*/
+                                switch (action) {
+                                    // Modified other slots
+                                    case PICKUP_ALL:
+                                    case MOVE_TO_OTHER_INVENTORY:
+                                    case HOTBAR_MOVE_AND_READD:
+                                    case HOTBAR_SWAP:
+                                    case COLLECT_TO_CURSOR:
+                                    case UNKNOWN:
+                                        this.player.containerMenu.sendAllDataToRemote();
+                                        break;
+                                    // Modified cursor and clicked
+                                    case PICKUP_SOME:
+                                    case PICKUP_HALF:
+                                    case PICKUP_ONE:
+                                    case PLACE_ALL:
+                                    case PLACE_SOME:
+                                    case PLACE_ONE:
+                                    case SWAP_WITH_CURSOR:
+                                        this.player.connection.send(new ClientboundContainerSetSlotPacket(-1, -1, this.player.inventoryMenu.incrementStateId(), this.player.containerMenu.getCarried()));
+                                        this.player.connection.send(new ClientboundContainerSetSlotPacket(this.player.containerMenu.containerId, this.player.inventoryMenu.incrementStateId(), packet.getSlotNum(), this.player.containerMenu.getSlot(packet.getSlotNum()).getItem()));
+                                        break;
+                                    // Modified clicked only
+                                    case DROP_ALL_SLOT:
+                                    case DROP_ONE_SLOT:
+                                        this.player.connection.send(new ClientboundContainerSetSlotPacket(this.player.containerMenu.containerId, this.player.inventoryMenu.incrementStateId(), packet.getSlotNum(), this.player.containerMenu.getSlot(packet.getSlotNum()).getItem()));
+                                        break;
+                                    // Modified cursor only
+                                    case DROP_ALL_CURSOR:
+                                    case DROP_ONE_CURSOR:
+                                    case CLONE_STACK:
+                                        this.player.connection.send(new ClientboundContainerSetSlotPacket(-1, -1, this.player.inventoryMenu.incrementStateId(), this.player.containerMenu.getCarried()));
+                                        break;
+                                    // Nothing
+                                    case NOTHING:
+                                        break;
+                                }
+                        }
+
+                        if (event instanceof CraftItemEvent || event instanceof SmithItemEvent) {
+                            // Need to update the inventory on crafting to
+                            // correctly support custom recipes
+                            player.containerMenu.sendAllDataToRemote();
+                        }
+                    }
+                    // CraftBukkit end
+                    ObjectIterator objectiterator = Int2ObjectMaps.fastIterable(packet.getChangedSlots()).iterator();
+
+                    while (objectiterator.hasNext()) {
+                        Entry<ItemStack> entry = (Entry) objectiterator.next();
+
+                        this.player.containerMenu.setRemoteSlotNoCopy(entry.getIntKey(), (ItemStack) entry.getValue());
                     }
 
                     this.player.containerMenu.setRemoteCarried(packet.getCarriedItem());
                     this.player.containerMenu.resumeRemoteUpdates();
-                    if (bl) {
+                    if (flag) {
                         this.player.containerMenu.broadcastFullState();
                     } else {
                         this.player.containerMenu.broadcastChanges();
@@ -1684,15 +3392,29 @@
 
     @Override
     public void handlePlaceRecipe(ServerboundPlaceRecipePacket packet) {
+        // Paper start
+        if (!org.bukkit.Bukkit.isPrimaryThread()) {
+            if (recipeSpamPackets.addAndGet(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamIncrement) > io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamLimit) {
+                server.scheduleOnMain(() -> this.disconnect(net.minecraft.network.chat.Component.translatable("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause
+                return;
+            }
+        }
+        // Paper end
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
         this.player.resetLastActionTime();
         if (!this.player.isSpectator() && this.player.containerMenu.containerId == packet.getContainerId() && this.player.containerMenu instanceof RecipeBookMenu) {
             if (!this.player.containerMenu.stillValid(this.player)) {
-                LOGGER.debug("Player {} interacted with invalid menu {}", this.player, this.player.containerMenu);
+                ServerGamePacketListenerImpl.LOGGER.debug("Player {} interacted with invalid menu {}", this.player, this.player.containerMenu);
             } else {
-                this.server.getRecipeManager().byKey(packet.getRecipe()).ifPresent((recipe) -> {
-                    ((RecipeBookMenu)this.player.containerMenu).handlePlacement(packet.isShiftDown(), recipe, this.player);
-                });
+                // Paper start - fire event for clicking recipes in the recipe book
+                com.destroystokyo.paper.event.player.PlayerRecipeBookClickEvent event = new com.destroystokyo.paper.event.player.PlayerRecipeBookClickEvent(
+                    player.getBukkitEntity(), org.bukkit.craftbukkit.v1_19_R1.util.CraftNamespacedKey.fromMinecraft(packet.getRecipe()), packet.isShiftDown());
+                if (event.callEvent() && this.player.containerMenu instanceof RecipeBookMenu<?> recipeBookMenu) { // check if inventory changed during event handling
+                    this.server.getRecipeManager().byKey(org.bukkit.craftbukkit.v1_19_R1.util.CraftNamespacedKey.toMinecraft(event.getRecipe())).ifPresent((irecipe) -> {
+                        recipeBookMenu.handlePlacement(event.isMakeAll(), irecipe, this.player);
+                    });
+                }
+                // Paper end
             }
         }
     }
@@ -1700,13 +3422,15 @@
     @Override
     public void handleContainerButtonClick(ServerboundContainerButtonClickPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
+        if (this.player.isImmobile()) return; // CraftBukkit
         this.player.resetLastActionTime();
         if (this.player.containerMenu.containerId == packet.getContainerId() && !this.player.isSpectator()) {
             if (!this.player.containerMenu.stillValid(this.player)) {
-                LOGGER.debug("Player {} interacted with invalid menu {}", this.player, this.player.containerMenu);
+                ServerGamePacketListenerImpl.LOGGER.debug("Player {} interacted with invalid menu {}", this.player, this.player.containerMenu);
             } else {
-                boolean bl = this.player.containerMenu.clickMenuButton(this.player, packet.getButtonId());
-                if (bl) {
+                boolean flag = this.player.containerMenu.clickMenuButton(this.player, packet.getButtonId());
+
+                if (flag) {
                     this.player.containerMenu.broadcastChanges();
                 }
 
@@ -1718,27 +3442,73 @@
     public void handleSetCreativeModeSlot(ServerboundSetCreativeModeSlotPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
         if (this.player.gameMode.isCreative()) {
-            boolean bl = packet.getSlotNum() < 0;
-            ItemStack itemStack = packet.getItem();
-            CompoundTag compoundTag = BlockItem.getBlockEntityData(itemStack);
-            if (!itemStack.isEmpty() && compoundTag != null && compoundTag.contains("x") && compoundTag.contains("y") && compoundTag.contains("z")) {
-                BlockPos blockPos = BlockEntity.getPosFromTag(compoundTag);
-                if (this.player.level.isLoaded(blockPos)) {
-                    BlockEntity blockEntity = this.player.level.getBlockEntity(blockPos);
-                    if (blockEntity != null) {
-                        blockEntity.saveToItem(itemStack);
+            boolean flag = packet.getSlotNum() < 0;
+            ItemStack itemstack = packet.getItem();
+            CompoundTag nbttagcompound = BlockItem.getBlockEntityData(itemstack);
+
+            if (!itemstack.isEmpty() && nbttagcompound != null && nbttagcompound.contains("x") && nbttagcompound.contains("y") && nbttagcompound.contains("z") && this.player.getBukkitEntity().hasPermission("minecraft.nbt.copy")) { // Spigot
+                BlockPos blockposition = BlockEntity.getPosFromTag(nbttagcompound);
+
+                if (this.player.level.isLoaded(blockposition)) {
+                    // Paper start
+                    BlockEntity tileentity = null;
+                    if (this.player.distanceToSqr(blockposition.getX(), blockposition.getY(), blockposition.getZ()) < 32 * 32 && this.player.getLevel().isLoadedAndInBounds(blockposition)) {
+                        tileentity = this.player.level.getBlockEntity(blockposition);
+                    }
+                    // Paper end
+
+                    if (tileentity != null) {
+                        tileentity.saveToItem(itemstack);
                     }
                 }
             }
 
-            boolean bl2 = packet.getSlotNum() >= 1 && packet.getSlotNum() <= 45;
-            boolean bl3 = itemStack.isEmpty() || itemStack.getDamageValue() >= 0 && itemStack.getCount() <= 64 && !itemStack.isEmpty();
-            if (bl2 && bl3) {
-                this.player.inventoryMenu.getSlot(packet.getSlotNum()).set(itemStack);
+            boolean flag1 = packet.getSlotNum() >= 1 && packet.getSlotNum() <= 45;
+            boolean flag2 = itemstack.isEmpty() || itemstack.getDamageValue() >= 0 && itemstack.getCount() <= 64 && !itemstack.isEmpty();
+            if (flag || (flag1 && !ItemStack.matches(this.player.inventoryMenu.getSlot(packet.getSlotNum()).getItem(), packet.getItem()))) { // Insist on valid slot
+                // CraftBukkit start - Call click event
+                InventoryView inventory = this.player.inventoryMenu.getBukkitView();
+                org.bukkit.inventory.ItemStack item = CraftItemStack.asBukkitCopy(packet.getItem());
+
+                SlotType type = SlotType.QUICKBAR;
+                if (flag) {
+                    type = SlotType.OUTSIDE;
+                } else if (packet.getSlotNum() < 36) {
+                    if (packet.getSlotNum() >= 5 && packet.getSlotNum() < 9) {
+                        type = SlotType.ARMOR;
+                    } else {
+                        type = SlotType.CONTAINER;
+                    }
+                }
+                InventoryCreativeEvent event = new InventoryCreativeEvent(inventory, type, flag ? -999 : packet.getSlotNum(), item);
+                this.cserver.getPluginManager().callEvent(event);
+
+                itemstack = CraftItemStack.asNMSCopy(event.getCursor());
+
+                switch (event.getResult()) {
+                case ALLOW:
+                    // Plugin cleared the id / stacksize checks
+                    flag2 = true;
+                    break;
+                case DEFAULT:
+                    break;
+                case DENY:
+                    // Reset the slot
+                    if (packet.getSlotNum() >= 0) {
+                        this.player.connection.send(new ClientboundContainerSetSlotPacket(this.player.inventoryMenu.containerId, this.player.inventoryMenu.incrementStateId(), packet.getSlotNum(), this.player.inventoryMenu.getSlot(packet.getSlotNum()).getItem()));
+                        this.player.connection.send(new ClientboundContainerSetSlotPacket(-1, this.player.inventoryMenu.incrementStateId(), -1, ItemStack.EMPTY));
+                    }
+                    return;
+                }
+            }
+            // CraftBukkit end
+
+            if (flag1 && flag2) {
+                this.player.inventoryMenu.getSlot(packet.getSlotNum()).set(itemstack);
                 this.player.inventoryMenu.broadcastChanges();
-            } else if (bl && bl3 && this.dropSpamTickCount < 200) {
+            } else if (flag && flag2 && this.dropSpamTickCount < 200) {
                 this.dropSpamTickCount += 20;
-                this.player.drop(itemStack, true);
+                this.player.drop(itemstack, true);
             }
         }
 
@@ -1746,52 +3516,96 @@
 
     @Override
     public void handleSignUpdate(ServerboundSignUpdatePacket packet) {
-        List<String> list = Stream.of(packet.getLines()).map(ChatFormatting::stripFormatting).collect(Collectors.toList());
-        this.filterTextPacket(list).thenAcceptAsync((texts) -> {
-            this.updateSignText(packet, texts);
+        // Paper start - cap line length - modified clients can send longer data than normal
+        String[] lines = packet.getLines();
+        for (int i = 0; i < lines.length; ++i) {
+            if (MAX_SIGN_LINE_LENGTH > 0 && lines[i].length() > MAX_SIGN_LINE_LENGTH) {
+                // This handles multibyte characters as 1
+                int offset = lines[i].codePoints().limit(MAX_SIGN_LINE_LENGTH).map(Character::charCount).sum();
+                if (offset < lines[i].length()) {
+                    lines[i] = lines[i].substring(0, offset); // this will break any filtering, but filtering is NYI as of 1.17
+                }
+            }
+        }
+        List<String> list = (List) Stream.of(lines).map(ChatFormatting::stripFormatting).collect(Collectors.toList());
+        // Paper end
+
+        this.filterTextPacket(list).thenAcceptAsync((list1) -> {
+            this.updateSignText(packet, list1);
         }, this.server);
     }
 
     private void updateSignText(ServerboundSignUpdatePacket packet, List<FilteredText> signText) {
+        if (this.player.isImmobile()) return; // CraftBukkit
         this.player.resetLastActionTime();
-        ServerLevel serverLevel = this.player.getLevel();
-        BlockPos blockPos = packet.getPos();
-        if (serverLevel.hasChunkAt(blockPos)) {
-            BlockState blockState = serverLevel.getBlockState(blockPos);
-            BlockEntity blockEntity = serverLevel.getBlockEntity(blockPos);
-            if (!(blockEntity instanceof SignBlockEntity)) {
+        ServerLevel worldserver = this.player.getLevel();
+        BlockPos blockposition = packet.getPos();
+
+        if (worldserver.hasChunkAt(blockposition)) {
+            BlockState iblockdata = worldserver.getBlockState(blockposition);
+            BlockEntity tileentity = worldserver.getBlockEntity(blockposition);
+
+            if (!(tileentity instanceof SignBlockEntity)) {
                 return;
             }
 
-            SignBlockEntity signBlockEntity = (SignBlockEntity)blockEntity;
-            if (!signBlockEntity.isEditable() || !this.player.getUUID().equals(signBlockEntity.getPlayerWhoMayEdit())) {
-                LOGGER.warn("Player {} just tried to change non-editable sign", (Object)this.player.getName().getString());
+            SignBlockEntity tileentitysign = (SignBlockEntity) tileentity;
+
+            if (!tileentitysign.isEditable() || !this.player.getUUID().equals(tileentitysign.getPlayerWhoMayEdit())) {
+                ServerGamePacketListenerImpl.LOGGER.warn("Player {} just tried to change non-editable sign", this.player.getName().getString());
+                if (this.player.distanceToSqr(blockposition.getX(), blockposition.getY(), blockposition.getZ()) < 32 * 32) // Paper
+                this.send(tileentity.getUpdatePacket()); // CraftBukkit
                 return;
             }
 
-            for(int i = 0; i < signText.size(); ++i) {
-                FilteredText filteredText = signText.get(i);
+            // CraftBukkit start // Paper start - Adventure
+            Player player = this.player.getBukkitEntity();
+            int x = packet.getPos().getX();
+            int y = packet.getPos().getY();
+            int z = packet.getPos().getZ();
+            List<net.kyori.adventure.text.Component> lines = new java.util.ArrayList<>();
+
+            for (int i = 0; i < signText.size(); ++i) {
+                FilteredText filteredtext = (FilteredText) signText.get(i);
+
                 if (this.player.isTextFilteringEnabled()) {
-                    signBlockEntity.setMessage(i, Component.literal(filteredText.filteredOrEmpty()));
+                    lines.add(net.kyori.adventure.text.Component.text(SharedConstants.filterText(filteredtext.filteredOrEmpty()))); // Paper - adventure
                 } else {
-                    signBlockEntity.setMessage(i, Component.literal(filteredText.raw()), Component.literal(filteredText.filteredOrEmpty()));
+                    lines.add(net.kyori.adventure.text.Component.text(SharedConstants.filterText(filteredtext.raw()))); // Paper - adventure
                 }
             }
+            SignChangeEvent event = new SignChangeEvent((org.bukkit.craftbukkit.v1_19_R1.block.CraftBlock) player.getWorld().getBlockAt(x, y, z), this.player.getBukkitEntity(), lines);
+            this.cserver.getPluginManager().callEvent(event);
 
-            signBlockEntity.setChanged();
-            serverLevel.sendBlockUpdated(blockPos, blockState, blockState, 3);
+            if (!event.isCancelled()) {
+                for (int i = 0; i < 4; i++) {
+                    tileentitysign.setMessage(i, PaperAdventure.asVanilla(event.line(i)));
+                }
+                // Paper end
+                tileentitysign.isEditable = false;
+            }
+            // CraftBukkit end
+
+            tileentitysign.setChanged();
+            worldserver.sendBlockUpdated(blockposition, iblockdata, iblockdata, 3);
         }
 
     }
 
     @Override
     public void handleKeepAlive(ServerboundKeepAlivePacket packet) {
+        //PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel()); // CraftBukkit // Paper - This shouldn't be on the main thread
         if (this.keepAlivePending && packet.getId() == this.keepAliveChallenge) {
-            int i = (int)(Util.getMillis() - this.keepAliveTime);
+            int i = (int) (Util.getMillis() - this.keepAliveTime);
+
             this.player.latency = (this.player.latency * 3 + i) / 4;
             this.keepAlivePending = false;
         } else if (!this.isSingleplayerOwner()) {
-            this.disconnect(Component.translatable("disconnect.timeout"));
+            // Paper start - This needs to be handled on the main thread for plugins
+            server.submit(() -> {
+            this.disconnect(Component.translatable("disconnect.timeout"), org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause
+            });
+            // Paper end
         }
 
     }
@@ -1799,24 +3613,99 @@
     @Override
     public void handlePlayerAbilities(ServerboundPlayerAbilitiesPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
-        this.player.getAbilities().flying = packet.isFlying() && this.player.getAbilities().mayfly;
+        // CraftBukkit start
+        if (this.player.getAbilities().mayfly && this.player.getAbilities().flying != packet.isFlying()) {
+            PlayerToggleFlightEvent event = new PlayerToggleFlightEvent(this.player.getBukkitEntity(), packet.isFlying());
+            this.cserver.getPluginManager().callEvent(event);
+            if (!event.isCancelled()) {
+                this.player.getAbilities().flying = packet.isFlying(); // Actually set the player's flying status
+            } else {
+                this.player.onUpdateAbilities(); // Tell the player their ability was reverted
+            }
+        }
+        // CraftBukkit end
     }
 
     @Override
     public void handleClientInformation(ServerboundClientInformationPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
+        // Paper start - do not accept invalid information
+        if (packet.viewDistance() < 0) {
+            LOGGER.warn("Disconnecting " + this.player.getScoreboardName() + " for invalid view distance: " + packet.viewDistance());
+            this.disconnect("Invalid client settings", PlayerKickEvent.Cause.ILLEGAL_ACTION);
+            return;
+        }
+        // Paper end - do not accept invalid information
         this.player.updateOptions(packet);
     }
 
+    // CraftBukkit start
+    private static final ResourceLocation CUSTOM_REGISTER = new ResourceLocation("register");
+    private static final ResourceLocation CUSTOM_UNREGISTER = new ResourceLocation("unregister");
+
+    private static final ResourceLocation MINECRAFT_BRAND = new ResourceLocation("brand"); // Paper - Brand support
+
     @Override
     public void handleCustomPayload(ServerboundCustomPayloadPacket packet) {
+        PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
+        if (packet.identifier.equals(CUSTOM_REGISTER)) {
+            try {
+                String channels = packet.data.toString(com.google.common.base.Charsets.UTF_8);
+                for (String channel : channels.split("\0")) {
+                    this.getCraftPlayer().addChannel(channel);
+                }
+            } catch (Exception ex) {
+                ServerGamePacketListenerImpl.LOGGER.error("Couldn\'t register custom payload", ex);
+                this.disconnect("Invalid payload REGISTER!", org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PAYLOAD); // Paper - kick event cause
+            }
+        } else if (packet.identifier.equals(CUSTOM_UNREGISTER)) {
+            try {
+                String channels = packet.data.toString(com.google.common.base.Charsets.UTF_8);
+                for (String channel : channels.split("\0")) {
+                    this.getCraftPlayer().removeChannel(channel);
+                }
+            } catch (Exception ex) {
+                ServerGamePacketListenerImpl.LOGGER.error("Couldn\'t unregister custom payload", ex);
+                this.disconnect("Invalid payload UNREGISTER!", org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PAYLOAD); // Paper - kick event cause
+            }
+        } else {
+            try {
+                byte[] data = new byte[packet.data.readableBytes()];
+                packet.data.readBytes(data);
+                // Paper start - Brand support
+                if (packet.identifier.equals(MINECRAFT_BRAND)) {
+                    try {
+                        this.clientBrandName = new net.minecraft.network.FriendlyByteBuf(io.netty.buffer.Unpooled.copiedBuffer(data)).readUtf(256);
+                    } catch (StringIndexOutOfBoundsException ex) {
+                        this.clientBrandName = "illegal";
+                    }
+                }
+                // Paper end
+                this.cserver.getMessenger().dispatchIncomingMessage(this.player.getBukkitEntity(), packet.identifier.toString(), data);
+            } catch (Exception ex) {
+                ServerGamePacketListenerImpl.LOGGER.error("Couldn\'t dispatch custom payload", ex);
+                this.disconnect("Invalid custom payload!", org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PAYLOAD); // Paper - kick event cause
+            }
+        }
+
     }
 
+    // Paper start - brand support
+    public String getClientBrandName() {
+        return clientBrandName;
+    }
+    // Paper end
+
+    public final boolean isDisconnected() {
+        return (!this.player.joining && !this.connection.isConnected()) || this.processedDisconnect; // Paper
+    }
+    // CraftBukkit end
+
     @Override
     public void handleChangeDifficulty(ServerboundChangeDifficultyPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.getLevel());
         if (this.player.hasPermissions(2) || this.isSingleplayerOwner()) {
-            this.server.setDifficulty(packet.getDifficulty(), false);
+            // this.server.setDifficulty(packet.getDifficulty(), false); // Paper - don't allow clients to change this
         }
     }
 
@@ -1834,7 +3723,8 @@
     }
 
     @FunctionalInterface
-    interface EntityInteraction {
+    private interface EntityInteraction {
+
         InteractionResult run(ServerPlayer player, Entity entity, InteractionHand hand);
     }
 }
