diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 862e9c0be..7bd9a19d0 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,10 +1,10 @@
 blank_issues_enabled: false
 contact_links:
   - name: Common Issues
-    url: https://github.com/GeyserMC/Geyser/wiki/Common-Issues
+    url: https://wiki.geysermc.org/geyser/common-issues
     about: Check the common issues to see if you are not alone with that issue and see how you can fix them.
   - name: Frequently Asked Questions
-    url: https://github.com/GeyserMC/Geyser/wiki/FAQ
+    url: https://wiki.geysermc.org/geyser/faq
     about: Look at the FAQ page for answers to frequently asked questions.
   - name: Get help on the GeyserMC Discord server
     url: https://discord.gg/geysermc
diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml
index f5bb4c042..9d925c4dc 100644
--- a/.github/workflows/pullrequest.yml
+++ b/.github/workflows/pullrequest.yml
@@ -9,11 +9,11 @@ jobs:
 
     steps:
       - uses: actions/checkout@v2
-      - name: Set up JDK 16
+      - name: Set up JDK 17
         uses: actions/setup-java@v1
         with:
           distribution: 'temurin'
-          java-version: 16
+          java-version: 17
           cache: 'gradle'
       - name: submodules-init
         uses: snickerbockers/submodules-init@v4
diff --git a/Jenkinsfile b/Jenkinsfile
index f92778318..b3df4bc95 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -2,7 +2,7 @@ pipeline {
     agent any
     tools {
         gradle 'Gradle 7'
-        jdk 'Java 16'
+        jdk 'Java 17'
     }
     options {
         buildDiscarder(logRotator(artifactNumToKeepStr: '20'))
diff --git a/README.md b/README.md
index aba8babf2..796170dfd 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t
 
 Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have joined us here!
 
-### Currently supporting Minecraft Bedrock 1.19 and Minecraft Java 1.19.0.
+### Currently supporting Minecraft Bedrock 1.19.0 - 1.19.10 and Minecraft Java 1.19.0.
 
 ## Setting Up
 Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Geyser.
diff --git a/bootstrap/spigot/build.gradle.kts b/bootstrap/spigot/build.gradle.kts
index 8e2b73cd1..02883999d 100644
--- a/bootstrap/spigot/build.gradle.kts
+++ b/bootstrap/spigot/build.gradle.kts
@@ -1,6 +1,6 @@
-val paperVersion = "1.17.1-R0.1-SNAPSHOT" // Needed because we do not support Java 17 yet
+val paperVersion = "1.19-R0.1-SNAPSHOT"
 val viaVersion = "4.0.0"
-val adaptersVersion = "1.4-SNAPSHOT"
+val adaptersVersion = "1.5-SNAPSHOT"
 val commodoreVersion = "1.13"
 
 dependencies {
@@ -9,6 +9,18 @@ dependencies {
     implementation("org.geysermc.geyser.adapters", "spigot-all", adaptersVersion)
 
     implementation("me.lucko", "commodore", commodoreVersion)
+    
+    // Both paper-api and paper-mojangapi only provide Java 17 versions for 1.19
+    compileOnly("io.papermc.paper", "paper-api", paperVersion) {
+        attributes {
+            attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
+        }
+    }
+    compileOnly("io.papermc.paper", "paper-mojangapi", paperVersion) {
+        attributes {
+            attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
+        }
+    }
 }
 
 platformRelocate("it.unimi.dsi.fastutil")
@@ -19,8 +31,6 @@ platformRelocate("me.lucko.commodore")
 platformRelocate("io.netty.channel.kqueue")
 
 // These dependencies are already present on the platform
-provided("io.papermc.paper", "paper-api", paperVersion)
-provided("io.papermc.paper", "paper-mojangapi", paperVersion)
 provided("com.viaversion", "viaversion", viaVersion)
 
 application {
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java
index 8d0641599..36dd81d44 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java
@@ -35,6 +35,7 @@ import org.geysermc.geyser.ping.IGeyserPingPassthrough;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import java.lang.reflect.Constructor;
 import java.net.InetSocketAddress;
 
 /**
@@ -42,6 +43,8 @@ import java.net.InetSocketAddress;
  * applied.
  */
 public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough {
+    private static final Constructor<PaperServerListPingEvent> OLD_CONSTRUCTOR = ReflectedNames.getOldPaperPingConstructor();
+
     private final GeyserSpigotLogger logger;
 
     public GeyserPaperPingPassthrough(GeyserSpigotLogger logger) {
@@ -54,9 +57,17 @@ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough
         try {
             // We'd rather *not* use deprecations here, but unfortunately any Adventure class would be relocated at
             // runtime because we still have to shade in our own Adventure class. For now.
-            PaperServerListPingEvent event = new PaperServerListPingEvent(new GeyserStatusClient(inetSocketAddress),
-                    Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(), Bukkit.getMaxPlayers(), Bukkit.getVersion(),
-                    GameProtocol.getJavaProtocolVersion(), null);
+            PaperServerListPingEvent event;
+            if (OLD_CONSTRUCTOR != null) {
+                // Approximately pre-1.19
+                event = OLD_CONSTRUCTOR.newInstance(new GeyserStatusClient(inetSocketAddress),
+                        Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(),
+                        Bukkit.getMaxPlayers(), Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion(), null);
+            } else {
+                event = new PaperServerListPingEvent(new GeyserStatusClient(inetSocketAddress),
+                        Bukkit.getMotd(), Bukkit.shouldSendChatPreviews(), Bukkit.getOnlinePlayers().size(),
+                        Bukkit.getMaxPlayers(), Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion(), null);
+            }
             Bukkit.getPluginManager().callEvent(event);
             if (event.isCancelled()) {
                 // We have to send a ping, so not really sure what else to do here.
@@ -80,7 +91,7 @@ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough
             }
 
             return geyserPingInfo;
-        } catch (Exception e) {
+        } catch (Exception | LinkageError e) { // LinkageError in the event that method/constructor signatures change
             logger.debug("Error while getting Paper ping passthrough: " + e);
             return null;
         }
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java
index eb328735d..634d1f8a8 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java
@@ -56,7 +56,7 @@ public class GeyserSpigotPingPassthrough implements IGeyserPingPassthrough {
             );
             Bukkit.getOnlinePlayers().stream().map(Player::getName).forEach(geyserPingInfo.getPlayerList()::add);
             return geyserPingInfo;
-        } catch (Exception e) {
+        } catch (Exception | LinkageError e) { // LinkageError in the event that method/constructor signatures change
             logger.debug("Error while getting Bukkit ping passthrough: " + e);
             return null;
         }
@@ -66,7 +66,7 @@ public class GeyserSpigotPingPassthrough implements IGeyserPingPassthrough {
     private static class GeyserPingEvent extends ServerListPingEvent {
 
         public GeyserPingEvent(InetAddress address, String motd, int numPlayers, int maxPlayers) {
-            super(address, motd, numPlayers, maxPlayers);
+            super(address, motd, Bukkit.shouldSendChatPreviews(), numPlayers, maxPlayers);
         }
 
         @Override
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java
index fed5dd6b9..20f4305dd 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java
@@ -168,14 +168,16 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
         if (geyserConfig.isLegacyPingPassthrough()) {
             this.geyserSpigotPingPassthrough = GeyserLegacyPingPassthrough.init(geyser);
         } else {
-            try {
-                Class.forName("com.destroystokyo.paper.event.server.PaperServerListPingEvent");
+            if (ReflectedNames.checkPaperPingEvent()) {
                 this.geyserSpigotPingPassthrough = new GeyserPaperPingPassthrough(geyserLogger);
-            } catch (ClassNotFoundException e) {
+            } else if (ReflectedNames.newSpigotPingConstructorExists()) {
                 this.geyserSpigotPingPassthrough = new GeyserSpigotPingPassthrough(geyserLogger);
+            } else {
+                // Can't enable one of the other options
+                this.geyserSpigotPingPassthrough = GeyserLegacyPingPassthrough.init(geyser);
             }
         }
-        geyserLogger.debug("Spigot ping passthrough type: " + (this.geyserSpigotPingPassthrough == null ? null : this.geyserSpigotPingPassthrough.getClass()));
+        geyserLogger.info("Spigot ping passthrough type: " + (this.geyserSpigotPingPassthrough == null ? null : this.geyserSpigotPingPassthrough.getClass()));
 
         this.geyserCommandManager = new GeyserSpigotCommandManager(geyser);
         this.geyserCommandManager.init();
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java
new file mode 100644
index 000000000..3185f2d30
--- /dev/null
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.platform.spigot;
+
+import com.destroystokyo.paper.event.server.PaperServerListPingEvent;
+import com.destroystokyo.paper.network.StatusClient;
+import org.bukkit.event.server.ServerListPingEvent;
+import org.bukkit.util.CachedServerIcon;
+
+import javax.annotation.Nullable;
+import java.lang.reflect.Constructor;
+import java.net.InetAddress;
+
+/**
+ * A utility class for checking on the existence of classes, constructors, fields, methods
+ */
+public final class ReflectedNames {
+
+    static boolean checkPaperPingEvent() {
+        return classExists("com.destroystokyo.paper.event.server.PaperServerListPingEvent");
+    }
+
+    /**
+     * @return if this class name exists
+     */
+    private static boolean classExists(String clazz) {
+        try {
+            Class.forName(clazz);
+            return true;
+        } catch (ClassNotFoundException e) {
+            return false;
+        }
+    }
+
+    static boolean newSpigotPingConstructorExists() {
+        return getConstructor(ServerListPingEvent.class, InetAddress.class, String.class, boolean.class, int.class, int.class) != null;
+    }
+
+    static Constructor<PaperServerListPingEvent> getOldPaperPingConstructor() {
+        if (getConstructor(PaperServerListPingEvent.class, StatusClient.class, String.class, boolean.class, int.class,
+                int.class, String.class, int.class, CachedServerIcon.class) != null) {
+            // @NotNull StatusClient client, @NotNull String motd, boolean shouldSendChatPreviews, int numPlayers, int maxPlayers,
+            //            @NotNull String version, int protocolVersion, @Nullable CachedServerIcon favicon
+            // New constructor is present
+            return null;
+        }
+        // @NotNull StatusClient client, @NotNull String motd, int numPlayers, int maxPlayers,
+        //            @NotNull String version, int protocolVersion, @Nullable CachedServerIcon favicon
+        return getConstructor(PaperServerListPingEvent.class, StatusClient.class, String.class, int.class, int.class,
+                String.class, int.class, CachedServerIcon.class);
+    }
+
+    /**
+     * @return if this class has a constructor with the specified arguments
+     */
+    @Nullable
+    private static <T> Constructor<T> getConstructor(Class<T> clazz, Class<?>... args) {
+        try {
+            return clazz.getConstructor(args);
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+
+    private ReflectedNames() {
+    }
+}
diff --git a/build-logic/src/main/kotlin/Versions.kt b/build-logic/src/main/kotlin/Versions.kt
index 779065bc5..27f7bcaf5 100644
--- a/build-logic/src/main/kotlin/Versions.kt
+++ b/build-logic/src/main/kotlin/Versions.kt
@@ -30,10 +30,12 @@ object Versions {
     const val guavaVersion = "29.0-jre"
     const val nbtVersion = "2.1.0"
     const val websocketVersion = "1.5.1"
-    const val protocolVersion = "977a9a1"
+    const val protocolVersion = "a78a64b"
+    // Not pinned to specific version due to possible gradle bug
+    // See comment in settings.gradle.kts
     const val raknetVersion = "1.6.28-SNAPSHOT"
     const val mcauthlibVersion = "d9d773e"
-    const val mcprotocollibversion = "bb2b414"
+    const val mcprotocollibversion = "54fc9f0"
     const val packetlibVersion = "3.0"
     const val adventureVersion = "4.9.3"
     const val eventVersion = "3.0.0"
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index e82af5687..561c2f554 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -31,7 +31,7 @@ dependencies {
     // Network libraries
     implementation("org.java-websocket", "Java-WebSocket", Versions.websocketVersion)
 
-    api("com.github.CloudburstMC.Protocol", "bedrock-v527", Versions.protocolVersion) {
+    api("com.github.CloudburstMC.Protocol", "bedrock-v534", Versions.protocolVersion) {
         exclude("com.nukkitx.network", "raknet")
         exclude("com.nukkitx", "nbt")
     }
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java
index e60daacd3..bba2e8d21 100644
--- a/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java
@@ -47,7 +47,7 @@ public class OffhandCommand extends GeyserCommand {
         }
 
         ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.SWAP_HANDS, Vector3i.ZERO,
-                Direction.DOWN, session.getNextSequence());
+                Direction.DOWN, session.getWorldCache().nextPredictionSequence());
         session.sendDownstreamPacket(releaseItemPacket);
     }
 
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java
index 75bdd9021..65662bbe4 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java
@@ -99,19 +99,9 @@ public class FishingHookEntity extends ThrowableEntity {
                 }
             }
 
-            int waterLevel = BlockStateValues.getWaterLevel(blockID);
-            if (BlockRegistries.WATERLOGGED.get().contains(blockID)) {
-                waterLevel = 0;
-            }
-            if (waterLevel >= 0) {
-                double waterMaxY = iter.getY() + 1 - (waterLevel + 1) / 9.0;
-                // Falling water is a full block
-                if (waterLevel >= 8) {
-                    waterMaxY = iter.getY() + 1;
-                }
-                if (position.getY() <= waterMaxY) {
-                    touchingWater = true;
-                }
+            double waterHeight = BlockStateValues.getWaterHeight(blockID);
+            if (waterHeight != -1 && position.getY() <= (iter.getY() + waterHeight)) {
+                touchingWater = true;
             }
         }
 
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java
index 04e4727d0..75b2ad991 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java
@@ -51,6 +51,7 @@ public class ArmorStandEntity extends LivingEntity {
     @Getter
     private boolean isMarker = false;
     private boolean isInvisible = false;
+    @Getter
     private boolean isSmall = false;
 
     /**
@@ -74,6 +75,7 @@ public class ArmorStandEntity extends LivingEntity {
      * - No armor, no name: false
      * - No armor, yes name: true
      */
+    @Getter
     private boolean positionRequiresOffset = false;
     /**
      * Whether we should update the position of this armor stand after metadata updates.
@@ -411,6 +413,8 @@ public class ArmorStandEntity extends LivingEntity {
             this.positionRequiresOffset = newValue;
             if (positionRequiresOffset) {
                 this.position = applyOffsetToPosition(position);
+                // Update the passenger offset as armorstand is moving up by roughly 2 blocks
+                updatePassengerOffsets();
             } else {
                 this.position = removeOffsetFromPosition(position);
             }
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java
index e5cbb2f89..52e4a6f2f 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/IronGolemEntity.java
@@ -45,6 +45,9 @@ public class IronGolemEntity extends GolemEntity {
         setFlag(EntityFlag.BRIBED, true);
         // Required, or else the overlay is black
         dirtyMetadata.put(EntityData.COLOR_2, (byte) 0);
+        // Default max health. Ensures correct cracked texture is used
+        // Bug reproducible in 1.19.0 JE vanilla/fabric when spawning a new iron golem
+        maxHealth = 100f;
     }
 
     @Nonnull
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WardenEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WardenEntity.java
index 1ca34037c..ff6eed975 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WardenEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/WardenEntity.java
@@ -41,7 +41,7 @@ import java.util.UUID;
 import java.util.concurrent.ThreadLocalRandom;
 
 public class WardenEntity extends MonsterEntity implements Tickable {
-    private int heartBeatDelay;
+    private int heartBeatDelay = 40;
     private int tickCount;
 
     private int sonicBoomTickDuration;
@@ -50,6 +50,12 @@ public class WardenEntity extends MonsterEntity implements Tickable {
         super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
     }
 
+    @Override
+    protected void initializeMetadata() {
+        super.initializeMetadata();
+        dirtyMetadata.put(EntityData.HEARTBEAT_INTERVAL_TICKS, heartBeatDelay);
+    }
+
     @Override
     public void setPose(Pose pose) {
         setFlag(EntityFlag.DIGGING, pose == Pose.DIGGING);
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java
index 6f2958ffd..8e600b1a8 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java
@@ -35,9 +35,7 @@ import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamColor;
 import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.math.vector.Vector3i;
-import com.nukkitx.protocol.bedrock.data.AttributeData;
-import com.nukkitx.protocol.bedrock.data.GameType;
-import com.nukkitx.protocol.bedrock.data.PlayerPermission;
+import com.nukkitx.protocol.bedrock.data.*;
 import com.nukkitx.protocol.bedrock.data.command.CommandPermission;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
@@ -59,6 +57,7 @@ import org.geysermc.geyser.translator.text.MessageTranslator;
 
 import javax.annotation.Nullable;
 import java.util.Collections;
+import java.util.List;
 import java.util.Optional;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
@@ -66,6 +65,16 @@ import java.util.concurrent.TimeUnit;
 @Getter @Setter
 public class PlayerEntity extends LivingEntity {
     public static final float SNEAKING_POSE_HEIGHT = 1.5f;
+    protected static final List<AbilityLayer> BASE_ABILITY_LAYER;
+
+    static {
+        AbilityLayer abilityLayer = new AbilityLayer();
+        abilityLayer.setLayerType(AbilityLayer.Type.BASE);
+        Ability[] abilities = Ability.values();
+        Collections.addAll(abilityLayer.getAbilitiesSet(), abilities); // Apparently all the abilities you're working with
+        Collections.addAll(abilityLayer.getAbilityValues(), abilities); // Apparently all the abilities the player can work with
+        BASE_ABILITY_LAYER = Collections.singletonList(abilityLayer);
+    }
 
     private String username;
     private boolean playerList = true; // Player is in the player list
@@ -127,6 +136,7 @@ public class PlayerEntity extends LivingEntity {
         addPlayerPacket.setDeviceId("");
         addPlayerPacket.setPlatformChatId("");
         addPlayerPacket.setGameType(GameType.SURVIVAL); //TODO
+        addPlayerPacket.setAbilityLayers(BASE_ABILITY_LAYER); // Recommended to be added since 1.19.10, but only needed here for permissions viewing
         addPlayerPacket.getMetadata().putFlags(flags);
         dirtyMetadata.apply(addPlayerPacket.getMetadata());
 
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java
index 6c15a4d3e..176d171de 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SkullPlayerEntity.java
@@ -81,6 +81,7 @@ public class SkullPlayerEntity extends PlayerEntity {
         addPlayerPacket.setDeviceId("");
         addPlayerPacket.setPlatformChatId("");
         addPlayerPacket.setGameType(GameType.SURVIVAL);
+        addPlayerPacket.setAbilityLayers(BASE_ABILITY_LAYER);
         addPlayerPacket.getMetadata().putFlags(flags);
         dirtyMetadata.apply(addPlayerPacket.getMetadata());
 
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java
index 56b6ee7ac..8f9eb415f 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java
@@ -42,16 +42,21 @@ public class StoredItemMappings {
     private final ItemMapping banner;
     private final ItemMapping barrier;
     private final int bowl;
+    private final int bucket;
     private final int chest;
     private final ItemMapping compass;
     private final ItemMapping crossbow;
     private final ItemMapping enchantedBook;
     private final ItemMapping fishingRod;
     private final int flintAndSteel;
+    private final int frogspawn;
+    private final int goatHorn;
+    private final int glassBottle;
     private final int goldenApple;
     private final int goldIngot;
     private final int ironIngot;
     private final int lead;
+    private final int lilyPad;
     private final ItemMapping milkBucket;
     private final int nameTag;
     private final ItemMapping powderSnowBucket;
@@ -70,16 +75,21 @@ public class StoredItemMappings {
         this.banner = load(itemMappings, "white_banner"); // As of 1.17.10, all banners have the same Bedrock ID
         this.barrier = load(itemMappings, "barrier");
         this.bowl = load(itemMappings, "bowl").getJavaId();
+        this.bucket = load(itemMappings, "bucket").getBedrockId();
         this.chest = load(itemMappings, "chest").getJavaId();
         this.compass = load(itemMappings, "compass");
         this.crossbow = load(itemMappings, "crossbow");
         this.enchantedBook = load(itemMappings, "enchanted_book");
         this.fishingRod = load(itemMappings, "fishing_rod");
         this.flintAndSteel = load(itemMappings, "flint_and_steel").getJavaId();
+        this.frogspawn = load(itemMappings, "frogspawn").getBedrockId();
+        this.goatHorn = load(itemMappings, "goat_horn").getJavaId();
+        this.glassBottle = load(itemMappings, "glass_bottle").getBedrockId();
         this.goldenApple = load(itemMappings, "golden_apple").getJavaId();
         this.goldIngot = load(itemMappings, "gold_ingot").getJavaId();
         this.ironIngot = load(itemMappings, "iron_ingot").getJavaId();
         this.lead = load(itemMappings, "lead").getJavaId();
+        this.lilyPad = load(itemMappings, "lily_pad").getBedrockId();
         this.milkBucket = load(itemMappings, "milk_bucket");
         this.nameTag = load(itemMappings, "name_tag").getJavaId();
         this.powderSnowBucket = load(itemMappings, "powder_snow_bucket");
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java b/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java
index a8d5859dc..58cbce77f 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java
+++ b/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java
@@ -44,6 +44,7 @@ import java.util.Locale;
  * Used for block entities if the Java block state contains Bedrock block information.
  */
 public final class BlockStateValues {
+    private static final IntSet ALL_CAULDRONS = new IntOpenHashSet();
     private static final Int2IntMap BANNER_COLORS = new FixedInt2IntMap();
     private static final Int2ByteMap BED_COLORS = new FixedInt2ByteMap();
     private static final Int2ByteMap COMMAND_BLOCK_VALUES = new Int2ByteOpenHashMap();
@@ -76,6 +77,8 @@ public final class BlockStateValues {
     public static int JAVA_SPAWNER_ID;
     public static int JAVA_WATER_ID;
 
+    public static final int NUM_WATER_LEVELS = 9;
+
     /**
      * Determines if the block state contains Bedrock block information
      *
@@ -193,6 +196,9 @@ public final class BlockStateValues {
             return;
         }
 
+        if (javaId.contains("cauldron")) {
+            ALL_CAULDRONS.add(javaBlockState);
+        }
         if (javaId.contains("_cauldron") && !javaId.contains("water_")) {
              NON_WATER_CAULDRONS.add(javaBlockState);
         }
@@ -225,10 +231,19 @@ public final class BlockStateValues {
      *
      * @return if this Java block state is a non-empty non-water cauldron
      */
-    public static boolean isCauldron(int state) {
+    public static boolean isNonWaterCauldron(int state) {
         return NON_WATER_CAULDRONS.contains(state);
     }
 
+    /**
+     * When using a bucket on a cauldron sending a ServerboundUseItemPacket can result in the liquid being placed.
+     *
+     * @return if this Java block state is a cauldron
+     */
+    public static boolean isCauldron(int state) {
+        return ALL_CAULDRONS.contains(state);
+    }
+
     /**
      * The block state in Java and Bedrock both contain the conditional bit, however command block block entity tags
      * in Bedrock need the conditional information.
@@ -436,7 +451,6 @@ public final class BlockStateValues {
 
     /**
      * Get the level of water from the block state.
-     * This is used in FishingHookEntity to create splash sounds when the hook hits the water.
      *
      * @param state BlockState of the block
      * @return The water level or -1 if the block isn't water
@@ -445,6 +459,30 @@ public final class BlockStateValues {
         return WATER_LEVEL.getOrDefault(state, -1);
     }
 
+    /**
+     * Get the height of water from the block state
+     * This is used in FishingHookEntity to create splash sounds when the hook hits the water. In addition,
+     * CollisionManager uses this to determine if the player's eyes are in water.
+     *
+     * @param state BlockState of the block
+     * @return The water height or -1 if the block does not contain water
+     */
+    public static double getWaterHeight(int state) {
+        int waterLevel = BlockStateValues.getWaterLevel(state);
+        if (BlockRegistries.WATERLOGGED.get().contains(state)) {
+            waterLevel = 0;
+        }
+        if (waterLevel >= 0) {
+            double waterHeight = 1 - (waterLevel + 1) / ((double) NUM_WATER_LEVELS);
+            // Falling water is a full block
+            if (waterLevel >= 8) {
+                waterHeight = 1;
+            }
+            return waterHeight;
+        }
+        return -1;
+    }
+
     /**
      * Get the slipperiness of a block.
      * This is used in ItemEntity to calculate the friction on an item as it slides across the ground
diff --git a/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java b/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java
index 2b38e4ed4..2a830cd70 100644
--- a/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java
+++ b/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java
@@ -25,6 +25,7 @@
 
 package org.geysermc.geyser.level.physics;
 
+import com.nukkitx.math.GenericMath;
 import com.nukkitx.math.vector.Vector3d;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.math.vector.Vector3i;
@@ -405,6 +406,18 @@ public class CollisionManager {
         return session.getGeyser().getWorldManager().getBlockAt(session, session.getPlayerEntity().getPosition().toInt()) == BlockStateValues.JAVA_WATER_ID;
     }
 
+    public boolean isWaterInEyes() {
+        double eyeX = playerBoundingBox.getMiddleX();
+        double eyeY = playerBoundingBox.getMiddleY() - playerBoundingBox.getSizeY() / 2d + session.getEyeHeight();
+        double eyeZ = playerBoundingBox.getMiddleZ();
+
+        eyeY -= 1 / ((double) BlockStateValues.NUM_WATER_LEVELS); // Subtract the height of one water layer
+        int blockID = session.getGeyser().getWorldManager().getBlockAt(session, GenericMath.floor(eyeX), GenericMath.floor(eyeY), GenericMath.floor(eyeZ));
+        double waterHeight = BlockStateValues.getWaterHeight(blockID);
+
+        return waterHeight != -1 && eyeY < (Math.floor(eyeY) + waterHeight);
+    }
+
     /**
      * Updates scaffolding entity flags
      * Scaffolding needs to be checked per-move since it's a flag in Bedrock but Java does it client-side
diff --git a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java
index 33f2a8dc0..1d7ceaa00 100644
--- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java
+++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java
@@ -29,6 +29,8 @@ import com.github.steveice10.mc.protocol.codec.MinecraftCodec;
 import com.github.steveice10.mc.protocol.codec.PacketCodec;
 import com.nukkitx.protocol.bedrock.BedrockPacketCodec;
 import com.nukkitx.protocol.bedrock.v527.Bedrock_v527;
+import com.nukkitx.protocol.bedrock.v534.Bedrock_v534;
+import org.geysermc.geyser.session.GeyserSession;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -43,7 +45,7 @@ public final class GameProtocol {
      * Default Bedrock codec that should act as a fallback. Should represent the latest available
      * release of the game that Geyser supports.
      */
-    public static final BedrockPacketCodec DEFAULT_BEDROCK_CODEC = Bedrock_v527.V527_CODEC;
+    public static final BedrockPacketCodec DEFAULT_BEDROCK_CODEC = Bedrock_v534.V534_CODEC;
     /**
      * A list of all supported Bedrock versions that can join Geyser
      */
@@ -56,9 +58,10 @@ public final class GameProtocol {
     private static final PacketCodec DEFAULT_JAVA_CODEC = MinecraftCodec.CODEC;
 
     static {
-        SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC.toBuilder()
-                .minecraftVersion("1.19.0")
+        SUPPORTED_BEDROCK_CODECS.add(Bedrock_v527.V527_CODEC.toBuilder()
+                .minecraftVersion("1.19.0/1.19.2")
                 .build());
+        SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC);
     }
 
     /**
@@ -75,6 +78,12 @@ public final class GameProtocol {
         return null;
     }
 
+    /* Bedrock convenience methods to gatekeep features and easily remove the check on version removal */
+
+    public static boolean supports1_19_10(GeyserSession session) {
+        return session.getUpstream().getProtocolVersion() >= Bedrock_v534.V534_CODEC.getProtocolVersion();
+    }
+
     /**
      * Gets the {@link PacketCodec} for Minecraft: Java Edition.
      *
diff --git a/core/src/main/java/org/geysermc/geyser/network/QueryPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/QueryPacketHandler.java
index f11851c1b..15a79dc76 100644
--- a/core/src/main/java/org/geysermc/geyser/network/QueryPacketHandler.java
+++ b/core/src/main/java/org/geysermc/geyser/network/QueryPacketHandler.java
@@ -91,8 +91,10 @@ public class QueryPacketHandler {
         switch (type) {
             case HANDSHAKE:
                 sendToken();
+                break;
             case STATISTICS:
                 sendQueryData();
+                break;
         }
     }
 
diff --git a/core/src/main/java/org/geysermc/geyser/ping/GeyserLegacyPingPassthrough.java b/core/src/main/java/org/geysermc/geyser/ping/GeyserLegacyPingPassthrough.java
index c3a242501..199e13918 100644
--- a/core/src/main/java/org/geysermc/geyser/ping/GeyserLegacyPingPassthrough.java
+++ b/core/src/main/java/org/geysermc/geyser/ping/GeyserLegacyPingPassthrough.java
@@ -28,6 +28,9 @@ package org.geysermc.geyser.ping;
 import com.fasterxml.jackson.core.JsonParseException;
 import com.fasterxml.jackson.databind.JsonMappingException;
 import com.nukkitx.nbt.util.VarInts;
+import io.netty.handler.codec.haproxy.HAProxyCommand;
+import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol;
+import io.netty.util.NetUtil;
 import org.geysermc.geyser.GeyserImpl;
 import org.geysermc.geyser.network.GameProtocol;
 
@@ -35,13 +38,12 @@ import java.io.ByteArrayOutputStream;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.IOException;
-import java.net.ConnectException;
-import java.net.InetSocketAddress;
-import java.net.Socket;
-import java.net.SocketTimeoutException;
+import java.net.*;
 import java.util.concurrent.TimeUnit;
 
 public class GeyserLegacyPingPassthrough implements IGeyserPingPassthrough, Runnable {
+    private static final byte[] HAPROXY_BINARY_PREFIX = new byte[]{13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10};
+
     private final GeyserImpl geyser;
 
     public GeyserLegacyPingPassthrough(GeyserImpl geyser) {
@@ -74,54 +76,68 @@ public class GeyserLegacyPingPassthrough implements IGeyserPingPassthrough, Runn
 
     @Override
     public void run() {
-        try {
-            Socket socket = new Socket();
+        try (Socket socket = new Socket()) {
             String address = geyser.getConfig().getRemote().getAddress();
             int port = geyser.getConfig().getRemote().getPort();
             socket.connect(new InetSocketAddress(address, port), 5000);
 
             ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
-            DataOutputStream handshake = new DataOutputStream(byteArrayStream);
-            handshake.write(0x0);
-            VarInts.writeUnsignedInt(handshake, GameProtocol.getJavaProtocolVersion());
-            VarInts.writeUnsignedInt(handshake, address.length());
-            handshake.writeBytes(address);
-            handshake.writeShort(port);
-            VarInts.writeUnsignedInt(handshake, 1);
+            try (DataOutputStream handshake = new DataOutputStream(byteArrayStream)) {
+                handshake.write(0x0);
+                VarInts.writeUnsignedInt(handshake, GameProtocol.getJavaProtocolVersion());
+                VarInts.writeUnsignedInt(handshake, address.length());
+                handshake.writeBytes(address);
+                handshake.writeShort(port);
+                VarInts.writeUnsignedInt(handshake, 1);
+            }
 
-            DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
-            VarInts.writeUnsignedInt(dataOutputStream, byteArrayStream.size());
-            dataOutputStream.write(byteArrayStream.toByteArray());
-            dataOutputStream.writeByte(0x01);
-            dataOutputStream.writeByte(0x00);
+            byte[] buffer;
 
-            DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
-            VarInts.readUnsignedInt(dataInputStream);
-            VarInts.readUnsignedInt(dataInputStream);
-            int length = VarInts.readUnsignedInt(dataInputStream);
-            byte[] buffer = new byte[length];
-            dataInputStream.readFully(buffer);
-            dataOutputStream.writeByte(0x09);
-            dataOutputStream.writeByte(0x01);
-            dataOutputStream.writeLong(System.currentTimeMillis());
+            try (DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream())) {
+                if (geyser.getConfig().getRemote().isUseProxyProtocol()) {
+                    // HAProxy support
+                    // Based on https://github.com/netty/netty/blob/d8ad931488f6b942dabe28ecd6c399b4438da0a8/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessageEncoder.java#L78
+                    dataOutputStream.write(HAPROXY_BINARY_PREFIX);
+                    dataOutputStream.writeByte((0x02 << 4) | HAProxyCommand.PROXY.byteValue());
+                    dataOutputStream.writeByte(socket.getLocalAddress() instanceof Inet4Address ?
+                            HAProxyProxiedProtocol.TCP4.byteValue() : HAProxyProxiedProtocol.TCP6.byteValue());
+                    byte[] srcAddrBytes = NetUtil.createByteArrayFromIpAddressString(
+                            ((InetSocketAddress) socket.getLocalSocketAddress()).getAddress().getHostAddress());
+                    byte[] dstAddrBytes = NetUtil.createByteArrayFromIpAddressString(address);
+                    dataOutputStream.writeShort(srcAddrBytes.length + dstAddrBytes.length + 4);
+                    dataOutputStream.write(srcAddrBytes);
+                    dataOutputStream.write(dstAddrBytes);
+                    dataOutputStream.writeShort(((InetSocketAddress) socket.getLocalSocketAddress()).getPort());
+                    dataOutputStream.writeShort(port);
+                }
 
-            VarInts.readUnsignedInt(dataInputStream);
-            String json = new String(buffer);
+                VarInts.writeUnsignedInt(dataOutputStream, byteArrayStream.size());
+                dataOutputStream.write(byteArrayStream.toByteArray());
+                dataOutputStream.writeByte(0x01);
+                dataOutputStream.writeByte(0x00);
 
-            this.pingInfo = GeyserImpl.JSON_MAPPER.readValue(json, GeyserPingInfo.class);
+                try (DataInputStream dataInputStream = new DataInputStream(socket.getInputStream())) {
+                    VarInts.readUnsignedInt(dataInputStream);
+                    VarInts.readUnsignedInt(dataInputStream);
+                    int length = VarInts.readUnsignedInt(dataInputStream);
+                    buffer = new byte[length];
+                    dataInputStream.readFully(buffer);
+                    dataOutputStream.writeByte(0x09);
+                    dataOutputStream.writeByte(0x01);
+                    dataOutputStream.writeLong(System.currentTimeMillis());
 
-            byteArrayStream.close();
-            handshake.close();
-            dataOutputStream.close();
-            dataInputStream.close();
-            socket.close();
+                    VarInts.readUnsignedInt(dataInputStream);
+                }
+            }
+
+            this.pingInfo = GeyserImpl.JSON_MAPPER.readValue(buffer, GeyserPingInfo.class);
         } catch (SocketTimeoutException | ConnectException ex) {
             this.pingInfo = null;
             this.geyser.getLogger().debug("Connection timeout for ping passthrough.");
         } catch (JsonParseException | JsonMappingException ex) {
             this.geyser.getLogger().error("Failed to parse json when pinging server!", ex);
         } catch (IOException e) {
-            e.printStackTrace();
+            this.geyser.getLogger().error("IO error while trying to use legacy ping passthrough", e);
         }
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java b/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java
index 609647b2d..586e7d08b 100644
--- a/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java
+++ b/core/src/main/java/org/geysermc/geyser/registry/BlockRegistries.java
@@ -72,6 +72,16 @@ public class BlockRegistries {
      */
     public static final SimpleRegistry<IntSet> WATERLOGGED = SimpleRegistry.create(RegistryLoaders.empty(IntOpenHashSet::new));
 
+    /**
+     * A registry containing all blockstates which are always interactive.
+     */
+    public static final SimpleRegistry<IntSet> INTERACTIVE = SimpleRegistry.create(RegistryLoaders.empty(IntOpenHashSet::new));
+
+    /**
+     * A registry containing all blockstates which are interactive if the player has the may build permission.
+     */
+    public static final SimpleRegistry<IntSet> INTERACTIVE_MAY_BUILD = SimpleRegistry.create(RegistryLoaders.empty(IntOpenHashSet::new));
+
     static {
         BlockRegistryPopulator.populate();
     }
diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java
index 25528a919..53c3e2310 100644
--- a/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java
+++ b/core/src/main/java/org/geysermc/geyser/registry/populator/BlockRegistryPopulator.java
@@ -26,6 +26,7 @@
 package org.geysermc.geyser.registry.populator;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.google.common.collect.ImmutableMap;
 import com.nukkitx.nbt.*;
 import com.nukkitx.protocol.bedrock.v527.Bedrock_v527;
@@ -355,6 +356,24 @@ public class BlockRegistryPopulator {
         BlockRegistries.CLEAN_JAVA_IDENTIFIERS.set(cleanIdentifiers.toArray(new String[0]));
 
         BLOCKS_JSON = blocksJson;
+
+        JsonNode blockInteractionsJson;
+        try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResource("mappings/interactions.json")) {
+            blockInteractionsJson = GeyserImpl.JSON_MAPPER.readTree(stream);
+        } catch (Exception e) {
+            throw new AssertionError("Unable to load Java block interaction mappings", e);
+        }
+
+        BlockRegistries.INTERACTIVE.set(toBlockStateSet((ArrayNode) blockInteractionsJson.get("always_consumes")));
+        BlockRegistries.INTERACTIVE_MAY_BUILD.set(toBlockStateSet((ArrayNode) blockInteractionsJson.get("requires_may_build")));
+    }
+
+    private static IntSet toBlockStateSet(ArrayNode node) {
+        IntSet blockStateSet = new IntOpenHashSet(node.size());
+        for (JsonNode javaIdentifier : node) {
+            blockStateSet.add(BlockRegistries.JAVA_IDENTIFIERS.get().getInt(javaIdentifier.textValue()));
+        }
+        return blockStateSet;
     }
 
     private static NbtMap buildBedrockState(JsonNode node, int blockStateVersion, BiFunction<String, NbtMapBuilder, String> statesMapper) {
diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java
index 22669fd79..9d6564fce 100644
--- a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java
+++ b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java
@@ -235,6 +235,9 @@ public class ItemRegistryPopulator {
                 } else if (identifier.equals("minecraft:empty_map") && damage == 2) {
                     // Bedrock-only as its own item
                     continue;
+                } else if (identifier.equals("minecraft:bordure_indented_banner_pattern") || identifier.equals("minecraft:field_masoned_banner_pattern")) {
+                    // Bedrock-only banner patterns
+                    continue;
                 }
                 StartGamePacket.ItemEntry entry = entries.get(identifier);
                 int id = -1;
diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java b/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java
index c4e967dff..ce7ac0b07 100644
--- a/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java
+++ b/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java
@@ -136,9 +136,9 @@ public class ItemMappings {
                     }
                 } else {
                     if (!(mapping.getBedrockData() == data.getDamage() ||
-                            // Make exceptions for potions, tipped arrows, and firework stars, whose damage values can vary
+                            // Make exceptions for potions, tipped arrows, firework stars, and goat horns, whose damage values can vary
                             (mapping.getJavaIdentifier().endsWith("potion") || mapping.getJavaIdentifier().equals("minecraft:arrow")
-                                    || mapping.getJavaIdentifier().equals("minecraft:firework_star")))) {
+                                    || mapping.getJavaIdentifier().equals("minecraft:firework_star") || mapping.getJavaIdentifier().equals("minecraft:goat_horn")))) {
                         continue;
                     }
                 }
diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
index 99e29dd21..334549a50 100644
--- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
+++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
@@ -80,6 +80,8 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
 import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
 import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
 import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
 import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
 import lombok.AccessLevel;
@@ -101,6 +103,7 @@ import org.geysermc.geyser.api.network.AuthType;
 import org.geysermc.geyser.api.network.RemoteServer;
 import org.geysermc.geyser.command.GeyserCommandSource;
 import org.geysermc.geyser.configuration.EmoteOffhandWorkaroundOption;
+import org.geysermc.geyser.entity.EntityDefinitions;
 import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
 import org.geysermc.geyser.entity.type.Entity;
 import org.geysermc.geyser.entity.type.ItemFrameEntity;
@@ -113,6 +116,7 @@ import org.geysermc.geyser.inventory.recipe.GeyserStonecutterData;
 import org.geysermc.geyser.level.JavaDimension;
 import org.geysermc.geyser.level.WorldManager;
 import org.geysermc.geyser.level.physics.CollisionManager;
+import org.geysermc.geyser.network.GameProtocol;
 import org.geysermc.geyser.network.netty.LocalSession;
 import org.geysermc.geyser.registry.Registries;
 import org.geysermc.geyser.registry.type.BlockMappings;
@@ -386,7 +390,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
      * Whether to work around 1.13's different behavior in villager trading menus.
      */
     @Setter
-    private boolean emulatePost1_14Logic = true;
+    private boolean emulatePost1_13Logic = true;
     /**
      * Starting in 1.17, Java servers expect the <code>carriedItem</code> parameter of the serverbound click container
      * packet to be the current contents of the mouse after the transaction has been done. 1.16 expects the clicked slot
@@ -395,6 +399,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
      */
     @Setter
     private boolean emulatePost1_16Logic = true;
+    @Setter
+    private boolean emulatePost1_18Logic = true;
 
     /**
      * The current attack speed of the player. Used for sending proper cooldown timings.
@@ -428,11 +434,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
     private long lastInteractionTime;
 
     /**
-     * Stores a future interaction to place a bucket. Will be cancelled if the client instead intended to
-     * interact with a block.
+     * Stores whether the player intended to place a bucket.
      */
     @Setter
-    private ScheduledFuture<?> bucketScheduledFuture;
+    private boolean placedBucket;
 
     /**
      * Used to send a movement packet every three seconds if the player hasn't moved. Prevents timeouts when AFK in certain instances.
@@ -480,6 +485,11 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
     @Setter
     private boolean instabuild = false;
 
+    @Setter
+    private float flySpeed;
+    @Setter
+    private float walkSpeed;
+
     /**
      * Caches current rain status.
      */
@@ -496,7 +506,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
      * Stores a map of all statistics sent from the server.
      * The server only sends new statistics back to us, so in order to show all statistics we need to cache existing ones.
      */
-    private final Map<Statistic, Integer> statistics = new HashMap<>();
+    private final Object2IntMap<Statistic> statistics = new Object2IntOpenHashMap<>(0);
 
     /**
      * Whether we're expecting statistics to be sent back to us.
@@ -519,6 +529,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
      */
     private ScheduledFuture<?> tickThread = null;
 
+    /**
+     * Used to return the player to their original rotation after using an item in BedrockInventoryTransactionTranslator
+     */
+    @Setter
+    private ScheduledFuture<?> lookBackScheduledFuture = null;
+
     private MinecraftProtocol protocol;
 
     public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop eventLoop) {
@@ -1265,9 +1281,9 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
 
         ServerboundUseItemPacket useItemPacket;
         if (playerInventory.getItemInHand().getJavaId() == shield.getJavaId()) {
-            useItemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, getNextSequence());
+            useItemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, worldCache.nextPredictionSequence());
         } else if (playerInventory.getOffhand().getJavaId() == shield.getJavaId()) {
-            useItemPacket = new ServerboundUseItemPacket(Hand.OFF_HAND, getNextSequence());
+            useItemPacket = new ServerboundUseItemPacket(Hand.OFF_HAND, worldCache.nextPredictionSequence());
         } else {
             // No blocking
             return false;
@@ -1296,7 +1312,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
     private boolean disableBlocking() {
         if (playerEntity.getFlag(EntityFlag.BLOCKING)) {
             ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM,
-                    Vector3i.ZERO, Direction.DOWN, getNextSequence());
+                    Vector3i.ZERO, Direction.DOWN, worldCache.nextPredictionSequence());
             sendDownstreamPacket(releaseItemPacket);
             playerEntity.setFlag(EntityFlag.BLOCKING, false);
             return true;
@@ -1610,23 +1626,83 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
         return geyser.getWorldManager().hasPermission(this, permission);
     }
 
+    private static final Ability[] USED_ABILITIES = Ability.values();
+
     /**
      * Send an AdventureSettingsPacket to the client with the latest flags
      */
     public void sendAdventureSettings() {
-        AdventureSettingsPacket adventureSettingsPacket = new AdventureSettingsPacket();
-        adventureSettingsPacket.setUniqueEntityId(playerEntity.getGeyserId());
+        long bedrockId = playerEntity.getGeyserId();
         // Set command permission if OP permission level is high enough
         // This allows mobile players access to a GUI for doing commands. The commands there do not change above OPERATOR
         // and all commands there are accessible with OP permission level 2
-        adventureSettingsPacket.setCommandPermission(opPermissionLevel >= 2 ? CommandPermission.OPERATOR : CommandPermission.NORMAL);
+        CommandPermission commandPermission = opPermissionLevel >= 2 ? CommandPermission.OPERATOR : CommandPermission.NORMAL;
         // Required to make command blocks destroyable
-        adventureSettingsPacket.setPlayerPermission(opPermissionLevel >= 2 ? PlayerPermission.OPERATOR : PlayerPermission.MEMBER);
+        PlayerPermission playerPermission = opPermissionLevel >= 2 ? PlayerPermission.OPERATOR : PlayerPermission.MEMBER;
 
         // Update the noClip and worldImmutable values based on the current gamemode
         boolean spectator = gameMode == GameMode.SPECTATOR;
         boolean worldImmutable = gameMode == GameMode.ADVENTURE || spectator;
 
+        if (GameProtocol.supports1_19_10(this)) {
+            UpdateAdventureSettingsPacket adventureSettingsPacket = new UpdateAdventureSettingsPacket();
+            adventureSettingsPacket.setNoMvP(false);
+            adventureSettingsPacket.setNoPvM(false);
+            adventureSettingsPacket.setImmutableWorld(worldImmutable);
+            adventureSettingsPacket.setShowNameTags(false);
+            adventureSettingsPacket.setAutoJump(true);
+            sendUpstreamPacket(adventureSettingsPacket);
+
+            UpdateAbilitiesPacket updateAbilitiesPacket = new UpdateAbilitiesPacket();
+            updateAbilitiesPacket.setUniqueEntityId(bedrockId);
+            updateAbilitiesPacket.setCommandPermission(commandPermission);
+            updateAbilitiesPacket.setPlayerPermission(playerPermission);
+
+            AbilityLayer abilityLayer = new AbilityLayer();
+            Set<Ability> abilities = abilityLayer.getAbilityValues();
+            if (canFly || spectator) {
+                abilities.add(Ability.MAY_FLY);
+            }
+
+            // Default stuff we have to fill in
+            abilities.add(Ability.BUILD);
+            abilities.add(Ability.MINE);
+            // Needed so you can drop items
+            abilities.add(Ability.DOORS_AND_SWITCHES);
+            if (gameMode == GameMode.CREATIVE) {
+                // Needed so the client doesn't attempt to take away items
+                abilities.add(Ability.INSTABUILD);
+            }
+
+            if (flying || spectator) {
+                if (spectator && !flying) {
+                    // We're "flying locked" in this gamemode
+                    flying = true;
+                    ServerboundPlayerAbilitiesPacket abilitiesPacket = new ServerboundPlayerAbilitiesPacket(true);
+                    sendDownstreamPacket(abilitiesPacket);
+                }
+                abilities.add(Ability.FLYING);
+            }
+
+            if (spectator) {
+                abilities.add(Ability.NO_CLIP);
+            }
+
+            abilityLayer.setLayerType(AbilityLayer.Type.BASE);
+            abilityLayer.setFlySpeed(flySpeed);
+            abilityLayer.setWalkSpeed(walkSpeed);
+            Collections.addAll(abilityLayer.getAbilitiesSet(), USED_ABILITIES);
+
+            updateAbilitiesPacket.getAbilityLayers().add(abilityLayer);
+            sendUpstreamPacket(updateAbilitiesPacket);
+            return;
+        }
+
+        AdventureSettingsPacket adventureSettingsPacket = new AdventureSettingsPacket();
+        adventureSettingsPacket.setUniqueEntityId(bedrockId);
+        adventureSettingsPacket.setCommandPermission(commandPermission);
+        adventureSettingsPacket.setPlayerPermission(playerPermission);
+
         Set<AdventureSetting> flags = adventureSettingsPacket.getSettings();
         if (canFly || spectator) {
             flags.add(AdventureSetting.MAY_FLY);
@@ -1676,16 +1752,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
         sendDownstreamPacket(clientSettingsPacket);
     }
 
-    public int getNextSequence() {
-        return 0;
-    }
-
     /**
      * Used for updating statistic values since we only get changes from the server
      *
      * @param statistics Updated statistics values
      */
-    public void updateStatistics(@NonNull Map<Statistic, Integer> statistics) {
+    public void updateStatistics(@Nonnull Object2IntMap<Statistic> statistics) {
         if (this.statistics.isEmpty()) {
             // Initialize custom statistics to 0, so that they appear in the form
             for (CustomStatistic customStatistic : CustomStatistic.values()) {
@@ -1757,6 +1829,17 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
         sendUpstreamPacket(packet);
     }
 
+    public float getEyeHeight() {
+        return switch (pose) {
+            case SNEAKING -> 1.27f;
+            case SWIMMING,
+                    FALL_FLYING, // Elytra
+                    SPIN_ATTACK -> 0.4f; // Trident spin attack
+            case SLEEPING -> 0.2f;
+            default -> EntityDefinitions.PLAYER.offset();
+        };
+    }
+
     public MinecraftCodecHelper getCodecHelper() {
         return (MinecraftCodecHelper) this.downstream.getCodecHelper();
     }
diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java
index d46a39616..ac0c93204 100644
--- a/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java
+++ b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java
@@ -28,6 +28,7 @@ package org.geysermc.geyser.session.cache;
 import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundUpdateTagsPacket;
 import it.unimi.dsi.fastutil.ints.IntList;
 import it.unimi.dsi.fastutil.ints.IntLists;
+import org.geysermc.geyser.GeyserLogger;
 import org.geysermc.geyser.inventory.GeyserItemStack;
 import org.geysermc.geyser.registry.type.BlockMapping;
 import org.geysermc.geyser.registry.type.ItemMapping;
@@ -82,6 +83,15 @@ public class TagCache {
         this.requiresIronTool = IntList.of(blockTags.get("minecraft:needs_iron_tool"));
         this.requiresDiamondTool = IntList.of(blockTags.get("minecraft:needs_diamond_tool"));
 
+        // Hack btw
+        GeyserLogger logger = session.getGeyser().getLogger();
+        int[] convertableToMud = blockTags.get("minecraft:convertable_to_mud");
+        boolean emulatePost1_18Logic = convertableToMud != null && convertableToMud.length != 0;
+        session.setEmulatePost1_18Logic(emulatePost1_18Logic);
+        if (logger.isDebug()) {
+            logger.debug("Emulating post 1.18 block predication logic for " + session.name() + "? " + emulatePost1_18Logic);
+        }
+
         Map<String, int[]> itemTags = packet.getTags().get("minecraft:item");
         this.axolotlTemptItems = IntList.of(itemTags.get("minecraft:axolotl_tempt_items"));
         this.fishes = IntList.of(itemTags.get("minecraft:fishes"));
@@ -91,10 +101,10 @@ public class TagCache {
         this.smallFlowers = IntList.of(itemTags.get("minecraft:small_flowers"));
 
         // Hack btw
-        boolean emulatePost1_14Logic = itemTags.get("minecraft:signs").length > 1;
-        session.setEmulatePost1_14Logic(emulatePost1_14Logic);
-        if (session.getGeyser().getLogger().isDebug()) {
-            session.getGeyser().getLogger().debug("Emulating post 1.14 villager logic for " + session.name() + "? " + emulatePost1_14Logic);
+        boolean emulatePost1_13Logic = itemTags.get("minecraft:signs").length > 1;
+        session.setEmulatePost1_13Logic(emulatePost1_13Logic);
+        if (logger.isDebug()) {
+            logger.debug("Emulating post 1.13 villager logic for " + session.name() + "? " + emulatePost1_13Logic);
         }
     }
 
diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java
index 17679ad3e..239f5c865 100644
--- a/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java
+++ b/core/src/main/java/org/geysermc/geyser/session/cache/WorldCache.java
@@ -26,12 +26,18 @@
 package org.geysermc.geyser.session.cache;
 
 import com.github.steveice10.mc.protocol.data.game.setting.Difficulty;
+import com.nukkitx.math.vector.Vector3i;
 import com.nukkitx.protocol.bedrock.packet.SetTitlePacket;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
 import lombok.Getter;
 import lombok.Setter;
 import org.geysermc.geyser.scoreboard.Scoreboard;
 import org.geysermc.geyser.scoreboard.ScoreboardUpdater.ScoreboardSession;
 import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.util.ChunkUtils;
+
+import java.util.Iterator;
+import java.util.Map;
 
 public final class WorldCache {
     private final GeyserSession session;
@@ -51,6 +57,9 @@ public final class WorldCache {
     private int trueTitleStayTime;
     private int trueTitleFadeOutTime;
 
+    private int currentSequence;
+    private final Map<Vector3i, ServerVerifiedState> unverifiedPredictions = new Object2ObjectOpenHashMap<>(1);
+
     public WorldCache(GeyserSession session) {
         this.session = session;
         this.scoreboard = new Scoreboard(session);
@@ -121,4 +130,75 @@ public final class WorldCache {
             forceSyncCorrectTitleTimes();
         }
     }
+
+    /* Code to support the prediction structure introduced in Java Edition 1.19.0
+    Blocks can be rolled back if invalid, but this requires some client-side information storage. */
+
+    public int nextPredictionSequence() {
+        return ++currentSequence;
+    }
+
+    /**
+     * Stores a record of a block at a certain position to rollback in the event it is incorrect.
+     */
+    public void addServerCorrectBlockState(Vector3i position, int blockState) {
+        if (session.isEmulatePost1_18Logic()) {
+            // Cheap hack
+            // On non-Bukkit platforms, ViaVersion will always confirm the sequence before the block is updated,
+            // meaning we'd send two block updates after (ChunkUtils.updateBlockClientSide in endPredictionsUpTo
+            // and the packet updating from the client)
+            this.unverifiedPredictions.compute(position, ($, serverVerifiedState) -> serverVerifiedState == null
+                    ? new ServerVerifiedState(currentSequence, blockState) : serverVerifiedState.setData(currentSequence, blockState));
+        }
+    }
+
+    public void updateServerCorrectBlockState(Vector3i position) {
+        if (this.unverifiedPredictions.isEmpty()) {
+            return;
+        }
+
+        this.unverifiedPredictions.remove(position);
+    }
+
+    public void endPredictionsUpTo(int sequence) {
+        if (this.unverifiedPredictions.isEmpty()) {
+            return;
+        }
+
+        Iterator<Map.Entry<Vector3i, ServerVerifiedState>> it = this.unverifiedPredictions.entrySet().iterator();
+        while (it.hasNext()) {
+            Map.Entry<Vector3i, ServerVerifiedState> entry = it.next();
+            ServerVerifiedState serverVerifiedState = entry.getValue();
+            if (serverVerifiedState.sequence <= sequence) {
+                // This block may be out of sync with the server
+                // In 1.19.0 Java, you can verify this by trying to mine in spawn protection
+                ChunkUtils.updateBlockClientSide(session, serverVerifiedState.blockState, entry.getKey());
+                it.remove();
+            }
+        }
+    }
+
+    private static class ServerVerifiedState {
+        private int sequence;
+        private int blockState;
+
+        ServerVerifiedState(int sequence, int blockState) {
+            this.sequence = sequence;
+            this.blockState = blockState;
+        }
+
+        ServerVerifiedState setData(int sequence, int blockState) {
+            this.sequence = sequence;
+            this.blockState = blockState;
+            return this;
+        }
+
+        @Override
+        public String toString() {
+            return "ServerVerifiedState{" +
+                    "sequence=" + sequence +
+                    ", blockState=" + blockState +
+                    '}';
+        }
+    }
 }
\ No newline at end of file
diff --git a/core/src/main/java/org/geysermc/geyser/text/ChatTypeEntry.java b/core/src/main/java/org/geysermc/geyser/text/ChatTypeEntry.java
index 800eb6c0f..ad2514e09 100644
--- a/core/src/main/java/org/geysermc/geyser/text/ChatTypeEntry.java
+++ b/core/src/main/java/org/geysermc/geyser/text/ChatTypeEntry.java
@@ -25,7 +25,7 @@
 
 package org.geysermc.geyser.text;
 
-import com.github.steveice10.mc.protocol.data.game.MessageType;
+import com.github.steveice10.mc.protocol.data.game.BuiltinChatType;
 import com.nukkitx.protocol.bedrock.packet.TextPacket;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 
@@ -45,13 +45,13 @@ public record ChatTypeEntry(@Nonnull TextPacket.Type bedrockChatType, @Nullable
         // So the proper way to do this, probably, would be to dump the NBT data from vanilla and load it.
         // But, the only way this happens is if a chat message is sent to us before the login packet, which is rare.
         // So we'll just make sure chat ends up in the right place.
-        chatTypes.put(MessageType.CHAT.ordinal(), CHAT);
-        chatTypes.put(MessageType.SYSTEM.ordinal(), SYSTEM);
-        chatTypes.put(MessageType.GAME_INFO.ordinal(), TIP);
-        chatTypes.put(MessageType.SAY_COMMAND.ordinal(), RAW);
-        chatTypes.put(MessageType.MSG_COMMAND.ordinal(), RAW);
-        chatTypes.put(MessageType.TEAM_MSG_COMMAND.ordinal(), RAW);
-        chatTypes.put(MessageType.EMOTE_COMMAND.ordinal(), RAW);
-        chatTypes.put(MessageType.TELLRAW_COMMAND.ordinal(), RAW);
+        chatTypes.put(BuiltinChatType.CHAT.ordinal(), CHAT);
+        chatTypes.put(BuiltinChatType.SYSTEM.ordinal(), SYSTEM);
+        chatTypes.put(BuiltinChatType.GAME_INFO.ordinal(), TIP);
+        chatTypes.put(BuiltinChatType.SAY_COMMAND.ordinal(), RAW);
+        chatTypes.put(BuiltinChatType.MSG_COMMAND.ordinal(), RAW);
+        chatTypes.put(BuiltinChatType.TEAM_MSG_COMMAND.ordinal(), RAW);
+        chatTypes.put(BuiltinChatType.EMOTE_COMMAND.ordinal(), RAW);
+        chatTypes.put(BuiltinChatType.TELLRAW_COMMAND.ordinal(), RAW);
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java b/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java
index e5f3c0554..94ad5eead 100644
--- a/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java
+++ b/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java
@@ -126,14 +126,20 @@ public class MinecraftLocale {
 
         // Check the locale isn't already loaded
         if (!ASSET_MAP.containsKey("minecraft/lang/" + locale + ".json") && !locale.equals("en_us")) {
-            GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.locale.fail.invalid", locale));
+            if (loadLocale(locale)) {
+                GeyserImpl.getInstance().getLogger().debug("Loaded locale locally while not being in asset map: " + locale);
+            } else {
+                GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.locale.fail.invalid", locale));
+            }
             return;
         }
 
         GeyserImpl.getInstance().getLogger().debug("Downloading and loading locale: " + locale);
 
         downloadLocale(locale);
-        loadLocale(locale);
+        if (!loadLocale(locale)) {
+            GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.locale.fail.missing", locale));
+        }
     }
 
     /**
@@ -199,7 +205,7 @@ public class MinecraftLocale {
      *
      * @param locale Locale to load
      */
-    private static void loadLocale(String locale) {
+    private static boolean loadLocale(String locale) {
         File localeFile = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/" + locale + ".json").toFile();
 
         // Load the locale
@@ -242,8 +248,9 @@ public class MinecraftLocale {
             } catch (IOException e) {
                 throw new AssertionError(GeyserLocale.getLocaleStringLog("geyser.locale.fail.file", locale, e.getMessage()));
             }
+            return true;
         } else {
-            GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.locale.fail.missing", locale));
+            return false;
         }
     }
 
@@ -300,9 +307,9 @@ public class MinecraftLocale {
      * @return Translated string or the original message if it was not found in the given locale
      */
     public static String getLocaleString(String messageText, String locale) {
-        Map<String, String> localeStrings = MinecraftLocale.LOCALE_MAPPINGS.get(locale.toLowerCase());
+        Map<String, String> localeStrings = LOCALE_MAPPINGS.get(locale.toLowerCase(Locale.ROOT));
         if (localeStrings == null) {
-            localeStrings = MinecraftLocale.LOCALE_MAPPINGS.get(GeyserLocale.getDefaultLocale());
+            localeStrings = LOCALE_MAPPINGS.get(GeyserLocale.getDefaultLocale());
             if (localeStrings == null) {
                 // Don't cause a NPE if the locale is STILL missing
                 GeyserImpl.getInstance().getLogger().debug("MISSING DEFAULT LOCALE: " + GeyserLocale.getDefaultLocale());
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java
index 54dc533c6..4dac5e86f 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java
@@ -119,7 +119,7 @@ public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator
     }
 
     private OptionalInt toJava(int effectChoice) {
-        return effectChoice == -1 ? OptionalInt.empty() : OptionalInt.of(effectChoice);
+        return effectChoice == 0 ? OptionalInt.empty() : OptionalInt.of(effectChoice);
     }
 
     @Override
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/LoomInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/LoomInventoryTranslator.java
index a7b736d72..5a237b72a 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/LoomInventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/LoomInventoryTranslator.java
@@ -60,7 +60,7 @@ public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator {
 
     static {
         // Added from left-to-right then up-to-down in the order Java presents it
-        int index = 1;
+        int index = 0;
         PATTERN_TO_INDEX.put("bl", index++);
         PATTERN_TO_INDEX.put("br", index++);
         PATTERN_TO_INDEX.put("tl", index++);
@@ -119,15 +119,16 @@ public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator {
     @Override
     protected boolean shouldHandleRequestFirst(StackRequestActionData action, Inventory inventory) {
         // If the LOOM_MATERIAL slot is not empty, we are crafting a pattern that does not come from an item
-        // Remove the CRAFT_NON_IMPLEMENTED_DEPRECATED when 1.17.30 is dropped
-        return (action.getType() == StackRequestActionType.CRAFT_NON_IMPLEMENTED_DEPRECATED || action.getType() == StackRequestActionType.CRAFT_LOOM)
-                && inventory.getItem(2).isEmpty();
+        return action.getType() == StackRequestActionType.CRAFT_LOOM && inventory.getItem(2).isEmpty();
     }
 
     @Override
     public ItemStackResponsePacket.Response translateSpecialRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
         StackRequestActionData headerData = request.getActions()[0];
         StackRequestActionData data = request.getActions()[1];
+        if (!(headerData instanceof CraftLoomStackRequestActionData)) {
+            return rejectRequest(request);
+        }
         if (!(data instanceof CraftResultsDeprecatedStackRequestActionData craftData)) {
             return rejectRequest(request);
         }
@@ -136,15 +137,7 @@ public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator {
         List<NbtMap> newBlockEntityTag = craftData.getResultItems()[0].getTag().getList("Patterns", NbtType.COMPOUND);
         // Get the pattern that the Bedrock client requests - the last pattern in the Patterns list
         NbtMap pattern = newBlockEntityTag.get(newBlockEntityTag.size() - 1);
-        String bedrockPattern;
-
-        if (headerData instanceof CraftLoomStackRequestActionData loomData) {
-            // Prioritize this if on 1.17.40
-            // Remove the below if statement when 1.17.30 is dropped
-            bedrockPattern = loomData.getPatternId();
-        } else {
-            bedrockPattern = pattern.getString("Pattern");
-        }
+        String bedrockPattern = ((CraftLoomStackRequestActionData) headerData).getPatternId();
 
         // Get the Java index of this pattern
         int index = PATTERN_TO_INDEX.getOrDefault(bedrockPattern, -1);
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java
index d4bac172c..5e9c99ae9 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java
@@ -155,7 +155,7 @@ public class MerchantInventoryTranslator extends BaseInventoryTranslator {
         ServerboundSelectTradePacket packet = new ServerboundSelectTradePacket(tradeChoice);
         session.sendDownstreamPacket(packet);
 
-        if (session.isEmulatePost1_14Logic()) {
+        if (session.isEmulatePost1_13Logic()) {
             // 1.18 Java cooperates nicer than older versions
             if (inventory instanceof MerchantContainer merchantInventory) {
                 merchantInventory.onTradeSelected(session, tradeChoice);
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/GoatHornTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/GoatHornTranslator.java
new file mode 100644
index 000000000..2cb9d7ec7
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/GoatHornTranslator.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.translator.inventory.item;
+
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
+import com.github.steveice10.opennbt.tag.builtin.StringTag;
+import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.network.GameProtocol;
+import org.geysermc.geyser.registry.Registries;
+import org.geysermc.geyser.registry.type.ItemMapping;
+import org.geysermc.geyser.registry.type.ItemMappings;
+
+import java.util.Collections;
+import java.util.List;
+
+@ItemRemapper
+public class GoatHornTranslator extends ItemTranslator {
+
+    private static final List<String> INSTRUMENTS = List.of(
+            "ponder_goat_horn",
+            "sing_goat_horn",
+            "seek_goat_horn",
+            "feel_goat_horn",
+            "admire_goat_horn",
+            "call_goat_horn",
+            "yearn_goat_horn",
+            "dream_goat_horn" // Called "Resist" on Bedrock 1.19.0 due to https://bugs.mojang.com/browse/MCPE-155059
+    );
+
+    @Override
+    protected ItemData.Builder translateToBedrock(ItemStack itemStack, ItemMapping mapping, ItemMappings mappings) {
+        ItemData.Builder builder = super.translateToBedrock(itemStack, mapping, mappings);
+        if (itemStack.getNbt() != null && itemStack.getNbt().get("instrument") instanceof StringTag instrumentTag) {
+            String instrument = instrumentTag.getValue();
+            // Drop the Minecraft namespace if applicable
+            if (instrument.startsWith("minecraft:")) {
+                instrument = instrument.substring("minecraft:".length());
+            }
+
+            int damage = INSTRUMENTS.indexOf(instrument);
+            if (damage == -1) {
+                damage = 0;
+                GeyserImpl.getInstance().getLogger().debug("Unknown goat horn instrument: " + instrumentTag.getValue());
+            }
+            builder.damage(damage);
+        }
+        return builder;
+    }
+
+    @Override
+    public ItemStack translateToJava(ItemData itemData, ItemMapping mapping, ItemMappings mappings) {
+        ItemStack itemStack = super.translateToJava(itemData, mapping, mappings);
+
+        int damage = itemData.getDamage();
+        if (damage < 0 || damage >= INSTRUMENTS.size()) {
+            GeyserImpl.getInstance().getLogger().debug("Unknown goat horn instrument for damage: " + damage);
+            damage = 0;
+        }
+
+        String instrument = INSTRUMENTS.get(damage);
+        StringTag instrumentTag = new StringTag("instrument", "minecraft:" + instrument);
+        itemStack.getNbt().put(instrumentTag);
+
+        return itemStack;
+    }
+
+    @Override
+    public List<ItemMapping> getAppliedItems() {
+        return Collections.singletonList(
+                Registries.ITEMS.forVersion(GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion())
+                        .getMapping("minecraft:goat_horn")
+        );
+    }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedrockOnlyBlockEntity.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedrockOnlyBlockEntity.java
index 94760b66c..9ae3300cd 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedrockOnlyBlockEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedrockOnlyBlockEntity.java
@@ -62,7 +62,7 @@ public interface BedrockOnlyBlockEntity extends RequiresBlockState {
             return FlowerPotBlockEntityTranslator.getTag(session, blockState, position);
         } else if (PistonBlockEntityTranslator.isBlock(blockState)) {
             return PistonBlockEntityTranslator.getTag(blockState, position);
-        } else if (BlockStateValues.isCauldron(blockState)) {
+        } else if (BlockStateValues.isNonWaterCauldron(blockState)) {
             // As of 1.18.30: this is required to make rendering not look weird on chunk load (lava and snow cauldrons look dim)
             return NbtMap.builder()
                     .putString("id", "Cauldron")
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java
index 243b1cede..24c046ef2 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java
@@ -33,17 +33,22 @@ import com.github.steveice10.mc.protocol.data.game.entity.player.InteractAction;
 import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.*;
+import com.nukkitx.math.vector.Vector3d;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.math.vector.Vector3i;
 import com.nukkitx.protocol.bedrock.data.LevelEventType;
 import com.nukkitx.protocol.bedrock.data.inventory.*;
-import com.nukkitx.protocol.bedrock.packet.*;
+import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket;
+import com.nukkitx.protocol.bedrock.packet.InventoryTransactionPacket;
+import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
+import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
 import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
 import org.geysermc.geyser.entity.EntityDefinitions;
 import org.geysermc.geyser.entity.type.Entity;
 import org.geysermc.geyser.entity.type.ItemFrameEntity;
+import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
 import org.geysermc.geyser.inventory.GeyserItemStack;
 import org.geysermc.geyser.inventory.Inventory;
 import org.geysermc.geyser.inventory.PlayerInventory;
@@ -53,6 +58,8 @@ import org.geysermc.geyser.registry.BlockRegistries;
 import org.geysermc.geyser.registry.type.ItemMapping;
 import org.geysermc.geyser.registry.type.ItemMappings;
 import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.translator.inventory.InventoryTranslator;
+import org.geysermc.geyser.translator.inventory.item.ItemTranslator;
 import org.geysermc.geyser.translator.protocol.PacketTranslator;
 import org.geysermc.geyser.translator.protocol.Translator;
 import org.geysermc.geyser.util.BlockUtils;
@@ -122,7 +129,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                                 dropAll ? PlayerAction.DROP_ITEM_STACK : PlayerAction.DROP_ITEM,
                                 Vector3i.ZERO,
                                 Direction.DOWN,
-                                session.getNextSequence()
+                                session.getWorldCache().nextPredictionSequence()
                         );
                         session.sendDownstreamPacket(dropPacket);
 
@@ -170,6 +177,11 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                             session.setLastInteractionTime(System.currentTimeMillis());
                         }
 
+                        if (isIncorrectHeldItem(session, packet)) {
+                            restoreCorrectBlock(session, blockPos, packet);
+                            return;
+                        }
+
                         // Bedrock sends block interact code for a Java entity so we send entity code back to Java
                         if (session.getBlockMappings().isItemFrame(packet.getBlockRuntimeId())) {
                             Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, packet.getBlockPosition());
@@ -192,18 +204,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
 
                         // CraftBukkit+ check - see https://github.com/PaperMC/Paper/blob/458db6206daae76327a64f4e2a17b67a7e38b426/Spigot-Server-Patches/0532-Move-range-check-for-block-placing-up.patch
                         Vector3f playerPosition = session.getPlayerEntity().getPosition();
-
-                        // Adjust position for current eye height
-                        switch (session.getPose()) {
-                            case SNEAKING ->
-                                playerPosition = playerPosition.sub(0, (EntityDefinitions.PLAYER.offset() - 1.27f), 0);
-                            case SWIMMING,
-                                FALL_FLYING, // Elytra
-                                SPIN_ATTACK -> // Trident spin attack
-                                playerPosition = playerPosition.sub(0, (EntityDefinitions.PLAYER.offset() - 0.4f), 0);
-                            case SLEEPING ->
-                                playerPosition = playerPosition.sub(0, (EntityDefinitions.PLAYER.offset() - 0.2f), 0);
-                        } // else, we don't have to modify the position
+                        playerPosition = playerPosition.down(EntityDefinitions.PLAYER.offset() - session.getEyeHeight());
 
                         boolean creative = session.getGameMode() == GameMode.CREATIVE;
 
@@ -255,9 +256,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                             int blockState = session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition());
                             if (blockState == BlockStateValues.JAVA_WATER_ID) {
                                 // Otherwise causes multiple mobs to spawn - just send a use item packet
-                                // TODO when we fix mobile bucket rotation, use it for this, too
-                                ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence());
-                                session.sendDownstreamPacket(itemPacket);
+                                useItem(session, packet, blockState);
                                 break;
                             }
                         }
@@ -268,33 +267,33 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                                 Hand.MAIN_HAND,
                                 packet.getClickPosition().getX(), packet.getClickPosition().getY(), packet.getClickPosition().getZ(),
                                 false,
-                                session.getNextSequence());
+                                session.getWorldCache().nextPredictionSequence());
                         session.sendDownstreamPacket(blockPacket);
 
                         if (packet.getItemInHand() != null) {
-                            // Otherwise boats will not be able to be placed in survival and buckets won't work on mobile
-                            if (session.getItemMappings().getBoatIds().contains(packet.getItemInHand().getId())) {
-                                ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence());
-                                session.sendDownstreamPacket(itemPacket);
-                            } else if (session.getItemMappings().getBucketIds().contains(packet.getItemInHand().getId())) {
-                                // Let the server decide if the bucket item should change, not the client, and revert the changes the client made
-                                InventorySlotPacket slotPacket = new InventorySlotPacket();
-                                slotPacket.setContainerId(ContainerId.INVENTORY);
-                                slotPacket.setSlot(packet.getHotbarSlot());
-                                slotPacket.setItem(packet.getItemInHand());
-                                session.sendUpstreamPacket(slotPacket);
+                            int itemId = packet.getItemInHand().getId();
+                            int blockState = session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition());
+                            // Otherwise boats will not be able to be placed in survival and buckets, lily pads, frogspawn, and glass bottles won't work on mobile
+                            if (session.getItemMappings().getBoatIds().contains(itemId) ||
+                                    itemId == session.getItemMappings().getStoredItems().lilyPad() ||
+                                    itemId == session.getItemMappings().getStoredItems().frogspawn()) {
+                                useItem(session, packet, blockState);
+                            } else if (itemId == session.getItemMappings().getStoredItems().glassBottle()) {
+                                if (!session.isSneaking() && BlockStateValues.isCauldron(blockState) && !BlockStateValues.isNonWaterCauldron(blockState)) {
+                                    // ServerboundUseItemPacket is not sent for water cauldrons and glass bottles
+                                    return;
+                                }
+                                useItem(session, packet, blockState);
+                            } else if (session.getItemMappings().getBucketIds().contains(itemId)) {
                                 // Don't send ServerboundUseItemPacket for powder snow buckets
-                                if (packet.getItemInHand().getId() != session.getItemMappings().getStoredItems().powderSnowBucket().getBedrockId()) {
-                                    // Special check for crafting tables since clients don't send BLOCK_INTERACT when interacting
-                                    int blockState = session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition());
-                                    if (session.isSneaking() || blockState != BlockRegistries.JAVA_IDENTIFIERS.get("minecraft:crafting_table")) {
-                                        // Delay the interaction in case the client doesn't intend to actually use the bucket
-                                        // See BedrockActionTranslator.java
-                                        session.setBucketScheduledFuture(session.scheduleInEventLoop(() -> {
-                                            ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence());
-                                            session.sendDownstreamPacket(itemPacket);
-                                        }, 5, TimeUnit.MILLISECONDS));
+                                if (itemId != session.getItemMappings().getStoredItems().powderSnowBucket().getBedrockId()) {
+                                    if (!session.isSneaking() && BlockStateValues.isCauldron(blockState)) {
+                                        // ServerboundUseItemPacket is not sent for cauldrons and buckets
+                                        return;
                                     }
+                                    session.setPlacedBucket(useItem(session, packet, blockState));
+                                } else {
+                                    session.setPlacedBucket(true);
                                 }
                             }
                         }
@@ -320,6 +319,11 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                         session.setInteracting(true);
                     }
                     case 1 -> {
+                        if (isIncorrectHeldItem(session, packet)) {
+                            InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), session.getPlayerInventory().getOffsetForHotbar(packet.getHotbarSlot()));
+                            break;
+                        }
+
                         // Handled when sneaking
                         if (session.getPlayerInventory().getItemInHand().getJavaId() == mappings.getStoredItems().shield().getJavaId()) {
                             break;
@@ -334,10 +338,13 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                             } else if (session.getItemMappings().getSpawnEggIds().contains(packet.getItemInHand().getId())) {
                                 // Handled in case 0
                                 break;
+                            } else if (packet.getItemInHand().getId() == session.getItemMappings().getStoredItems().glassBottle()) {
+                                // Handled in case 0
+                                break;
                             }
                         }
 
-                        ServerboundUseItemPacket useItemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence());
+                        ServerboundUseItemPacket useItemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getWorldCache().nextPredictionSequence());
                         session.sendDownstreamPacket(useItemPacket);
 
                         List<LegacySetItemSlotData> legacySlots = packet.getLegacySlots();
@@ -402,12 +409,22 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                             return;
                         }
 
+                        int sequence = session.getWorldCache().nextPredictionSequence();
+                        if (blockState != -1) {
+                            session.getWorldCache().addServerCorrectBlockState(packet.getBlockPosition(), blockState);
+                        } else {
+                            blockState = BlockStateValues.JAVA_AIR_ID;
+                            // Client will desync here anyway
+                            session.getWorldCache().addServerCorrectBlockState(packet.getBlockPosition(),
+                                    session.getGeyser().getWorldManager().getBlockAt(session, packet.getBlockPosition()));
+                        }
+
                         LevelEventPacket blockBreakPacket = new LevelEventPacket();
                         blockBreakPacket.setType(LevelEventType.PARTICLE_DESTROY_BLOCK);
                         blockBreakPacket.setPosition(packet.getBlockPosition().toFloat());
                         blockBreakPacket.setData(session.getBlockMappings().getBedrockBlockId(blockState));
                         session.sendUpstreamPacket(blockBreakPacket);
-                        session.setBreakingBlock(BlockStateValues.JAVA_AIR_ID);
+                        session.setBreakingBlock(-1);
 
                         Entity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, packet.getBlockPosition());
                         if (itemFrameEntity != null) {
@@ -418,7 +435,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                         }
 
                         PlayerAction action = session.getGameMode() == GameMode.CREATIVE ? PlayerAction.START_DIGGING : PlayerAction.FINISH_DIGGING;
-                        ServerboundPlayerActionPacket breakPacket = new ServerboundPlayerActionPacket(action, packet.getBlockPosition(), Direction.VALUES[packet.getBlockFace()], session.getNextSequence());
+                        ServerboundPlayerActionPacket breakPacket = new ServerboundPlayerActionPacket(action, packet.getBlockPosition(), Direction.VALUES[packet.getBlockFace()], sequence);
                         session.sendDownstreamPacket(breakPacket);
                     }
                 }
@@ -427,7 +444,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                 if (packet.getActionType() == 0) {
                     // Followed to the Minecraft Protocol specification outlined at wiki.vg
                     ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, Vector3i.ZERO,
-                            Direction.DOWN, session.getNextSequence());
+                            Direction.DOWN, session.getWorldCache().nextPredictionSequence());
                     session.sendDownstreamPacket(releaseItemPacket);
                 }
                 break;
@@ -520,10 +537,117 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
         session.sendUpstreamPacket(updateWaterPacket);
 
         // Reset the item in hand to prevent "missing" blocks
-        InventorySlotPacket slotPacket = new InventorySlotPacket();
-        slotPacket.setContainerId(ContainerId.INVENTORY);
-        slotPacket.setSlot(packet.getHotbarSlot());
-        slotPacket.setItem(packet.getItemInHand());
-        session.sendUpstreamPacket(slotPacket);
+        InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), session.getPlayerInventory().getOffsetForHotbar(packet.getHotbarSlot()));
+    }
+
+    private boolean isIncorrectHeldItem(GeyserSession session, InventoryTransactionPacket packet) {
+        int javaSlot = session.getPlayerInventory().getOffsetForHotbar(packet.getHotbarSlot());
+        int expectedItemId = ItemTranslator.getBedrockItemMapping(session, session.getPlayerInventory().getItem(javaSlot)).getBedrockId();
+        int heldItemId = packet.getItemInHand() == null ? ItemData.AIR.getId() : packet.getItemInHand().getId();
+
+        if (expectedItemId != heldItemId) {
+            session.getGeyser().getLogger().debug(session.name() + "'s held item has desynced! Expected: " + expectedItemId + " Received: " + heldItemId);
+            session.getGeyser().getLogger().debug("Packet: " + packet);
+            return true;
+        }
+        return false;
+    }
+
+    private boolean useItem(GeyserSession session, InventoryTransactionPacket packet, int blockState) {
+        // Update the player's inventory to remove any items added by the client itself
+        Inventory playerInventory = session.getPlayerInventory();
+        int heldItemSlot = playerInventory.getOffsetForHotbar(packet.getHotbarSlot());
+        InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, playerInventory, heldItemSlot);
+        if (playerInventory.getItem(heldItemSlot).getAmount() > 1) {
+            if (packet.getItemInHand().getId() == session.getItemMappings().getStoredItems().bucket() ||
+                packet.getItemInHand().getId() == session.getItemMappings().getStoredItems().glassBottle()) {
+                // Using a stack of buckets or glass bottles will result in an item being added to the first empty slot.
+                // We need to revert the item in case the interaction fails. The order goes from left to right in the
+                // hotbar. Then left to right and top to bottom in the inventory.
+                for (int i = 0; i < 36; i++) {
+                    int slot = i;
+                    if (i < 9) {
+                        slot = playerInventory.getOffsetForHotbar(slot);
+                    }
+                    if (playerInventory.getItem(slot).isEmpty()) {
+                        InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, playerInventory, slot);
+                        break;
+                    }
+                }
+            }
+        }
+        // Check if the player is interacting with a block
+        if (!session.isSneaking()) {
+            if (BlockRegistries.INTERACTIVE.get().contains(blockState)) {
+                return false;
+            }
+
+            boolean mayBuild = session.getGameMode() == GameMode.SURVIVAL || session.getGameMode() == GameMode.CREATIVE;
+            if (mayBuild && BlockRegistries.INTERACTIVE_MAY_BUILD.get().contains(blockState)) {
+                return false;
+            }
+        }
+
+        Vector3f target = packet.getBlockPosition().toFloat().add(packet.getClickPosition());
+        lookAt(session, target);
+
+        ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getWorldCache().nextPredictionSequence());
+        session.sendDownstreamPacket(itemPacket);
+        return true;
+    }
+
+    /**
+     * Determine the rotation necessary to activate this transaction.
+     *
+     * The position between the intended click position and the player can be determined with two triangles.
+     * First, we compute the difference of the X and Z coordinates:
+     *
+     * Player position (0, 0)
+     * |
+     * |
+     * |
+     * |_____________ Intended target (-3, 2)
+     *
+     * We then use the Pythagorean Theorem to find the direct line (hypotenuse) on the XZ plane. Finding the angle of the
+     * triangle from there, closest to the player, gives us our yaw rotation value
+     * Then doing the same using the new XZ distance and Y difference, we can find the direct line of sight from the
+     * player to the intended target, and the pitch rotation value. We can then send the necessary packets to update
+     * the player's rotation.
+     *
+     * @param session the Geyser Session
+     * @param target the position to look at
+     */
+    private void lookAt(GeyserSession session, Vector3f target) {
+        // Use the bounding box's position since we need the player's position seen by the Java server
+        Vector3d playerPosition = session.getCollisionManager().getPlayerBoundingBox().getBottomCenter();
+        float xDiff = (float) (target.getX() - playerPosition.getX());
+        float yDiff = (float) (target.getY() - (playerPosition.getY() + session.getEyeHeight()));
+        float zDiff = (float) (target.getZ() - playerPosition.getZ());
+
+        // First triangle on the XZ plane
+        float yaw = (float) -Math.toDegrees(Math.atan2(xDiff, zDiff));
+        // Second triangle on the Y axis using the hypotenuse of the first triangle as a side
+        double xzHypot = Math.sqrt(xDiff * xDiff + zDiff * zDiff);
+        float pitch = (float) -Math.toDegrees(Math.atan2(yDiff, xzHypot));
+
+        SessionPlayerEntity entity = session.getPlayerEntity();
+        ServerboundMovePlayerPosRotPacket returnPacket = new ServerboundMovePlayerPosRotPacket(entity.isOnGround(), playerPosition.getX(), playerPosition.getY(), playerPosition.getZ(), entity.getYaw(), entity.getPitch());
+        // This matches Java edition behavior
+        ServerboundMovePlayerPosRotPacket movementPacket = new ServerboundMovePlayerPosRotPacket(entity.isOnGround(), playerPosition.getX(), playerPosition.getY(), playerPosition.getZ(), yaw, pitch);
+        session.sendDownstreamPacket(movementPacket);
+
+        if (session.getLookBackScheduledFuture() != null) {
+            session.getLookBackScheduledFuture().cancel(false);
+        }
+        if (Math.abs(entity.getYaw() - yaw) > 1f || Math.abs(entity.getPitch() - pitch) > 1f) {
+            session.setLookBackScheduledFuture(session.scheduleInEventLoop(() -> {
+                Vector3d newPlayerPosition = session.getCollisionManager().getPlayerBoundingBox().getBottomCenter();
+                if (!newPlayerPosition.equals(playerPosition) || entity.getYaw() != returnPacket.getYaw() || entity.getPitch() != returnPacket.getPitch()) {
+                    // The player moved/rotated so there is no need to change their rotation back
+                    return;
+                }
+                session.sendDownstreamPacket(returnPacket);
+            }, 150, TimeUnit.MILLISECONDS));
+        }
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockLecternUpdateTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockLecternUpdateTranslator.java
index 499a54322..25a579dc7 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockLecternUpdateTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockLecternUpdateTranslator.java
@@ -57,7 +57,7 @@ public class BedrockLecternUpdateTranslator extends PacketTranslator<LecternUpda
                     Hand.MAIN_HAND,
                     0, 0, 0, // Java doesn't care about these when dealing with a lectern
                     false,
-                    session.getNextSequence());
+                    session.getWorldCache().nextPredictionSequence());
             session.sendDownstreamPacket(blockPacket);
         } else {
             // Bedrock wants to either move a page or exit
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMobEquipmentTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMobEquipmentTranslator.java
index 1875e8fe5..b8decad78 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMobEquipmentTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMobEquipmentTranslator.java
@@ -65,7 +65,7 @@ public class BedrockMobEquipmentTranslator extends PacketTranslator<MobEquipment
             // Activate shield since we are already sneaking
             // (No need to send a release item packet - Java doesn't do this when swapping items)
             // Required to do it a tick later or else it doesn't register
-            session.scheduleInEventLoop(() -> session.sendDownstreamPacket(new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getNextSequence())),
+            session.scheduleInEventLoop(() -> session.sendDownstreamPacket(new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getWorldCache().nextPredictionSequence())),
                     50, TimeUnit.MILLISECONDS);
         }
 
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java
index fe519c329..5001fc2d2 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java
@@ -41,7 +41,6 @@ import com.nukkitx.protocol.bedrock.packet.*;
 import org.geysermc.geyser.entity.type.Entity;
 import org.geysermc.geyser.entity.type.ItemFrameEntity;
 import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
-import org.geysermc.geyser.level.block.BlockStateValues;
 import org.geysermc.geyser.registry.BlockRegistries;
 import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.translator.protocol.PacketTranslator;
@@ -129,21 +128,13 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
                 break;
             case DROP_ITEM:
                 ServerboundPlayerActionPacket dropItemPacket = new ServerboundPlayerActionPacket(PlayerAction.DROP_ITEM,
-                        vector, Direction.VALUES[packet.getFace()], session.getNextSequence());
+                        vector, Direction.VALUES[packet.getFace()], session.getWorldCache().nextPredictionSequence());
                 session.sendDownstreamPacket(dropItemPacket);
                 break;
             case STOP_SLEEP:
                 ServerboundPlayerCommandPacket stopSleepingPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.LEAVE_BED);
                 session.sendDownstreamPacket(stopSleepingPacket);
                 break;
-            case BLOCK_INTERACT:
-                // Client means to interact with a block; cancel bucket interaction, if any
-                if (session.getBucketScheduledFuture() != null) {
-                    session.getBucketScheduledFuture().cancel(true);
-                    session.setBucketScheduledFuture(null);
-                }
-                // Otherwise handled in BedrockInventoryTransactionTranslator
-                break;
             case START_BREAK:
                 // Start the block breaking animation
                 if (session.getGameMode() != GameMode.CREATIVE) {
@@ -163,7 +154,7 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
                 String identifier = BlockRegistries.JAVA_IDENTIFIERS.get().get(blockUp);
                 if (identifier.startsWith("minecraft:fire") || identifier.startsWith("minecraft:soul_fire")) {
                     ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING, fireBlockPos,
-                            Direction.VALUES[packet.getFace()], session.getNextSequence());
+                            Direction.VALUES[packet.getFace()], session.getWorldCache().nextPredictionSequence());
                     session.sendDownstreamPacket(startBreakingPacket);
                     if (session.getGameMode() == GameMode.CREATIVE) {
                         break;
@@ -171,17 +162,22 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
                 }
 
                 ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING,
-                        vector, Direction.VALUES[packet.getFace()], session.getNextSequence());
+                        vector, Direction.VALUES[packet.getFace()], session.getWorldCache().nextPredictionSequence());
                 session.sendDownstreamPacket(startBreakingPacket);
                 break;
             case CONTINUE_BREAK:
                 if (session.getGameMode() == GameMode.CREATIVE) {
                     break;
                 }
+                int breakingBlock = session.getBreakingBlock();
+                if (breakingBlock == -1) {
+                    break;
+                }
+
                 Vector3f vectorFloat = vector.toFloat();
                 LevelEventPacket continueBreakPacket = new LevelEventPacket();
                 continueBreakPacket.setType(LevelEventType.PARTICLE_CRACK_BLOCK);
-                continueBreakPacket.setData((session.getBlockMappings().getBedrockBlockId(session.getBreakingBlock())) | (packet.getFace() << 24));
+                continueBreakPacket.setData((session.getBlockMappings().getBedrockBlockId(breakingBlock)) | (packet.getFace() << 24));
                 continueBreakPacket.setPosition(vectorFloat);
                 session.sendUpstreamPacket(continueBreakPacket);
 
@@ -189,7 +185,7 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
                 LevelEventPacket updateBreak = new LevelEventPacket();
                 updateBreak.setType(LevelEventType.BLOCK_UPDATE_BREAK);
                 updateBreak.setPosition(vectorFloat);
-                double breakTime = BlockUtils.getSessionBreakTime(session, BlockRegistries.JAVA_BLOCKS.get(session.getBreakingBlock())) * 20;
+                double breakTime = BlockUtils.getSessionBreakTime(session, BlockRegistries.JAVA_BLOCKS.get(breakingBlock)) * 20;
                 updateBreak.setData((int) (65535 / breakTime));
                 session.sendUpstreamPacket(updateBreak);
                 break;
@@ -206,13 +202,13 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
                     }
                 }
 
-                ServerboundPlayerActionPacket abortBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.CANCEL_DIGGING, vector, Direction.DOWN, session.getNextSequence());
+                ServerboundPlayerActionPacket abortBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.CANCEL_DIGGING, vector, Direction.DOWN, session.getWorldCache().nextPredictionSequence());
                 session.sendDownstreamPacket(abortBreakingPacket);
                 LevelEventPacket stopBreak = new LevelEventPacket();
                 stopBreak.setType(LevelEventType.BLOCK_STOP_BREAK);
                 stopBreak.setPosition(vector.toFloat());
                 stopBreak.setData(0);
-                session.setBreakingBlock(BlockStateValues.JAVA_AIR_ID);
+                session.setBreakingBlock(-1);
                 session.sendUpstreamPacket(stopBreak);
                 break;
             case STOP_BREAK:
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockEmoteTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockEmoteTranslator.java
index 187f6a98b..5d15761bd 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockEmoteTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockEmoteTranslator.java
@@ -44,7 +44,7 @@ public class BedrockEmoteTranslator extends PacketTranslator<EmotePacket> {
         if (session.getGeyser().getConfig().getEmoteOffhandWorkaround() != EmoteOffhandWorkaroundOption.DISABLED) {
             // Activate the workaround - we should trigger the offhand now
             ServerboundPlayerActionPacket swapHandsPacket = new ServerboundPlayerActionPacket(PlayerAction.SWAP_HANDS, Vector3i.ZERO,
-                    Direction.DOWN, session.getNextSequence());
+                    Direction.DOWN, session.getWorldCache().nextPredictionSequence());
             session.sendDownstreamPacket(swapHandsPacket);
 
             if (session.getGeyser().getConfig().getEmoteOffhandWorkaround() == EmoteOffhandWorkaroundOption.NO_EMOTES) {
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java
index 6926d33d2..b9f593961 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java
@@ -77,6 +77,13 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
         boolean positionChanged = !entity.getPosition().equals(packet.getPosition());
         boolean rotationChanged = entity.getYaw() != yaw || entity.getPitch() != pitch || entity.getHeadYaw() != headYaw;
 
+        if (session.getLookBackScheduledFuture() != null) {
+            // Resend the rotation if it was changed by Geyser
+            rotationChanged |= !session.getLookBackScheduledFuture().isDone();
+            session.getLookBackScheduledFuture().cancel(false);
+            session.setLookBackScheduledFuture(null);
+        }
+
         // If only the pitch and yaw changed
         // This isn't needed, but it makes the packets closer to vanilla
         // It also means you can't "lag back" while only looking, in theory
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginDisconnectTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginDisconnectTranslator.java
index ebe70a856..356fe645b 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginDisconnectTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginDisconnectTranslator.java
@@ -50,6 +50,8 @@ public class JavaLoginDisconnectTranslator extends PacketTranslator<ClientboundL
         if (disconnectReason instanceof TranslatableComponent component) {
             String key = component.key();
             isOutdatedMessage = "multiplayer.disconnect.incompatible".equals(key) ||
+                    // Seen with Velocity 1.18 rejecting a 1.19 client
+                    "multiplayer.disconnect.outdated_client".equals(key) ||
                     // Legacy string (starting from at least 1.15.2)
                     "multiplayer.disconnect.outdated_server".equals(key)
                     // Reproduced on 1.15.2 server with ViaVersion 4.0.0-21w20a with 1.18.2 Java client
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java
index 1c83bf2cc..b042eae2a 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java
@@ -25,7 +25,7 @@
 
 package org.geysermc.geyser.translator.protocol.java;
 
-import com.github.steveice10.mc.protocol.data.game.MessageType;
+import com.github.steveice10.mc.protocol.data.game.BuiltinChatType;
 import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundLoginPacket;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundCustomPayloadPacket;
 import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
@@ -82,14 +82,15 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
                     textDecoration = new TextDecoration(decorationTag);
                 }
             }
-            MessageType type = MessageType.from(((StringTag) tag.get("name")).getValue());
+            BuiltinChatType type = BuiltinChatType.from(((StringTag) tag.get("name")).getValue());
             // TODO new types?
-            TextPacket.Type bedrockType = switch (type) {
+            // The built-in type can be null if custom plugins/mods add in new types
+            TextPacket.Type bedrockType = type != null ? switch (type) {
                 case CHAT -> TextPacket.Type.CHAT;
                 case SYSTEM -> TextPacket.Type.SYSTEM;
                 case GAME_INFO -> TextPacket.Type.TIP;
                 default -> TextPacket.Type.RAW;
-            };
+            } : TextPacket.Type.RAW;
             chatTypes.put(id, new ChatTypeEntry(bedrockType, textDecoration));
         }
 
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSystemChatTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSystemChatTranslator.java
index 22942457a..af7928477 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSystemChatTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaSystemChatTranslator.java
@@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.protocol.java;
 import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundSystemChatPacket;
 import com.nukkitx.protocol.bedrock.packet.TextPacket;
 import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.text.ChatTypeEntry;
 import org.geysermc.geyser.translator.protocol.PacketTranslator;
 import org.geysermc.geyser.translator.protocol.Translator;
 import org.geysermc.geyser.translator.text.MessageTranslator;
@@ -37,11 +38,15 @@ public class JavaSystemChatTranslator extends PacketTranslator<ClientboundSystem
 
     @Override
     public void translate(GeyserSession session, ClientboundSystemChatPacket packet) {
+        ChatTypeEntry chatTypeEntry = session.getChatTypes().get(packet.getTypeId());
+        // This probably isn't proper but system chat won't care about the registry in 1.19.1 anyway
+        TextPacket.Type chatType = chatTypeEntry == null ? TextPacket.Type.RAW : chatTypeEntry.bedrockChatType();
+
         TextPacket textPacket = new TextPacket();
         textPacket.setPlatformChatId("");
         textPacket.setSourceName("");
         textPacket.setXuid(session.getAuthData().xuid());
-        textPacket.setType(session.getChatTypes().get(packet.getTypeId()).bedrockChatType());
+        textPacket.setType(chatType);
 
         textPacket.setNeedsTranslation(false);
         textPacket.setMessage(MessageTranslator.convertMessage(packet.getContent(), session.locale()));
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaEntityEventTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaEntityEventTranslator.java
index d82a20a27..3d44ce2fd 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaEntityEventTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaEntityEventTranslator.java
@@ -147,6 +147,7 @@ public class JavaEntityEventTranslator extends PacketTranslator<ClientboundEntit
                 soundPacket.setRelativeVolumeDisabled(false);
                 session.sendUpstreamPacket(soundPacket);
                 return;
+            case VILLAGER_MATE:
             case ANIMAL_EMIT_HEARTS:
                 entityEventPacket.setType(EntityEventType.LOVE_PARTICLES);
                 break;
@@ -176,6 +177,18 @@ public class JavaEntityEventTranslator extends PacketTranslator<ClientboundEntit
             case IRON_GOLEM_HOLD_POPPY:
                 entityEventPacket.setType(EntityEventType.GOLEM_FLOWER_OFFER);
                 break;
+            case VILLAGER_ANGRY:
+                entityEventPacket.setType(EntityEventType.VILLAGER_ANGRY);
+                break;
+            case VILLAGER_HAPPY:
+                entityEventPacket.setType(EntityEventType.VILLAGER_HAPPY);
+                break;
+            case VILLAGER_SWEAT:
+                LevelEventPacket levelEventPacket = new LevelEventPacket();
+                levelEventPacket.setType(LevelEventType.PARTICLE_SPLASH);
+                levelEventPacket.setPosition(entity.getPosition().up(entity.getDefinition().height()));
+                session.sendUpstreamPacket(levelEventPacket);
+                return;
             case IRON_GOLEM_EMPTY_HAND:
                 entityEventPacket.setType(EntityEventType.GOLEM_FLOWER_WITHDRAW);
                 break;
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaBlockChangedAckTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaBlockChangedAckTranslator.java
index 6afb0b3ef..523d0fdc4 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaBlockChangedAckTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaBlockChangedAckTranslator.java
@@ -35,6 +35,6 @@ public class JavaBlockChangedAckTranslator extends PacketTranslator<ClientboundB
 
     @Override
     public void translate(GeyserSession session, ClientboundBlockChangedAckPacket packet) {
-        // TODO
+        session.getWorldCache().endPredictionsUpTo(packet.getSequence());
     }
 }
\ No newline at end of file
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerAbilitiesTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerAbilitiesTranslator.java
index 44ae7f425..783f4e824 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerAbilitiesTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerAbilitiesTranslator.java
@@ -38,6 +38,8 @@ public class JavaPlayerAbilitiesTranslator extends PacketTranslator<ClientboundP
         session.setCanFly(packet.isCanFly());
         session.setFlying(packet.isFlying());
         session.setInstabuild(packet.isCreative());
+        session.setFlySpeed(packet.getFlySpeed());
+        session.setWalkSpeed(packet.getWalkSpeed());
         session.sendAdventureSettings();
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerCombatKillTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerCombatKillTranslator.java
new file mode 100644
index 000000000..89be26e4a
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerCombatKillTranslator.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.translator.protocol.java.entity.player;
+
+import com.github.steveice10.mc.protocol.packet.ingame.clientbound.entity.player.ClientboundPlayerCombatKillPacket;
+import com.nukkitx.protocol.bedrock.packet.DeathInfoPacket;
+import net.kyori.adventure.text.Component;
+import org.geysermc.geyser.network.GameProtocol;
+import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.translator.protocol.PacketTranslator;
+import org.geysermc.geyser.translator.protocol.Translator;
+import org.geysermc.geyser.translator.text.MessageTranslator;
+
+@Translator(packet = ClientboundPlayerCombatKillPacket.class)
+public class JavaPlayerCombatKillTranslator extends PacketTranslator<ClientboundPlayerCombatKillPacket> {
+
+    @Override
+    public void translate(GeyserSession session, ClientboundPlayerCombatKillPacket packet) {
+        if (packet.getPlayerId() == session.getPlayerEntity().getEntityId() && GameProtocol.supports1_19_10(session)) {
+            Component deathMessage = packet.getMessage();
+            // TODO - could inject score in, but as of 1.19.10 newlines don't center and start at the left of the first text
+            DeathInfoPacket deathInfoPacket = new DeathInfoPacket();
+            deathInfoPacket.setCauseAttackName(MessageTranslator.convertMessage(deathMessage, session.locale()));
+            session.sendUpstreamPacket(deathInfoPacket);
+        }
+    }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java
index f5d21ecc9..2d2d7279f 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java
@@ -31,6 +31,7 @@ import com.github.steveice10.mc.protocol.packet.ingame.serverbound.level.Serverb
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosRotPacket;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.entity.EntityLinkData;
+import com.nukkitx.protocol.bedrock.packet.ChunkRadiusUpdatedPacket;
 import com.nukkitx.protocol.bedrock.packet.MovePlayerPacket;
 import com.nukkitx.protocol.bedrock.packet.RespawnPacket;
 import com.nukkitx.protocol.bedrock.packet.SetEntityLinkPacket;
@@ -84,6 +85,15 @@ public class JavaPlayerPositionTranslator extends PacketTranslator<ClientboundPl
 
             acceptTeleport(session, packet.getX(), packet.getY(), packet.getZ(), packet.getYaw(), packet.getPitch(), packet.getTeleportId());
 
+            if (session.getServerRenderDistance() > 47 && !session.isEmulatePost1_13Logic()) {
+                // See DimensionUtils for an explanation
+                ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket();
+                chunkRadiusUpdatedPacket.setRadius(session.getServerRenderDistance());
+                session.sendUpstreamPacket(chunkRadiusUpdatedPacket);
+
+                session.setLastChunkPosition(null);
+            }
+
             ChunkUtils.updateChunkPosition(session, pos.toInt());
 
             if (session.getGeyser().getConfig().isDebugMode()) {
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java
index 36307e7bd..aef8cf8b2 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java
@@ -33,6 +33,7 @@ import com.nukkitx.protocol.bedrock.data.inventory.CraftingData;
 import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
 import com.nukkitx.protocol.bedrock.packet.CraftingDataPacket;
 import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
+import org.geysermc.geyser.GeyserImpl;
 import org.geysermc.geyser.inventory.GeyserItemStack;
 import org.geysermc.geyser.inventory.Inventory;
 import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe;
@@ -66,27 +67,41 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
         if (inventory == null)
             return;
 
-        // Intentional behavior here below the cursor; Minecraft 1.18.1 also does this.
-        int stateId = packet.getStateId();
-        session.setEmulatePost1_16Logic(stateId > 0 || stateId != inventory.getStateId());
-        inventory.setStateId(stateId);
-
         InventoryTranslator translator = session.getInventoryTranslator();
         if (translator != null) {
             if (session.getCraftingGridFuture() != null) {
                 session.getCraftingGridFuture().cancel(false);
             }
-            updateCraftingGrid(session, packet.getSlot(), packet.getItem(), inventory, translator);
+
+            int slot = packet.getSlot();
+            if (slot >= inventory.getSize()) {
+                GeyserImpl geyser = session.getGeyser();
+                geyser.getLogger().warning("ClientboundContainerSetSlotPacket sent to " + session.name()
+                        + " that exceeds inventory size!");
+                if (geyser.getConfig().isDebugMode()) {
+                    geyser.getLogger().debug(packet);
+                    geyser.getLogger().debug(inventory);
+                }
+                // 1.19.0 behavior: the state ID will not be set due to exception
+                return;
+            }
+
+            updateCraftingGrid(session, slot, packet.getItem(), inventory, translator);
 
             GeyserItemStack newItem = GeyserItemStack.from(packet.getItem());
             if (packet.getContainerId() == 0 && !(translator instanceof PlayerInventoryTranslator)) {
                 // In rare cases, the window ID can still be 0 but Java treats it as valid
-                session.getPlayerInventory().setItem(packet.getSlot(), newItem, session);
-                InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), packet.getSlot());
+                session.getPlayerInventory().setItem(slot, newItem, session);
+                InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), slot);
             } else {
-                inventory.setItem(packet.getSlot(), newItem, session);
-                translator.updateSlot(session, inventory, packet.getSlot());
+                inventory.setItem(slot, newItem, session);
+                translator.updateSlot(session, inventory, slot);
             }
+
+            // Intentional behavior here below the cursor; Minecraft 1.18.1 also does this.
+            int stateId = packet.getStateId();
+            session.setEmulatePost1_16Logic(stateId > 0 || stateId != inventory.getStateId());
+            inventory.setStateId(stateId);
         }
     }
 
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaCooldownTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaCooldownTranslator.java
new file mode 100644
index 000000000..f222682ae
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaCooldownTranslator.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.translator.protocol.java.level;
+
+import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundCooldownPacket;
+import com.nukkitx.protocol.bedrock.packet.PlayerStartItemCooldownPacket;
+import org.geysermc.geyser.inventory.item.StoredItemMappings;
+import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.translator.protocol.PacketTranslator;
+import org.geysermc.geyser.translator.protocol.Translator;
+
+@Translator(packet = ClientboundCooldownPacket.class)
+public class JavaCooldownTranslator extends PacketTranslator<ClientboundCooldownPacket> {
+
+    @Override
+    public void translate(GeyserSession session, ClientboundCooldownPacket packet) {
+        StoredItemMappings itemMappings = session.getItemMappings().getStoredItems();
+
+        int itemId = packet.getItemId();
+        // Not every item, as of 1.19, appears to be server-driven. Just these two.
+        // Use a map here if it gets too big.
+        String cooldownCategory;
+        if (itemId == itemMappings.goatHorn()) {
+            cooldownCategory = "goat_horn";
+        } else if (itemId == itemMappings.shield().getJavaId()) {
+            cooldownCategory = "shield";
+        } else {
+            cooldownCategory = null;
+        }
+
+        if (cooldownCategory != null) {
+            PlayerStartItemCooldownPacket bedrockPacket = new PlayerStartItemCooldownPacket();
+            bedrockPacket.setItemCategory(cooldownCategory);
+            bedrockPacket.setCooldownDuration(packet.getCooldownTicks());
+            session.sendUpstreamPacket(bedrockPacket);
+        }
+    }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java
index 5c7b2dd95..c4fabbd3d 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java
@@ -142,7 +142,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
                         }
 
                         // Check if block is piston or flower to see if we'll need to create additional block entities, as they're only block entities in Bedrock
-                        if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isCauldron(javaId)) {
+                        if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isNonWaterCauldron(javaId)) {
                             bedrockBlockEntities.add(BedrockOnlyBlockEntity.getTag(session,
                                     Vector3i.from((packet.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (packet.getZ() << 4) + ((yzx >> 4) & 0xF)),
                                     javaId
@@ -183,7 +183,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
                     }
 
                     // Check if block is piston, flower or cauldron to see if we'll need to create additional block entities, as they're only block entities in Bedrock
-                    if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isCauldron(javaId)) {
+                    if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isNonWaterCauldron(javaId)) {
                         bedrockOnlyBlockEntityIds.set(i);
                     }
                 }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/sound/block/BucketSoundInteractionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/sound/block/BucketSoundInteractionTranslator.java
index 2cbcd329a..62378b3d9 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/sound/block/BucketSoundInteractionTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/sound/block/BucketSoundInteractionTranslator.java
@@ -38,7 +38,7 @@ public class BucketSoundInteractionTranslator implements BlockSoundInteractionTr
 
     @Override
     public void translate(GeyserSession session, Vector3f position, String identifier) {
-        if (session.getBucketScheduledFuture() == null) {
+        if (!session.isPlacedBucket()) {
             return; // No bucket was really interacted with
         }
         GeyserItemStack itemStack = session.getPlayerInventory().getItemInHand();
@@ -71,6 +71,7 @@ public class BucketSoundInteractionTranslator implements BlockSoundInteractionTr
             case "minecraft:salmon_bucket":
             case "minecraft:pufferfish_bucket":
             case "minecraft:tropical_fish_bucket":
+            case "minecraft:tadpole_bucket":
                 soundEvent = SoundEvent.BUCKET_EMPTY_FISH;
                 break;
             case "minecraft:water_bucket":
@@ -83,7 +84,7 @@ public class BucketSoundInteractionTranslator implements BlockSoundInteractionTr
         if (soundEvent != null) {
             soundEventPacket.setSound(soundEvent);
             session.sendUpstreamPacket(soundEventPacket);
-            session.setBucketScheduledFuture(null);
+            session.setPlacedBucket(false);
         }
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java b/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java
index 7059c9a8b..c0d484919 100644
--- a/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java
+++ b/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java
@@ -36,6 +36,8 @@ import org.geysermc.geyser.registry.type.ItemMapping;
 import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.translator.collision.BlockCollision;
 
+import javax.annotation.Nullable;
+
 public final class BlockUtils {
 
     private static boolean correctTool(GeyserSession session, BlockMapping blockMapping, String itemToolType) {
@@ -101,7 +103,7 @@ public final class BlockUtils {
     // https://minecraft.gamepedia.com/Breaking
     private static double calculateBreakTime(double blockHardness, String toolTier, boolean canHarvestWithHand, boolean correctTool, boolean canTierMineBlock,
                                              String toolType, boolean isShearsEffective, int toolEfficiencyLevel, int hasteLevel, int miningFatigueLevel,
-                                             boolean insideOfWaterWithoutAquaAffinity, boolean outOfWaterButNotOnGround, boolean insideWaterAndNotOnGround) {
+                                             boolean insideOfWaterWithoutAquaAffinity, boolean onGround) {
         double baseTime = (((correctTool && canTierMineBlock) || canHarvestWithHand) ? 1.5 : 5.0) * blockHardness;
         double speed = 1.0 / baseTime;
 
@@ -129,12 +131,11 @@ public final class BlockUtils {
         }
 
         if (insideOfWaterWithoutAquaAffinity) speed *= 0.2;
-        if (outOfWaterButNotOnGround) speed *= 0.2;
-        if (insideWaterAndNotOnGround) speed *= 0.2;
+        if (!onGround) speed *= 0.2;
         return 1.0 / speed;
     }
 
-    public static double getBreakTime(GeyserSession session, BlockMapping blockMapping, ItemMapping item, CompoundTag nbtData, boolean isSessionPlayer) {
+    public static double getBreakTime(GeyserSession session, BlockMapping blockMapping, ItemMapping item, @Nullable CompoundTag nbtData, boolean isSessionPlayer) {
         boolean isShearsEffective = session.getTagCache().isShearsEffective(blockMapping); //TODO called twice
         boolean canHarvestWithHand = blockMapping.isCanBreakWithHand();
         String toolType = "";
@@ -154,36 +155,28 @@ public final class BlockUtils {
         if (!isSessionPlayer) {
             // Another entity is currently mining; we have all the information we know
             return calculateBreakTime(blockMapping.getHardness(), toolTier, canHarvestWithHand, correctTool, toolCanBreak, toolType, isShearsEffective,
-                    toolEfficiencyLevel, hasteLevel, miningFatigueLevel, false,
-                    false, false);
+                    toolEfficiencyLevel, hasteLevel, miningFatigueLevel, false, true);
         }
 
         hasteLevel = Math.max(session.getEffectCache().getHaste(), session.getEffectCache().getConduitPower());
         miningFatigueLevel = session.getEffectCache().getMiningFatigue();
 
-        boolean isInWater = session.getCollisionManager().isPlayerInWater();
-
-        boolean insideOfWaterWithoutAquaAffinity = isInWater &&
+        boolean waterInEyes = session.getCollisionManager().isWaterInEyes();
+        boolean insideOfWaterWithoutAquaAffinity = waterInEyes &&
                 ItemUtils.getEnchantmentLevel(session.getPlayerInventory().getItem(5).getNbt(), "minecraft:aqua_affinity") < 1;
 
-        boolean outOfWaterButNotOnGround = (!isInWater) && (!session.getPlayerEntity().isOnGround());
-        boolean insideWaterNotOnGround = isInWater && !session.getPlayerEntity().isOnGround();
         return calculateBreakTime(blockMapping.getHardness(), toolTier, canHarvestWithHand, correctTool, toolCanBreak, toolType, isShearsEffective,
-                toolEfficiencyLevel, hasteLevel, miningFatigueLevel, insideOfWaterWithoutAquaAffinity,
-                outOfWaterButNotOnGround, insideWaterNotOnGround);
+                toolEfficiencyLevel, hasteLevel, miningFatigueLevel, insideOfWaterWithoutAquaAffinity, session.getPlayerEntity().isOnGround());
     }
 
     public static double getSessionBreakTime(GeyserSession session, BlockMapping blockMapping) {
         PlayerInventory inventory = session.getPlayerInventory();
         GeyserItemStack item = inventory.getItemInHand();
-        ItemMapping mapping;
-        CompoundTag nbtData;
+        ItemMapping mapping = ItemMapping.AIR;
+        CompoundTag nbtData = null;
         if (item != null) {
             mapping = item.getMapping(session);
             nbtData = item.getNbt();
-        } else {
-            mapping = ItemMapping.AIR;
-            nbtData = new CompoundTag("");
         }
         return getBreakTime(session, blockMapping, mapping, nbtData, true);
     }
diff --git a/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java b/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java
index d1ee9f165..b87f82f90 100644
--- a/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java
+++ b/core/src/main/java/org/geysermc/geyser/util/ChunkUtils.java
@@ -123,13 +123,21 @@ public class ChunkUtils {
      * @param position the position of the block
      */
     public static void updateBlock(GeyserSession session, int blockState, Vector3i position) {
+        updateBlockClientSide(session, blockState, position);
+        session.getChunkCache().updateBlock(position.getX(), position.getY(), position.getZ(), blockState);
+        session.getWorldCache().updateServerCorrectBlockState(position);
+    }
+
+    /**
+     * Updates a block, but client-side only.
+     */
+    public static void updateBlockClientSide(GeyserSession session, int blockState, Vector3i position) {
         // Checks for item frames so they aren't tripped up and removed
         ItemFrameEntity itemFrameEntity = ItemFrameEntity.getItemFrameEntity(session, position);
         if (itemFrameEntity != null) {
             if (blockState == JAVA_AIR_ID) { // Item frame is still present and no block overrides that; refresh it
                 itemFrameEntity.updateBlock(true);
-                // Still update the chunk cache with the new block
-                session.getChunkCache().updateBlock(position.getX(), position.getY(), position.getZ(), blockState);
+                // Still update the chunk cache with the new block if updateBlock is called
                 return;
             }
             // Otherwise, let's still store our reference to the item frame, but let the new block take precedence for now
@@ -175,7 +183,6 @@ public class ChunkUtils {
                 break; //No block will be a part of two classes
             }
         }
-        session.getChunkCache().updateBlock(position.getX(), position.getY(), position.getZ(), blockState);
     }
 
     public static void sendEmptyChunk(GeyserSession session, int chunkX, int chunkZ, boolean forceUpdate) {
diff --git a/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java b/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java
index fbc891131..7e5d65a97 100644
--- a/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java
+++ b/core/src/main/java/org/geysermc/geyser/util/DimensionUtils.java
@@ -28,6 +28,7 @@ package org.geysermc.geyser.util;
 import com.github.steveice10.mc.protocol.data.game.entity.Effect;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.packet.ChangeDimensionPacket;
+import com.nukkitx.protocol.bedrock.packet.ChunkRadiusUpdatedPacket;
 import com.nukkitx.protocol.bedrock.packet.MobEffectPacket;
 import com.nukkitx.protocol.bedrock.packet.StopSoundPacket;
 import org.geysermc.geyser.entity.type.Entity;
@@ -69,6 +70,22 @@ public class DimensionUtils {
         session.getPistonCache().clear();
         session.getSkullCache().clear();
 
+        if (session.getServerRenderDistance() > 47 && !session.isEmulatePost1_13Logic()) {
+            // The server-sided view distance wasn't a thing until Minecraft Java 1.14
+            // So ViaVersion compensates by sending a "view distance" of 64
+            // That's fine, except when the actual view distance sent from the server is five chunks
+            // The client locks up when switching dimensions, expecting more chunks than it's getting
+            // To solve this, we cap at 32 unless we know that the render distance actually exceeds 32
+            // 47 is the Bedrock equivalent of 32
+            // Also, as of 1.19: PS4 crashes with a ChunkRadiusUpdatedPacket too large
+            session.getGeyser().getLogger().debug("Applying dimension switching workaround for Bedrock render distance of "
+                    + session.getServerRenderDistance());
+            ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket();
+            chunkRadiusUpdatedPacket.setRadius(47);
+            session.sendUpstreamPacket(chunkRadiusUpdatedPacket);
+            // Will be re-adjusted on spawn
+        }
+
         Vector3f pos = Vector3f.from(0, Short.MAX_VALUE, 0);
 
         ChangeDimensionPacket changeDimensionPacket = new ChangeDimensionPacket();
diff --git a/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java b/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java
index 43ccff5e0..d128989a8 100644
--- a/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java
+++ b/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java
@@ -187,6 +187,15 @@ public final class EntityUtils {
                 case MINECART, HOPPER_MINECART, TNT_MINECART, CHEST_MINECART, FURNACE_MINECART, SPAWNER_MINECART,
                         COMMAND_BLOCK_MINECART, BOAT, CHEST_BOAT -> yOffset -= mount.getDefinition().height() * 0.5f;
             }
+            if (passenger.getDefinition().entityType() == EntityType.FALLING_BLOCK) {
+                yOffset += 0.5f;
+            }
+            if (mount.getDefinition().entityType() == EntityType.ARMOR_STAND) {
+                ArmorStandEntity armorStand = (ArmorStandEntity) mount;
+                if (armorStand.isPositionRequiresOffset()) {
+                    yOffset -= EntityDefinitions.ARMOR_STAND.height() * (armorStand.isSmall() ? 0.55d : 1d);
+                }
+            }
             Vector3f offset = Vector3f.from(xOffset, yOffset, zOffset);
             passenger.setRiderSeatPosition(offset);
         }
diff --git a/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java b/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java
index abc95839d..c8d6e42d7 100644
--- a/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java
+++ b/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java
@@ -314,7 +314,14 @@ public class LoginEncryptionUtils {
                         .label("geyser.auth.login.form.details.desc")
                         .input("geyser.auth.login.form.details.email", "account@geysermc.org", "")
                         .input("geyser.auth.login.form.details.pass", "123456", "")
-                        .closedOrInvalidResultHandler(() -> buildAndShowLoginDetailsWindow(session))
+                        .invalidResultHandler(() -> buildAndShowLoginDetailsWindow(session))
+                        .closedResultHandler(() -> {
+                            if (session.isMicrosoftAccount()) {
+                                buildAndShowMicrosoftAuthenticationWindow(session);
+                            } else {
+                                buildAndShowLoginWindow(session);
+                            }
+                        })
                         .validResultHandler((response) -> session.authenticate(response.next(), response.next())));
     }
 
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 0bc694b43..dd08f3922 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -8,7 +8,12 @@ dependencyResolutionManagement {
             mavenContent { releasesOnly() }
         }
         maven("https://repo.opencollab.dev/maven-snapshots") {
-            mavenContent { snapshotsOnly() }
+            mavenContent {
+                // This has the unintended side effect of not allowing snapshot version pinning.
+                // Likely a bug in Gradle's implementation of snapshot pinning
+                // See https://github.com/gradle/gradle/pull/406
+                snapshotsOnly() 
+            }
         }
 
         // Paper, Velocity