--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
@@ -208,6 +_,38 @@
 import net.minecraft.world.phys.shapes.VoxelShape;
 import org.slf4j.Logger;
 
+// CraftBukkit start
+import io.papermc.paper.adventure.PaperAdventure; // Paper
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.phys.HitResult;
+import org.bukkit.Location;
+import org.bukkit.NamespacedKey;
+import org.bukkit.craftbukkit.entity.CraftEntity;
+import org.bukkit.craftbukkit.event.CraftEventFactory;
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.craftbukkit.util.CraftLocation;
+import org.bukkit.event.Event;
+import org.bukkit.event.block.Action;
+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.InventoryCreativeEvent;
+import org.bukkit.event.inventory.InventoryType.SlotType;
+import org.bukkit.event.inventory.SmithItemEvent;
+import org.bukkit.event.player.PlayerCommandPreprocessEvent;
+import org.bukkit.event.player.PlayerInputEvent;
+import org.bukkit.event.player.PlayerInteractAtEntityEvent;
+import org.bukkit.event.player.PlayerInteractEntityEvent;
+import org.bukkit.event.player.PlayerItemHeldEvent;
+import org.bukkit.event.player.PlayerMoveEvent;
+import org.bukkit.event.player.PlayerRespawnEvent.RespawnReason;
+import org.bukkit.event.player.PlayerSwapHandItemsEvent;
+import org.bukkit.event.player.PlayerTeleportEvent;
+import org.bukkit.event.player.PlayerToggleFlightEvent;
+import org.bukkit.event.player.PlayerToggleSprintEvent;
+// CraftBukkit end
+
 public class ServerGamePacketListenerImpl
     extends ServerCommonPacketListenerImpl
     implements GameProtocols.Context,
@@ -226,7 +_,9 @@
     private int tickCount;
     private int ackBlockChangesUpTo = -1;
     private final TickThrottler chatSpamThrottler = new TickThrottler(20, 200);
+    private final TickThrottler tabSpamThrottler = new TickThrottler(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamIncrement, io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamLimit); // Paper - configurable tab spam limits
     private final TickThrottler dropSpamThrottler = new TickThrottler(20, 1480);
+    private final TickThrottler recipeSpamPackets = new TickThrottler(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamIncrement, io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamLimit);
     private double firstGoodX;
     private double firstGoodY;
     private double firstGoodZ;
@@ -252,14 +_,33 @@
     private int receivedMovePacketCount;
     private int knownMovePacketCount;
     private boolean receivedMovementThisTick;
+    // CraftBukkit start - add fields
+    private int lastTick = MinecraftServer.currentTick;
+    private int allowedPlayerTicks = 1;
+    private int lastDropTick = MinecraftServer.currentTick;
+    private int lastBookTick  = MinecraftServer.currentTick;
+    private int dropCount = 0;
+
+    private boolean hasMoved = false;
+    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;
+    // CraftBukkit end
     @Nullable
     private RemoteChatSession chatSession;
+    private boolean hasLoggedExpiry = false; // Paper - Prevent causing expired keys from impacting new joins
     private SignedMessageChain.Decoder signedMessageDecoder;
     private final LastSeenMessagesValidator lastSeenMessages = new LastSeenMessagesValidator(20);
     private int nextChatIndex;
     private final MessageSignatureCache messageSignatureCache = MessageSignatureCache.createDefault();
     private final FutureChain chatMessageChain;
     private boolean waitingForSwitchToConfig;
+    private static final int MAX_SIGN_LINE_LENGTH = Integer.getInteger("Paper.maxSignLength", 80); // Paper - Limit client sign length
+    private final io.papermc.paper.event.packet.ClientTickEndEvent tickEndEvent; // Paper - add client tick end event
+    public final io.papermc.paper.connection.PaperPlayerGameConnection playerGameConnection; // Paper
 
     public ServerGamePacketListenerImpl(MinecraftServer server, Connection connection, ServerPlayer player, CommonListenerCookie cookie) {
         super(server, connection, cookie);
@@ -268,8 +_,22 @@
         player.connection = this;
         player.getTextFilter().join();
         this.signedMessageDecoder = SignedMessageChain.Decoder.unsigned(player.getUUID(), server::enforceSecureProfile);
-        this.chatMessageChain = new FutureChain(server);
-    }
+        this.chatMessageChain = new FutureChain(server.chatExecutor); // CraftBukkit - async chat
+        this.tickEndEvent = new io.papermc.paper.event.packet.ClientTickEndEvent(player.getBukkitEntity()); // Paper - add client tick end event
+        this.playerGameConnection =  new io.papermc.paper.connection.PaperPlayerGameConnection(this); // Paper
+    }
+
+    // Paper start - configuration phase API
+    @Override
+    public io.papermc.paper.connection.PlayerCommonConnection getApiConnection() {
+        return this.playerGameConnection;
+    }
+
+    @Override
+    public net.kyori.adventure.audience.Audience getAudience() {
+        return this.getCraftPlayer();
+    }
+    // Paper end - configuration phase API
 
     @Override
     public void tick() {
@@ -288,8 +_,8 @@
         this.knownMovePacketCount = this.receivedMovePacketCount;
         if (this.clientIsFloating && !this.player.isSleeping() && !this.player.isPassenger() && !this.player.isDeadOrDying()) {
             if (++this.aboveGroundTickCount > this.getMaximumFlyingTicks(this.player)) {
-                LOGGER.warn("{} was kicked for floating too long!", this.player.getName().getString());
-                this.disconnect(Component.translatable("multiplayer.disconnect.flying"));
+                // LOGGER.warn("{} was kicked for floating too long!", this.player.getName().getString()); // Paper - Logging moved to net.minecraft.server.network.ServerCommonPacketListenerImpl.disconnect()
+                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 {
@@ -307,8 +_,8 @@
             this.vehicleLastGoodZ = this.lastVehicle.getZ();
             if (this.clientVehicleIsFloating && this.lastVehicle.getControllingPassenger() == this.player) {
                 if (++this.aboveGroundVehicleTickCount > this.getMaximumFlyingTicks(this.lastVehicle)) {
-                    LOGGER.warn("{} was kicked for floating a vehicle too long!", this.player.getName().getString());
-                    this.disconnect(Component.translatable("multiplayer.disconnect.flying"));
+                    // LOGGER.warn("{} was kicked for floating a vehicle too long!", this.player.getName().getString()); // Paper - Logging moved to net.minecraft.server.network.ServerCommonPacketListenerImpl.disconnect()
+                    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 {
@@ -324,11 +_,20 @@
         this.keepConnectionAlive();
         this.chatSpamThrottler.tick();
         this.dropSpamThrottler.tick();
+        this.tabSpamThrottler.tick(); // Paper - configurable tab spam limits
+        this.recipeSpamPackets.tick(); // Paper - auto recipe limit
         if (this.player.getLastActionTime() > 0L
             && this.server.getPlayerIdleTimeout() > 0
-            && Util.getMillis() - this.player.getLastActionTime() > this.server.getPlayerIdleTimeout() * 1000L * 60L) {
-            this.disconnect(Component.translatable("multiplayer.disconnect.idling"));
-        }
+            && Util.getMillis() - this.player.getLastActionTime() > this.server.getPlayerIdleTimeout() * 1000L * 60L && !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
+        }
+        // Paper start - Prevent causing expired keys from impacting new joins
+        if (!this.hasLoggedExpiry && this.chatSession != null && this.chatSession.profilePublicKey().data().hasExpired()) {
+            LOGGER.info("Player profile key for {} has expired!", this.player.getName().getString());
+            this.hasLoggedExpiry = true;
+        }
+        // Paper end - Prevent causing expired keys from impacting new joins
     }
 
     private int getMaximumFlyingTicks(Entity entity) {
@@ -388,11 +_,37 @@
     @Override
     public void handlePlayerInput(ServerboundPlayerInputPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
+        // CraftBukkit start
+        if (!packet.input().equals(this.player.getLastClientInput())) {
+            PlayerInputEvent event = new PlayerInputEvent(this.player.getBukkitEntity(), new org.bukkit.craftbukkit.CraftInput(packet.input()));
+            this.cserver.getPluginManager().callEvent(event);
+        }
+        // CraftBukkit end
+        // Paper start - PlayerToggleSneakEvent
+        net.minecraft.world.entity.player.Input lastInput = this.player.getLastClientInput();
+        boolean shiftKeyDown = packet.input().shift();
+        if (lastInput.shift() != packet.input().shift()) {
+            // Has sneak changed
+            org.bukkit.event.player.PlayerToggleSneakEvent event = new org.bukkit.event.player.PlayerToggleSneakEvent(this.getCraftPlayer(), packet.input().shift());
+            this.cserver.getPluginManager().callEvent(event);
+
+            // Technically the player input and the flag is desynced, but this is previous behavior.. so should be fine?
+            if (event.isCancelled()) {
+               shiftKeyDown = this.player.isShiftKeyDown();
+            }
+        }
+        // Paper end - PlayerToggleSneakEvent
         this.player.setLastClientInput(packet.input());
         if (this.player.hasClientLoaded()) {
             this.player.resetLastActionTime();
-            this.player.setShiftKeyDown(packet.input().shift());
-        }
+            this.player.setShiftKeyDown(shiftKeyDown); // Paper
+        }
+        // Paper start - Add option to make parrots stay
+        if (packet.input().shift() && this.player.level().paperConfig().entities.behavior.parrotsAreUnaffectedByPlayerMovement) {
+            this.player.removeEntitiesOnShoulder();
+        }
+        // Paper end - Add option to make parrots stay
+
     }
 
     private static boolean containsInvalidValues(double x, double y, double z, float yRot, float xRot) {
@@ -411,17 +_,29 @@
     public void handleMoveVehicle(ServerboundMoveVehiclePacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
         if (containsInvalidValues(packet.position().x(), packet.position().y(), packet.position().z(), packet.yRot(), packet.xRot())) {
-            this.disconnect(Component.translatable("multiplayer.disconnect.invalid_vehicle_movement"));
+            this.disconnect(Component.translatable("multiplayer.disconnect.invalid_vehicle_movement"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_VEHICLE_MOVEMENT); // Paper - kick event cause
         } else if (!this.updateAwaitingTeleport() && this.player.hasClientLoaded()) {
             Entity rootVehicle = this.player.getRootVehicle();
+            // Paper start - Don't allow vehicle movement from players while teleporting
+            if (this.awaitingPositionFromClient != null || this.player.isImmobile() || rootVehicle.isRemoved()) {
+                return;
+            }
+            // Paper end - Don't allow vehicle movement from players while teleporting
             if (rootVehicle != this.player && rootVehicle.getControllingPassenger() == this.player && rootVehicle == this.lastVehicle) {
                 ServerLevel serverLevel = this.player.level();
+                // CraftBukkit - store current player position
+                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 x = rootVehicle.getX();
                 double y = rootVehicle.getY();
                 double z = rootVehicle.getZ();
-                double d = clampHorizontal(packet.position().x());
-                double d1 = clampVertical(packet.position().y());
-                double d2 = clampHorizontal(packet.position().z());
+                double d = clampHorizontal(packet.position().x()); final double toX = d; // Paper - OBFHELPER
+                double d1 = clampVertical(packet.position().y()); final double toY = d1; // Paper - OBFHELPER
+                double d2 = clampHorizontal(packet.position().z()); final double toZ = d2; // Paper - OBFHELPER
                 float f = Mth.wrapDegrees(packet.yRot());
                 float f1 = Mth.wrapDegrees(packet.xRot());
                 double d3 = d - this.vehicleFirstGoodX;
@@ -429,7 +_,52 @@
                 double d5 = d2 - this.vehicleFirstGoodZ;
                 double d6 = rootVehicle.getDeltaMovement().lengthSqr();
                 double d7 = d3 * d3 + d4 * d4 + d5 * d5;
-                if (d7 - d6 > 100.0 && !this.isSingleplayerOwner()) {
+                // Paper start - fix large move vectors killing the server
+                double currDeltaX = toX - x;
+                double currDeltaY = toY - y;
+                double currDeltaZ = toZ - z;
+                d7 = Math.max(d7, (currDeltaX * currDeltaX + currDeltaY * currDeltaY + currDeltaZ * currDeltaZ) - 1);
+                double otherFieldX = toX - this.vehicleLastGoodX;
+                double otherFieldY = toY - this.vehicleLastGoodY;
+                double otherFieldZ = toZ - this.vehicleLastGoodZ;
+                d7 = Math.max(d7, (otherFieldX * otherFieldX + otherFieldY * otherFieldY + otherFieldZ * otherFieldZ) - 1);
+                // Paper end - fix large move vectors killing the server
+
+                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 (d7 > 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 (this.player.level().paperConfig().chunks.preventMovingIntoUnloadedChunks && (
+                    !serverLevel.areChunksLoadedForMove(this.player.getBoundingBox().expandTowards(new Vec3(toX, toY, toZ).subtract(this.player.position()))) ||
+                        !serverLevel.areChunksLoadedForMove(rootVehicle.getBoundingBox().expandTowards(new Vec3(toX, toY, toZ).subtract(rootVehicle.position())))
+                )) {
+                    this.connection.send(ClientboundMoveVehiclePacket.fromEntity(rootVehicle));
+                    return;
+                }
+                // Paper end - Prevent moving into unloaded chunks
+                if (d7 - d6 > Math.max(100.0, Mth.square(org.spigotmc.SpigotConfig.movedTooQuicklyMultiplier * (float) i * speed)) && !this.isSingleplayerOwner()) {
+                    // CraftBukkit end
                     LOGGER.warn(
                         "{} (vehicle of {}) moved too quickly! {},{},{}", rootVehicle.getName().getString(), this.player.getName().getString(), d3, d4, d5
                     );
@@ -438,9 +_,9 @@
                 }
 
                 AABB boundingBox = rootVehicle.getBoundingBox();
-                d3 = d - this.vehicleLastGoodX;
-                d4 = d1 - this.vehicleLastGoodY;
-                d5 = d2 - this.vehicleLastGoodZ;
+                d3 = d - this.vehicleLastGoodX; // Paper - diff on change, used for checking large move vectors above
+                d4 = d1 - this.vehicleLastGoodY; // Paper - diff on change, used for checking large move vectors above
+                d5 = d2 - this.vehicleLastGoodZ; // Paper - diff on change, used for checking large move vectors above
                 boolean flag = rootVehicle.verticalCollisionBelow;
                 if (rootVehicle instanceof LivingEntity livingEntity && livingEntity.onClimbable()) {
                     livingEntity.resetFallDistance();
@@ -457,7 +_,7 @@
                 d5 = d2 - rootVehicle.getZ();
                 d7 = d3 * d3 + d4 * d4 + d5 * d5;
                 boolean flag1 = false;
-                if (d7 > 0.0625) {
+                if (d7 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot
                     flag1 = true;
                     LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", rootVehicle.getName().getString(), this.player.getName().getString(), Math.sqrt(d7));
                 }
@@ -471,6 +_,57 @@
                 }
 
                 rootVehicle.absSnapTo(d, d1, d2, f, f1);
+                // CraftBukkit start - fire PlayerMoveEvent TODO: this should be removed.
+                this.player.absSnapTo(d, d1, d2, this.player.getYRot(), this.player.getXRot()); // Paper - TODO: This breaks alot of stuff
+                org.bukkit.entity.Player player = this.getCraftPlayer();
+                if (!this.hasMoved) {
+                    this.lastPosX = prevX;
+                    this.lastPosY = prevY;
+                    this.lastPosZ = prevZ;
+                    this.lastYaw = prevYaw;
+                    this.lastPitch = prevPitch;
+                    this.hasMoved = true;
+                }
+                Location from = new Location(player.getWorld(), this.lastPosX, this.lastPosY, this.lastPosZ, this.lastYaw, this.lastPitch); // Get the Players previous Event location.
+                Location to = CraftLocation.toBukkit(packet.position(), player.getWorld(), packet.yRot(), packet.xRot());
+
+                // Prevent 40 event-calls for less than a single pixel of movement >.>
+                double delta = Mth.square(this.lastPosX - to.getX()) + Mth.square(this.lastPosY - to.getY()) + Mth.square(this.lastPosZ - to.getZ());
+                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();
+
+                    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.level().getChunkSource().move(this.player);
                 Vec3 vec3 = new Vec3(rootVehicle.getX() - x, rootVehicle.getY() - y, rootVehicle.getZ() - z);
                 this.handlePlayerKnownMovement(vec3);
@@ -501,12 +_,12 @@
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
         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
-                .absSnapTo(
+                 .snapTo( // Paper - Fix Entity Teleportation and cancel velocity if teleported
                     this.awaitingPositionFromClient.x,
                     this.awaitingPositionFromClient.y,
                     this.awaitingPositionFromClient.z,
@@ -518,12 +_,20 @@
             this.lastGoodZ = this.awaitingPositionFromClient.z;
             this.player.hasChangedDimension();
             this.awaitingPositionFromClient = null;
+            this.player.level().getChunkSource().move(this.player); // CraftBukkit
         }
     }
 
     @Override
     public void handleAcceptPlayerLoad(ServerboundPlayerLoadedPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
+        // Paper start - PlayerLoadedWorldEvent
+        if (this.player.hasClientLoaded()) {
+            return;
+        }
+        final io.papermc.paper.event.player.PlayerClientLoadedWorldEvent event = new io.papermc.paper.event.player.PlayerClientLoadedWorldEvent(this.player.getBukkitEntity(), false);
+        event.callEvent();
+        // Paper end - PlayerLoadedWorldEvent
         this.player.setClientLoaded(true);
     }
 
@@ -545,6 +_,7 @@
     @Override
     public void handleRecipeBookChangeSettingsPacket(ServerboundRecipeBookChangeSettingsPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
+        CraftEventFactory.callRecipeBookSettingsEvent(this.player, packet.getBookType(), packet.isOpen(), packet.isFiltering()); // CraftBukkit
         this.player.getRecipeBook().setBookSetting(packet.getBookType(), packet.isOpen(), packet.isFiltering());
     }
 
@@ -560,25 +_,110 @@
         }
     }
 
+    // Paper start - AsyncTabCompleteEvent
+    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(MinecraftServer.LOGGER)).build());
+    // Paper end - AsyncTabCompleteEvent
+
     @Override
     public void handleCustomCommandSuggestions(ServerboundCommandSuggestionPacket packet) {
-        PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
+        // PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level()); // Paper - AsyncTabCompleteEvent; run this async
+        // CraftBukkit start
+        if (!this.tabSpamThrottler.isIncrementAndUnderThreshold() && !this.server.getPlayerList().isOp(this.player.getGameProfile()) && !this.server.isSingleplayerOwner(this.player.getGameProfile())) { // Paper - configurable tab spam limits
+            this.disconnectAsync(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - Kick event cause // Paper - add proper async disconnect
+            return;
+        }
+        // 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
+        final int index;
+        if (packet.getCommand().length() > 64 && ((index = packet.getCommand().indexOf(' ')) == -1 || index >= 64)) {
+            this.disconnectAsync(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - add proper async disconnect
+            return;
+        }
+        // Paper end
+        // Paper start - AsyncTabCompleteEvent
+        TAB_COMPLETE_EXECUTOR.execute(() -> this.handleCustomCommandSuggestions0(packet));
+    }
+
+    private void handleCustomCommandSuggestions0(final ServerboundCommandSuggestionPacket packet) {
+        // Paper end - AsyncTabCompleteEvent
         StringReader stringReader = new StringReader(packet.getCommand());
         if (stringReader.canRead() && stringReader.peek() == '/') {
             stringReader.skip();
         }
 
+        // Paper start - AsyncTabCompleteEvent
+        final com.destroystokyo.paper.event.server.AsyncTabCompleteEvent event = new com.destroystokyo.paper.event.server.AsyncTabCompleteEvent(this.getCraftPlayer(), packet.getCommand(), true, null);
+        event.callEvent();
+        final 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()) {
+                return;
+            }
+
+            // This needs to be on main
+            this.server.scheduleOnMain(() -> this.sendServerSuggestions(packet, stringReader));
+        } else if (!completions.isEmpty()) {
+            final com.mojang.brigadier.suggestion.SuggestionsBuilder builder0 = new com.mojang.brigadier.suggestion.SuggestionsBuilder(packet.getCommand(), stringReader.getTotalLength());
+            final com.mojang.brigadier.suggestion.SuggestionsBuilder builder = builder0.createOffset(builder0.getInput().lastIndexOf(' ') + 1);
+            for (final com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion completion : completions) {
+                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, packet.getCommand());
+            suggestEvent.setCancelled(suggestions.isEmpty());
+            if (suggestEvent.callEvent()) {
+                this.connection.send(new ClientboundCommandSuggestionsPacket(packet.getId(), limitTo(suggestEvent.getSuggestions(), ServerGamePacketListenerImpl.MAX_COMMAND_SUGGESTIONS)));
+            }
+            // Paper end - Brigadier API
+        }
+    }
+    // Paper start - brig API
+    private static Suggestions limitTo(final Suggestions suggestions, final int size) {
+        return suggestions.getList().size() <= size ? suggestions : new Suggestions(suggestions.getRange(), suggestions.getList().subList(0, size));
+    }
+    // Paper end - brig API
+
+    private void sendServerSuggestions(final ServerboundCommandSuggestionPacket packet, final StringReader stringReader) {
+        // Paper end - AsyncTabCompleteEvent
         ParseResults<CommandSourceStack> parseResults = this.server.getCommands().getDispatcher().parse(stringReader, this.player.createCommandSourceStack());
+        // Paper start - Handle non-recoverable exceptions
+        if (!parseResults.getExceptions().isEmpty()
+            && parseResults.getExceptions().values().stream().anyMatch(e -> e instanceof io.papermc.paper.brigadier.TagParseCommandSyntaxException)) {
+            this.disconnect(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM);
+            return;
+        }
+        // Paper end - Handle non-recoverable exceptions
         this.server
             .getCommands()
             .getDispatcher()
             .getCompletionSuggestions(parseResults)
             .thenAccept(
                 suggestions -> {
-                    Suggestions suggestions1 = suggestions.getList().size() <= 1000
-                        ? suggestions
-                        : new Suggestions(suggestions.getRange(), suggestions.getList().subList(0, 1000));
-                    this.send(new ClientboundCommandSuggestionsPacket(packet.getId(), suggestions1));
+                    // Paper start - Don't tab-complete namespaced commands if send-namespaced is false
+                    if (!org.spigotmc.SpigotConfig.sendNamespaced && suggestions.getRange().getStart() <= 1) {
+                        suggestions.getList().removeIf(suggestion -> suggestion.getText().contains(":"));
+                    }
+                    // Paper end - Don't tab-complete namespaced commands if send-namespaced is false
+                    // Paper start - Brigadier API
+                    com.destroystokyo.paper.event.brigadier.AsyncPlayerSendSuggestionsEvent suggestEvent = new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendSuggestionsEvent(this.getCraftPlayer(), suggestions, packet.getCommand());
+                    suggestEvent.setCancelled(suggestions.isEmpty());
+                    if (suggestEvent.callEvent()) {
+                        this.send(new ClientboundCommandSuggestionsPacket(packet.getId(), limitTo(suggestEvent.getSuggestions(), ServerGamePacketListenerImpl.MAX_COMMAND_SUGGESTIONS)));
+                    }
+                    // Paper end - Brigadier API
                 }
             );
     }
@@ -588,7 +_,7 @@
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
         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;
@@ -643,7 +_,7 @@
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
         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 commandBlock = packet.getCommandBlock(this.player.level());
@@ -671,11 +_,11 @@
                 boolean flag = this.player.hasInfiniteMaterials() && packet.includeData();
                 ItemStack cloneItemStack = blockState.getCloneItemStack(serverLevel, blockPos, flag);
                 if (!cloneItemStack.isEmpty()) {
-                    if (flag) {
+                    if (flag && this.player.getBukkitEntity().hasPermission("minecraft.nbt.copy")) { // Spigot
                         addBlockDataToItem(blockState, serverLevel, blockPos, cloneItemStack);
                     }
 
-                    this.tryPickItem(cloneItemStack);
+                    this.tryPickItem(cloneItemStack, blockPos, null, packet.includeData()); // Paper - Extend PlayerPickItemEvent API
                 }
             }
         }
@@ -702,27 +_,40 @@
         if (entityOrPart != null && this.player.canInteractWithEntity(entityOrPart, 3.0)) {
             ItemStack pickResult = entityOrPart.getPickResult();
             if (pickResult != null && !pickResult.isEmpty()) {
-                this.tryPickItem(pickResult);
+                this.tryPickItem(pickResult, null, entityOrPart, packet.includeData()); // Paper - Extend PlayerPickItemEvent API
             }
         }
     }
 
-    private void tryPickItem(ItemStack stack) {
+    private void tryPickItem(ItemStack stack, @Nullable BlockPos blockPos, @Nullable Entity entity, boolean includeData) { // Paper - Extend PlayerPickItemEvent API
         if (stack.isItemEnabled(this.player.level().enabledFeatures())) {
             Inventory inventory = this.player.getInventory();
             int i = inventory.findSlotMatchingItem(stack);
+            // Paper start - Add PlayerPickItemEvent
+            final int sourceSlot = i;
+            final int targetSlot = Inventory.isHotbarSlot(sourceSlot) ? sourceSlot : inventory.getSuitableHotbarSlot();
+            final org.bukkit.entity.Player bukkitPlayer = this.player.getBukkitEntity();
+            final io.papermc.paper.event.player.PlayerPickItemEvent event = entity != null
+                ? new io.papermc.paper.event.player.PlayerPickEntityEvent(bukkitPlayer, entity.getBukkitEntity(), includeData, targetSlot, sourceSlot)
+                : new io.papermc.paper.event.player.PlayerPickBlockEvent(bukkitPlayer, org.bukkit.craftbukkit.block.CraftBlock.at(this.player.level(), blockPos), includeData, targetSlot, sourceSlot);
+            if (!event.callEvent()) {
+                return;
+            }
+            i = event.getSourceSlot();
+            // Paper end - Add PlayerPickItemEvent
             if (i != -1) {
-                if (Inventory.isHotbarSlot(i)) {
-                    inventory.setSelectedSlot(i);
+                if (Inventory.isHotbarSlot(i) && Inventory.isHotbarSlot(event.getTargetSlot())) { // Paper - Add PlayerPickItemEvent
+                    inventory.setSelectedSlot(event.getTargetSlot()); // Paper - Add PlayerPickItemEvent
                 } else {
-                    inventory.pickSlot(i);
+                    inventory.pickSlot(i, event.getTargetSlot()); // Paper - Add PlayerPickItemEvent
                 }
             } else if (this.player.hasInfiniteMaterials()) {
-                inventory.addAndPickItem(stack);
+                inventory.addAndPickItem(stack, event.getTargetSlot()); // Paper - Add PlayerPickItemEvent
             }
 
             this.send(new ClientboundSetHeldSlotPacket(inventory.getSelectedSlot()));
             this.player.inventoryMenu.broadcastChanges();
+            if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.updateEquipmentOnPlayerActions) this.player.detectEquipmentUpdates(); // Paper - Force update attributes.
         }
     }
 
@@ -900,6 +_,13 @@
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
         int item = packet.getItem();
         if (this.player.containerMenu instanceof MerchantMenu merchantMenu) {
+            // CraftBukkit start
+            final org.bukkit.event.inventory.TradeSelectEvent tradeSelectEvent = CraftEventFactory.callTradeSelectEvent(item, merchantMenu);
+            if (tradeSelectEvent.isCancelled()) {
+                this.player.containerMenu.sendAllDataToRemote();
+                return;
+            }
+            // CraftBukkit end
             if (!merchantMenu.stillValid(this.player)) {
                 LOGGER.debug("Player {} interacted with invalid menu {}", this.player, merchantMenu);
                 return;
@@ -912,6 +_,51 @@
 
     @Override
     public void handleEditBook(ServerboundEditBookPacket packet) {
+        // Paper start - Book size limits
+        final io.papermc.paper.configuration.type.number.IntOr.Disabled pageMax = io.papermc.paper.configuration.GlobalConfiguration.get().itemValidation.bookSize.pageMax;
+        if (!this.cserver.isPrimaryThread() && pageMax.enabled()) {
+            final List<String> pageList = packet.pages();
+            long byteTotal = 0;
+            final int maxBookPageSize = pageMax.intValue();
+            final double multiplier = Math.clamp(io.papermc.paper.configuration.GlobalConfiguration.get().itemValidation.bookSize.totalMultiplier, 0.3D, 1D);
+            long byteAllowed = maxBookPageSize;
+            for (final String page : pageList) {
+                final int byteLength = page.getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
+                byteTotal += byteLength;
+                final int length = page.length();
+                int multiByteCharacters = 0;
+                if (byteLength != length) {
+                    // Count the number of multi byte characters
+                    for (final char c : page.toCharArray()) {
+                        if (c > 127) {
+                            multiByteCharacters++;
+                        }
+                    }
+                }
+
+                // Allow pages with fewer characters to consume less of the allowed byte quota
+                byteAllowed += maxBookPageSize * Math.clamp((double) length / 255D, 0.1D, 1) * multiplier;
+
+                if (multiByteCharacters > 1) {
+                    // Penalize multibyte characters
+                    byteAllowed -= multiByteCharacters;
+                }
+            }
+
+            if (byteTotal > byteAllowed) {
+                ServerGamePacketListenerImpl.LOGGER.warn("{} tried to send a book too large. Book size: {} - Allowed: {} - Pages: {}", this.player.getScoreboardName(), byteTotal, byteAllowed, pageList.size());
+                this.disconnectAsync(Component.literal("Book too large!"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause // Paper - add proper async disconnect
+                return;
+            }
+        }
+        // Paper end - Book size limits
+        // CraftBukkit start
+        if (this.lastBookTick + 20 > MinecraftServer.currentTick) {
+            this.disconnectAsync(Component.literal("Book edited too quickly!"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause // Paper - add proper async disconnect
+            return;
+        }
+        this.lastBookTick = MinecraftServer.currentTick;
+        // CraftBukkit end
         int slot = packet.slot();
         if (Inventory.isHotbarSlot(slot) || slot == 40) {
             List<String> list = Lists.newArrayList();
@@ -926,10 +_,14 @@
     }
 
     private void updateBookContents(List<FilteredText> pages, int index) {
-        ItemStack item = this.player.getInventory().getItem(index);
+        // CraftBukkit start
+        ItemStack handItem = this.player.getInventory().getItem(index);
+        ItemStack item = handItem.copy();
+        // CraftBukkit end
         if (item.has(DataComponents.WRITABLE_BOOK_CONTENT)) {
             List<Filterable<String>> list = pages.stream().map(this::filterableFromOutgoing).toList();
             item.set(DataComponents.WRITABLE_BOOK_CONTENT, new WritableBookContent(list));
+            this.player.getInventory().setItem(index, CraftEventFactory.handleEditBookEvent(this.player, index, handItem, item)); // CraftBukkit // Paper - Don't ignore result (see other callsite for handleEditBookEvent)
         }
     }
 
@@ -943,7 +_,8 @@
                 DataComponents.WRITTEN_BOOK_CONTENT,
                 new WrittenBookContent(this.filterableFromOutgoing(title), this.player.getName().getString(), 0, list, true)
             );
-            this.player.getInventory().setItem(index, itemStack);
+            CraftEventFactory.handleEditBookEvent(this.player, index, item, itemStack); // CraftBukkit
+            this.player.getInventory().setItem(index, item); // CraftBukkit - event factory updates the hand book
         }
     }
 
@@ -991,27 +_,35 @@
     public void handleMovePlayer(ServerboundMovePlayerPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
         if (containsInvalidValues(packet.getX(0.0), packet.getY(0.0), packet.getZ(0.0), packet.getYRot(0.0F), packet.getXRot(0.0F))) {
-            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
         } else {
             ServerLevel serverLevel = this.player.level();
-            if (!this.player.wonGame) {
+            if (!this.player.wonGame && !this.player.isImmobile()) { // CraftBukkit
                 if (this.tickCount == 0) {
                     this.resetPosition();
                 }
 
                 if (this.player.hasClientLoaded()) {
-                    float f = Mth.wrapDegrees(packet.getYRot(this.player.getYRot()));
-                    float f1 = Mth.wrapDegrees(packet.getXRot(this.player.getXRot()));
+                    float f = Mth.wrapDegrees(packet.getYRot(this.player.getYRot())); final float toYaw = f; // Paper - OBFHELPER
+                    float f1 = Mth.wrapDegrees(packet.getXRot(this.player.getXRot())); final float toPitch = f1; // Paper - OBFHELPER
                     if (this.updateAwaitingTeleport()) {
                         this.player.absSnapRotationTo(f, f1);
                     } else {
-                        double d = clampHorizontal(packet.getX(this.player.getX()));
-                        double d1 = clampVertical(packet.getY(this.player.getY()));
-                        double d2 = clampHorizontal(packet.getZ(this.player.getZ()));
+                        double d = clampHorizontal(packet.getX(this.player.getX())); final double toX = d; // Paper - OBFHELPER
+                        double d1 = clampVertical(packet.getY(this.player.getY())); final double toY = d1; // Paper - OBFHELPER
+                        double d2 = clampHorizontal(packet.getZ(this.player.getZ())); final double toZ = d2; // Paper - OBFHELPER
                         if (this.player.isPassenger()) {
                             this.player.absSnapTo(this.player.getX(), this.player.getY(), this.player.getZ(), f, f1);
                             this.player.level().getChunkSource().move(this.player);
+                            this.allowedPlayerTicks = 20; // CraftBukkit
                         } else {
+                            // 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 x = this.player.getX();
                             double y = this.player.getY();
                             double z = this.player.getZ();
@@ -1020,6 +_,16 @@
                             double d5 = d2 - this.firstGoodZ;
                             double d6 = this.player.getDeltaMovement().lengthSqr();
                             double d7 = d3 * d3 + d4 * d4 + d5 * d5;
+                            // Paper start - fix large move vectors killing the server
+                            double currDeltaX = toX - prevX;
+                            double currDeltaY = toY - prevY;
+                            double currDeltaZ = toZ - prevZ;
+                            d7 = Math.max(d7, (currDeltaX * currDeltaX + currDeltaY * currDeltaY + currDeltaZ * currDeltaZ) - 1);
+                            double otherFieldX = d - this.lastGoodX;
+                            double otherFieldY = d1 - this.lastGoodY;
+                            double otherFieldZ = d2 - this.lastGoodZ;
+                            d7 = Math.max(d7, (otherFieldX * otherFieldX + otherFieldY * otherFieldY + otherFieldZ * otherFieldZ) - 1);
+                            // Paper end - fix large move vectors killing the server
                             if (this.player.isSleeping()) {
                                 if (d7 > 1.0) {
                                     this.teleport(this.player.getX(), this.player.getY(), this.player.getZ(), f, f1);
@@ -1029,36 +_,108 @@
                                 if (serverLevel.tickRateManager().runsNormally()) {
                                     this.receivedMovePacketCount++;
                                     int i = this.receivedMovePacketCount - this.knownMovePacketCount;
-                                    if (i > 5) {
+                                    // 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)) {
+                                    // CraftBukkit end
                                         LOGGER.debug(
                                             "{} is sending move packets too frequently ({} packets since last tick)", this.player.getName().getString(), i
                                         );
                                         i = 1;
                                     }
+                                    // CraftBukkit start - handle custom speeds and skipped ticks
+                                    if (packet.hasRot || d7 > 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 (this.player.level().paperConfig().chunks.preventMovingIntoUnloadedChunks && (this.player.getX() != toX || this.player.getZ() != toZ) && !serverLevel.areChunksLoadedForMove(this.player.getBoundingBox().expandTowards(new Vec3(toX, toY, toZ).subtract(this.player.position())))) {
+                                        // Paper start - Add fail move event
+                                        io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.MOVED_INTO_UNLOADED_CHUNK,
+                                                toX, toY, toZ, toYaw, toPitch, false);
+                                        if (!event.isAllowed()) {
+                                            this.internalTeleport(PositionMoveRotation.of(this.player), Collections.emptySet());
+                                            return;
+                                        }
+                                        // Paper end - Add fail move event
+                                    }
+                                    // Paper end - Prevent moving into unloaded chunks
 
                                     if (this.shouldCheckPlayerMovement(isFallFlying)) {
                                         float f2 = isFallFlying ? 300.0F : 100.0F;
-                                        if (d7 - d6 > f2 * i) {
-                                            LOGGER.warn("{} moved too quickly! {},{},{}", this.player.getName().getString(), d3, d4, d5);
-                                            this.teleport(
-                                                this.player.getX(), this.player.getY(), this.player.getZ(), this.player.getYRot(), this.player.getXRot()
-                                            );
-                                            return;
+                                        if (d7 - d6 > Math.max(f2, Mth.square(org.spigotmc.SpigotConfig.movedTooQuicklyMultiplier * (float) i * speed))) {
+                                            // CraftBukkit end
+                                            // Paper start - Add fail move event
+                                            io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.MOVED_TOO_QUICKLY,
+                                                    toX, toY, toZ, toYaw, toPitch, true);
+                                            if (!event.isAllowed()) {
+                                                if (event.getLogWarning()) {
+                                                LOGGER.warn("{} moved too quickly! {},{},{}", this.player.getName().getString(), d3, d4, d5);
+                                                }
+                                                this.teleport(
+                                                        this.player.getX(), this.player.getY(), this.player.getZ(), this.player.getYRot(), this.player.getXRot()
+                                                );
+                                                return;
+                                            }
+                                            // Paper end - Add fail move event
                                         }
                                     }
                                 }
 
                                 AABB boundingBox = this.player.getBoundingBox();
-                                d3 = d - this.lastGoodX;
-                                d4 = d1 - this.lastGoodY;
-                                d5 = d2 - this.lastGoodZ;
+                                d3 = d - this.lastGoodX; // Paper - diff on change, used for checking large move vectors above
+                                d4 = d1 - this.lastGoodY; // Paper - diff on change, used for checking large move vectors above
+                                d5 = d2 - this.lastGoodZ; // Paper - diff on change, used for checking large move vectors above
                                 boolean flag = d4 > 0.0;
                                 if (this.player.onGround() && !packet.isOnGround() && flag) {
-                                    this.player.jumpFromGround();
+                                    // Paper start - Add PlayerJumpEvent
+                                    org.bukkit.entity.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);
+                                    }
+
+                                    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(new PositionMoveRotation(org.bukkit.craftbukkit.util.CraftLocation.toVec3(from), Vec3.ZERO, from.getYaw(), from.getPitch()), Collections.emptySet());
+                                        return;
+                                    }
+                                    // Paper end - Add PlayerJumpEvent
                                 }
 
                                 boolean flag1 = this.player.verticalCollisionBelow;
                                 this.player.move(MoverType.PLAYER, new Vec3(d3, d4, d5));
+                                this.player.onGround = packet.isOnGround(); // CraftBukkit - SPIGOT-5810, SPIGOT-5835, SPIGOT-6828: reset by this.player.move
+                                // 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 verticalDelta = d4;
                                 d3 = d - this.player.getX();
                                 d4 = d1 - this.player.getY();
@@ -1068,20 +_,100 @@
 
                                 d5 = d2 - this.player.getZ();
                                 d7 = d3 * d3 + d4 * d4 + d5 * d5;
-                                boolean flag2 = false;
+                                boolean movedWrongly = false; // Paper - Add fail move event; rename
                                 if (!this.player.isChangingDimension()
-                                    && d7 > 0.0625
+                                    && d7 > org.spigotmc.SpigotConfig.movedWronglyThreshold // Spigot
                                     && !this.player.isSleeping()
                                     && !this.player.isCreative()
                                     && !this.player.isSpectator()) {
-                                    flag2 = true;
+                                    // Paper start - Add fail move event
+                                    io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.MOVED_WRONGLY,
+                                            toX, toY, toZ, toYaw, toPitch, true);
+                                    if (!event.isAllowed()) {
+                                        movedWrongly = true;
+                                        if (event.getLogWarning())
+                                     // Paper end
                                     LOGGER.warn("{} moved wrongly!", this.player.getName().getString());
-                                }
-
-                                if (this.player.noPhysics
-                                    || this.player.isSleeping()
-                                    || (!flag2 || !serverLevel.noCollision(this.player, boundingBox))
-                                        && !this.isEntityCollidingWithAnythingNew(serverLevel, this.player, boundingBox, d, d1, d2)) {
+                                    } // Paper
+                                }
+
+                                // Paper start - Add fail move event
+                                boolean allowMovement = this.player.noPhysics || this.player.isSleeping() || (!movedWrongly || !serverLevel.noCollision(this.player, boundingBox)) && !this.isEntityCollidingWithAnythingNew(serverLevel, this.player, boundingBox, d, d1, d2);
+                                if (!allowMovement) {
+                                    io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.CLIPPED_INTO_BLOCK,
+                                            toX, toY, toZ, toYaw, toPitch, false);
+                                    if (event.isAllowed()) {
+                                        allowMovement = true;
+                                    }
+                                }
+                                if (allowMovement) {
+                                    // Paper end - Add fail move event
+                                    // CraftBukkit start - fire PlayerMoveEvent
+                                    // Reset to old location first
+                                    this.player.absSnapTo(prevX, prevY, prevZ, prevYaw, prevPitch);
+
+                                    org.bukkit.entity.Player player = this.getCraftPlayer();
+                                    if (!this.hasMoved) {
+                                        this.lastPosX = prevX;
+                                        this.lastPosY = prevY;
+                                        this.lastPosZ = prevZ;
+                                        this.lastYaw = prevYaw;
+                                        this.lastPitch = prevPitch;
+                                        this.hasMoved = true;
+                                    }
+                                    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 = Mth.square(this.lastPosX - to.getX()) + Mth.square(this.lastPosY - to.getY()) + Mth.square(this.lastPosZ - to.getZ());
+                                    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();
+
+                                        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;
+                                        }
+                                    }
+                                    // Paper end
                                     this.player.absSnapTo(d, d1, d2, f, f1);
                                     boolean isAutoSpinAttack = this.player.isAutoSpinAttack();
                                     this.clientIsFloating = verticalDelta >= -0.03125
@@ -1116,7 +_,7 @@
                                     this.lastGoodY = this.player.getY();
                                     this.lastGoodZ = this.player.getZ();
                                 } else {
-                                    this.teleport(x, y, z, f, f1);
+                                    this.internalTeleport(x, y, z, f, f1); // 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.getX() - x, this.player.getY() - y, this.player.getZ() - z, packet.isOnGround());
                                     this.player.removeLatestMovementRecording();
                                 }
@@ -1152,6 +_,7 @@
                     this.player.getXRot()
                 );
             }
+            this.allowedPlayerTicks = 20; // CraftBukkit
 
             return true;
         } else {
@@ -1175,10 +_,77 @@
     }
 
     public void teleport(double x, double y, double z, float yaw, float pitch) {
-        this.teleport(new PositionMoveRotation(new Vec3(x, y, z), Vec3.ZERO, yaw, pitch), Collections.emptySet());
+        // CraftBukkit start - Delegate to teleport(Location)
+        this.teleport(x, y, z, yaw, pitch, PlayerTeleportEvent.TeleportCause.UNKNOWN);
+    }
+
+    public boolean teleport(double d0, double d1, double d2, float f, float f1, PlayerTeleportEvent.TeleportCause cause) {
+        return this.teleport(new PositionMoveRotation(new Vec3(d0, d1, d2), Vec3.ZERO, f, f1), Collections.emptySet(), cause);
+        // CraftBukkit end
     }
 
     public void teleport(PositionMoveRotation posMoveRotation, Set<Relative> relatives) {
+        // CraftBukkit start
+        this.teleport(posMoveRotation, relatives, PlayerTeleportEvent.TeleportCause.UNKNOWN);
+    }
+
+    public boolean teleport(PositionMoveRotation posMoveRotation, Set<Relative> relatives, PlayerTeleportEvent.TeleportCause cause) { // CraftBukkit - Return event status
+        org.bukkit.entity.Player player = this.getCraftPlayer();
+        Location from = player.getLocation();
+        PositionMoveRotation absolutePosition = PositionMoveRotation.calculateAbsolute(PositionMoveRotation.of(this.player), posMoveRotation, relatives);
+        Location to = CraftLocation.toBukkit(absolutePosition.position(), this.getCraftPlayer().getWorld(), absolutePosition.yRot(), absolutePosition.xRot());
+        // SPIGOT-5171: Triggered on join
+        if (from.equals(to)) {
+            this.internalTeleport(posMoveRotation, relatives);
+            return true; // CraftBukkit - Return event status
+        }
+
+        // Paper start - Teleport API
+        final Set<io.papermc.paper.entity.TeleportFlag.Relative> relativeFlags = java.util.EnumSet.noneOf(io.papermc.paper.entity.TeleportFlag.Relative.class);
+        for (final Relative relativeArgument : relatives) {
+            final io.papermc.paper.entity.TeleportFlag.Relative flag = org.bukkit.craftbukkit.entity.CraftPlayer.deltaRelativeToAPI(relativeArgument);
+            if (flag != null) relativeFlags.add(flag);
+        }
+        PlayerTeleportEvent event = new PlayerTeleportEvent(player, from.clone(), to.clone(), cause, java.util.Set.copyOf(relativeFlags));
+        // Paper end - Teleport API
+        this.cserver.getPluginManager().callEvent(event);
+
+        if (event.isCancelled() || !to.equals(event.getTo())) {
+            // set = Collections.emptySet(); // Can't relative teleport // Paper - Teleport API; Now you can!
+            to = event.isCancelled() ? event.getFrom() : event.getTo();
+            posMoveRotation = new PositionMoveRotation(CraftLocation.toVec3(to), Vec3.ZERO, to.getYaw(), to.getPitch());
+        }
+
+        this.internalTeleport(posMoveRotation, relatives);
+        return !event.isCancelled(); // CraftBukkit - Return event status
+    }
+
+    public void teleport(Location dest) {
+        this.internalTeleport(dest.getX(), dest.getY(), dest.getZ(), dest.getYaw(), dest.getPitch());
+    }
+
+    private void internalTeleport(double d0, double d1, double d2, float f, float f1) {
+        this.internalTeleport(new PositionMoveRotation(new Vec3(d0, d1, d2), Vec3.ZERO, f, f1), Collections.emptySet());
+    }
+
+    public void internalTeleport(PositionMoveRotation posMoveRotation, Set<Relative> relatives) {
+        org.spigotmc.AsyncCatcher.catchOp("teleport"); // Paper
+        // Paper start - Prevent teleporting dead entities
+        if (this.player.isRemoved()) {
+            LOGGER.info("Attempt to teleport removed player {} restricted", player.getScoreboardName());
+            if (this.server.isDebugging()) io.papermc.paper.util.TraceUtil.dumpTraceForThread("Attempt to teleport removed player");
+            return;
+        }
+        // Paper end - Prevent teleporting dead entities
+        if (Float.isNaN(posMoveRotation.yRot())) {
+            posMoveRotation = new PositionMoveRotation(posMoveRotation.position(), posMoveRotation.deltaMovement(), 0, posMoveRotation.xRot());
+        }
+        if (Float.isNaN(posMoveRotation.xRot())) {
+            posMoveRotation = new PositionMoveRotation(posMoveRotation.position(), posMoveRotation.deltaMovement(), posMoveRotation.yRot(), 0);
+        }
+
+        this.justTeleported = true;
+        // CraftBukkit end
         this.awaitingTeleportTime = this.tickCount;
         if (++this.awaitingTeleport == Integer.MAX_VALUE) {
             this.awaitingTeleport = 0;
@@ -1186,12 +_,20 @@
 
         this.player.teleportSetPosition(posMoveRotation, relatives);
         this.awaitingPositionFromClient = this.player.position();
+        // CraftBukkit start - update last location
+        this.lastPosX = this.awaitingPositionFromClient.x;
+        this.lastPosY = this.awaitingPositionFromClient.y;
+        this.lastPosZ = this.awaitingPositionFromClient.z;
+        this.lastYaw = this.player.getYRot();
+        this.lastPitch = this.player.getXRot();
+        // CraftBukkit end
         this.send(ClientboundPlayerPositionPacket.of(this.awaitingTeleport, posMoveRotation, relatives));
     }
 
     @Override
     public void handlePlayerAction(ServerboundPlayerActionPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
+        if (this.player.isImmobile()) return; // CraftBukkit
         if (this.player.hasClientLoaded()) {
             BlockPos pos = packet.getPos();
             this.player.resetLastActionTime();
@@ -1200,32 +_,95 @@
                 case SWAP_ITEM_WITH_OFFHAND:
                     if (!this.player.isSpectator()) {
                         ItemStack itemInHand = this.player.getItemInHand(InteractionHand.OFF_HAND);
-                        this.player.setItemInHand(InteractionHand.OFF_HAND, this.player.getItemInHand(InteractionHand.MAIN_HAND));
-                        this.player.setItemInHand(InteractionHand.MAIN_HAND, itemInHand);
+                        // CraftBukkit start - inspiration taken from DispenserRegistry (See SpigotCraft#394)
+                        CraftItemStack mainHand = CraftItemStack.asCraftMirror(itemInHand);
+                        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, itemInHand);
+                        } else {
+                            this.player.setItemInHand(InteractionHand.MAIN_HAND, CraftItemStack.asNMSCopy(swapItemsEvent.getMainHandItem()));
+                        }
+                        // CraftBukkit end
                         this.player.stopUsingItem();
+                        if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.updateEquipmentOnPlayerActions) this.player.detectEquipmentUpdates(); // Paper - Force update attributes.
                     }
 
                     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(Component.literal("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);
+                        if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.updateEquipmentOnPlayerActions) this.player.detectEquipmentUpdates(); // Paper - Force update attributes.
                     }
 
                     return;
                 case DROP_ALL_ITEMS:
                     if (!this.player.isSpectator()) {
                         this.player.drop(true);
+                        if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.updateEquipmentOnPlayerActions) this.player.detectEquipmentUpdates(); // Paper - Force update attributes.
                     }
 
                     return;
                 case RELEASE_USE_ITEM:
                     this.player.releaseUsingItem();
+                    if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.updateEquipmentOnPlayerActions) this.player.detectEquipmentUpdates(); // Paper - Force update attributes.
                     return;
                 case START_DESTROY_BLOCK:
                 case ABORT_DESTROY_BLOCK:
                 case STOP_DESTROY_BLOCK:
+                    // Paper start - Don't allow digging into unloaded chunks
+                    if (this.player.level().getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4) == null || !this.player.canInteractWithBlock(pos, 1.0)) {
+                        this.player.connection.ackBlockChangesUpTo(packet.getSequence());
+                        return;
+                    }
+                    // Paper end - Don't allow digging into unloaded chunks
+                    // Paper start - Send block entities after destroy prediction
+                    this.player.gameMode.capturedBlockEntity = false;
+                    this.player.gameMode.captureSentBlockEntities = true;
+                    // Paper end - Send block entities after destroy prediction
                     this.player.gameMode.handleBlockBreakAction(pos, action, packet.getDirection(), this.player.level().getMaxY(), packet.getSequence());
                     this.ackBlockChangesUpTo(packet.getSequence());
+                    // Paper start - Send block entities after destroy prediction
+                    this.player.gameMode.captureSentBlockEntities = false;
+                    // If a block entity was modified speedup the block change ack to avoid the block entity
+                    // being overridden.
+                    if (this.player.gameMode.capturedBlockEntity) {
+                        // manually tick
+                        this.send(new ClientboundBlockChangedAckPacket(this.ackBlockChangesUpTo));
+                        this.player.connection.ackBlockChangesUpTo = -1;
+
+                        this.player.gameMode.capturedBlockEntity = false;
+                        BlockEntity blockEntity = this.player.level().getBlockEntity(pos);
+                        if (blockEntity != null) {
+                            this.player.connection.send(blockEntity.getUpdatePacket());
+                        }
+                    }
+                    // Paper end - Send block entities after destroy prediction
+                    if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.updateEquipmentOnPlayerActions) this.player.detectEquipmentUpdates(); // Paper - Force update attributes.
                     return;
                 default:
                     throw new IllegalArgumentException("Invalid player action");
@@ -1242,9 +_,31 @@
         }
     }
 
+    // Spigot start - limit place/interactions
+    private int limitedPackets;
+    private long lastLimitedPacket = -1;
+    private static int getSpamThreshold() { return io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.incomingPacketThreshold; } // Paper - Configurable threshold
+
+    private boolean checkLimit(long timestamp) {
+        if (this.lastLimitedPacket != -1 && timestamp - this.lastLimitedPacket < getSpamThreshold() && this.limitedPackets++ >= 8) { // Paper - Configurable threshold; raise packet limit to 8
+            return false;
+        }
+
+        if (this.lastLimitedPacket == -1 || timestamp - this.lastLimitedPacket >= getSpamThreshold()) { // Paper - Configurable threshold
+            this.lastLimitedPacket = timestamp;
+            this.limitedPackets = 0;
+            return true;
+        }
+
+        return true;
+    }
+    // Spigot end - limit place/interactions
+
     @Override
     public void handleUseItemOn(ServerboundUseItemOnPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
+        if (this.player.isImmobile()) return; // CraftBukkit
+        if (!this.checkLimit(packet.timestamp)) return; // Spigot - check limit
         if (this.player.hasClientLoaded()) {
             this.ackBlockChangesUpTo(packet.getSequence());
             ServerLevel serverLevel = this.player.level();
@@ -1253,6 +_,11 @@
             if (itemInHand.isItemEnabled(serverLevel.enabledFeatures())) {
                 BlockHitResult hitResult = packet.getHitResult();
                 Vec3 location = hitResult.getLocation();
+                // Paper start - improve distance check
+                if (!Double.isFinite(location.x()) || !Double.isFinite(location.y()) || !Double.isFinite(location.z())) {
+                    return;
+                }
+                // Paper end - improve distance check
                 BlockPos blockPos = hitResult.getBlockPos();
                 if (this.player.canInteractWithBlock(blockPos, 1.0)) {
                     Vec3 vec3 = location.subtract(Vec3.atCenterOf(blockPos));
@@ -1262,7 +_,8 @@
                         this.player.resetLastActionTime();
                         int maxY = this.player.level().getMaxY();
                         if (blockPos.getY() <= maxY) {
-                            if (this.awaitingPositionFromClient == null && serverLevel.mayInteract(this.player, blockPos)) {
+                            if (this.awaitingPositionFromClient == null && (serverLevel.mayInteract(this.player, blockPos) || (serverLevel.paperConfig().spawn.allowUsingSignsInsideSpawnProtection && serverLevel.getBlockState(blockPos).getBlock() instanceof net.minecraft.world.level.block.SignBlock))) { // Paper - Allow using signs inside spawn protection
+                                this.player.stopUsingItem(); // CraftBukkit - SPIGOT-4706
                                 InteractionResult interactionResult = this.player.gameMode.useItemOn(this.player, serverLevel, itemInHand, hand, hitResult);
                                 if (interactionResult.consumesAction()) {
                                     CriteriaTriggers.ANY_BLOCK_USE.trigger(this.player, hitResult.getBlockPos(), itemInHand.copy());
@@ -1275,10 +_,10 @@
                                     Component component = Component.translatable("build.tooHigh", maxY).withStyle(ChatFormatting.RED);
                                     this.player.sendSystemMessage(component, true);
                                 } else if (interactionResult instanceof InteractionResult.Success success
-                                    && success.swingSource() == InteractionResult.SwingSource.SERVER) {
+                                    && success.swingSource() == InteractionResult.SwingSource.SERVER && !this.player.gameMode.interactResult) { // Paper - Call interact event
                                     this.player.swing(hand, true);
                                 }
-                            }
+                            } else { this.player.containerMenu.forceHeldSlot(hand); } // Paper - Fix inventory desync; MC-99075
                         } else {
                             Component component1 = Component.translatable("build.tooHigh", maxY).withStyle(ChatFormatting.RED);
                             this.player.sendSystemMessage(component1, true);
@@ -1286,13 +_,8 @@
 
                         this.send(new ClientboundBlockUpdatePacket(serverLevel, blockPos));
                         this.send(new ClientboundBlockUpdatePacket(serverLevel, blockPos.relative(direction)));
-                    } else {
-                        LOGGER.warn(
-                            "Rejecting UseItemOnPacket from {}: Location {} too far away from hit block {}.",
-                            this.player.getGameProfile().getName(),
-                            location,
-                            blockPos
-                        );
+                        if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.updateEquipmentOnPlayerActions) this.player.detectEquipmentUpdates(); // Paper - Force update attributes.
+                        // Paper - Remove unused warning
                     }
                 }
             }
@@ -1302,6 +_,8 @@
     @Override
     public void handleUseItem(ServerboundUseItemPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
+        if (this.player.isImmobile()) return; // CraftBukkit
+        if (!this.checkLimit(packet.timestamp)) return; // Spigot - check limit
         if (this.player.hasClientLoaded()) {
             this.ackBlockChangesUpTo(packet.getSequence());
             ServerLevel serverLevel = this.player.level();
@@ -1315,6 +_,48 @@
                     this.player.absSnapRotationTo(f, f1);
                 }
 
+                // CraftBukkit start
+                // Raytrace to look for 'rogue armswings'
+                double x = this.player.getX();
+                double eyeY = this.player.getEyeY();
+                double z = this.player.getZ();
+                Vec3 from = new Vec3(x, eyeY, z);
+
+                float f3 = Mth.cos(-f * 0.017453292F - 3.1415927F);
+                float f4 = Mth.sin(-f * 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 = this.player.blockInteractionRange();
+                Vec3 to = from.add((double) f7 * d3, (double) f6 * d3, (double) f8 * d3);
+                BlockHitResult hitResult = this.player.level().clip(new net.minecraft.world.level.ClipContext(from, to, net.minecraft.world.level.ClipContext.Block.OUTLINE, net.minecraft.world.level.ClipContext.Fluid.NONE, this.player));
+
+                boolean cancelled;
+                if (hitResult == null || hitResult.getType() != HitResult.Type.BLOCK) {
+                    org.bukkit.event.player.PlayerInteractEvent event = CraftEventFactory.callPlayerInteractEvent(this.player, Action.RIGHT_CLICK_AIR, itemInHand, hand);
+                    cancelled = event.useItemInHand() == Event.Result.DENY;
+                } else {
+                    if (this.player.gameMode.firedInteract && this.player.gameMode.interactPosition.equals(hitResult.getBlockPos()) && this.player.gameMode.interactHand == hand && ItemStack.isSameItemSameComponents(this.player.gameMode.interactItemStack, itemInHand)) {
+                        cancelled = this.player.gameMode.interactResult;
+                    } else {
+                        org.bukkit.event.player.PlayerInteractEvent event = CraftEventFactory.callPlayerInteractEvent(this.player, Action.RIGHT_CLICK_BLOCK, hitResult.getBlockPos(), hitResult.getDirection(), itemInHand, true, hand, hitResult.getLocation());
+                        cancelled = event.useItemInHand() == Event.Result.DENY;
+                    }
+                    this.player.gameMode.firedInteract = false;
+                }
+
+                if (cancelled) {
+                    this.player.resyncUsingItem(this.player); // Paper - Properly cancel usable items
+                    this.player.containerMenu.forceHeldSlotAndArmor(hand); // SPIGOT-2524
+                    return;
+                }
+                itemInHand = this.player.getItemInHand(hand); // Update in case it was changed in the event
+                if (itemInHand.isEmpty()) {
+                    return;
+                }
+                // CraftBukkit end
+
                 if (this.player.gameMode.useItem(this.player, serverLevel, itemInHand, hand) instanceof InteractionResult.Success success
                     && success.swingSource() == InteractionResult.SwingSource.SERVER) {
                     this.player.swing(hand, true);
@@ -1330,7 +_,7 @@
             for (ServerLevel serverLevel : this.server.getAllLevels()) {
                 Entity entity = packet.getEntity(serverLevel);
                 if (entity != null) {
-                    this.player.teleportTo(serverLevel, entity.getX(), entity.getY(), entity.getZ(), Set.of(), entity.getYRot(), entity.getXRot(), true);
+                    this.player.teleportTo(serverLevel, entity.getX(), entity.getY(), entity.getZ(), Set.of(), entity.getYRot(), entity.getXRot(), true, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE); // CraftBukkit
                     return;
                 }
             }
@@ -1347,24 +_,50 @@
 
     @Override
     public void onDisconnect(DisconnectionDetails details) {
+        // CraftBukkit start - Rarely it would send a disconnect line twice
+        if (this.processedDisconnect) {
+            return;
+        } else {
+            this.processedDisconnect = true;
+        }
+        // CraftBukkit end
         LOGGER.info("{} lost connection: {}", this.player.getName().getString(), details.reason().getString());
-        this.removePlayerFromWorld();
+        // Paper start - Fix kick event leave message not being sent
+        final net.kyori.adventure.text.Component quitMessage = details.quitMessage().map(io.papermc.paper.adventure.PaperAdventure::asAdventure).orElse(null);
+        this.removePlayerFromWorld(quitMessage);
+        // Paper end - Fix kick event leave message not being sent
         super.onDisconnect(details);
     }
 
     private void removePlayerFromWorld() {
+        // Paper start - Fix kick event leave message not being sent
+        this.removePlayerFromWorld(null);
+    }
+
+    private void removePlayerFromWorld(@Nullable net.kyori.adventure.text.Component quitMessage) {
+        // Paper end - Fix kick event leave message not being sent
         this.chatMessageChain.close();
+        // 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.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 - Adventure
+        }
+        // CraftBukkit end
         this.player.getTextFilter().leave();
     }
 
     public void ackBlockChangesUpTo(int sequence) {
         if (sequence < 0) {
+            this.disconnect(Component.literal("Expected packet sequence nr >= 0"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - Treat sequence violations like they should be
             throw new IllegalArgumentException("Expected packet sequence nr >= 0");
         } else {
             this.ackBlockChangesUpTo = Math.max(sequence, this.ackBlockChangesUpTo);
@@ -1374,20 +_,38 @@
     @Override
     public void handleSetCarriedItem(ServerboundSetCarriedItemPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
+        if (this.player.isImmobile()) return; // CraftBukkit
         if (packet.getSlot() >= 0 && packet.getSlot() < Inventory.getSelectionSize()) {
+            if (packet.getSlot() == this.player.getInventory().getSelectedSlot()) { return; } // Paper - don't fire itemheldevent when there wasn't a slot change
+            PlayerItemHeldEvent event = new PlayerItemHeldEvent(this.getCraftPlayer(), this.player.getInventory().getSelectedSlot(), packet.getSlot());
+            this.cserver.getPluginManager().callEvent(event);
+            if (event.isCancelled()) {
+                this.send(new ClientboundSetHeldSlotPacket(this.player.getInventory().getSelectedSlot()));
+                this.player.resetLastActionTime();
+                return;
+            }
+            // CraftBukkit end
             if (this.player.getInventory().getSelectedSlot() != packet.getSlot() && this.player.getUsedItemHand() == InteractionHand.MAIN_HAND) {
                 this.player.stopUsingItem();
             }
 
             this.player.getInventory().setSelectedSlot(packet.getSlot());
             this.player.resetLastActionTime();
+            if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.updateEquipmentOnPlayerActions) this.player.detectEquipmentUpdates(); // Paper - Force update attributes.
         } else {
             LOGGER.warn("{} tried to set an invalid carried item", this.player.getName().getString());
+            this.disconnect(Component.literal("Invalid hotbar selection (Hacking?)"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // CraftBukkit // Paper - kick event cause
         }
     }
 
     @Override
     public void handleChat(ServerboundChatPacket packet) {
+        // CraftBukkit start - async chat
+        // SPIGOT-3638
+        if (this.server.isStopped()) {
+            return;
+        }
+        // CraftBukkit end
         Optional<LastSeenMessages> optional = this.unpackAndApplyLastSeen(packet.lastSeenMessages());
         if (!optional.isEmpty()) {
             this.tryHandleChat(packet.message(), () -> {
@@ -1399,25 +_,45 @@
                     return;
                 }
 
-                CompletableFuture<FilteredText> completableFuture = this.filterTextPacket(signedMessage.signedContent());
-                Component component = this.server.getChatDecorator().decorate(this.player, signedMessage.decoratedContent());
-                this.chatMessageChain.append(completableFuture, filteredText -> {
-                    PlayerChatMessage playerChatMessage = signedMessage.withUnsignedContent(component).filter(filteredText.mask());
+                CompletableFuture<FilteredText> completableFuture = this.filterTextPacket(signedMessage.signedContent()).thenApplyAsync(java.util.function.Function.identity(), this.server.chatExecutor); // CraftBukkit - async chat
+                CompletableFuture<Component> componentFuture = this.server.getChatDecorator().decorate(this.player, null, signedMessage.decoratedContent()); // Paper - Adventure
+
+                this.chatMessageChain.append(CompletableFuture.allOf(completableFuture, componentFuture), ($) -> { // Paper - Adventure
+                    PlayerChatMessage playerChatMessage = signedMessage.withUnsignedContent(componentFuture.join()).filter(completableFuture.join().mask()); // Paper - Adventure
                     this.broadcastChatMessage(playerChatMessage);
                 });
-            });
+            }, false); // CraftBukkit - async chat
         }
     }
 
     @Override
     public void handleChatCommand(ServerboundChatCommandPacket packet) {
         this.tryHandleChat(packet.command(), () -> {
+            // CraftBukkit start - SPIGOT-7346: Prevent disconnected players from executing commands
+            if (this.player.hasDisconnected()) {
+                return;
+            }
+            // CraftBukkit end
             this.performUnsignedChatCommand(packet.command());
-            this.detectRateSpam();
-        });
+            this.detectRateSpam("/" + packet.command()); // Spigot
+        }, true); // CraftBukkit - sync commands
     }
 
     private void performUnsignedChatCommand(String command) {
+        // CraftBukkit start
+        String prefixedCommand = "/" + command;
+        if (org.spigotmc.SpigotConfig.logCommands) { // Paper - Add missing SpigotConfig logCommands check
+            LOGGER.info("{} issued server command: {}", this.player.getScoreboardName(), prefixedCommand);
+        }
+
+        PlayerCommandPreprocessEvent event = new PlayerCommandPreprocessEvent(this.getCraftPlayer(), prefixedCommand, new org.bukkit.craftbukkit.util.LazyPlayerSet(this.server));
+        this.cserver.getPluginManager().callEvent(event);
+
+        if (event.isCancelled()) {
+            return;
+        }
+        command = event.getMessage().substring(1);
+        // CraftBukkit end
         ParseResults<CommandSourceStack> parseResults = this.parseCommand(command);
         if (this.server.enforceSecureProfile() && SignableCommand.hasSignableArguments(parseResults)) {
             LOGGER.error(
@@ -1434,28 +_,57 @@
         Optional<LastSeenMessages> optional = this.unpackAndApplyLastSeen(packet.lastSeenMessages());
         if (!optional.isEmpty()) {
             this.tryHandleChat(packet.command(), () -> {
+                // CraftBukkit start - SPIGOT-7346: Prevent disconnected players from executing commands
+                if (this.player.hasDisconnected()) {
+                    return;
+                }
+                // CraftBukkit end
                 this.performSignedChatCommand(packet, optional.get());
-                this.detectRateSpam();
-            });
+                this.detectRateSpam("/" + packet.command()); // Spigot
+            }, true); // CraftBukkit - sync commands
         }
     }
 
     private void performSignedChatCommand(ServerboundChatCommandSignedPacket packet, LastSeenMessages lastSeenMessages) {
+        // CraftBukkit start
+        String command = "/" + packet.command();
+        if (org.spigotmc.SpigotConfig.logCommands) { // Paper - Add missing SpigotConfig logCommands check
+            LOGGER.info("{} issued server command: {}", this.player.getScoreboardName(), command);
+        } // Paper - Add missing SpigotConfig logCommands check
+
+        PlayerCommandPreprocessEvent event = new PlayerCommandPreprocessEvent(this.getCraftPlayer(), command, new org.bukkit.craftbukkit.util.LazyPlayerSet(this.server));
+        this.cserver.getPluginManager().callEvent(event);
+        command = event.getMessage().substring(1);
+
         ParseResults<CommandSourceStack> parseResults = this.parseCommand(packet.command());
 
         Map<String, PlayerChatMessage> map;
         try {
+            // Paper - Always parse the original command to add to the chat chain
             map = this.collectSignedArguments(packet, SignableCommand.of(parseResults), lastSeenMessages);
         } catch (SignedMessageChain.DecodeException var6) {
             this.handleMessageDecodeFailure(var6);
             return;
         }
 
+        // Paper start - Fix cancellation and message changing
+        if (event.isCancelled()) {
+            // Only now are we actually good to return
+            return;
+        }
+
+        // Remove signed parts if the command was changed
+        if (!command.equals(packet.command())) {
+            parseResults = this.parseCommand(command);
+            map = Collections.emptyMap();
+        }
+        // Paper end - Fix cancellation and message changing
+
         CommandSigningContext commandSigningContext = new CommandSigningContext.SignedArguments(map);
         parseResults = Commands.mapSource(
             parseResults, commandSourceStack -> commandSourceStack.withSigningContext(commandSigningContext, this.chatMessageChain)
         );
-        this.server.getCommands().performCommand(parseResults, packet.command());
+        this.server.getCommands().performCommand(parseResults, command); // CraftBukkit
     }
 
     private void handleMessageDecodeFailure(SignedMessageChain.DecodeException exception) {
@@ -1519,14 +_,20 @@
         return dispatcher.parse(command, this.player.createCommandSourceStack());
     }
 
-    private void tryHandleChat(String message, Runnable handler) {
+    private void tryHandleChat(String message, Runnable handler, boolean sync) { // CraftBukkit
         if (isChatMessageIllegal(message)) {
-            this.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"));
-        } else if (this.player.getChatVisibility() == ChatVisiblity.HIDDEN) {
+            this.disconnectAsync(Component.translatable("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper - add proper async disconnect
+        } 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));
         } else {
             this.player.resetLastActionTime();
-            this.server.execute(handler);
+            // CraftBukkit start
+            if (sync) {
+                this.server.execute(handler);
+            } else {
+                handler.run();
+            }
+            // CraftBukkit end
         }
     }
 
@@ -1538,7 +_,7 @@
                 var10000 = Optional.of(lastSeenMessages);
             } catch (LastSeenMessagesValidator.ValidationException var5) {
                 LOGGER.error("Failed to validate message acknowledgements from {}: {}", this.player.getName().getString(), var5.getMessage());
-                this.disconnect(CHAT_VALIDATION_FAILED);
+                this.disconnectAsync(CHAT_VALIDATION_FAILED, org.bukkit.event.player.PlayerKickEvent.Cause.CHAT_VALIDATION_FAILED); // Paper - kick event causes & add proper async disconnect
                 return Optional.empty();
             }
 
@@ -1556,22 +_,81 @@
         return false;
     }
 
+    // CraftBukkit start
+    public void chat(String msg, PlayerChatMessage original, boolean async) {
+        if (msg.isEmpty() || this.player.getChatVisibility() == ChatVisiblity.HIDDEN) {
+            return;
+        }
+
+        if (this.player.getChatVisibility() == ChatVisiblity.SYSTEM) {
+            // Do nothing, this is coming from a plugin
+            // Paper start
+        } else {
+            if (!async && !org.bukkit.Bukkit.isPrimaryThread()) {
+                org.spigotmc.AsyncCatcher.catchOp("Asynchronous player chat is not allowed here");
+            }
+            new io.papermc.paper.adventure.ChatProcessor(this.server, this.player, original, async).process();
+            // Paper end
+        }
+    }
+
+    @Deprecated // Paper
+    public void handleCommand(String command) {
+        // Paper start - Remove all this old duplicated logic
+        if (command.startsWith("/")) {
+            command = command.substring(1);
+        }
+        /*
+        It should be noted that this represents the "legacy" command execution path.
+        Api can call commands even if there is no additional context provided.
+        This method should ONLY be used if you need to execute a command WITHOUT
+        an actual player's input.
+        */
+        this.performUnsignedChatCommand(command);
+        // Paper end
+    }
+    // CraftBukkit end
+
     private PlayerChatMessage getSignedMessage(ServerboundChatPacket packet, LastSeenMessages lastSeenMessages) throws SignedMessageChain.DecodeException {
         SignedMessageBody signedMessageBody = new SignedMessageBody(packet.message(), packet.timeStamp(), packet.salt(), lastSeenMessages);
         return this.signedMessageDecoder.unpack(packet.signature(), signedMessageBody);
     }
 
     private void broadcastChatMessage(PlayerChatMessage message) {
-        this.server.getPlayerList().broadcastChatMessage(message, this.player, ChatType.bind(ChatType.CHAT, this.player));
-        this.detectRateSpam();
+        // CraftBukkit start
+        String rawMessage = message.signedContent();
+        if (rawMessage.isEmpty()) {
+            LOGGER.warn("{} tried to send an empty message", this.player.getScoreboardName());
+        } else if (this.getCraftPlayer().isConversing()) {
+            final String conversationInput = rawMessage;
+            this.server.processQueue.add(() -> 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(rawMessage, message, true);
+        }
+        // this.server.getPlayerList().broadcastChatMessage(message, this.player, ChatType.bind(ChatType.CHAT, this.player));
+        // CraftBukkit end
+        this.detectRateSpam(rawMessage); // Spigot
     }
 
-    private void detectRateSpam() {
-        this.chatSpamThrottler.increment();
-        if (!this.chatSpamThrottler.isUnderThreshold()
+    // Spigot start - spam exclusions
+    private void detectRateSpam(String message) {
+        // CraftBukkit start - replaced with thread safe throttle
+        if (org.spigotmc.SpigotConfig.enableSpamExclusions) {
+            for (String exclude : org.spigotmc.SpigotConfig.spamExclusions) {
+                if (exclude != null && message.startsWith(exclude)) {
+                    return;
+                }
+            }
+        }
+        // Spigot end
+        // this.chatSpamThrottler.increment();
+        if (!this.chatSpamThrottler.isIncrementAndUnderThreshold()
+            // CraftBukkit end
             && !this.server.getPlayerList().isOp(this.player.getGameProfile())
             && !this.server.isSingleplayerOwner(this.player.getGameProfile())) {
-            this.disconnect(Component.translatable("disconnect.spam"));
+            this.disconnectAsync(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - kick event cause & add proper async disconnect
         }
     }
 
@@ -1582,7 +_,7 @@
                 this.lastSeenMessages.applyOffset(packet.offset());
             } catch (LastSeenMessagesValidator.ValidationException var5) {
                 LOGGER.error("Failed to validate message acknowledgement offset from {}: {}", this.player.getName().getString(), var5.getMessage());
-                this.disconnect(CHAT_VALIDATION_FAILED);
+                this.disconnectAsync(ServerGamePacketListenerImpl.CHAT_VALIDATION_FAILED, org.bukkit.event.player.PlayerKickEvent.Cause.CHAT_VALIDATION_FAILED); // Paper - kick event causes & add proper async disconnect
             }
         }
     }
@@ -1590,7 +_,40 @@
     @Override
     public void handleAnimate(ServerboundSwingPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
+        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 = Math.max(this.player.blockInteractionRange(), this.player.entityInteractionRange());
+        // 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
+        // SPIGOT-7429: Make sure to call PlayerInteractEvent for spectators and non-pickable entities
+        org.bukkit.util.RayTraceResult result = this.player.level().getWorld().rayTrace(origin, origin.getDirection(), d3, org.bukkit.FluidCollisionMode.NEVER, false, 0.0, entity -> { // Paper - Call interact event; change raySize from 0.1 to 0.0
+            Entity handle = ((CraftEntity) entity).getHandle();
+            return entity != this.player.getBukkitEntity() && this.player.getBukkitEntity().canSee(entity) && !handle.isSpectator() && handle.isPickable() && !handle.isPassengerOfSameVehicle(this.player);
+        });
+        if (result == null) {
+            CraftEventFactory.callPlayerInteractEvent(this.player, Action.LEFT_CLICK_AIR, this.player.getInventory().getSelectedItem(), InteractionHand.MAIN_HAND);
+        } else { // Paper start - Call interact event
+            GameType gameType = this.player.gameMode.getGameModeForPlayer();
+            if (gameType == GameType.ADVENTURE && result.getHitBlock() != null) {
+                CraftEventFactory.callPlayerInteractEvent(this.player, Action.LEFT_CLICK_BLOCK, ((org.bukkit.craftbukkit.block.CraftBlock) result.getHitBlock()).getPosition(), org.bukkit.craftbukkit.block.CraftBlock.blockFaceToNotch(result.getHitBlockFace()), this.player.getInventory().getSelectedItem(), InteractionHand.MAIN_HAND);
+            } else if (gameType != GameType.CREATIVE && result.getHitEntity() != null && origin.toVector().distanceSquared(result.getHitPosition()) > this.player.entityInteractionRange() * this.player.entityInteractionRange()) {
+                CraftEventFactory.callPlayerInteractEvent(this.player, Action.LEFT_CLICK_AIR, this.player.getInventory().getSelectedItem(), InteractionHand.MAIN_HAND);
+            }
+        } // Paper end - Call interact event
+
+        // Arm swing animation
+        io.papermc.paper.event.player.PlayerArmSwingEvent event = new io.papermc.paper.event.player.PlayerArmSwingEvent(this.getCraftPlayer(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(packet.getHand())); // Paper - Add PlayerArmSwingEvent
+        this.cserver.getPluginManager().callEvent(event);
+
+        if (event.isCancelled()) return;
+        // CraftBukkit end
         this.player.swing(packet.getHand());
     }
 
@@ -1598,6 +_,22 @@
     public void handlePlayerCommand(ServerboundPlayerCommandPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
         if (this.player.hasClientLoaded()) {
+            // CraftBukkit start
+            if (this.player.isRemoved()) return;
+            switch (packet.getAction()) {
+                case START_SPRINTING:
+                case STOP_SPRINTING: {
+                    PlayerToggleSprintEvent event = new PlayerToggleSprintEvent(this.getCraftPlayer(), packet.getAction() == ServerboundPlayerCommandPacket.Action.START_SPRINTING);
+                    this.cserver.getPluginManager().callEvent(event);
+
+                    if (event.isCancelled()) {
+                        return;
+                    }
+                    break;
+                }
+            }
+            // CraftBukkit end
+
             this.player.resetLastActionTime();
             switch (packet.getAction()) {
                 case START_SPRINTING:
@@ -1642,6 +_,14 @@
     }
 
     public void sendPlayerChatMessage(PlayerChatMessage chatMessage, ChatType.Bound boundType) {
+        // CraftBukkit start - SPIGOT-7262: if hidden we have to send as disguised message. Query whether we should send at all (but changing this may not be expected).
+        if (!this.getCraftPlayer().canSeePlayer(chatMessage.link().sender())) {
+            this.sendDisguisedChatMessage(chatMessage.decoratedContent(), boundType);
+            return;
+        }
+        // CraftBukkit end
+        // Paper start - Ensure that client receives chat packets in the same order that we add into the message signature cache
+        synchronized (this.messageSignatureCache) {
         this.send(
             new ClientboundPlayerChatPacket(
                 this.nextChatIndex++,
@@ -1664,9 +_,11 @@
             }
 
             if (i > 4096) {
-                this.disconnect(Component.translatable("multiplayer.disconnect.too_many_pending_chats"));
+                this.disconnectAsync(Component.translatable("multiplayer.disconnect.too_many_pending_chats"), org.bukkit.event.player.PlayerKickEvent.Cause.TOO_MANY_PENDING_CHATS); // Paper - kick event cause & add proper async disconnect
             }
         }
+        }
+        // Paper end - Ensure that client receives chat packets in the same order that we add into the message signature cache
     }
 
     public void sendDisguisedChatMessage(Component message, ChatType.Bound boundType) {
@@ -1677,6 +_,17 @@
         return this.connection.getRemoteAddress();
     }
 
+    // Spigot start
+    public SocketAddress getRawAddress() {
+        // Paper start - Unix domain socket support; this can be nullable in the case of a Unix domain socket, so if it is, fake something
+        if (this.connection.channel.remoteAddress() == null) {
+            return new java.net.InetSocketAddress(java.net.InetAddress.getLoopbackAddress(), 0);
+        }
+        // Paper end - Unix domain socket support
+        return this.connection.channel.remoteAddress();
+    }
+    // Spigot end
+
     public void switchToConfig() {
         this.waitingForSwitchToConfig = true;
         this.removePlayerFromWorld();
@@ -1692,9 +_,16 @@
     @Override
     public void handleInteract(ServerboundInteractPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
+        if (this.player.isImmobile()) return; // CraftBukkit
         if (this.player.hasClientLoaded()) {
             final ServerLevel serverLevel = this.player.level();
             final Entity target = packet.getTarget(serverLevel);
+            // Spigot start
+            if (target == this.player && !this.player.isSpectator()) {
+                this.disconnect(Component.literal("Cannot interact with self!"), org.bukkit.event.player.PlayerKickEvent.Cause.SELF_INTERACTION); // Paper - kick event cause
+                return;
+            }
+            // Spigot end
             this.player.resetLastActionTime();
             this.player.setShiftKeyDown(packet.isUsingSecondaryAction());
             if (target != null) {
@@ -1703,16 +_,53 @@
                 }
 
                 AABB boundingBox = target.getBoundingBox();
-                if (this.player.canInteractWithEntity(boundingBox, 3.0)) {
+                if (this.player.canInteractWithEntity(boundingBox, io.papermc.paper.configuration.GlobalConfiguration.get().misc.clientInteractionLeniencyDistance.or(3.0))) { // Paper - configurable lenience value for interact range
                     packet.dispatch(
                         new ServerboundInteractPacket.Handler() {
-                            private void performInteraction(InteractionHand hand, ServerGamePacketListenerImpl.EntityInteraction entityInteraction) {
+                            private void performInteraction(InteractionHand hand, ServerGamePacketListenerImpl.EntityInteraction entityInteraction, PlayerInteractEntityEvent event) { // CraftBukkit
                                 ItemStack itemInHand = ServerGamePacketListenerImpl.this.player.getItemInHand(hand);
                                 if (itemInHand.isItemEnabled(serverLevel.enabledFeatures())) {
                                     ItemStack itemStack = itemInHand.copy();
-                                    if (entityInteraction.run(ServerGamePacketListenerImpl.this.player, target, hand) instanceof InteractionResult.Success success
-                                        )
-                                     {
+                                    // CraftBukkit start
+                                    boolean triggerLeashUpdate = itemInHand.is(net.minecraft.world.item.Items.LEAD) && target instanceof net.minecraft.world.entity.Leashable;
+                                    Item origItem = ServerGamePacketListenerImpl.this.player.getInventory().getSelectedItem().isEmpty() ? null : ServerGamePacketListenerImpl.this.player.getInventory().getSelectedItem().getItem();
+
+                                    ServerGamePacketListenerImpl.this.cserver.getPluginManager().callEvent(event);
+
+                                    // Entity in bucket - SPIGOT-4048 and SPIGOT-6859
+                                    if ((target instanceof net.minecraft.world.entity.animal.Bucketable && target instanceof LivingEntity && origItem != null && origItem == Items.WATER_BUCKET) && (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelectedItem().isEmpty() || !ServerGamePacketListenerImpl.this.player.getInventory().getSelectedItem().is(origItem))) {
+                                        target.resendPossiblyDesyncedEntityData(ServerGamePacketListenerImpl.this.player); // Paper - The entire mob gets deleted, so resend it
+                                        ServerGamePacketListenerImpl.this.player.containerMenu.sendAllDataToRemote();
+                                    }
+
+                                    if (triggerLeashUpdate && (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelectedItem().isEmpty() || ServerGamePacketListenerImpl.this.player.getInventory().getSelectedItem().getItem() != origItem)) {
+                                        // Refresh the current leash state
+                                        ServerGamePacketListenerImpl.this.send(new net.minecraft.network.protocol.game.ClientboundSetEntityLinkPacket(target, ((net.minecraft.world.entity.Leashable) target).getLeashHolder()));
+                                    }
+
+                                    if (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelectedItem().isEmpty() || ServerGamePacketListenerImpl.this.player.getInventory().getSelectedItem().getItem() != origItem) {
+                                        // Refresh the current entity metadata
+                                        target.refreshEntityData(ServerGamePacketListenerImpl.this.player);
+                                        // SPIGOT-7136 - Allays
+                                        if (target instanceof net.minecraft.world.entity.animal.allay.Allay || target instanceof net.minecraft.world.entity.animal.horse.AbstractHorse) { // Paper - Fix horse armor desync
+                                            ServerGamePacketListenerImpl.this.send(new net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket(
+                                                target.getId(), java.util.Arrays.stream(net.minecraft.world.entity.EquipmentSlot.values())
+                                                .map((slot) -> com.mojang.datafixers.util.Pair.of(slot, ((LivingEntity) target).getItemBySlot(slot).copy()))
+                                                .collect(Collectors.toList()), true)); // Paper - sanitize
+                                            player.containerMenu.sendAllDataToRemote();
+                                        } else {
+                                            ServerGamePacketListenerImpl.this.player.containerMenu.forceHeldSlot(hand); // Paper - fix slot desync (is this needed?)
+                                        }
+                                    }
+
+                                    if (event.isCancelled()) {
+                                        return;
+                                    }
+                                    // CraftBukkit end
+                                    InteractionResult result = entityInteraction.run(ServerGamePacketListenerImpl.this.player, target, hand);
+
+                                    if (result instanceof InteractionResult.Success success // CraftBukkit
+                                    ) {
                                         ItemStack itemStack1 = success.wasItemInteraction() ? itemStack : ItemStack.EMPTY;
                                         CriteriaTriggers.PLAYER_INTERACTED_WITH_ENTITY.trigger(ServerGamePacketListenerImpl.this.player, itemStack1, target);
                                         if (success.swingSource() == InteractionResult.SwingSource.SERVER) {
@@ -1724,13 +_,13 @@
 
                             @Override
                             public void onInteraction(InteractionHand hand) {
-                                this.performInteraction(hand, Player::interactOn);
+                                this.performInteraction(hand, Player::interactOn, new PlayerInteractEntityEvent(ServerGamePacketListenerImpl.this.getCraftPlayer(), target.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand))); // CraftBukkit
                             }
 
                             @Override
                             public void onInteraction(InteractionHand hand, Vec3 interactionLocation) {
                                 this.performInteraction(
-                                    hand, (player, entity, interactionHand) -> entity.interactAt(player, interactionLocation, interactionHand)
+                                    hand, (player, entity, interactionHand) -> entity.interactAt(player, interactionLocation, interactionHand), new PlayerInteractAtEntityEvent(ServerGamePacketListenerImpl.this.getCraftPlayer(), target.getBukkitEntity(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(interactionLocation), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand)) // CraftBukkit
                                 );
                             }
 
@@ -1738,14 +_,14 @@
                             public void onAttack() {
                                 if (!(target instanceof ItemEntity)
                                     && !(target instanceof ExperienceOrb)
-                                    && target != ServerGamePacketListenerImpl.this.player
+                                    && (target != ServerGamePacketListenerImpl.this.player || ServerGamePacketListenerImpl.this.player.isSpectator()) // CraftBukkit
                                     && !(target instanceof AbstractArrow abstractArrow && !abstractArrow.isAttackable())) {
                                     ItemStack itemInHand = ServerGamePacketListenerImpl.this.player.getItemInHand(InteractionHand.MAIN_HAND);
                                     if (itemInHand.isItemEnabled(serverLevel.enabledFeatures())) {
                                         ServerGamePacketListenerImpl.this.player.attack(target);
                                     }
                                 } else {
-                                    ServerGamePacketListenerImpl.this.disconnect(Component.translatable("multiplayer.disconnect.invalid_entity_attacked"));
+                                    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());
                                 }
@@ -1754,6 +_,27 @@
                     );
                 }
             }
+            // Paper start - PlayerUseUnknownEntityEvent
+            else {
+                packet.dispatch(new net.minecraft.network.protocol.game.ServerboundInteractPacket.Handler() {
+                    @Override
+                    public void onInteraction(net.minecraft.world.InteractionHand hand) {
+                        CraftEventFactory.callPlayerUseUnknownEntityEvent(ServerGamePacketListenerImpl.this.player, packet, hand, null);
+                    }
+
+                    @Override
+                    public void onInteraction(net.minecraft.world.InteractionHand hand, net.minecraft.world.phys.Vec3 pos) {
+                        CraftEventFactory.callPlayerUseUnknownEntityEvent(ServerGamePacketListenerImpl.this.player, packet, hand, pos);
+                    }
+
+                    @Override
+                    public void onAttack() {
+                        CraftEventFactory.callPlayerUseUnknownEntityEvent(ServerGamePacketListenerImpl.this.player, packet, net.minecraft.world.InteractionHand.MAIN_HAND, null);
+                    }
+                });
+            }
+            // Paper end - PlayerUseUnknownEntityEvent
+            if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.updateEquipmentOnPlayerActions) this.player.detectEquipmentUpdates(); // Paper - Force update attributes.
         }
     }
 
@@ -1766,7 +_,7 @@
             case PERFORM_RESPAWN:
                 if (this.player.wonGame) {
                     this.player.wonGame = false;
-                    this.player = this.server.getPlayerList().respawn(this.player, true, Entity.RemovalReason.CHANGED_DIMENSION);
+                    this.player = this.server.getPlayerList().respawn(this.player, true, Entity.RemovalReason.CHANGED_DIMENSION, RespawnReason.END_PORTAL); // CraftBukkit
                     this.resetPosition();
                     CriteriaTriggers.CHANGED_DIMENSION.trigger(this.player, Level.END, Level.OVERWORLD);
                 } else {
@@ -1774,11 +_,11 @@
                         return;
                     }
 
-                    this.player = this.server.getPlayerList().respawn(this.player, false, Entity.RemovalReason.KILLED);
+                    this.player = this.server.getPlayerList().respawn(this.player, false, Entity.RemovalReason.KILLED, RespawnReason.DEATH); // CraftBukkit
                     this.resetPosition();
                     if (this.server.isHardcore()) {
-                        this.player.setGameMode(GameType.SPECTATOR);
-                        this.player.level().getGameRules().getRule(GameRules.RULE_SPECTATORSGENERATECHUNKS).set(false, this.server);
+                        this.player.setGameMode(GameType.SPECTATOR, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.HARDCORE_DEATH, null); // Paper - Expand PlayerGameModeChangeEvent
+                        this.player.level().getGameRules().getRule(GameRules.RULE_SPECTATORSGENERATECHUNKS).set(false, this.player.level()); // CraftBukkit - per-world
                     }
                 }
                 break;
@@ -1789,16 +_,27 @@
 
     @Override
     public void handleContainerClose(ServerboundContainerClosePacket packet) {
+        // Paper start - Inventory close reason
+        this.handleContainerClose(packet, org.bukkit.event.inventory.InventoryCloseEvent.Reason.PLAYER);
+    }
+
+    public void handleContainerClose(ServerboundContainerClosePacket packet, org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) {
+        // Paper end - Inventory close reason
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
+
+        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.level());
+        if (this.player.isImmobile()) return; // CraftBukkit
         this.player.resetLastActionTime();
-        if (this.player.containerMenu.containerId == packet.containerId()) {
-            if (this.player.isSpectator()) {
+        if (this.player.containerMenu.containerId == packet.containerId() && 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);
@@ -1811,7 +_,292 @@
                 } else {
                     boolean flag = packet.stateId() != this.player.containerMenu.getStateId();
                     this.player.containerMenu.suppressRemoteUpdates();
-                    this.player.containerMenu.clicked(slotNum, packet.buttonNum(), packet.clickType(), this.player);
+                    // CraftBukkit start - Call InventoryClickEvent
+                    if (slotNum < -1 && slotNum != net.minecraft.world.inventory.AbstractContainerMenu.SLOT_CLICKED_OUTSIDE) {
+                        return;
+                    }
+
+                    org.bukkit.inventory.InventoryView inventory = this.player.containerMenu.getBukkitView();
+                    SlotType type = inventory.getSlotType(slotNum);
+
+                    InventoryClickEvent event;
+                    ClickType click = ClickType.UNKNOWN;
+                    InventoryAction action = InventoryAction.UNKNOWN;
+
+                    switch (packet.clickType()) {
+                        case PICKUP:
+                            if (packet.buttonNum() == 0) {
+                                click = ClickType.LEFT;
+                            } else if (packet.buttonNum() == 1) {
+                                click = ClickType.RIGHT;
+                            }
+                            if (packet.buttonNum() == 0 || packet.buttonNum() == 1) {
+                                action = InventoryAction.NOTHING; // Don't want to repeat ourselves
+                                if (slotNum == net.minecraft.world.inventory.AbstractContainerMenu.SLOT_CLICKED_OUTSIDE) {
+                                    if (!this.player.containerMenu.getCarried().isEmpty()) {
+                                        action = packet.buttonNum() == 0 ? InventoryAction.DROP_ALL_CURSOR : InventoryAction.DROP_ONE_CURSOR;
+                                    }
+                                } else if (slotNum < 0)  {
+                                    action = InventoryAction.NOTHING;
+                                } else {
+                                    Slot slot = this.player.containerMenu.getSlot(slotNum);
+                                    if (slot != null) {
+                                        ItemStack clickedItem = slot.getItem();
+                                        ItemStack cursor = this.player.containerMenu.getCarried();
+                                        if (clickedItem.isEmpty()) {
+                                            if (!cursor.isEmpty()) {
+                                                if (cursor.getItem() instanceof net.minecraft.world.item.BundleItem && cursor.has(DataComponents.BUNDLE_CONTENTS) && packet.buttonNum() != 0) {
+                                                    action = cursor.get(DataComponents.BUNDLE_CONTENTS).isEmpty() ? InventoryAction.NOTHING : InventoryAction.PLACE_FROM_BUNDLE;
+                                                } else {
+                                                    action = packet.buttonNum() == 0 ? InventoryAction.PLACE_ALL : InventoryAction.PLACE_ONE;
+                                                }
+                                            }
+                                        } else if (slot.mayPickup(this.player)) {
+                                            if (cursor.isEmpty()) {
+                                                if (slot.getItem().getItem() instanceof net.minecraft.world.item.BundleItem && slot.getItem().has(DataComponents.BUNDLE_CONTENTS) && packet.buttonNum() != 0) {
+                                                    action = slot.getItem().get(DataComponents.BUNDLE_CONTENTS).isEmpty() ? InventoryAction.NOTHING : InventoryAction.PICKUP_FROM_BUNDLE;
+                                                } else {
+                                                    action = packet.buttonNum() == 0 ? InventoryAction.PICKUP_ALL : InventoryAction.PICKUP_HALF;
+                                                }
+                                            } else if (slot.mayPlace(cursor)) {
+                                                if (ItemStack.isSameItemSameComponents(clickedItem, cursor)) {
+                                                    int toPlace = packet.buttonNum() == 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()) {
+                                                    if (cursor.getItem() instanceof net.minecraft.world.item.BundleItem && cursor.has(DataComponents.BUNDLE_CONTENTS) && packet.buttonNum() == 0) {
+                                                        int toPickup = cursor.get(DataComponents.BUNDLE_CONTENTS).getMaxAmountToAdd(slot.getItem());
+                                                        if (toPickup >= slot.getItem().getCount()) {
+                                                            action = InventoryAction.PICKUP_ALL_INTO_BUNDLE;
+                                                        } else if (toPickup == 0) {
+                                                            action = InventoryAction.NOTHING;
+                                                        } else {
+                                                            action = InventoryAction.PICKUP_SOME_INTO_BUNDLE;
+                                                        }
+                                                    } else if (slot.getItem().getItem() instanceof net.minecraft.world.item.BundleItem && slot.getItem().has(DataComponents.BUNDLE_CONTENTS) && packet.buttonNum() == 0) {
+                                                        int toPickup = slot.getItem().get(DataComponents.BUNDLE_CONTENTS).getMaxAmountToAdd(cursor);
+                                                        if (toPickup >= cursor.getCount()) {
+                                                            action = InventoryAction.PLACE_ALL_INTO_BUNDLE;
+                                                        } else if (toPickup == 0) {
+                                                            action = InventoryAction.NOTHING;
+                                                        } else {
+                                                            action = InventoryAction.PLACE_SOME_INTO_BUNDLE;
+                                                        }
+                                                    } else {
+                                                        action = InventoryAction.SWAP_WITH_CURSOR;
+                                                    }
+                                                }
+                                            } else if (ItemStack.isSameItemSameComponents(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.buttonNum() == 0) {
+                                click = ClickType.SHIFT_LEFT;
+                            } else if (packet.buttonNum() == 1) {
+                                click = ClickType.SHIFT_RIGHT;
+                            }
+                            if (packet.buttonNum() == 0 || packet.buttonNum() == 1) {
+                                if (slotNum < 0) {
+                                    action = InventoryAction.NOTHING;
+                                } else {
+                                    Slot slot = this.player.containerMenu.getSlot(slotNum);
+                                    if (slot != null && slot.mayPickup(this.player) && slot.hasItem()) {
+                                        action = InventoryAction.MOVE_TO_OTHER_INVENTORY;
+                                    } else {
+                                        action = InventoryAction.NOTHING;
+                                    }
+                                }
+                            }
+                            break;
+                        case SWAP:
+                            if ((packet.buttonNum() >= 0 && packet.buttonNum() < 9) || packet.buttonNum() == Inventory.SLOT_OFFHAND) {
+                                // Paper start - Add slot sanity checks to container clicks
+                                if (slotNum < 0) {
+                                    action = InventoryAction.NOTHING;
+                                    break;
+                                }
+                                // Paper end - Add slot sanity checks to container clicks
+                                click = (packet.buttonNum() == Inventory.SLOT_OFFHAND) ? ClickType.SWAP_OFFHAND : ClickType.NUMBER_KEY;
+                                Slot clickedSlot = this.player.containerMenu.getSlot(slotNum);
+                                if (clickedSlot.mayPickup(this.player)) {
+                                    ItemStack hotbar = this.player.getInventory().getItem(packet.buttonNum());
+                                    if ((!hotbar.isEmpty() && clickedSlot.mayPlace(hotbar)) || (hotbar.isEmpty() && clickedSlot.hasItem())) { // Paper - modernify this logic (no such thing as a "hotbar move and readd"
+                                        action = InventoryAction.HOTBAR_SWAP;
+                                    } else {
+                                        action = InventoryAction.NOTHING;
+                                    }
+                                } else {
+                                    action = InventoryAction.NOTHING;
+                                }
+                            }
+                            break;
+                        case CLONE:
+                            if (packet.buttonNum() == 2) {
+                                click = ClickType.MIDDLE;
+                                if (slotNum < 0) {
+                                    action = InventoryAction.NOTHING;
+                                } else {
+                                    Slot slot = this.player.containerMenu.getSlot(slotNum);
+                                    if (slot != null && slot.hasItem() && this.player.getAbilities().instabuild && this.player.containerMenu.getCarried().isEmpty()) {
+                                        action = InventoryAction.CLONE_STACK;
+                                    } else {
+                                        action = InventoryAction.NOTHING;
+                                    }
+                                }
+                            } else {
+                                click = ClickType.UNKNOWN;
+                                action = InventoryAction.UNKNOWN;
+                            }
+                            break;
+                        case THROW:
+                            if (slotNum >= 0) {
+                                if (packet.buttonNum() == 0) {
+                                    click = ClickType.DROP;
+                                    Slot slot = this.player.containerMenu.getSlot(slotNum);
+                                    if (slot != null && slot.hasItem() && slot.mayPickup(this.player) && !slot.getItem().isEmpty() && slot.getItem().getItem() != Items.AIR) {
+                                        action = InventoryAction.DROP_ONE_SLOT;
+                                    } else {
+                                        action = InventoryAction.NOTHING;
+                                    }
+                                } else if (packet.buttonNum() == 1) {
+                                    click = ClickType.CONTROL_DROP;
+                                    Slot slot = this.player.containerMenu.getSlot(slotNum);
+                                    if (slot != null && slot.hasItem() && slot.mayPickup(this.player) && !slot.getItem().isEmpty() && slot.getItem().getItem() != Items.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.buttonNum() == 1) {
+                                    click = ClickType.RIGHT;
+                                }
+                                action = InventoryAction.NOTHING;
+                            }
+                            break;
+                        case QUICK_CRAFT:
+                            // Paper start - Fix CraftBukkit drag system
+                            net.minecraft.world.inventory.AbstractContainerMenu containerMenu = this.player.containerMenu;
+                            int currentStatus = this.player.containerMenu.quickcraftStatus;
+                            int newStatus = net.minecraft.world.inventory.AbstractContainerMenu.getQuickcraftHeader(packet.buttonNum());
+                            if ((currentStatus != 1 || newStatus != 2 && currentStatus != newStatus)) {
+                            } else if (containerMenu.getCarried().isEmpty()) {
+                            } else if (newStatus == 0) {
+                            } else if (newStatus == 1) {
+                            } else if (newStatus == 2) {
+                                if (!this.player.containerMenu.quickcraftSlots.isEmpty()) {
+                                    if (this.player.containerMenu.quickcraftSlots.size() == 1) {
+                                        int index = containerMenu.quickcraftSlots.iterator().next().index;
+                                        containerMenu.resetQuickCraft();
+                                        this.handleContainerClick(new ServerboundContainerClickPacket(packet.containerId(), packet.stateId(), (short) index, (byte) containerMenu.quickcraftType, net.minecraft.world.inventory.ClickType.PICKUP, packet.changedSlots(), packet.carriedItem()));
+                                        return;
+                                    }
+                                }
+                            }
+                            // Paper end - Fix CraftBukkit drag system
+                            this.player.containerMenu.clicked(slotNum, packet.buttonNum(), packet.clickType(), this.player);
+                            break;
+                        case PICKUP_ALL:
+                            click = ClickType.DOUBLE_CLICK;
+                            action = InventoryAction.NOTHING;
+                            if (slotNum >= 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(org.bukkit.craftbukkit.inventory.CraftItemType.minecraftToBukkit(cursor.getItem())) || inventory.getBottomInventory().contains(org.bukkit.craftbukkit.inventory.CraftItemType.minecraftToBukkit(cursor.getItem()))) {
+                                    action = InventoryAction.COLLECT_TO_CURSOR;
+                                }
+                            }
+                            break;
+                        default:
+                            break;
+                    }
+
+                    if (packet.clickType() != net.minecraft.world.inventory.ClickType.QUICK_CRAFT) {
+                        if (click == ClickType.NUMBER_KEY) {
+                            event = new InventoryClickEvent(inventory, type, slotNum, click, action, packet.buttonNum());
+                        } else {
+                            event = new InventoryClickEvent(inventory, type, slotNum, click, action);
+                        }
+
+                        org.bukkit.inventory.Inventory top = inventory.getTopInventory();
+                        if (slotNum == 0 && top instanceof final org.bukkit.inventory.CraftingInventory craftingInv) {
+                            org.bukkit.inventory.Recipe recipe = craftingInv.getRecipe();
+                            if (recipe != null) {
+                                if (click == ClickType.NUMBER_KEY) {
+                                    event = new CraftItemEvent(recipe, inventory, type, slotNum, click, action, packet.buttonNum());
+                                } else {
+                                    event = new CraftItemEvent(recipe, inventory, type, slotNum, click, action);
+                                }
+                            }
+                        }
+
+                        if (slotNum == 3 && top instanceof final org.bukkit.inventory.SmithingInventory smithingInv) {
+                            org.bukkit.inventory.ItemStack result = smithingInv.getResult();
+                            if (result != null) {
+                                if (click == ClickType.NUMBER_KEY) {
+                                    event = new SmithItemEvent(inventory, type, slotNum, click, action, packet.buttonNum());
+                                } else {
+                                    event = new SmithItemEvent(inventory, type, slotNum, click, action);
+                                }
+                            }
+                        }
+
+                        // Paper start - cartography item event
+                        if (slotNum == net.minecraft.world.inventory.CartographyTableMenu.RESULT_SLOT && top instanceof org.bukkit.inventory.CartographyInventory cartographyInventory) {
+                            org.bukkit.inventory.ItemStack result = cartographyInventory.getResult();
+                            if (result != null && !result.isEmpty()) {
+                                if (click == ClickType.NUMBER_KEY) {
+                                    event = new io.papermc.paper.event.player.CartographyItemEvent(inventory, type, slotNum, click, action, packet.buttonNum());
+                                } else {
+                                    event = new io.papermc.paper.event.player.CartographyItemEvent(inventory, type, slotNum, click, action);
+                                }
+                            }
+                        }
+                        // Paper end - cartography item event
+
+                        event.setCancelled(cancelled);
+                        net.minecraft.world.inventory.AbstractContainerMenu oldContainer = this.player.containerMenu; // SPIGOT-1224
+                        this.cserver.getPluginManager().callEvent(event);
+                        if (this.player.containerMenu != oldContainer) {
+                            this.player.containerMenu.resumeRemoteUpdates();
+                            this.player.containerMenu.broadcastFullState();
+                            return;
+                        }
+
+                        if (event.getResult() != org.bukkit.event.Event.Result.DENY) { // if denied, synchronizing is fully handled by broadcastChanges at the end
+                            this.player.containerMenu.clicked(slotNum, packet.buttonNum(), packet.clickType(), this.player);
+                        }
+
+                        if (event instanceof CraftItemEvent || event instanceof SmithItemEvent) {
+                            // Need to update the inventory on crafting to
+                            // correctly support custom recipes
+                            this.player.containerMenu.sendAllDataToRemote();
+                        }
+                    }
+                    // CraftBukkit end
 
                     for (Entry<HashedStack> entry : Int2ObjectMaps.fastIterable(packet.changedSlots())) {
                         this.player.containerMenu.setRemoteSlotUnsafe(entry.getIntKey(), entry.getValue());
@@ -1824,6 +_,7 @@
                     } else {
                         this.player.containerMenu.broadcastChanges();
                     }
+                    if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.updateEquipmentOnPlayerActions) this.player.detectEquipmentUpdates(); // Paper - Force update attributes.
                 }
             }
         }
@@ -1831,6 +_,14 @@
 
     @Override
     public void handlePlaceRecipe(ServerboundPlaceRecipePacket packet) {
+        // Paper start - auto recipe limit
+        if (!org.bukkit.Bukkit.isPrimaryThread()) {
+            if (!this.recipeSpamPackets.isIncrementAndUnderThreshold()) {
+                this.disconnectAsync(net.minecraft.network.chat.Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - kick event cause // Paper - add proper async disconnect
+                return;
+            }
+        }
+        // Paper end - auto recipe limit
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
         this.player.resetLastActionTime();
         if (!this.player.isSpectator() && this.player.containerMenu.containerId == packet.containerId()) {
@@ -1847,9 +_,44 @@
                                 return;
                             }
 
+                            // Paper start - Add PlayerRecipeBookClickEvent
+                            NamespacedKey recipeName = org.bukkit.craftbukkit.util.CraftNamespacedKey.fromMinecraft(recipeHolder.id().location());
+                            boolean makeAll = packet.useMaxItems();
+                            com.destroystokyo.paper.event.player.PlayerRecipeBookClickEvent paperEvent = new com.destroystokyo.paper.event.player.PlayerRecipeBookClickEvent(
+                                this.player.getBukkitEntity(), recipeName, makeAll
+                            );
+                            if (!paperEvent.callEvent()) {
+                                return;
+                            }
+                            recipeName = paperEvent.getRecipe();
+                            makeAll = paperEvent.isMakeAll();
+                            if (org.bukkit.event.player.PlayerRecipeBookClickEvent.getHandlerList().getRegisteredListeners().length > 0) {
+                                // Paper end - Add PlayerRecipeBookClickEvent
+                                // CraftBukkit start - implement PlayerRecipeBookClickEvent
+                                org.bukkit.inventory.Recipe recipe = this.cserver.getRecipe(recipeName); // Paper - Add PlayerRecipeBookClickEvent - forward to legacy event
+                                if (recipe == null) {
+                                    return;
+                                }
+                                // Paper start - Add PlayerRecipeBookClickEvent - forward to legacy event
+                                org.bukkit.event.player.PlayerRecipeBookClickEvent event = CraftEventFactory.callRecipeBookClickEvent(this.player, recipe, makeAll);
+                                recipeName = ((org.bukkit.Keyed) event.getRecipe()).getKey();
+                                makeAll = event.isShiftClick();
+                            }
+                            if (!(this.player.containerMenu instanceof RecipeBookMenu)) {
+                                return;
+                            }
+                            // Paper end - Add PlayerRecipeBookClickEvent - forward to legacy event
+
+                            // Cast to keyed should be safe as the recipe will never be a MerchantRecipe.
+                            recipeHolder = this.server.getRecipeManager().byKey(net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.RECIPE, org.bukkit.craftbukkit.util.CraftNamespacedKey.toMinecraft(recipeName))).orElse(null); // Paper - Add PlayerRecipeBookClickEvent - forward to legacy event
+                            if (recipeHolder == null) {
+                                return;
+                            }
+
                             RecipeBookMenu.PostPlaceAction postPlaceAction = recipeBookMenu.handlePlacement(
-                                packet.useMaxItems(), this.player.isCreative(), recipeHolder, this.player.level(), this.player.getInventory()
+                                makeAll, this.player.isCreative(), recipeHolder, this.player.level(), this.player.getInventory()
                             );
+                            // CraftBukkit end
                             if (postPlaceAction == RecipeBookMenu.PostPlaceAction.PLACE_GHOST_RECIPE) {
                                 this.send(new ClientboundPlaceGhostRecipePacket(this.player.containerMenu.containerId, recipeFromDisplay.display().display()));
                             }
@@ -1863,6 +_,7 @@
     @Override
     public void handleContainerButtonClick(ServerboundContainerButtonClickPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
+        if (this.player.isImmobile()) return; // CraftBukkit
         this.player.resetLastActionTime();
         if (this.player.containerMenu.containerId == packet.containerId() && !this.player.isSpectator()) {
             if (!this.player.containerMenu.stillValid(this.player)) {
@@ -1872,6 +_,7 @@
                 if (flag) {
                     this.player.containerMenu.broadcastChanges();
                 }
+                if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.updateEquipmentOnPlayerActions) this.player.detectEquipmentUpdates(); // Paper - Force update attributes.
             }
         }
     }
@@ -1888,10 +_,48 @@
 
             boolean flag1 = packet.slotNum() >= 1 && packet.slotNum() <= 45;
             boolean flag2 = itemStack.isEmpty() || itemStack.getCount() <= itemStack.getMaxStackSize();
+            if (flag || (flag1 && !ItemStack.matches(this.player.inventoryMenu.getSlot(packet.slotNum()).getItem(), packet.itemStack()))) { // Insist on valid slot
+                // CraftBukkit start - Call click event
+                org.bukkit.inventory.InventoryView inventory = this.player.inventoryMenu.getBukkitView();
+                org.bukkit.inventory.ItemStack item = CraftItemStack.asBukkitCopy(packet.itemStack());
+
+                SlotType type = SlotType.QUICKBAR;
+                if (flag) {
+                    type = SlotType.OUTSIDE;
+                } else if (packet.slotNum() < 36) {
+                    if (packet.slotNum() >= 5 && packet.slotNum() < 9) {
+                        type = SlotType.ARMOR;
+                    } else {
+                        type = SlotType.CONTAINER;
+                    }
+                }
+                InventoryCreativeEvent event = new InventoryCreativeEvent(inventory, type, flag ? net.minecraft.world.inventory.AbstractContainerMenu.SLOT_CLICKED_OUTSIDE : packet.slotNum(), 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.slotNum() >= 0) {
+                            this.player.connection.send(new net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket(this.player.inventoryMenu.containerId, this.player.inventoryMenu.incrementStateId(), packet.slotNum(), this.player.inventoryMenu.getSlot(packet.slotNum()).getItem()));
+                            this.player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetCursorItemPacket(ItemStack.EMPTY.copy())); // Paper - correctly set cursor
+                        }
+                        return;
+                }
+            }
+            // CraftBukkit end
             if (flag1 && flag2) {
                 this.player.inventoryMenu.getSlot(packet.slotNum()).setByPlayer(itemStack);
                 this.player.inventoryMenu.setRemoteSlot(packet.slotNum(), itemStack);
                 this.player.inventoryMenu.broadcastChanges();
+                if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.updateEquipmentOnPlayerActions) this.player.detectEquipmentUpdates(); // Paper - Force update attributes.
             } else if (flag && flag2) {
                 if (this.dropSpamThrottler.isUnderThreshold()) {
                     this.dropSpamThrottler.increment();
@@ -1905,15 +_,38 @@
 
     @Override
     public void handleSignUpdate(ServerboundSignUpdatePacket packet) {
-        List<String> list = Stream.of(packet.getLines()).map(ChatFormatting::stripFormatting).collect(Collectors.toList());
+        // Paper start - Limit client sign length
+        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 = Stream.of(lines).map(ChatFormatting::stripFormatting).collect(Collectors.toList());
+        // Paper end - Limit client sign length
         this.filterTextPacket(list).thenAcceptAsync(texts -> this.updateSignText(packet, (List<FilteredText>)texts), this.server);
     }
 
     private void updateSignText(ServerboundSignUpdatePacket packet, List<FilteredText> filteredText) {
+        if (this.player.isImmobile()) return; // CraftBukkit
         this.player.resetLastActionTime();
         ServerLevel serverLevel = this.player.level();
         BlockPos pos = packet.getPos();
         if (serverLevel.hasChunkAt(pos)) {
+            // Paper start - Add API for client-side signs
+            if (!new io.papermc.paper.event.packet.UncheckedSignChangeEvent(
+                this.player.getBukkitEntity(),
+                io.papermc.paper.util.MCUtil.toPosition(pos),
+                packet.isFrontText() ? org.bukkit.block.sign.Side.FRONT : org.bukkit.block.sign.Side.BACK,
+                filteredText.stream().<net.kyori.adventure.text.Component>map(line -> net.kyori.adventure.text.Component.text(line.raw())).toList())
+                .callEvent()) {
+                return;
+            }
+            // Paper end - Add API for client-side signs
             if (!(serverLevel.getBlockEntity(pos) instanceof SignBlockEntity signBlockEntity)) {
                 return;
             }
@@ -1925,14 +_,32 @@
     @Override
     public void handlePlayerAbilities(ServerboundPlayerAbilitiesPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
-        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.level());
+        // Paper start - do not accept invalid information
+        if (packet.information().viewDistance() < 0) {
+            LOGGER.warn("Disconnecting {} for invalid view distance: {}", this.player.getScoreboardName(), packet.information().viewDistance());
+            this.disconnect(Component.literal("Invalid client settings"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION);
+            return;
+        }
+        // Paper end - do not accept invalid information
         boolean isModelPartShown = this.player.isModelPartShown(PlayerModelPart.HAT);
         this.player.updateOptions(packet.information());
+        this.connection.channel.attr(io.papermc.paper.adventure.PaperAdventure.LOCALE_ATTRIBUTE).set(net.kyori.adventure.translation.Translator.parseLocale(packet.information().language())); // Paper
         if (this.player.isModelPartShown(PlayerModelPart.HAT) != isModelPartShown) {
             this.server.getPlayerList().broadcastAll(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_HAT, this.player));
         }
@@ -1948,7 +_,7 @@
                 packet.difficulty().getDisplayName()
             );
         } else {
-            this.server.setDifficulty(packet.difficulty(), false);
+            // this.server.setDifficulty(packet.difficulty(), false); // Paper - per level difficulty; don't allow clients to change this
         }
     }
 
@@ -1982,7 +_,7 @@
         ProfilePublicKey.Data data2 = data.profilePublicKey();
         if (!Objects.equals(data1, data2)) {
             if (data1 != null && data2.expiresAt().isBefore(data1.expiresAt())) {
-                this.disconnect(ProfilePublicKey.EXPIRED_PROFILE_PUBLIC_KEY);
+                this.disconnect(ProfilePublicKey.EXPIRED_PROFILE_PUBLIC_KEY, org.bukkit.event.player.PlayerKickEvent.Cause.EXPIRED_PROFILE_PUBLIC_KEY); // Paper - kick event causes
             } else {
                 try {
                     SignatureValidator profileKeySignatureValidator = this.server.getProfileKeySignatureValidator();
@@ -1993,8 +_,8 @@
 
                     this.resetPlayerChatState(data.validate(this.player.getGameProfile(), profileKeySignatureValidator));
                 } catch (ProfilePublicKey.ValidationException var6) {
-                    LOGGER.error("Failed to validate profile key: {}", var6.getMessage());
-                    this.disconnect(var6.getComponent());
+                    // LOGGER.error("Failed to validate profile key: {}", var6.getMessage()); // Paper - Improve logging and errors
+                    this.disconnect(var6.getComponent(), var6.kickCause); // Paper - kick event causes
                 }
             }
         }
@@ -2005,11 +_,13 @@
         if (!this.waitingForSwitchToConfig) {
             throw new IllegalStateException("Client acknowledged config, but none was requested");
         } else {
+            final ServerConfigurationPacketListenerImpl listener = new ServerConfigurationPacketListenerImpl(this.server, this.connection, this.createCookie(this.player.clientInformation())); // Paper
             this.connection
                 .setupInboundProtocol(
                     ConfigurationProtocols.SERVERBOUND,
-                    new ServerConfigurationPacketListenerImpl(this.server, this.connection, this.createCookie(this.player.clientInformation()))
+                    listener // Paper
                 );
+            new io.papermc.paper.event.connection.configuration.PlayerConnectionReconfigureEvent(listener.paperConnection).callEvent(); // Paper
         }
     }
 
@@ -2027,27 +_,32 @@
 
     private void resetPlayerChatState(RemoteChatSession chatSession) {
         this.chatSession = chatSession;
+        this.hasLoggedExpiry = false; // Paper - Prevent causing expired keys from impacting new joins
         this.signedMessageDecoder = chatSession.createMessageDecoder(this.player.getUUID());
         this.chatMessageChain
             .append(
                 () -> {
+                    server.executeBlocking(() -> { // Paper - Broadcast chat session update sync
                     this.player.setChatSession(chatSession);
                     this.server
                         .getPlayerList()
                         .broadcastAll(
-                            new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.INITIALIZE_CHAT), List.of(this.player))
+                            new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.INITIALIZE_CHAT), List.of(this.player)), this.player // Paper - Use single player info update packet on join
                         );
+                    });
                 }
             );
     }
 
     @Override
     public void handleCustomPayload(ServerboundCustomPayloadPacket packet) {
+        super.handleCustomPayload(packet); // Paper
     }
 
     @Override
     public void handleClientTickEnd(ServerboundClientTickEndPacket packet) {
         PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level());
+        this.tickEndEvent.callEvent(); // Paper - add client tick end event
         if (!this.receivedMovementThisTick) {
             this.player.setKnownMovement(Vec3.ZERO);
         }
@@ -2078,4 +_,80 @@
     interface EntityInteraction {
         InteractionResult run(ServerPlayer player, Entity entity, InteractionHand hand);
     }
+
+    // Paper start - Add fail move event
+    private io.papermc.paper.event.player.PlayerFailMoveEvent fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason failReason,
+                                                                           double toX, double toY, double toZ, float toYaw, float toPitch, boolean logWarning) {
+        org.bukkit.entity.Player player = this.getCraftPlayer();
+        Location from = new Location(player.getWorld(), this.lastPosX, this.lastPosY, this.lastPosZ, this.lastYaw, this.lastPitch);
+        Location to = new Location(player.getWorld(), toX, toY, toZ, toYaw, toPitch);
+        io.papermc.paper.event.player.PlayerFailMoveEvent event = new io.papermc.paper.event.player.PlayerFailMoveEvent(player, failReason,
+            false, logWarning, from, to);
+        event.callEvent();
+        return event;
+    }
+    // Paper end - Add fail move event
+
+    // Paper start - add utilities
+    public org.bukkit.craftbukkit.entity.CraftPlayer getCraftPlayer() {
+        return this.player == null ? null : this.player.getBukkitEntity();
+    }
+
+    @Override
+    public void disconnectAsync(final net.minecraft.network.DisconnectionDetails disconnectionInfo) {
+        if (this.cserver.isPrimaryThread()) {
+            this.disconnect(disconnectionInfo);
+            return;
+        }
+
+        this.connection.setReadOnly();
+        this.server.scheduleOnMain(() -> {
+            this.disconnect(disconnectionInfo);
+            if (this.player.quitReason == null) {
+                // cancelled
+                this.connection.enableAutoRead();
+            }
+        });
+    }
+
+    public final boolean isDisconnected() {
+        return (!this.player.joining && !this.connection.isConnected()) || this.processedDisconnect; // Paper - Fix duplication bugs
+    }
+
+    @Override
+    public void handleResourcePackResponse(final net.minecraft.network.protocol.common.ServerboundResourcePackPacket packet) {
+        super.handleResourcePackResponse(packet);
+        // Paper start - store last pack status
+        org.bukkit.event.player.PlayerResourcePackStatusEvent.Status packStatus = org.bukkit.event.player.PlayerResourcePackStatusEvent.Status.values()[packet.action().ordinal()];
+        this.player.getBukkitEntity().resourcePackStatus = packStatus;
+        this.cserver.getPluginManager().callEvent(new org.bukkit.event.player.PlayerResourcePackStatusEvent(this.getCraftPlayer(), packet.id(), packStatus)); // CraftBukkit
+        // Paper end - store last pack status
+    }
+
+    // Paper start - adventure
+    public void disconnect(final net.kyori.adventure.text.Component reason) {
+        this.disconnect(reason, org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN);
+    }
+
+    public void disconnect(final net.kyori.adventure.text.Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
+        this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(reason), io.papermc.paper.connection.DisconnectionReason.game(cause));
+    }
+
+    public void disconnect(final Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
+        this.disconnect(reason, io.papermc.paper.connection.DisconnectionReason.game(cause));
+    }
+
+    public void disconnectAsync(net.kyori.adventure.text.Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
+        this.disconnectAsync(io.papermc.paper.adventure.PaperAdventure.asVanilla(reason), cause);
+    }
+
+    public void disconnectAsync(Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
+        this.disconnectAsync(reason, io.papermc.paper.connection.DisconnectionReason.game(cause));
+    }
+
+    @Override
+    public io.papermc.paper.connection.PaperPlayerGameConnection paperConnection() {
+        return this.playerGameConnection;
+    }
+    // Paper end
 }
