diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotFallbackWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotFallbackWorldManager.java
index 4cac791a0..a9de94db5 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotFallbackWorldManager.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotFallbackWorldManager.java
@@ -52,7 +52,7 @@ public class GeyserSpigotFallbackWorldManager extends GeyserSpigotWorldManager {
     }
 
     @Override
-    public boolean hasMoreBlockDataThanChunkCache() {
+    public boolean hasOwnChunkCache() {
         return false;
     }
 
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotWorldManager.java
index 13f696fd5..ba61eeb72 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotWorldManager.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/platform/spigot/world/manager/GeyserSpigotWorldManager.java
@@ -140,7 +140,7 @@ public class GeyserSpigotWorldManager extends GeyserWorldManager {
     }
 
     @Override
-    public boolean hasMoreBlockDataThanChunkCache() {
+    public boolean hasOwnChunkCache() {
         return true;
     }
 
@@ -235,6 +235,7 @@ public class GeyserSpigotWorldManager extends GeyserWorldManager {
             NbtMap blockEntityTag = lecternTag.build();
             BlockEntityUtils.updateBlockEntity(session, blockEntityTag, Vector3i.from(x, y, z));
         };
+
         if (isChunkLoad) {
             // Delay to ensure the chunk is sent first, and then the lectern data
             Bukkit.getScheduler().runTaskLater(this.plugin, lecternInfoGet, 5);
diff --git a/connector/pom.xml b/connector/pom.xml
index 5a462c5c5..4ae7e1383 100644
--- a/connector/pom.xml
+++ b/connector/pom.xml
@@ -13,7 +13,7 @@
     <properties>
         <netty.version>4.1.59.Final</netty.version>
         <fastutil.version>8.5.2</fastutil.version>
-        <adventure.version>4.5.0</adventure.version>
+        <adventure.version>4.7.0</adventure.version>
     </properties>
 
     <dependencies>
@@ -151,9 +151,9 @@
             </exclusions>
         </dependency>
         <dependency>
-            <groupId>com.github.steveice10</groupId>
+            <groupId>com.github.GeyserMC</groupId>
             <artifactId>PacketLib</artifactId>
-            <version>54f761c</version>
+            <version>b77a427</version>
             <scope>compile</scope>
             <exclusions>
                 <exclusion> <!-- Move this exclusion back to MCProtocolLib it gets the latest PacketLib -->
diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java
index 049c58221..3a40474f1 100644
--- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java
+++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java
@@ -40,7 +40,6 @@ import org.geysermc.connector.common.AuthType;
 import org.geysermc.connector.configuration.GeyserConfiguration;
 import org.geysermc.connector.metrics.Metrics;
 import org.geysermc.connector.network.ConnectorServerEventHandler;
-import org.geysermc.connector.network.remote.RemoteServer;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.BiomeTranslator;
 import org.geysermc.connector.network.translators.EntityIdentifierRegistry;
@@ -103,9 +102,8 @@ public class GeyserConnector {
 
     private static GeyserConnector instance;
 
-    private RemoteServer remoteServer;
     @Setter
-    private AuthType authType;
+    private AuthType defaultAuthType;
 
     private FloodgateCipher cipher;
     private FloodgateSkinUploader skinUploader;
@@ -175,7 +173,7 @@ public class GeyserConnector {
         String remoteAddress = config.getRemote().getAddress();
         int remotePort = config.getRemote().getPort();
         // Filters whether it is not an IP address or localhost, because otherwise it is not possible to find out an SRV entry.
-        if ((config.isLegacyPingPassthrough() || platformType == PlatformType.STANDALONE) && !remoteAddress.matches(IP_REGEX) && !remoteAddress.equalsIgnoreCase("localhost")) {
+        if (!remoteAddress.matches(IP_REGEX) && !remoteAddress.equalsIgnoreCase("localhost")) {
             try {
                 // Searches for a server address and a port from a SRV record of the specified host name
                 InitialDirContext ctx = new InitialDirContext();
@@ -195,8 +193,7 @@ public class GeyserConnector {
             }
         }
 
-        remoteServer = new RemoteServer(config.getRemote().getAddress(), remotePort);
-        authType = AuthType.getByName(config.getRemote().getAuthType());
+        defaultAuthType = AuthType.getByName(config.getRemote().getAuthType());
 
         if (authType == AuthType.FLOODGATE) {
             try {
@@ -355,8 +352,7 @@ public class GeyserConnector {
         generalThreadPool.shutdown();
         bedrockServer.close();
         players.clear();
-        remoteServer = null;
-        authType = null;
+        defaultAuthType = null;
         this.getCommandManager().getCommands().clear();
 
         bootstrap.getGeyserLogger().info(LanguageUtils.getLocaleStringLog("geyser.core.shutdown.done"));
@@ -392,6 +388,7 @@ public class GeyserConnector {
      * @param xuid the Xbox user identifier
      * @return the player or <code>null</code> if there is no player online with this xuid
      */
+    @SuppressWarnings("unused") // API usage
     public GeyserSession getPlayerByXuid(String xuid) {
         for (GeyserSession session : players) {
             if (session.getAuthData() != null && session.getAuthData().getXboxUUID().equals(xuid)) {
diff --git a/connector/src/main/java/org/geysermc/connector/entity/Entity.java b/connector/src/main/java/org/geysermc/connector/entity/Entity.java
index 741f5fcd0..2dcd49fb0 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/Entity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/Entity.java
@@ -28,12 +28,6 @@ package org.geysermc.connector.entity;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.MetadataType;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.Pose;
-import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
-import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
-import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction;
-import com.github.steveice10.mc.protocol.data.game.world.block.BlockFace;
-import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerActionPacket;
-import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerUseItemPacket;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.protocol.bedrock.data.AttributeData;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
@@ -50,11 +44,9 @@ import org.geysermc.connector.entity.attribute.AttributeType;
 import org.geysermc.connector.entity.living.ArmorStandEntity;
 import org.geysermc.connector.entity.player.PlayerEntity;
 import org.geysermc.connector.entity.type.EntityType;
-import org.geysermc.connector.inventory.PlayerInventory;
 import org.geysermc.connector.network.session.GeyserSession;
-import org.geysermc.connector.network.translators.item.ItemRegistry;
-import org.geysermc.connector.utils.AttributeUtils;
 import org.geysermc.connector.network.translators.chat.MessageTranslator;
+import org.geysermc.connector.utils.AttributeUtils;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -278,29 +270,6 @@ public class Entity {
                     if (!this.is(ArmorStandEntity.class)) {
                         metadata.getFlags().setFlag(EntityFlag.INVISIBLE, (xd & 0x20) == 0x20);
                     }
-
-                    // Shield code
-                    if (session.getPlayerEntity().getEntityId() == entityId && metadata.getFlags().getFlag(EntityFlag.SNEAKING)) {
-                        PlayerInventory playerInv = session.getPlayerInventory();
-                        if ((playerInv.getItemInHand().getJavaId() == ItemRegistry.SHIELD.getJavaId()) ||
-                                (playerInv.getOffhand().getJavaId() == ItemRegistry.SHIELD.getJavaId())) {
-                            ClientPlayerUseItemPacket useItemPacket;
-                            metadata.getFlags().setFlag(EntityFlag.BLOCKING, true);
-                            if (playerInv.getItemInHand().getJavaId() == ItemRegistry.SHIELD.getJavaId()) {
-                                useItemPacket = new ClientPlayerUseItemPacket(Hand.MAIN_HAND);
-                            }
-                            // Else we just assume it's the offhand, to simplify logic and to assure the packet gets sent
-                            else {
-                                useItemPacket = new ClientPlayerUseItemPacket(Hand.OFF_HAND);
-                            }
-                            session.sendDownstreamPacket(useItemPacket);
-                        }
-                    } else if (session.getPlayerEntity().getEntityId() == entityId && !metadata.getFlags().getFlag(EntityFlag.SNEAKING) && metadata.getFlags().getFlag(EntityFlag.BLOCKING)) {
-                        metadata.getFlags().setFlag(EntityFlag.BLOCKING, false);
-                        metadata.getFlags().setFlag(EntityFlag.IS_AVOIDING_BLOCK, true);
-                        ClientPlayerActionPacket releaseItemPacket = new ClientPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, new Position(0, 0, 0), BlockFace.DOWN);
-                        session.sendDownstreamPacket(releaseItemPacket);
-                    }
                 }
                 break;
             case 1: // Air/bubbles
@@ -331,15 +300,12 @@ public class Entity {
             case 6: // Pose change
                 if (entityMetadata.getValue().equals(Pose.SLEEPING)) {
                     metadata.getFlags().setFlag(EntityFlag.SLEEPING, true);
-                    // Has to be a byte or it does not work
-                    metadata.put(EntityData.PLAYER_FLAGS, (byte) 2);
                     metadata.put(EntityData.BOUNDING_BOX_WIDTH, 0.2f);
                     metadata.put(EntityData.BOUNDING_BOX_HEIGHT, 0.2f);
                 } else if (metadata.getFlags().getFlag(EntityFlag.SLEEPING)) {
                     metadata.getFlags().setFlag(EntityFlag.SLEEPING, false);
                     metadata.put(EntityData.BOUNDING_BOX_WIDTH, getEntityType().getWidth());
                     metadata.put(EntityData.BOUNDING_BOX_HEIGHT, getEntityType().getHeight());
-                    metadata.put(EntityData.PLAYER_FLAGS, (byte) 0);
                 }
                 break;
         }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java b/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java
index 06e56ad03..0738c3819 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/FishingHookEntity.java
@@ -96,27 +96,25 @@ public class FishingHookEntity extends ThrowableEntity {
         boolean touchingWater = false;
         boolean collided = false;
         for (Vector3i blockPos : collidableBlocks) {
-            if (0 <= blockPos.getY() && blockPos.getY() <= 255) {
-                int blockID = session.getConnector().getWorldManager().getBlockAt(session, blockPos);
-                BlockCollision blockCollision = CollisionTranslator.getCollision(blockID, blockPos.getX(), blockPos.getY(), blockPos.getZ());
-                if (blockCollision != null && blockCollision.checkIntersection(boundingBox)) {
-                    // TODO Push bounding box out of collision to improve movement
-                    collided = true;
-                }
+            int blockID = session.getConnector().getWorldManager().getBlockAt(session, blockPos);
+            BlockCollision blockCollision = CollisionTranslator.getCollision(blockID, blockPos.getX(), blockPos.getY(), blockPos.getZ());
+            if (blockCollision != null && blockCollision.checkIntersection(boundingBox)) {
+                // TODO Push bounding box out of collision to improve movement
+                collided = true;
+            }
 
-                int waterLevel = BlockStateValues.getWaterLevel(blockID);
-                if (BlockTranslator.isWaterlogged(blockID)) {
-                    waterLevel = 0;
+            int waterLevel = BlockStateValues.getWaterLevel(blockID);
+            if (BlockTranslator.isWaterlogged(blockID)) {
+                waterLevel = 0;
+            }
+            if (waterLevel >= 0) {
+                double waterMaxY = blockPos.getY() + 1 - (waterLevel + 1) / 9.0;
+                // Falling water is a full block
+                if (waterLevel >= 8) {
+                    waterMaxY = blockPos.getY() + 1;
                 }
-                if (waterLevel >= 0) {
-                    double waterMaxY = blockPos.getY() + 1 - (waterLevel + 1) / 9.0;
-                    // Falling water is a full block
-                    if (waterLevel >= 8) {
-                        waterMaxY = blockPos.getY() + 1;
-                    }
-                    if (position.getY() <= waterMaxY) {
-                        touchingWater = true;
-                    }
+                if (position.getY() <= waterMaxY) {
+                    touchingWater = true;
                 }
             }
         }
@@ -177,10 +175,8 @@ public class FishingHookEntity extends ThrowableEntity {
      */
     protected boolean isInAir(GeyserSession session) {
         if (session.getConnector().getConfig().isCacheChunks()) {
-            if (0 <= position.getFloorY() && position.getFloorY() <= 255) {
-                int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt());
-                return block == BlockTranslator.JAVA_AIR_ID;
-            }
+            int block = session.getConnector().getWorldManager().getBlockAt(session, position.toInt());
+            return block == BlockTranslator.JAVA_AIR_ID;
         }
         return false;
     }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/LivingEntity.java b/connector/src/main/java/org/geysermc/connector/entity/LivingEntity.java
index f38f1e6b2..4dc0998aa 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/LivingEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/LivingEntity.java
@@ -99,6 +99,13 @@ public class LivingEntity extends Entity {
                         // Bed has to be updated, or else player is floating in the air
                         ChunkUtils.updateBlock(session, bed, bedPosition);
                     }
+                    // Indicate that the player should enter the sleep cycle
+                    // Has to be a byte or it does not work
+                    // (Bed position is what actually triggers sleep - "pose" is only optional)
+                    metadata.put(EntityData.PLAYER_FLAGS, (byte) 2);
+                } else {
+                    // Player is no longer sleeping
+                    metadata.put(EntityData.PLAYER_FLAGS, (byte) 0);
                 }
                 break;
         }
diff --git a/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java b/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java
index 56354774d..fa5785fe5 100644
--- a/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java
+++ b/connector/src/main/java/org/geysermc/connector/entity/living/merchant/VillagerEntity.java
@@ -26,7 +26,6 @@
 package org.geysermc.connector.entity.living.merchant;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
-import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.VillagerData;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.math.vector.Vector3i;
@@ -101,11 +100,17 @@ public class VillagerEntity extends AbstractMerchantEntity {
     
     @Override
     public void moveRelative(GeyserSession session, double relX, double relY, double relZ, Vector3f rotation, boolean isOnGround) {
+        if (!metadata.getFlags().getFlag(EntityFlag.SLEEPING)) {
+            // No need to worry about extra processing to compensate for sleeping
+            super.moveRelative(session, relX, relY, relZ, rotation, isOnGround);
+            return;
+        }
+
         int z = 0;
         int bedId = 0;
         float bedPositionSubtractorW = 0;
         float bedPositionSubtractorN = 0;
-        Vector3i bedPosition = metadata.getPos(EntityData.BED_POSITION);
+        Vector3i bedPosition = metadata.getPos(EntityData.BED_POSITION, null);
         if (session.getConnector().getConfig().isCacheChunks() && bedPosition != null) {
             bedId = session.getConnector().getWorldManager().getBlockAt(session, bedPosition);
         }
@@ -117,39 +122,33 @@ public class VillagerEntity extends AbstractMerchantEntity {
         MoveEntityAbsolutePacket moveEntityPacket = new MoveEntityAbsolutePacket();
         moveEntityPacket.setRuntimeEntityId(geyserId);
         //Sets Villager position and rotation when sleeping
-        if (!metadata.getFlags().getFlag(EntityFlag.SLEEPING)) {
-            moveEntityPacket.setPosition(position);
-            moveEntityPacket.setRotation(getBedrockRotation());
-        } else {
-            //String Setup
-            Pattern r = Pattern.compile("facing=([a-z]+)");
-            Matcher m = r.matcher(bedRotationZ);
-            if (m.find()) {
-                switch (m.group(0)) {
-                    case "facing=south":
-                        //bed is facing south
-                        z = 180;
-                        bedPositionSubtractorW = -.5f; 
-                        break;
-                    case "facing=east":
-                        //bed is facing east
-                        z = 90;
-                        bedPositionSubtractorW = -.5f;
-                        break;
-                    case "facing=west":
-                        //bed is facing west
-                        z = 270;
-                        bedPositionSubtractorW = .5f;
-                        break;
-                    case "facing=north":
-                        //rotation does not change because north is 0
-                        bedPositionSubtractorN = .5f;
-                        break;
-                }
+        Pattern r = Pattern.compile("facing=([a-z]+)");
+        Matcher m = r.matcher(bedRotationZ);
+        if (m.find()) {
+            switch (m.group(0)) {
+                case "facing=south":
+                    //bed is facing south
+                    z = 180;
+                    bedPositionSubtractorW = -.5f;
+                    break;
+                case "facing=east":
+                    //bed is facing east
+                    z = 90;
+                    bedPositionSubtractorW = -.5f;
+                    break;
+                case "facing=west":
+                    //bed is facing west
+                    z = 270;
+                    bedPositionSubtractorW = .5f;
+                    break;
+                case "facing=north":
+                    //rotation does not change because north is 0
+                    bedPositionSubtractorN = .5f;
+                    break;
             }
-            moveEntityPacket.setRotation(Vector3f.from(0, 0, z));
-            moveEntityPacket.setPosition(Vector3f.from(position.getX() + bedPositionSubtractorW, position.getY(), position.getZ() + bedPositionSubtractorN));
         }
+        moveEntityPacket.setRotation(Vector3f.from(0, 0, z));
+        moveEntityPacket.setPosition(Vector3f.from(position.getX() + bedPositionSubtractorW, position.getY(), position.getZ() + bedPositionSubtractorN));
         moveEntityPacket.setOnGround(isOnGround);
         moveEntityPacket.setTeleported(false);
         session.sendUpstreamPacket(moveEntityPacket);
diff --git a/connector/src/main/java/org/geysermc/connector/inventory/Generic3X3Container.java b/connector/src/main/java/org/geysermc/connector/inventory/Generic3X3Container.java
index 8c89cdeb6..080e11982 100644
--- a/connector/src/main/java/org/geysermc/connector/inventory/Generic3X3Container.java
+++ b/connector/src/main/java/org/geysermc/connector/inventory/Generic3X3Container.java
@@ -27,10 +27,13 @@ package org.geysermc.connector.inventory;
 
 import com.github.steveice10.mc.protocol.data.game.window.WindowType;
 import lombok.Getter;
+import org.geysermc.connector.network.session.GeyserSession;
 
 public class Generic3X3Container extends Container {
     /**
-     * Whether we need to set the container type as {@link com.nukkitx.protocol.bedrock.data.inventory.ContainerType#DROPPER}
+     * Whether we need to set the container type as {@link com.nukkitx.protocol.bedrock.data.inventory.ContainerType#DROPPER}.
+     *
+     * Used at {@link org.geysermc.connector.network.translators.inventory.translators.Generic3X3InventoryTranslator#openInventory(GeyserSession, Inventory)}
      */
     @Getter
     private boolean isDropper = false;
diff --git a/connector/src/main/java/org/geysermc/connector/inventory/GeyserItemStack.java b/connector/src/main/java/org/geysermc/connector/inventory/GeyserItemStack.java
index 7cdaf1801..b4e91c1d6 100644
--- a/connector/src/main/java/org/geysermc/connector/inventory/GeyserItemStack.java
+++ b/connector/src/main/java/org/geysermc/connector/inventory/GeyserItemStack.java
@@ -62,6 +62,10 @@ public class GeyserItemStack {
         this.netId = netId;
     }
 
+    public static GeyserItemStack from(ItemStack itemStack) {
+        return itemStack == null ? EMPTY : new GeyserItemStack(itemStack.getId(), itemStack.getAmount(), itemStack.getNbt());
+    }
+
     public int getJavaId() {
         return isEmpty() ? 0 : javaId;
     }
@@ -74,10 +78,6 @@ public class GeyserItemStack {
         return isEmpty() ? null : nbt;
     }
 
-    public void setNetId(int netId) {
-        this.netId = netId;
-    }
-
     public int getNetId() {
         return isEmpty() ? 0 : netId;
     }
@@ -90,10 +90,6 @@ public class GeyserItemStack {
         amount -= sub;
     }
 
-    public static GeyserItemStack from(ItemStack itemStack) {
-        return itemStack == null ? EMPTY : new GeyserItemStack(itemStack.getId(), itemStack.getAmount(), itemStack.getNbt());
-    }
-
     public ItemStack getItemStack() {
         return getItemStack(amount);
     }
diff --git a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java
index fa2670be1..b073e3baf 100644
--- a/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java
+++ b/connector/src/main/java/org/geysermc/connector/network/UpstreamPacketHandler.java
@@ -101,7 +101,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
     public boolean handle(ResourcePackClientResponsePacket packet) {
         switch (packet.getStatus()) {
             case COMPLETED:
-                session.connect(connector.getRemoteServer());
+                session.connect();
                 connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.connect", session.getAuthData().getName()));
                 break;
 
@@ -176,7 +176,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
     public boolean handle(SetLocalPlayerAsInitializedPacket packet) {
         LanguageUtils.loadGeyserLocale(session.getLocale());
 
-        if (!session.isLoggedIn() && !session.isLoggingIn() && session.getConnector().getAuthType() == AuthType.ONLINE) {
+        if (!session.isLoggedIn() && !session.isLoggingIn() && session.getRemoteAuthType() == AuthType.ONLINE) {
             // TODO it is safer to key authentication on something that won't change (UUID, not username)
             if (!couldLoginUserByName(session.getAuthData().getName())) {
                 LoginEncryptionUtils.buildAndShowLoginWindow(session);
diff --git a/connector/src/main/java/org/geysermc/connector/network/remote/RemoteServer.java b/connector/src/main/java/org/geysermc/connector/network/remote/RemoteServer.java
deleted file mode 100644
index b957b90d6..000000000
--- a/connector/src/main/java/org/geysermc/connector/network/remote/RemoteServer.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (c) 2019-2021 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.connector.network.remote;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-@Getter
-@AllArgsConstructor
-public class RemoteServer {
-
-    private String address;
-    private int port;
-}
\ No newline at end of file
diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java
index ee2a63e6f..253088bba 100644
--- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java
+++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java
@@ -79,7 +79,6 @@ import org.geysermc.connector.entity.player.SessionPlayerEntity;
 import org.geysermc.connector.entity.player.SkullPlayerEntity;
 import org.geysermc.connector.inventory.Inventory;
 import org.geysermc.connector.inventory.PlayerInventory;
-import org.geysermc.connector.network.remote.RemoteServer;
 import org.geysermc.connector.network.session.auth.AuthData;
 import org.geysermc.connector.network.session.auth.BedrockClientData;
 import org.geysermc.connector.network.session.cache.*;
@@ -112,13 +111,21 @@ public class GeyserSession implements CommandSender {
 
     private final GeyserConnector connector;
     private final UpstreamSession upstream;
-    private RemoteServer remoteServer;
     private Client downstream;
     @Setter
     private AuthData authData;
     @Setter
     private BedrockClientData clientData;
 
+    /* Setter for GeyserConnect */
+    @Setter
+    private String remoteAddress;
+    @Setter
+    private int remotePort;
+    @Setter
+    private AuthType remoteAuthType;
+    /* Setter for GeyserConnect */
+
     @Deprecated
     @Setter
     private boolean microsoftAccount;
@@ -256,6 +263,12 @@ public class GeyserSession implements CommandSender {
     @Setter
     private Entity ridingVehicleEntity;
 
+    /**
+     * The entity that the client is currently looking at.
+     */
+    @Setter
+    private Entity mouseoverEntity;
+
     @Setter
     private Int2ObjectMap<Recipe> craftingRecipes;
     private final Set<String> unlockedRecipes;
@@ -431,9 +444,14 @@ public class GeyserSession implements CommandSender {
         });
     }
 
-    public void connect(RemoteServer remoteServer) {
+    /**
+     * Send all necessary packets to load Bedrock into the server
+     */
+    public void connect() {
         startGame();
-        this.remoteServer = remoteServer;
+        this.remoteAddress = connector.getConfig().getRemote().getAddress();
+        this.remotePort = connector.getConfig().getRemote().getPort();
+        this.remoteAuthType = connector.getDefaultAuthType();
 
         // Set the hardcoded shield ID to the ID we just defined in StartGamePacket
         upstream.getSession().getHardcodedBlockingId().set(ItemRegistry.SHIELD.getBedrockId());
@@ -478,8 +496,8 @@ public class GeyserSession implements CommandSender {
     }
 
     public void login() {
-        if (connector.getAuthType() != AuthType.ONLINE) {
-            if (connector.getAuthType() == AuthType.OFFLINE) {
+        if (this.remoteAuthType != AuthType.ONLINE) {
+            if (this.remoteAuthType == AuthType.OFFLINE) {
                 connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.offline"));
             } else {
                 connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.auth.login.floodgate"));
@@ -588,12 +606,13 @@ public class GeyserSession implements CommandSender {
      * After getting whatever credentials needed, we attempt to join the Java server.
      */
     private void connectDownstream() {
-        boolean floodgate = connector.getAuthType() == AuthType.FLOODGATE;
+        boolean floodgate = this.remoteAuthType == AuthType.FLOODGATE;
 
         // Start ticking
         tickThread = connector.getGeneralThreadPool().scheduleAtFixedRate(this::tick, 50, 50, TimeUnit.MILLISECONDS);
 
-        downstream = new Client(remoteServer.getAddress(), remoteServer.getPort(), protocol, new TcpSessionFactory());
+        downstream = new Client(this.remoteAddress, this.remotePort, protocol, new TcpSessionFactory());
+        disableSrvResolving();
         if (connector.getConfig().getRemote().isUseProxyProtocol()) {
             downstream.getSession().setFlag(BuiltinFlags.ENABLE_CLIENT_PROXY_PROTOCOL, true);
             downstream.getSession().setFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS, upstream.getAddress());
@@ -652,7 +671,7 @@ public class GeyserSession implements CommandSender {
                     disconnect(LanguageUtils.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode()));
                     return;
                 }
-                connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.connect", authData.getName(), protocol.getProfile().getName(), remoteServer.getAddress()));
+                connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.connect", authData.getName(), protocol.getProfile().getName(), remoteAddress));
                 playerEntity.setUuid(protocol.getProfile().getId());
                 playerEntity.setUsername(protocol.getProfile().getName());
 
@@ -673,7 +692,7 @@ public class GeyserSession implements CommandSender {
             public void disconnected(DisconnectedEvent event) {
                 loggingIn = false;
                 loggedIn = false;
-                connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect", authData.getName(), remoteServer.getAddress(), event.getReason()));
+                connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.remote.disconnect", authData.getName(), remoteAddress, event.getReason()));
                 if (event.getCause() != null) {
                     event.getCause().printStackTrace();
                 }
@@ -691,7 +710,7 @@ public class GeyserSession implements CommandSender {
                         playerEntity.setUuid(profile.getId());
 
                         // Check if they are not using a linked account
-                        if (connector.getAuthType() == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) {
+                        if (remoteAuthType == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) {
                             SkinManager.handleBedrockSkin(playerEntity, clientData);
                         }
 
@@ -781,6 +800,18 @@ public class GeyserSession implements CommandSender {
         this.sneaking = sneaking;
         collisionManager.updatePlayerBoundingBox();
         collisionManager.updateScaffoldingFlags();
+
+        if (mouseoverEntity != null) {
+            // Horses, etc can change their property depending on if you're sneaking
+            InteractiveTagManager.updateTag(this, mouseoverEntity);
+        }
+    }
+
+    /**
+     * Will be overwritten for GeyserConnect.
+     */
+    protected void disableSrvResolving() {
+        this.downstream.getSession().setFlag(BuiltinFlags.ATTEMPT_SRV_RESOLVE, false);
     }
 
     @Override
diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/ChunkCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/ChunkCache.java
index a48b20cee..d182a6f12 100644
--- a/connector/src/main/java/org/geysermc/connector/network/session/cache/ChunkCache.java
+++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/ChunkCache.java
@@ -29,23 +29,24 @@ import com.github.steveice10.mc.protocol.data.game.chunk.Chunk;
 import com.github.steveice10.mc.protocol.data.game.chunk.Column;
 import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
 import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
-import org.geysermc.connector.bootstrap.GeyserBootstrap;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.world.block.BlockTranslator;
 import org.geysermc.connector.utils.MathUtils;
 
 public class ChunkCache {
+    private static final int MINIMUM_WORLD_HEIGHT = 0;
 
     private final boolean cache;
 
-    private final Long2ObjectMap<Column> chunks = new Long2ObjectOpenHashMap<>();
+    private final Long2ObjectMap<Column> chunks;
 
     public ChunkCache(GeyserSession session) {
-        if (session.getConnector().getWorldManager().getClass() == GeyserBootstrap.DEFAULT_CHUNK_MANAGER.getClass()) {
-            this.cache = session.getConnector().getConfig().isCacheChunks();
-        } else {
+        if (session.getConnector().getWorldManager().hasOwnChunkCache()) {
             this.cache = false; // To prevent Spigot from initializing
+        } else {
+            this.cache = session.getConnector().getConfig().isCacheChunks();
         }
+        chunks = cache ? new Long2ObjectOpenHashMap<>() : null;
     }
 
     public Column addToCache(Column chunk) {
@@ -86,6 +87,11 @@ public class ChunkCache {
             return;
         }
 
+        if (y < MINIMUM_WORLD_HEIGHT || (y >> 4) > column.getChunks().length - 1) {
+            // Y likely goes above or below the height limit of this world
+            return;
+        }
+
         Chunk chunk = column.getChunks()[y >> 4];
         if (chunk != null) {
             chunk.set(x & 0xF, y & 0xF, z & 0xF, block);
@@ -102,6 +108,11 @@ public class ChunkCache {
             return BlockTranslator.JAVA_AIR_ID;
         }
 
+        if (y < MINIMUM_WORLD_HEIGHT || (y >> 4) > column.getChunks().length - 1) {
+            // Y likely goes above or below the height limit of this world
+            return BlockTranslator.JAVA_AIR_ID;
+        }
+
         Chunk chunk = column.getChunks()[y >> 4];
         if (chunk != null) {
             return chunk.get(x & 0xF, y & 0xF, z & 0xF);
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java
index 5258219ba..36c5be44f 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockInventoryTransactionTranslator.java
@@ -94,7 +94,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                             boolean dropAll = worldAction.getToItem().getCount() > 1;
                             ClientPlayerActionPacket dropAllPacket = new ClientPlayerActionPacket(
                                     dropAll ? PlayerAction.DROP_ITEM_STACK : PlayerAction.DROP_ITEM,
-                                    new Position(0, 0, 0),
+                                    BlockUtils.POSITION_ZERO,
                                     BlockFace.DOWN
                             );
                             session.sendDownstreamPacket(dropAllPacket);
@@ -292,7 +292,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
             case ITEM_RELEASE:
                 if (packet.getActionType() == 0) {
                     // Followed to the Minecraft Protocol specification outlined at wiki.vg
-                    ClientPlayerActionPacket releaseItemPacket = new ClientPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, new Position(0,0,0),
+                    ClientPlayerActionPacket releaseItemPacket = new ClientPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, BlockUtils.POSITION_ZERO,
                             BlockFace.DOWN);
                     session.sendDownstreamPacket(releaseItemPacket);
                 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockItemFrameDropItemTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockItemFrameDropItemTranslator.java
index e7915bff3..959d6dc29 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockItemFrameDropItemTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockItemFrameDropItemTranslator.java
@@ -28,20 +28,23 @@ package org.geysermc.connector.network.translators.bedrock;
 import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
 import com.github.steveice10.mc.protocol.data.game.entity.player.InteractAction;
 import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerInteractEntityPacket;
-import com.nukkitx.math.vector.Vector3i;
 import com.nukkitx.protocol.bedrock.packet.ItemFrameDropItemPacket;
 import org.geysermc.connector.entity.ItemFrameEntity;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
 import org.geysermc.connector.network.translators.Translator;
 
+/**
+ * Pre-1.16.210: used for both survival and creative item frame item removal
+ *
+ * 1.16.210: only used in creative.
+ */
 @Translator(packet = ItemFrameDropItemPacket.class)
 public class BedrockItemFrameDropItemTranslator extends PacketTranslator<ItemFrameDropItemPacket> {
 
     @Override
     public void translate(ItemFrameDropItemPacket packet, GeyserSession session) {
-        Vector3i position = Vector3i.from(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ());
-        ClientPlayerInteractEntityPacket interactPacket = new ClientPlayerInteractEntityPacket((int) ItemFrameEntity.getItemFrameEntityId(session, position),
+        ClientPlayerInteractEntityPacket interactPacket = new ClientPlayerInteractEntityPacket((int) ItemFrameEntity.getItemFrameEntityId(session, packet.getBlockPosition()),
                 InteractAction.ATTACK, Hand.MAIN_HAND, session.isSneaking());
         session.sendDownstreamPacket(interactPacket);
     }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockLecternUpdateTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockLecternUpdateTranslator.java
index 99dcebed9..ae99fec07 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockLecternUpdateTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockLecternUpdateTranslator.java
@@ -56,7 +56,7 @@ public class BedrockLecternUpdateTranslator extends PacketTranslator<LecternUpda
                     new Position(packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ()),
                     BlockFace.values()[0],
                     Hand.MAIN_HAND,
-                    packet.getBlockPosition().getX(), packet.getBlockPosition().getY(), packet.getBlockPosition().getZ(), //TODO
+                    0, 0, 0, // Java doesn't care about these when dealing with a lectern
                     false);
             session.sendDownstreamPacket(blockPacket);
         } else {
@@ -65,6 +65,7 @@ public class BedrockLecternUpdateTranslator extends PacketTranslator<LecternUpda
                 session.getConnector().getLogger().debug("Expected lectern but it wasn't open!");
                 return;
             }
+
             LecternContainer lecternContainer = (LecternContainer) session.getOpenInventory();
             if (lecternContainer.getCurrentBedrockPage() == packet.getPage()) {
                 // The same page means Bedrock is closing the window
@@ -76,9 +77,10 @@ public class BedrockLecternUpdateTranslator extends PacketTranslator<LecternUpda
                 // Each "page" on Java is just one page (think a spiral notebook folded back to only show one page)
                 int newJavaPage = (packet.getPage() * 2);
                 int currentJavaPage = (lecternContainer.getCurrentBedrockPage() * 2);
+
                 // Send as many click button packets as we need to
                 // Java has the option to specify exact page numbers by adding 100 to the number, but buttonId variable
-                // is a byte and therefore this stops us at 128
+                // is a byte when transmitted over the network and therefore this stops us at 128
                 if (newJavaPage > currentJavaPage) {
                     for (int i = currentJavaPage; i < newJavaPage; i++) {
                         ClientClickWindowButtonPacket clickButtonPacket = new ClientClickWindowButtonPacket(session.getOpenInventory().getId(), 2);
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMobEquipmentTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMobEquipmentTranslator.java
index 3ffc2a8f3..e07f0ae1e 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMobEquipmentTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockMobEquipmentTranslator.java
@@ -25,14 +25,19 @@
 
 package org.geysermc.connector.network.translators.bedrock;
 
+import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
+import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerChangeHeldItemPacket;
+import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerUseItemPacket;
+import com.nukkitx.protocol.bedrock.data.inventory.ContainerId;
+import com.nukkitx.protocol.bedrock.packet.MobEquipmentPacket;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
 import org.geysermc.connector.network.translators.Translator;
-
-import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerChangeHeldItemPacket;
-import com.nukkitx.protocol.bedrock.data.inventory.ContainerId;
-import com.nukkitx.protocol.bedrock.packet.MobEquipmentPacket;
+import org.geysermc.connector.network.translators.item.ItemRegistry;
 import org.geysermc.connector.utils.CooldownUtils;
+import org.geysermc.connector.utils.InteractiveTagManager;
+
+import java.util.concurrent.TimeUnit;
 
 @Translator(packet = MobEquipmentPacket.class)
 public class BedrockMobEquipmentTranslator extends PacketTranslator<MobEquipmentPacket> {
@@ -53,7 +58,20 @@ public class BedrockMobEquipmentTranslator extends PacketTranslator<MobEquipment
         ClientPlayerChangeHeldItemPacket changeHeldItemPacket = new ClientPlayerChangeHeldItemPacket(packet.getHotbarSlot());
         session.sendDownstreamPacket(changeHeldItemPacket);
 
+        if (session.isSneaking() && session.getPlayerInventory().getItemInHand().getJavaId() == ItemRegistry.SHIELD.getJavaId()) {
+            // 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.getConnector().getGeneralThreadPool().schedule(() -> session.sendDownstreamPacket(new ClientPlayerUseItemPacket(Hand.MAIN_HAND)),
+                    50, TimeUnit.MILLISECONDS);
+        }
+
         // Java sends a cooldown indicator whenever you switch an item
         CooldownUtils.sendCooldown(session);
+
+        // Update the interactive tag, if an entity is present
+        if (session.getMouseoverEntity() != null) {
+            InteractiveTagManager.updateTag(session, session.getMouseoverEntity());
+        }
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java
index f0bbbeada..7751fb024 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockActionTranslator.java
@@ -26,29 +26,28 @@
 package org.geysermc.connector.network.translators.bedrock.entity.player;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
-import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
-import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerAction;
-import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerState;
+import com.github.steveice10.mc.protocol.data.game.entity.player.*;
 import com.github.steveice10.mc.protocol.data.game.world.block.BlockFace;
-import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerAbilitiesPacket;
-import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerActionPacket;
-import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerStatePacket;
+import com.github.steveice10.mc.protocol.packet.ingame.client.player.*;
 import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
 import com.nukkitx.math.vector.Vector3i;
 import com.nukkitx.protocol.bedrock.data.LevelEventType;
 import com.nukkitx.protocol.bedrock.data.PlayerActionType;
 import com.nukkitx.protocol.bedrock.data.entity.EntityEventType;
+import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import com.nukkitx.protocol.bedrock.packet.EntityEventPacket;
 import com.nukkitx.protocol.bedrock.packet.LevelEventPacket;
 import com.nukkitx.protocol.bedrock.packet.PlayStatusPacket;
 import com.nukkitx.protocol.bedrock.packet.PlayerActionPacket;
 import org.geysermc.connector.entity.Entity;
+import org.geysermc.connector.entity.ItemFrameEntity;
 import org.geysermc.connector.inventory.GeyserItemStack;
 import org.geysermc.connector.inventory.PlayerInventory;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
 import org.geysermc.connector.network.translators.Translator;
 import org.geysermc.connector.network.translators.item.ItemEntry;
+import org.geysermc.connector.network.translators.item.ItemRegistry;
 import org.geysermc.connector.network.translators.world.block.BlockTranslator;
 import org.geysermc.connector.utils.BlockUtils;
 
@@ -101,11 +100,37 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
             case START_SNEAK:
                 ClientPlayerStatePacket startSneakPacket = new ClientPlayerStatePacket((int) entity.getEntityId(), PlayerState.START_SNEAKING);
                 session.sendDownstreamPacket(startSneakPacket);
+
+                // Toggle the shield, if relevant
+                PlayerInventory playerInv = session.getPlayerInventory();
+                if ((playerInv.getItemInHand().getJavaId() == ItemRegistry.SHIELD.getJavaId()) ||
+                        (playerInv.getOffhand().getJavaId() == ItemRegistry.SHIELD.getJavaId())) {
+                    ClientPlayerUseItemPacket useItemPacket;
+                    if (playerInv.getItemInHand().getJavaId() == ItemRegistry.SHIELD.getJavaId()) {
+                        useItemPacket = new ClientPlayerUseItemPacket(Hand.MAIN_HAND);
+                    } else {
+                        // Else we just assume it's the offhand, to simplify logic and to assure the packet gets sent
+                        useItemPacket = new ClientPlayerUseItemPacket(Hand.OFF_HAND);
+                    }
+                    session.sendDownstreamPacket(useItemPacket);
+                    session.getPlayerEntity().getMetadata().getFlags().setFlag(EntityFlag.BLOCKING, true);
+                    session.getPlayerEntity().updateBedrockMetadata(session);
+                }
+
                 session.setSneaking(true);
                 break;
             case STOP_SNEAK:
                 ClientPlayerStatePacket stopSneakPacket = new ClientPlayerStatePacket((int) entity.getEntityId(), PlayerState.STOP_SNEAKING);
                 session.sendDownstreamPacket(stopSneakPacket);
+
+                // Stop shield, if necessary
+                if (session.getPlayerEntity().getMetadata().getFlags().getFlag(EntityFlag.BLOCKING)) {
+                    ClientPlayerActionPacket releaseItemPacket = new ClientPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, BlockUtils.POSITION_ZERO, BlockFace.DOWN);
+                    session.sendDownstreamPacket(releaseItemPacket);
+                    session.getPlayerEntity().getMetadata().getFlags().setFlag(EntityFlag.BLOCKING, false);
+                    session.getPlayerEntity().updateBedrockMetadata(session);
+                }
+
                 session.setSneaking(false);
                 break;
             case START_SPRINT:
@@ -184,6 +209,18 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
                 session.sendUpstreamPacket(continueBreakPacket);
                 break;
             case ABORT_BREAK:
+                if (session.getGameMode() != GameMode.CREATIVE) {
+                    // As of 1.16.210: item frame items are taken out here.
+                    // Survival also sends START_BREAK, but by attaching our process here adventure mode also works
+                    long entityId = ItemFrameEntity.getItemFrameEntityId(session, packet.getBlockPosition());
+                    if (entityId != -1) {
+                        ClientPlayerInteractEntityPacket interactPacket = new ClientPlayerInteractEntityPacket((int) entityId,
+                                InteractAction.ATTACK, Hand.MAIN_HAND, session.isSneaking());
+                        session.sendDownstreamPacket(interactPacket);
+                        break;
+                    }
+                }
+
                 ClientPlayerActionPacket abortBreakingPacket = new ClientPlayerActionPacket(PlayerAction.CANCEL_DIGGING, position, BlockFace.DOWN);
                 session.sendDownstreamPacket(abortBreakingPacket);
                 LevelEventPacket stopBreak = new LevelEventPacket();
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockInteractTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockInteractTranslator.java
index ca71a1975..740ab8a37 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockInteractTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/entity/player/BedrockInteractTranslator.java
@@ -31,62 +31,21 @@ import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerState;
 import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerInteractEntityPacket;
 import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerStatePacket;
 import com.nukkitx.protocol.bedrock.data.entity.EntityData;
-import com.nukkitx.protocol.bedrock.data.entity.EntityDataMap;
 import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
 import com.nukkitx.protocol.bedrock.data.inventory.ContainerType;
 import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket;
 import com.nukkitx.protocol.bedrock.packet.InteractPacket;
-import lombok.Getter;
 import org.geysermc.connector.entity.Entity;
 import org.geysermc.connector.entity.living.animal.horse.AbstractHorseEntity;
-import org.geysermc.connector.entity.living.animal.horse.HorseEntity;
-import org.geysermc.connector.entity.type.EntityType;
-import org.geysermc.connector.inventory.GeyserItemStack;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
 import org.geysermc.connector.network.translators.Translator;
-import org.geysermc.connector.network.translators.item.ItemEntry;
 import org.geysermc.connector.network.translators.item.ItemRegistry;
-
-import java.util.Arrays;
-import java.util.List;
+import org.geysermc.connector.utils.InteractiveTagManager;
 
 @Translator(packet = InteractPacket.class)
 public class BedrockInteractTranslator extends PacketTranslator<InteractPacket> {
 
-    /**
-     * A list of all foods a horse/donkey can eat on Java Edition.
-     * Used to display interactive tag if needed.
-     */
-    private static final List<String> DONKEY_AND_HORSE_FOODS = Arrays.asList("golden_apple", "enchanted_golden_apple",
-            "golden_carrot", "sugar", "apple", "wheat", "hay_block");
-
-    /**
-     * A list of all flowers. Used for feeding bees.
-     */
-    private static final List<String> FLOWERS = Arrays.asList("dandelion", "poppy", "blue_orchid", "allium", "azure_bluet",
-            "red_tulip", "pink_tulip", "white_tulip", "orange_tulip", "cornflower", "lily_of_the_valley", "wither_rose",
-            "sunflower", "lilac", "rose_bush", "peony");
-
-    /**
-     * All entity types that can be leashed on Java Edition
-     */
-    private static final List<EntityType> LEASHABLE_MOB_TYPES = Arrays.asList(EntityType.BEE, EntityType.CAT, EntityType.CHICKEN,
-            EntityType.COW, EntityType.DOLPHIN, EntityType.DONKEY, EntityType.FOX, EntityType.HOGLIN, EntityType.HORSE, EntityType.SKELETON_HORSE,
-            EntityType.ZOMBIE_HORSE, EntityType.IRON_GOLEM, EntityType.LLAMA, EntityType.TRADER_LLAMA, EntityType.MOOSHROOM,
-            EntityType.MULE, EntityType.OCELOT, EntityType.PARROT, EntityType.PIG, EntityType.POLAR_BEAR, EntityType.RABBIT,
-            EntityType.SHEEP, EntityType.SNOW_GOLEM, EntityType.STRIDER, EntityType.WOLF, EntityType.ZOGLIN);
-
-    private static final List<EntityType> SADDLEABLE_WHEN_TAMED_MOB_TYPES = Arrays.asList(EntityType.DONKEY, EntityType.HORSE,
-            EntityType.ZOMBIE_HORSE, EntityType.MULE);
-    /**
-     * A list of all foods a wolf can eat on Java Edition.
-     * Used to display interactive tag if needed.
-     */
-    private static final List<String> WOLF_FOODS = Arrays.asList("pufferfish", "tropical_fish", "chicken", "cooked_chicken",
-            "porkchop", "beef", "rabbit", "cooked_porkchop", "cooked_beef", "rotten_flesh", "mutton", "cooked_mutton",
-            "cooked_rabbit");
-
     @Override
     public void translate(InteractPacket packet, GeyserSession session) {
         Entity entity;
@@ -122,241 +81,16 @@ public class BedrockInteractTranslator extends PacketTranslator<InteractPacket>
                 // Handle the buttons for mobile - "Mount", etc; and the suggestions for console - "ZL: Mount", etc
                 if (packet.getRuntimeEntityId() != 0) {
                     Entity interactEntity = session.getEntityCache().getEntityByGeyserId(packet.getRuntimeEntityId());
-                    if (interactEntity == null)
+                    session.setMouseoverEntity(interactEntity);
+                    if (interactEntity == null) {
                         return;
-                    EntityDataMap entityMetadata = interactEntity.getMetadata();
-                    ItemEntry itemEntry = session.getPlayerInventory().getItemInHand() == GeyserItemStack.EMPTY ? ItemEntry.AIR : ItemRegistry.getItem(session.getPlayerInventory().getItemInHand().getItemStack());
-                    String javaIdentifierStripped = itemEntry.getJavaIdentifier().replace("minecraft:", "");
-
-                    // TODO - in the future, update these in the metadata? So the client doesn't have to wiggle their cursor around for it to happen
-                    // TODO - also, might be good to abstract out the eating thing. I know there will need to be food tracked for https://github.com/GeyserMC/Geyser/issues/1005 but not all food is breeding food
-                    InteractiveTag interactiveTag = InteractiveTag.NONE;
-                    if (entityMetadata.getLong(EntityData.LEASH_HOLDER_EID) == session.getPlayerEntity().getGeyserId()) {
-                        // Unleash the entity
-                        interactiveTag = InteractiveTag.REMOVE_LEASH;
-                    } else if (javaIdentifierStripped.equals("saddle") && !entityMetadata.getFlags().getFlag(EntityFlag.SADDLED) &&
-                            ((SADDLEABLE_WHEN_TAMED_MOB_TYPES.contains(interactEntity.getEntityType()) && entityMetadata.getFlags().getFlag(EntityFlag.TAMED)) ||
-                            interactEntity.getEntityType() == EntityType.PIG || interactEntity.getEntityType() == EntityType.STRIDER)) {
-                        // Entity can be saddled and the conditions meet (entity can be saddled and, if needed, is tamed)
-                        interactiveTag = InteractiveTag.SADDLE;
-                    } else if (javaIdentifierStripped.equals("name_tag") && session.getPlayerInventory().getItemInHand().getNbt() != null &&
-                        session.getPlayerInventory().getItemInHand().getNbt().contains("display")) {
-                        // Holding a named name tag
-                        interactiveTag = InteractiveTag.NAME;
-                    } else if (javaIdentifierStripped.equals("lead") && LEASHABLE_MOB_TYPES.contains(interactEntity.getEntityType()) &&
-                            entityMetadata.getLong(EntityData.LEASH_HOLDER_EID) == -1L) {
-                        // Holding a leash and the mob is leashable for sure
-                        // (Plugins can change this behavior so that's something to look into in the far far future)
-                        interactiveTag = InteractiveTag.LEASH;
-                    } else {
-                        switch (interactEntity.getEntityType()) {
-                            case BEE:
-                                if (FLOWERS.contains(javaIdentifierStripped)) {
-                                    interactiveTag = InteractiveTag.FEED;
-                                }
-                                break;
-                            case BOAT:
-                                interactiveTag = InteractiveTag.BOARD_BOAT;
-                                break;
-                            case CAT:
-                                if (javaIdentifierStripped.equals("cod") || javaIdentifierStripped.equals("salmon")) {
-                                    interactiveTag = InteractiveTag.FEED;
-                                } else if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) &&
-                                        entityMetadata.getLong(EntityData.OWNER_EID) == session.getPlayerEntity().getGeyserId()) {
-                                    // Tamed and owned by player - can sit/stand
-                                    interactiveTag = entityMetadata.getFlags().getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT;
-                                    break;
-                                }
-                                break;
-                            case CHICKEN:
-                                if (javaIdentifierStripped.contains("seeds")) {
-                                    interactiveTag = InteractiveTag.FEED;
-                                }
-                                break;
-                            case MOOSHROOM:
-                                // Shear the mooshroom
-                                if (javaIdentifierStripped.equals("shears")) {
-                                    interactiveTag = InteractiveTag.MOOSHROOM_SHEAR;
-                                    break;
-                                }
-                                // Bowls are acceptable here
-                                else if (javaIdentifierStripped.equals("bowl")) {
-                                    interactiveTag = InteractiveTag.MOOSHROOM_MILK_STEW;
-                                    break;
-                                }
-                                // Fall down to COW as this works on mooshrooms
-                            case COW:
-                                if (javaIdentifierStripped.equals("wheat")) {
-                                    interactiveTag = InteractiveTag.FEED;
-                                } else if (javaIdentifierStripped.equals("bucket")) {
-                                    // Milk the cow
-                                    interactiveTag = InteractiveTag.MILK;
-                                }
-                                break;
-                            case CREEPER:
-                                if (javaIdentifierStripped.equals("flint_and_steel")) {
-                                    // Today I learned that you can ignite a creeper with flint and steel! Huh.
-                                    interactiveTag = InteractiveTag.IGNITE_CREEPER;
-                                }
-                                break;
-                            case DONKEY:
-                            case LLAMA:
-                            case MULE:
-                                if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && !entityMetadata.getFlags().getFlag(EntityFlag.CHESTED)
-                                        && javaIdentifierStripped.equals("chest")) {
-                                    // Can attach a chest
-                                    interactiveTag = InteractiveTag.ATTACH_CHEST;
-                                    break;
-                                }
-                                // Intentional fall-through
-                            case HORSE:
-                            case SKELETON_HORSE:
-                            case TRADER_LLAMA:
-                            case ZOMBIE_HORSE:
-                                boolean tamed = entityMetadata.getFlags().getFlag(EntityFlag.TAMED);
-                                if (session.isSneaking() && tamed && (interactEntity instanceof HorseEntity || entityMetadata.getFlags().getFlag(EntityFlag.CHESTED))) {
-                                    interactiveTag = InteractiveTag.OPEN_CONTAINER;
-                                    break;
-                                }
-                                // have another switch statement as, while these share mount attributes they don't share food
-                                switch (interactEntity.getEntityType()) {
-                                    case LLAMA:
-                                    case TRADER_LLAMA:
-                                        if (javaIdentifierStripped.equals("wheat") || javaIdentifierStripped.equals("hay_block")) {
-                                            interactiveTag = InteractiveTag.FEED;
-                                            break;
-                                        }
-                                    case DONKEY:
-                                    case HORSE:
-                                        // Undead can't eat
-                                        if (DONKEY_AND_HORSE_FOODS.contains(javaIdentifierStripped)) {
-                                            interactiveTag = InteractiveTag.FEED;
-                                            break;
-                                        }
-                                }
-                                if (!entityMetadata.getFlags().getFlag(EntityFlag.BABY)) {
-                                    // Can't ride a baby
-                                    if (tamed) {
-                                        interactiveTag = InteractiveTag.RIDE_HORSE;
-                                    } else if (itemEntry.equals(ItemEntry.AIR)) {
-                                        // Can't hide an untamed entity without having your hand empty
-                                        interactiveTag = InteractiveTag.MOUNT;
-                                    }
-                                }
-                                break;
-                            case FOX:
-                                if (javaIdentifierStripped.equals("sweet_berries")) {
-                                    interactiveTag = InteractiveTag.FEED;
-                                }
-                                break;
-                            case HOGLIN:
-                                if (javaIdentifierStripped.equals("crimson_fungus")) {
-                                    interactiveTag = InteractiveTag.FEED;
-                                }
-                                break;
-                            case MINECART:
-                                interactiveTag = InteractiveTag.RIDE_MINECART;
-                                break;
-                            case MINECART_CHEST:
-                            case MINECART_COMMAND_BLOCK:
-                            case MINECART_HOPPER:
-                                interactiveTag = InteractiveTag.OPEN_CONTAINER;
-                                break;
-                            case OCELOT:
-                                if (javaIdentifierStripped.equals("cod") || javaIdentifierStripped.equals("salmon")) {
-                                    interactiveTag = InteractiveTag.FEED;
-                                }
-                                break;
-                            case PANDA:
-                                if (javaIdentifierStripped.equals("bamboo")) {
-                                    interactiveTag = InteractiveTag.FEED;
-                                }
-                                break;
-                            case PARROT:
-                                if (javaIdentifierStripped.contains("seeds") || javaIdentifierStripped.equals("cookie")) {
-                                    interactiveTag = InteractiveTag.FEED;
-                                }
-                                break;
-                            case PIG:
-                                if (javaIdentifierStripped.equals("carrot") || javaIdentifierStripped.equals("potato") || javaIdentifierStripped.equals("beetroot")) {
-                                    interactiveTag = InteractiveTag.FEED;
-                                } else if (entityMetadata.getFlags().getFlag(EntityFlag.SADDLED)) {
-                                    interactiveTag = InteractiveTag.MOUNT;
-                                }
-                                break;
-                            case PIGLIN:
-                                if (!entityMetadata.getFlags().getFlag(EntityFlag.BABY) && javaIdentifierStripped.equals("gold_ingot")) {
-                                    interactiveTag = InteractiveTag.BARTER;
-                                }
-                                break;
-                            case RABBIT:
-                                if (javaIdentifierStripped.equals("dandelion") || javaIdentifierStripped.equals("carrot") || javaIdentifierStripped.equals("golden_carrot")) {
-                                    interactiveTag = InteractiveTag.FEED;
-                                }
-                                break;
-                            case SHEEP:
-                                if (javaIdentifierStripped.equals("wheat")) {
-                                    interactiveTag = InteractiveTag.FEED;
-                                } else if (!entityMetadata.getFlags().getFlag(EntityFlag.SHEARED)) {
-                                    if (javaIdentifierStripped.equals("shears")) {
-                                        // Shear the sheep
-                                        interactiveTag = InteractiveTag.SHEAR;
-                                    } else if (javaIdentifierStripped.contains("_dye")) {
-                                        // Dye the sheep
-                                        interactiveTag = InteractiveTag.DYE;
-                                    }
-                                }
-                                break;
-                            case STRIDER:
-                                if (javaIdentifierStripped.equals("warped_fungus")) {
-                                    interactiveTag = InteractiveTag.FEED;
-                                } else if (entityMetadata.getFlags().getFlag(EntityFlag.SADDLED)) {
-                                    interactiveTag = InteractiveTag.RIDE_STRIDER;
-                                }
-                                break;
-                            case TURTLE:
-                                if (javaIdentifierStripped.equals("seagrass")) {
-                                    interactiveTag = InteractiveTag.FEED;
-                                }
-                                break;
-                            case VILLAGER:
-                                if (entityMetadata.getInt(EntityData.VARIANT) != 14 && entityMetadata.getInt(EntityData.VARIANT) != 0
-                                        && entityMetadata.getFloat(EntityData.SCALE) >= 0.75f) { // Not a nitwit, has a profession and is not a baby
-                                    interactiveTag = InteractiveTag.TRADE;
-                                }
-                                break;
-                            case WANDERING_TRADER:
-                                interactiveTag = InteractiveTag.TRADE; // Since you can always trade with a wandering villager, presumably.
-                                break;
-                            case WOLF:
-                                if (javaIdentifierStripped.equals("bone") && !entityMetadata.getFlags().getFlag(EntityFlag.TAMED)) {
-                                    // Bone and untamed - can tame
-                                    interactiveTag = InteractiveTag.TAME;
-                                } else if (WOLF_FOODS.contains(javaIdentifierStripped)) {
-                                    // Compatible food in hand - feed
-                                    // Sometimes just sits/stands when the wolf isn't hungry - there doesn't appear to be a way to fix this
-                                    interactiveTag = InteractiveTag.FEED;
-                                } else if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) &&
-                                        entityMetadata.getLong(EntityData.OWNER_EID) == session.getPlayerEntity().getGeyserId()) {
-                                    // Tamed and owned by player - can sit/stand
-                                    interactiveTag = entityMetadata.getFlags().getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT;
-                                }
-                                break;
-                            case ZOMBIE_VILLAGER:
-                                // We can't guarantee the existence of the weakness effect so we just always show it.
-                                if (javaIdentifierStripped.equals("golden_apple")) {
-                                    interactiveTag = InteractiveTag.CURE;
-                                }
-                                break;
-                            default:
-                                break;
-                        }
                     }
-                    session.getPlayerEntity().getMetadata().put(EntityData.INTERACTIVE_TAG, interactiveTag.getValue());
-                    session.getPlayerEntity().updateBedrockMetadata(session);
+
+                    InteractiveTagManager.updateTag(session, interactEntity);
                 } else {
-                    if (!session.getPlayerEntity().getMetadata().getString(EntityData.INTERACTIVE_TAG).isEmpty()) {
+                    if (session.getMouseoverEntity() != null) {
                         // No interactive tag should be sent
+                        session.setMouseoverEntity(null);
                         session.getPlayerEntity().getMetadata().put(EntityData.INTERACTIVE_TAG, "");
                         session.getPlayerEntity().updateBedrockMetadata(session);
                     }
@@ -368,7 +102,7 @@ public class BedrockInteractTranslator extends PacketTranslator<InteractPacket>
                     if (ridingEntity instanceof AbstractHorseEntity) {
                         if (ridingEntity.getMetadata().getFlags().getFlag(EntityFlag.TAMED)) {
                             // We should request to open the horse inventory instead
-                            ClientPlayerStatePacket openHorseWindowPacket = new ClientPlayerStatePacket((int)session.getPlayerEntity().getEntityId(), PlayerState.OPEN_HORSE_INVENTORY);
+                            ClientPlayerStatePacket openHorseWindowPacket = new ClientPlayerStatePacket((int) session.getPlayerEntity().getEntityId(), PlayerState.OPEN_HORSE_INVENTORY);
                             session.sendDownstreamPacket(openHorseWindowPacket);
                         }
                     } else {
@@ -385,65 +119,4 @@ public class BedrockInteractTranslator extends PacketTranslator<InteractPacket>
                 break;
         }
     }
-
-    /**
-     * All interactive tags in enum form. For potential API usage.
-     */
-    public enum InteractiveTag {
-        NONE(true),
-        IGNITE_CREEPER("creeper"),
-        EDIT,
-        LEAVE_BOAT("exit.boat"),
-        FEED,
-        FISH("fishing"),
-        MILK,
-        MOOSHROOM_SHEAR("mooshear"),
-        MOOSHROOM_MILK_STEW("moostew"),
-        BOARD_BOAT("ride.boat"),
-        RIDE_MINECART("ride.minecart"),
-        RIDE_HORSE("ride.horse"),
-        RIDE_STRIDER("ride.strider"),
-        SHEAR,
-        SIT,
-        STAND,
-        TALK,
-        TAME,
-        DYE,
-        CURE,
-        OPEN_CONTAINER("opencontainer"),
-        CREATE_MAP("createMap"),
-        TAKE_PICTURE("takepicture"),
-        SADDLE,
-        MOUNT,
-        BOOST,
-        WRITE,
-        LEASH,
-        REMOVE_LEASH("unleash"),
-        NAME,
-        ATTACH_CHEST("attachchest"),
-        TRADE,
-        POSE_ARMOR_STAND("armorstand.pose"),
-        EQUIP_ARMOR_STAND("armorstand.equip"),
-        READ,
-        WAKE_VILLAGER("wakevillager"),
-        BARTER;
-
-        /**
-         * The full string that should be passed on to the client.
-         */
-        @Getter
-        private final String value;
-
-        InteractiveTag(boolean isNone) {
-            this.value = "";
-        }
-
-        InteractiveTag(String value) {
-            this.value = "action.interact." + value;
-        }
-
-        InteractiveTag() {
-            this.value = "action.interact." + name().toLowerCase();
-        }
-    }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java
index 1eef679f5..4a45b5c9f 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java
@@ -32,6 +32,8 @@ import com.github.steveice10.mc.protocol.data.game.recipe.Recipe;
 import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapedRecipeData;
 import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData;
 import com.github.steveice10.mc.protocol.data.game.window.WindowType;
+import com.github.steveice10.opennbt.tag.builtin.IntTag;
+import com.github.steveice10.opennbt.tag.builtin.Tag;
 import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType;
 import com.nukkitx.protocol.bedrock.data.inventory.ItemStackRequest;
 import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData;
@@ -39,7 +41,11 @@ import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.*;
 import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket;
 import it.unimi.dsi.fastutil.ints.*;
 import lombok.AllArgsConstructor;
-import org.geysermc.connector.inventory.*;
+import org.geysermc.connector.GeyserConnector;
+import org.geysermc.connector.inventory.CartographyContainer;
+import org.geysermc.connector.inventory.GeyserItemStack;
+import org.geysermc.connector.inventory.Inventory;
+import org.geysermc.connector.inventory.PlayerInventory;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.inventory.click.Click;
 import org.geysermc.connector.network.translators.inventory.click.ClickPlan;
@@ -108,8 +114,8 @@ public abstract class InventoryTranslator {
     public abstract void updateInventory(GeyserSession session, Inventory inventory);
     public abstract void updateSlot(GeyserSession session, Inventory inventory, int slot);
     public abstract int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData);
-    public abstract int javaSlotToBedrock(int javaSlot); //TODO
-    public abstract BedrockContainerSlot javaSlotToBedrockContainer(int javaSlot); //TODO
+    public abstract int javaSlotToBedrock(int javaSlot);
+    public abstract BedrockContainerSlot javaSlotToBedrockContainer(int javaSlot);
     public abstract SlotType getSlotType(int javaSlot);
     public abstract Inventory createInventory(String name, int windowId, WindowType windowType, PlayerInventory playerInventory);
 
@@ -138,7 +144,7 @@ public abstract class InventoryTranslator {
      * If {@link #shouldHandleRequestFirst(StackRequestActionData, Inventory)} returns true, this will be called
      */
     public ItemStackResponsePacket.Response translateSpecialRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
-        return null;
+        return rejectRequest(request);
     }
 
     public void translateRequests(GeyserSession session, Inventory inventory, List<ItemStackRequest> requests) {
@@ -151,22 +157,32 @@ public abstract class InventoryTranslator {
                 if (shouldHandleRequestFirst(firstAction, inventory)) {
                     // Some special request that shouldn't be processed normally
                     response = translateSpecialRequest(session, inventory, request);
-                } else if (firstAction.getType() == StackRequestActionType.CRAFT_RECIPE) {
-                    response = translateCraftingRequest(session, inventory, request);
-                } else if (firstAction.getType() == StackRequestActionType.CRAFT_RECIPE_AUTO) {
-                    response = translateAutoCraftingRequest(session, inventory, request);
-                } else if (firstAction.getType() == StackRequestActionType.CRAFT_CREATIVE) {
-                    // This is also used for pulling items out of creative
-                    response = translateCreativeRequest(session, inventory, request);
                 } else {
-                    response = translateRequest(session, inventory, request);
+                    switch (firstAction.getType()) {
+                        case CRAFT_RECIPE:
+                            response = translateCraftingRequest(session, inventory, request);
+                            break;
+                        case CRAFT_RECIPE_AUTO:
+                            response = translateAutoCraftingRequest(session, inventory, request);
+                            break;
+                        case CRAFT_CREATIVE:
+                            // This is also used for pulling items out of creative
+                            response = translateCreativeRequest(session, inventory, request);
+                            break;
+                        default:
+                            response = translateRequest(session, inventory, request);
+                            break;
+                    }
                 }
             } else {
                 response = rejectRequest(request);
             }
-            if (response.getResult() == ItemStackResponsePacket.ResponseStatus.ERROR) {
+
+            if (response.getResult() != ItemStackResponsePacket.ResponseStatus.OK) {
+                // Sync our copy of the inventory with Bedrock's to prevent desyncs
                 refresh = true;
             }
+
             responsePacket.getEntries().add(response);
         }
         session.sendUpstreamPacket(responsePacket);
@@ -191,11 +207,10 @@ public abstract class InventoryTranslator {
                                 transferAction.getSource().getSlot() >= 28 && transferAction.getSource().getSlot() <= 31) {
                             return rejectRequest(request, false);
                         }
-                        session.getConnector().getLogger().error("DEBUG: About to reject TAKE/PLACE request made by " + session.getName());
-                        session.getConnector().getLogger().error("Source: " + transferAction.getSource().toString() + " Result: " + checkNetId(session, inventory, transferAction.getSource()));
-                        session.getConnector().getLogger().error("Destination: " + transferAction.getDestination().toString() + " Result: " + checkNetId(session, inventory, transferAction.getDestination()));
-                        session.getConnector().getLogger().error("Geyser's record of source slot: " + inventory.getItem(bedrockSlotToJava(transferAction.getSource())));
-                        session.getConnector().getLogger().error("Geyser's record of destination slot: " + inventory.getItem(bedrockSlotToJava(transferAction.getDestination())));
+                        if (session.getConnector().getConfig().isDebugMode()) {
+                            session.getConnector().getLogger().error("DEBUG: About to reject TAKE/PLACE request made by " + session.getName());
+                            dumpStackRequestDetails(session, inventory, transferAction.getSource(), transferAction.getDestination());
+                        }
                         return rejectRequest(request);
                     }
 
@@ -278,11 +293,10 @@ public abstract class InventoryTranslator {
                 case SWAP: {
                     SwapStackRequestActionData swapAction = (SwapStackRequestActionData) action;
                     if (!(checkNetId(session, inventory, swapAction.getSource()) && checkNetId(session, inventory, swapAction.getDestination()))) {
-                        session.getConnector().getLogger().error("DEBUG: About to reject SWAP request made by " + session.getName());
-                        session.getConnector().getLogger().error("Source: " + swapAction.getSource().toString() + " Result: " + checkNetId(session, inventory, swapAction.getSource()));
-                        session.getConnector().getLogger().error("Destination: " + swapAction.getDestination().toString() + " Result: " + checkNetId(session, inventory, swapAction.getDestination()));
-                        session.getConnector().getLogger().error("Geyser's record of source slot: " + inventory.getItem(bedrockSlotToJava(swapAction.getSource())));
-                        session.getConnector().getLogger().error("Geyser's record of destination slot: " + inventory.getItem(bedrockSlotToJava(swapAction.getDestination())));
+                        if (session.getConnector().getConfig().isDebugMode()) {
+                            session.getConnector().getLogger().error("DEBUG: About to reject SWAP request made by " + session.getName());
+                            dumpStackRequestDetails(session, inventory, swapAction.getSource(), swapAction.getDestination());
+                        }
                         return rejectRequest(request);
                     }
 
@@ -357,17 +371,28 @@ public abstract class InventoryTranslator {
                     if (inventory instanceof CartographyContainer) {
                         // TODO add this for more inventories? Only seems to glitch out the cartography table, though.
                         ConsumeStackRequestActionData consumeData = (ConsumeStackRequestActionData) action;
+
                         int sourceSlot = bedrockSlotToJava(consumeData.getSource());
-                        if (sourceSlot == 0 && inventory.getItem(1).isEmpty()) {
-                            // Java doesn't allow an item to be renamed; this is why CARTOGRAPHY_ADDITIONAL could remain empty for Bedrock
-                            // We check this during slot 0 since setting the inventory slots here messes up shouldRejectItemPlace
+                        if ((sourceSlot == 0 && inventory.getItem(1).isEmpty()) || (sourceSlot == 1 && inventory.getItem(0).isEmpty())) {
+                            // Java doesn't allow an item to be renamed; this is why one of the slots could remain empty for Bedrock
+                            // We check this now since setting the inventory slots here messes up shouldRejectItemPlace
                             return rejectRequest(request, false);
                         }
 
-                        GeyserItemStack item = inventory.getItem(sourceSlot);
-                        item.setAmount(item.getAmount() - consumeData.getCount());
-                        if (item.isEmpty()) {
-                            inventory.setItem(sourceSlot, GeyserItemStack.EMPTY, session);
+                        if (sourceSlot == 1) {
+                            // Decrease the item count, but only after both slots are checked.
+                            // Otherwise, the slot 1 check will fail
+                            GeyserItemStack item = inventory.getItem(sourceSlot);
+                            item.setAmount(item.getAmount() - consumeData.getCount());
+                            if (item.isEmpty()) {
+                                inventory.setItem(sourceSlot, GeyserItemStack.EMPTY, session);
+                            }
+
+                            GeyserItemStack itemZero = inventory.getItem(0);
+                            itemZero.setAmount(itemZero.getAmount() - consumeData.getCount());
+                            if (itemZero.isEmpty()) {
+                                inventory.setItem(0, GeyserItemStack.EMPTY, session);
+                            }
                         }
                         affectedSlots.add(sourceSlot);
                     }
@@ -682,8 +707,10 @@ public abstract class InventoryTranslator {
         return acceptRequest(request, makeContainerEntries(session, inventory, plan.getAffectedSlots()));
     }
 
+    /**
+     * Handled in {@link PlayerInventoryTranslator}
+     */
     public ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
-        // Handled in PlayerInventoryTranslator
         return rejectRequest(request);
     }
 
@@ -736,13 +763,22 @@ public abstract class InventoryTranslator {
      *                   as bad (false).
      */
     public static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request, boolean throwError) {
-        if (throwError) {
-            // Currently for debugging, but might be worth it to keep in the future if something goes terribly wrong.
+        if (throwError && GeyserConnector.getInstance().getConfig().isDebugMode()) {
             new Throwable("DEBUGGING: ItemStackRequest rejected " + request.toString()).printStackTrace();
         }
         return new ItemStackResponsePacket.Response(ItemStackResponsePacket.ResponseStatus.ERROR, request.getRequestId(), Collections.emptyList());
     }
 
+    /**
+     * Print out the contents of an ItemStackRequest, should the net ID check fail.
+     */
+    protected void dumpStackRequestDetails(GeyserSession session, Inventory inventory, StackRequestSlotInfoData source, StackRequestSlotInfoData destination) {
+        session.getConnector().getLogger().error("Source: " + source.toString() + " Result: " + checkNetId(session, inventory, source));
+        session.getConnector().getLogger().error("Destination: " + destination.toString() + " Result: " + checkNetId(session, inventory, destination));
+        session.getConnector().getLogger().error("Geyser's record of source slot: " + inventory.getItem(bedrockSlotToJava(source)));
+        session.getConnector().getLogger().error("Geyser's record of destination slot: " + inventory.getItem(bedrockSlotToJava(destination)));
+    }
+
     public boolean checkNetId(GeyserSession session, Inventory inventory, StackRequestSlotInfoData slotInfoData) {
         int netId = slotInfoData.getStackNetworkId();
         // "In my testing, sometimes the client thinks the netId of an item in the crafting grid is 1, even though we never said it was.
@@ -823,7 +859,17 @@ public abstract class InventoryTranslator {
     public static ItemStackResponsePacket.ItemEntry makeItemEntry(int bedrockSlot, GeyserItemStack itemStack) {
         ItemStackResponsePacket.ItemEntry itemEntry;
         if (!itemStack.isEmpty()) {
-            itemEntry = new ItemStackResponsePacket.ItemEntry((byte) bedrockSlot, (byte) bedrockSlot, (byte) itemStack.getAmount(), itemStack.getNetId(), "", 0);
+            // As of 1.16.210: Bedrock needs confirmation on what the current item durability is.
+            // If 0 is sent, then Bedrock thinks the item is not damaged
+            int durability = 0;
+            if (itemStack.getNbt() != null) {
+                Tag damage = itemStack.getNbt().get("Damage");
+                if (damage instanceof IntTag) {
+                    durability = ((IntTag) damage).getValue();
+                }
+            }
+
+            itemEntry = new ItemStackResponsePacket.ItemEntry((byte) bedrockSlot, (byte) bedrockSlot, (byte) itemStack.getAmount(), itemStack.getNetId(), "", durability);
         } else {
             itemEntry = new ItemStackResponsePacket.ItemEntry((byte) bedrockSlot, (byte) bedrockSlot, (byte) 0, 0, "", 0);
         }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/holder/BlockInventoryHolder.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/holder/BlockInventoryHolder.java
index e7bfd90f0..b7f67879b 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/holder/BlockInventoryHolder.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/holder/BlockInventoryHolder.java
@@ -74,7 +74,7 @@ public class BlockInventoryHolder extends InventoryHolder {
         // Check to see if there is an existing block we can use that the player just selected.
         // First, verify that the player's position has not changed, so we don't try to select a block wildly out of range.
         // (This could be a virtual inventory that the player is opening)
-        if (session.getLastInteractionPlayerPosition().equals(session.getPlayerEntity().getPosition())) {
+        if (checkInteractionPosition(session)) {
             // Then, check to see if the interacted block is valid for this inventory by ensuring the block state identifier is valid
             int javaBlockId = session.getConnector().getWorldManager().getBlockAt(session, session.getLastInteractionBlockPosition());
             String[] javaBlockString = BlockTranslator.getJavaIdBlockMap().inverse().getOrDefault(javaBlockId, "minecraft:air").split("\\[");
@@ -101,6 +101,16 @@ public class BlockInventoryHolder extends InventoryHolder {
         setCustomName(session, position, inventory, defaultJavaBlockState);
     }
 
+    /**
+     * Will be overwritten in the beacon inventory translator to remove the check, since virtual inventories can't exist.
+     *
+     * @return if the player's last interaction position and current position match. Used to ensure that we don't select
+     * a block to hold the inventory that's wildly out of range.
+     */
+    protected boolean checkInteractionPosition(GeyserSession session) {
+        return session.getLastInteractionPlayerPosition().equals(session.getPlayerEntity().getPosition());
+    }
+
     /**
      * @return true if this Java block ID can be used for player inventory.
      */
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/BeaconInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/BeaconInventoryTranslator.java
index 46c09b66b..5af921f2d 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/BeaconInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/BeaconInventoryTranslator.java
@@ -44,13 +44,40 @@ import org.geysermc.connector.inventory.Inventory;
 import org.geysermc.connector.inventory.PlayerInventory;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot;
+import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
+import org.geysermc.connector.network.translators.inventory.holder.BlockInventoryHolder;
 import org.geysermc.connector.network.translators.inventory.updater.UIInventoryUpdater;
+import org.geysermc.connector.utils.InventoryUtils;
 
 import java.util.Collections;
 
 public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator {
     public BeaconInventoryTranslator() {
-        super(1, "minecraft:beacon", ContainerType.BEACON, UIInventoryUpdater.INSTANCE);
+        super(1, new BlockInventoryHolder("minecraft:beacon", ContainerType.BEACON) {
+            @Override
+            public void prepareInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) {
+                if (!session.getConnector().getConfig().isCacheChunks()) {
+                    // Beacons cannot work without knowing their physical location
+                    return;
+                }
+                super.prepareInventory(translator, session, inventory);
+            }
+
+            @Override
+            protected boolean checkInteractionPosition(GeyserSession session) {
+                // Since we can't fall back to a virtual inventory, let's make opening one easier
+                return true;
+            }
+
+            @Override
+            public void openInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) {
+                if (!session.getConnector().getConfig().isCacheChunks() || !((BeaconContainer) inventory).isUsingRealBlock()) {
+                    InventoryUtils.closeInventory(session, inventory.getId(), false);
+                    return;
+                }
+                super.openInventory(translator, session, inventory);
+            }
+        }, UIInventoryUpdater.INSTANCE);
     }
 
     @Override
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/BrewingInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/BrewingInventoryTranslator.java
index 992a74511..c54722849 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/BrewingInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/BrewingInventoryTranslator.java
@@ -91,6 +91,8 @@ public class BrewingInventoryTranslator extends AbstractBlockInventoryTranslator
                 return 3;
             case 3:
                 return 0;
+            case 4:
+                return 4;
         }
         return super.javaSlotToBedrock(slot);
     }
@@ -105,7 +107,7 @@ public class BrewingInventoryTranslator extends AbstractBlockInventoryTranslator
             case 3:
                 return new BedrockContainerSlot(ContainerSlotType.BREWING_INPUT, 0);
             case 4:
-                return new BedrockContainerSlot(ContainerSlotType.BREWING_INPUT, 0);
+                return new BedrockContainerSlot(ContainerSlotType.BREWING_FUEL, 4);
         }
         return super.javaSlotToBedrockContainer(slot);
     }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/CartographyInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/CartographyInventoryTranslator.java
index 319d9ec0a..a3b50dace 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/CartographyInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/CartographyInventoryTranslator.java
@@ -46,27 +46,27 @@ public class CartographyInventoryTranslator extends AbstractBlockInventoryTransl
     public boolean shouldRejectItemPlace(GeyserSession session, Inventory inventory, ContainerSlotType bedrockSourceContainer,
                                          int javaSourceSlot, ContainerSlotType bedrockDestinationContainer, int javaDestinationSlot) {
         if (javaDestinationSlot == 0) {
-            // Bedrock Edition can use paper in slot 0
+            // Bedrock Edition can use paper or an empty map in slot 0
             GeyserItemStack itemStack = javaSourceSlot == -1 ? session.getPlayerInventory().getCursor() : inventory.getItem(javaSourceSlot);
-            return itemStack.getItemEntry().getJavaIdentifier().equals("minecraft:paper");
+            return itemStack.getItemEntry().getJavaIdentifier().equals("minecraft:paper") || itemStack.getItemEntry().getJavaIdentifier().equals("minecraft:map");
         } else if (javaDestinationSlot == 1) {
-            // Bedrock Edition can use a compass to create locator maps in the ADDITIONAL slot
+            // Bedrock Edition can use a compass to create locator maps, or use a filled map, in the ADDITIONAL slot
             GeyserItemStack itemStack = javaSourceSlot == -1 ? session.getPlayerInventory().getCursor() : inventory.getItem(javaSourceSlot);
-            return itemStack.getItemEntry().getJavaIdentifier().equals("minecraft:compass");
+            return itemStack.getItemEntry().getJavaIdentifier().equals("minecraft:compass") || itemStack.getItemEntry().getJavaIdentifier().equals("minecraft:filled_map");
         }
         return false;
     }
 
     @Override
     public int bedrockSlotToJava(StackRequestSlotInfoData slotInfoData) {
-        if (slotInfoData.getContainer() == ContainerSlotType.CARTOGRAPHY_INPUT) {
-            return 0;
-        }
-        if (slotInfoData.getContainer() == ContainerSlotType.CARTOGRAPHY_ADDITIONAL) {
-            return 1;
-        }
-        if (slotInfoData.getContainer() == ContainerSlotType.CARTOGRAPHY_RESULT || slotInfoData.getContainer() == ContainerSlotType.CREATIVE_OUTPUT) {
-            return 2;
+        switch (slotInfoData.getContainer()) {
+            case CARTOGRAPHY_INPUT:
+                return 0;
+            case CARTOGRAPHY_ADDITIONAL:
+                return 1;
+            case CARTOGRAPHY_RESULT:
+            case CREATIVE_OUTPUT:
+                return 2;
         }
         return super.bedrockSlotToJava(slotInfoData);
     }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/CraftingInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/CraftingInventoryTranslator.java
index 81769c00a..363c9b702 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/CraftingInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/CraftingInventoryTranslator.java
@@ -39,8 +39,9 @@ public class CraftingInventoryTranslator extends AbstractBlockInventoryTranslato
 
     @Override
     public SlotType getSlotType(int javaSlot) {
-        if (javaSlot == 0)
+        if (javaSlot == 0) {
             return SlotType.OUTPUT;
+        }
         return SlotType.NORMAL;
     }
 
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/LecternInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/LecternInventoryTranslator.java
index dbbc418ba..c08dfd995 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/LecternInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/LecternInventoryTranslator.java
@@ -51,22 +51,19 @@ public class LecternInventoryTranslator extends BaseInventoryTranslator {
 
     public LecternInventoryTranslator() {
         super(1);
-        this.updater = new LecternInventoryUpdater();
+        this.updater = new InventoryUpdater();
     }
 
     @Override
     public void prepareInventory(GeyserSession session, Inventory inventory) {
-
     }
 
     @Override
     public void openInventory(GeyserSession session, Inventory inventory) {
-
     }
 
     @Override
     public void closeInventory(GeyserSession session, Inventory inventory) {
-
     }
 
     @Override
@@ -81,7 +78,6 @@ public class LecternInventoryTranslator extends BaseInventoryTranslator {
 
     @Override
     public void updateInventory(GeyserSession session, Inventory inventory) {
-
     }
 
     @Override
@@ -171,8 +167,4 @@ public class LecternInventoryTranslator extends BaseInventoryTranslator {
         }
         return builder;
     }
-
-    private static class LecternInventoryUpdater extends InventoryUpdater {
-
-    }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/LoomInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/LoomInventoryTranslator.java
index 38758c5f8..17c93c15b 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/LoomInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/LoomInventoryTranslator.java
@@ -44,6 +44,7 @@ import org.geysermc.connector.inventory.GeyserItemStack;
 import org.geysermc.connector.inventory.Inventory;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot;
+import org.geysermc.connector.network.translators.inventory.SlotType;
 import org.geysermc.connector.network.translators.inventory.updater.UIInventoryUpdater;
 import org.geysermc.connector.network.translators.item.translators.BannerTranslator;
 
@@ -144,7 +145,7 @@ public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator {
         ClientClickWindowButtonPacket packet = new ClientClickWindowButtonPacket(inventory.getId(), index);
         session.sendDownstreamPacket(packet);
 
-        GeyserItemStack inputCopy = inventory.getItem(0).copy();
+        GeyserItemStack inputCopy = inventory.getItem(0).copy(1);
         inputCopy.setNetId(session.getNextItemNetId());
         // Add the pattern manually, for better item synchronization
         if (inputCopy.getNbt() == null) {
@@ -219,4 +220,12 @@ public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator {
         }
         return super.javaSlotToBedrock(slot);
     }
+
+    @Override
+    public SlotType getSlotType(int javaSlot) {
+        if (javaSlot == 3) {
+            return SlotType.OUTPUT;
+        }
+        return super.getSlotType(javaSlot);
+    }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/AbstractHorseInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/AbstractHorseInventoryTranslator.java
index 6c6c9a0c2..0e365aca1 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/AbstractHorseInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/horse/AbstractHorseInventoryTranslator.java
@@ -41,17 +41,14 @@ public abstract class AbstractHorseInventoryTranslator extends BaseInventoryTran
 
     @Override
     public void prepareInventory(GeyserSession session, Inventory inventory) {
-
     }
 
     @Override
     public void openInventory(GeyserSession session, Inventory inventory) {
-
     }
 
     @Override
     public void closeInventory(GeyserSession session, Inventory inventory) {
-
     }
 
     @Override
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/HorseInventoryUpdater.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/HorseInventoryUpdater.java
index d238b4148..db067a74c 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/HorseInventoryUpdater.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/HorseInventoryUpdater.java
@@ -64,5 +64,4 @@ public class HorseInventoryUpdater extends InventoryUpdater {
         session.sendUpstreamPacket(slotPacket);
         return true;
     }
-
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/InventoryUpdater.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/InventoryUpdater.java
index d7c137177..e94c0944b 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/InventoryUpdater.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/InventoryUpdater.java
@@ -35,7 +35,7 @@ import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
 
 import java.util.Arrays;
 
-public abstract class InventoryUpdater {
+public class InventoryUpdater {
     public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) {
         ItemData[] bedrockItems = new ItemData[36];
         for (int i = 0; i < 36; i++) {
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/LeatherArmorTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/LeatherArmorTranslator.java
index f78eadc25..c2305738d 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/LeatherArmorTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/translators/nbt/LeatherArmorTranslator.java
@@ -35,29 +35,28 @@ import org.geysermc.connector.network.translators.item.ItemEntry;
 @ItemRemapper
 public class LeatherArmorTranslator extends NbtItemStackTranslator {
 
-    private static final String[] ITEMS = new String[]{"minecraft:leather_helmet", "minecraft:leather_chestplate", "minecraft:leather_leggings", "minecraft:leather_boots"};
+    private static final String[] ITEMS = new String[]{"minecraft:leather_helmet", "minecraft:leather_chestplate",
+            "minecraft:leather_leggings", "minecraft:leather_boots", "minecraft:leather_horse_armor"};
 
     @Override
     public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemEntry itemEntry) {
-        if (!itemTag.contains("display")) {
+        CompoundTag displayTag = itemTag.get("display");
+        if (displayTag == null) {
             return;
         }
-        CompoundTag displayTag = itemTag.get("display");
-        if (displayTag.contains("color")) {
-            IntTag color = displayTag.get("color");
-            if (color != null) {
-                itemTag.put(new IntTag("customColor", color.getValue()));
-                displayTag.remove("color");
-            }
+        IntTag color = displayTag.get("color");
+        if (color != null) {
+            itemTag.put(new IntTag("customColor", color.getValue()));
+            displayTag.remove("color");
         }
     }
 
     @Override
     public void translateToJava(CompoundTag itemTag, ItemEntry itemEntry) {
-        if (!itemTag.contains("customColor")) {
+        IntTag color = itemTag.get("customColor");
+        if (color == null) {
             return;
         }
-        IntTag color = itemTag.get("customColor");
         CompoundTag displayTag = itemTag.get("display");
         if (displayTag == null) {
             displayTag = new CompoundTag("display");
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityMetadataTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityMetadataTranslator.java
index e3c64d55f..73047d0c4 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityMetadataTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/JavaEntityMetadataTranslator.java
@@ -32,6 +32,7 @@ import org.geysermc.connector.network.translators.Translator;
 
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
 import com.github.steveice10.mc.protocol.packet.ingame.server.entity.ServerEntityMetadataPacket;
+import org.geysermc.connector.utils.InteractiveTagManager;
 import org.geysermc.connector.utils.LanguageUtils;
 
 @Translator(packet = ServerEntityMetadataPacket.class)
@@ -39,9 +40,11 @@ public class JavaEntityMetadataTranslator extends PacketTranslator<ServerEntityM
 
     @Override
     public void translate(ServerEntityMetadataPacket packet, GeyserSession session) {
-        Entity entity = session.getEntityCache().getEntityByJavaId(packet.getEntityId());
+        Entity entity;
         if (packet.getEntityId() == session.getPlayerEntity().getEntityId()) {
             entity = session.getPlayerEntity();
+        } else {
+            entity = session.getEntityCache().getEntityByJavaId(packet.getEntityId());
         }
         if (entity == null) return;
 
@@ -61,5 +64,10 @@ public class JavaEntityMetadataTranslator extends PacketTranslator<ServerEntityM
         }
 
         entity.updateBedrockMetadata(session);
+
+        // Update the interactive tag, if necessary
+        if (session.getMouseoverEntity() != null && session.getMouseoverEntity().getEntityId() == entity.getEntityId()) {
+            InteractiveTagManager.updateTag(session, entity);
+        }
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaSetSlotTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaSetSlotTranslator.java
index b5978ba76..a0e9901f3 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaSetSlotTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaSetSlotTranslator.java
@@ -134,7 +134,6 @@ public class JavaSetSlotTranslator extends PacketTranslator<ServerSetSlotPacket>
             height += -firstRow + 1;
             width += -firstCol + 1;
 
-            //TODO
             recipes:
             for (Recipe recipe : session.getCraftingRecipes().values()) {
                 if (recipe.getType() == RecipeType.CRAFTING_SHAPED) {
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaTradeListTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaTradeListTranslator.java
index df8339079..d31dbb617 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaTradeListTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/world/JavaTradeListTranslator.java
@@ -58,6 +58,7 @@ public class JavaTradeListTranslator extends PacketTranslator<ServerTradeListPac
             return;
         }
 
+        // Retrieve the fake villager involved in the trade, and update its metadata to match with the window information
         MerchantContainer merchantInventory = (MerchantContainer) openInventory;
         merchantInventory.setVillagerTrades(packet.getTrades());
         Entity villager = merchantInventory.getVillager();
@@ -66,6 +67,7 @@ public class JavaTradeListTranslator extends PacketTranslator<ServerTradeListPac
         villager.getMetadata().put(EntityData.TRADE_XP, packet.getExperience());
         villager.updateBedrockMetadata(session);
 
+        // Construct the packet that opens the trading window
         UpdateTradePacket updateTradePacket = new UpdateTradePacket();
         updateTradePacket.setTradeTier(packet.getVillagerLevel() - 1);
         updateTradePacket.setContainerId((short) packet.getWindowId());
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/GeyserWorldManager.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/GeyserWorldManager.java
index 6d2d8720d..014f3e366 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/GeyserWorldManager.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/GeyserWorldManager.java
@@ -73,7 +73,7 @@ public class GeyserWorldManager extends WorldManager {
     }
 
     @Override
-    public boolean hasMoreBlockDataThanChunkCache() {
+    public boolean hasOwnChunkCache() {
         // This implementation can only fetch data from the session chunk cache
         return false;
     }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/WorldManager.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/WorldManager.java
index 6795ae4bf..e97dcec32 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/WorldManager.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/WorldManager.java
@@ -88,14 +88,14 @@ public abstract class WorldManager {
     public abstract void getBlocksInSection(GeyserSession session, int x, int y, int z, Chunk section);
 
     /**
-     * Checks whether or not this world manager has access to more block data than the chunk cache.
+     * Checks whether or not this world manager requires a separate chunk cache/has access to more block data than the chunk cache.
      * <p>
      * Some world managers (e.g. Spigot) can provide access to block data outside of the chunk cache, and even with chunk caching disabled. This
      * method provides a means to check if this manager has this capability.
      *
      * @return whether or not this world manager has access to more block data than the chunk cache
      */
-    public abstract boolean hasMoreBlockDataThanChunkCache();
+    public abstract boolean hasOwnChunkCache();
 
     /**
      * Gets the Java biome data for the specified chunk.
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java
index ebc90b722..ff51562b9 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockStateValues.java
@@ -29,6 +29,7 @@ import com.fasterxml.jackson.databind.JsonNode;
 import it.unimi.dsi.fastutil.ints.*;
 
 import java.util.Map;
+import java.util.function.BiFunction;
 
 /**
  * Used for block entities if the Java block state contains Bedrock block information.
@@ -199,7 +200,13 @@ public class BlockStateValues {
         return FLOWER_POT_VALUES;
     }
 
-    public static Int2BooleanMap getLecternBookStates() {
+    /**
+     * This returns a Map interface so IntelliJ doesn't complain about {@link Int2BooleanMap#compute(int, BiFunction)}
+     * not returning null.
+     *
+     * @return the lectern book state map pointing to book present state
+     */
+    public static Map<Integer, Boolean> getLecternBookStates() {
         return LECTERN_BOOK_STATES;
     }
 
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java
index ec1c79950..057c74d2b 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/BlockTranslator.java
@@ -112,8 +112,6 @@ public abstract class BlockTranslator {
      */
     private final Map<String, NbtMap> javaIdentifierToBedrockTag;
 
-    private static final int BLOCK_STATE_VERSION = 17825808;
-
     /**
      * Stores the raw blocks JSON until it is no longer needed.
      */
@@ -413,6 +411,10 @@ public abstract class BlockTranslator {
         return bedrockWaterId;
     }
 
+    /**
+     * @return the "block state version" generated in the Bedrock block palette that completes an NBT indication of a
+     * block state.
+     */
     public abstract int getBlockStateVersion();
 
     public byte[] getEmptyChunkData() {
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SignBlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SignBlockEntityTranslator.java
index a92fac599..61ca4fa9c 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SignBlockEntityTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/SignBlockEntityTranslator.java
@@ -26,6 +26,7 @@
 package org.geysermc.connector.network.translators.world.block.entity;
 
 import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
+import com.github.steveice10.opennbt.tag.builtin.Tag;
 import com.nukkitx.nbt.NbtMapBuilder;
 import org.geysermc.connector.network.translators.chat.MessageTranslator;
 import org.geysermc.connector.utils.SignUtils;
@@ -35,7 +36,7 @@ public class SignBlockEntityTranslator extends BlockEntityTranslator {
     /**
      * Maps a color stored in a sign's Color tag to a Bedrock Edition formatting code.
      * <br>
-     * The color names correspond to dye names, because of this we can't use {@link MessageTranslator#getColor(String)}.
+     * The color names correspond to dye names, because of this we can't use a more global method.
      *
      * @param javaColor The dye color stored in the sign's Color tag.
      * @return A Bedrock Edition formatting code for valid dye colors, otherwise an empty string.
@@ -101,27 +102,34 @@ public class SignBlockEntityTranslator extends BlockEntityTranslator {
             String signLine = getOrDefault(tag.getValue().get("Text" + currentLine), "");
             signLine = MessageTranslator.convertMessageLenient(signLine);
 
-            // Trim any trailing formatting codes
-            if (signLine.length() > 2 && signLine.toCharArray()[signLine.length() - 2] == '\u00a7') {
-                signLine = signLine.substring(0, signLine.length() - 2);
-            }
-
             // Check the character width on the sign to ensure there is no overflow that is usually hidden
             // to Java Edition clients but will appear to Bedrock clients
             int signWidth = 0;
             StringBuilder finalSignLine = new StringBuilder();
+            boolean previousCharacterWasFormatting = false; // Color changes do not count for maximum width
             for (char c : signLine.toCharArray()) {
-                signWidth += SignUtils.getCharacterWidth(c);
+                if (c == '\u00a7') {
+                    // Don't count this character
+                    previousCharacterWasFormatting = true;
+                } else if (previousCharacterWasFormatting) {
+                    // Don't count this character either
+                    previousCharacterWasFormatting = false;
+                } else {
+                    signWidth += SignUtils.getCharacterWidth(c);
+                }
+
                 if (signWidth <= SignUtils.BEDROCK_CHARACTER_WIDTH_MAX) {
                     finalSignLine.append(c);
                 } else {
+                    // Adding the character would make Bedrock move to the next line - Java doesn't do that, so we do not want to
                     break;
                 }
             }
 
             // Java Edition 1.14 added the ability to change the text color of the whole sign using dye
-            if (tag.contains("Color")) {
-                signText.append(getBedrockSignColor(tag.get("Color").getValue().toString()));
+            Tag color = tag.get("Color");
+            if (color != null) {
+                signText.append(getBedrockSignColor(color.getValue().toString()));
             }
 
             signText.append(finalSignLine.toString());
diff --git a/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java b/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java
index 5a0e41ed5..5af08292a 100644
--- a/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java
+++ b/connector/src/main/java/org/geysermc/connector/skin/SkinManager.java
@@ -286,7 +286,7 @@ public class SkinManager {
 
             String skinUrl = isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl();
             String capeUrl = SkinProvider.EMPTY_CAPE.getTextureUrl();
-            if (("steve".equals(skinUrl) || "alex".equals(skinUrl)) && GeyserConnector.getInstance().getAuthType() != AuthType.ONLINE) {
+            if (("steve".equals(skinUrl) || "alex".equals(skinUrl)) && GeyserConnector.getInstance().getDefaultAuthType() != AuthType.ONLINE) {
                 GeyserSession session = GeyserConnector.getInstance().getPlayerByUuid(profile.getId());
 
                 if (session != null) {
diff --git a/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java b/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java
index a75c20872..36d1f5826 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/BlockUtils.java
@@ -26,17 +26,19 @@
 package org.geysermc.connector.utils;
 
 import com.github.steveice10.mc.protocol.data.game.entity.Effect;
-import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
 import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
 import com.nukkitx.math.vector.Vector3i;
 import org.geysermc.connector.network.session.GeyserSession;
-import org.geysermc.connector.network.translators.world.block.BlockTranslator;
 import org.geysermc.connector.network.translators.item.ItemEntry;
 import org.geysermc.connector.network.translators.item.ToolItemEntry;
-
-import java.util.Optional;
+import org.geysermc.connector.network.translators.world.block.BlockTranslator;
 
 public class BlockUtils {
+    /**
+     * A static constant of {@link Position} with all values being zero.
+     */
+    public static final Position POSITION_ZERO = new Position(0, 0, 0);
 
     private static boolean correctTool(String blockToolType, String itemToolType) {
         return (blockToolType.equals("sword") && itemToolType.equals("sword")) ||
diff --git a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java
index b3a31e1ab..b6e387237 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java
@@ -91,7 +91,7 @@ public class ChunkUtils {
         BitSet waterloggedPaletteIds = new BitSet();
         BitSet pistonOrFlowerPaletteIds = new BitSet();
 
-        boolean worldManagerHasMoreBlockDataThanCache = session.getConnector().getWorldManager().hasMoreBlockDataThanChunkCache();
+        boolean worldManagerHasMoreBlockDataThanCache = session.getConnector().getWorldManager().hasOwnChunkCache();
 
         // If the received packet was a full chunk update, null sections in the chunk are guaranteed to also be null in the world manager
         boolean shouldCheckWorldManagerOnMissingSections = isNonFullChunk && worldManagerHasMoreBlockDataThanCache;
@@ -372,27 +372,30 @@ public class ChunkUtils {
         }
         session.sendUpstreamPacket(waterPacket);
 
-        if (BlockStateValues.getLecternBookStates().containsKey(blockState)) {
-            boolean lecternCachedHasBook = session.getLecternCache().contains(position);
-            boolean newLecternHasBook = BlockStateValues.getLecternBookStates().get(blockState);
-            if (!session.getConnector().getWorldManager().shouldExpectLecternHandled() && lecternCachedHasBook != newLecternHasBook) {
-                // Refresh the block entirely - it either has a book or no longer has a book
-                NbtMap newLecternTag;
-                if (newLecternHasBook) {
-                    newLecternTag = session.getConnector().getWorldManager().getLecternDataAt(session, position.getX(), position.getY(), position.getZ(), false);
+        BlockStateValues.getLecternBookStates().compute(blockState, (key, newLecternHasBook) -> {
+            // Determine if this block is a lectern
+            if (newLecternHasBook != null) {
+                boolean lecternCachedHasBook = session.getLecternCache().contains(position);
+                if (!session.getConnector().getWorldManager().shouldExpectLecternHandled() && lecternCachedHasBook != newLecternHasBook) {
+                    // Refresh the block entirely - it either has a book or no longer has a book
+                    NbtMap newLecternTag;
+                    if (newLecternHasBook) {
+                        newLecternTag = session.getConnector().getWorldManager().getLecternDataAt(session, position.getX(), position.getY(), position.getZ(), false);
+                    } else {
+                        session.getLecternCache().remove(position);
+                        newLecternTag = LecternInventoryTranslator.getBaseLecternTag(position.getX(), position.getY(), position.getZ(), 0).build();
+                    }
+                    BlockEntityUtils.updateBlockEntity(session, newLecternTag, position);
                 } else {
-                    session.getLecternCache().remove(position);
-                    newLecternTag = LecternInventoryTranslator.getBaseLecternTag(position.getX(), position.getY(), position.getZ(), 0).build();
+                    // As of right now, no tag can be added asynchronously
+                    session.getConnector().getWorldManager().getLecternDataAt(session, position.getX(), position.getY(), position.getZ(), false);
                 }
-                BlockEntityUtils.updateBlockEntity(session, newLecternTag, position);
             } else {
-                // As of right now, no tag can be added asynchronously
-                session.getConnector().getWorldManager().getLecternDataAt(session, position.getX(), position.getY(), position.getZ(), false);
+                // Lectern has been destroyed, if it existed
+                session.getLecternCache().remove(position);
             }
-        } else {
-            // Lectern has been destroyed, if it existed
-            session.getLecternCache().remove(position);
-        }
+            return newLecternHasBook;
+        });
 
         // Since Java stores bed colors/skull information as part of the namespaced ID and Bedrock stores it as a tag
         // This is the only place I could find that interacts with the Java block state and block updates
diff --git a/connector/src/main/java/org/geysermc/connector/utils/InteractiveTagManager.java b/connector/src/main/java/org/geysermc/connector/utils/InteractiveTagManager.java
new file mode 100644
index 000000000..0b59efff3
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/utils/InteractiveTagManager.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (c) 2019-2021 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.connector.utils;
+
+import com.google.common.collect.ImmutableSet;
+import com.nukkitx.protocol.bedrock.data.entity.EntityData;
+import com.nukkitx.protocol.bedrock.data.entity.EntityDataMap;
+import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
+import lombok.Getter;
+import org.geysermc.connector.entity.Entity;
+import org.geysermc.connector.entity.living.animal.horse.HorseEntity;
+import org.geysermc.connector.entity.type.EntityType;
+import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.item.ItemEntry;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+public class InteractiveTagManager {
+    /**
+     * A list of all foods a horse/donkey can eat on Java Edition.
+     * Used to display interactive tag if needed.
+     */
+    private static final Set<String> DONKEY_AND_HORSE_FOODS = ImmutableSet.of("golden_apple", "enchanted_golden_apple",
+            "golden_carrot", "sugar", "apple", "wheat", "hay_block");
+
+    /**
+     * A list of all flowers. Used for feeding bees.
+     */
+    private static final Set<String> FLOWERS = ImmutableSet.of("dandelion", "poppy", "blue_orchid", "allium", "azure_bluet",
+            "red_tulip", "pink_tulip", "white_tulip", "orange_tulip", "cornflower", "lily_of_the_valley", "wither_rose",
+            "sunflower", "lilac", "rose_bush", "peony");
+
+    /**
+     * All entity types that can be leashed on Java Edition
+     */
+    private static final Set<EntityType> LEASHABLE_MOB_TYPES = EnumSet.of(EntityType.BEE, EntityType.CAT, EntityType.CHICKEN,
+            EntityType.COW, EntityType.DOLPHIN, EntityType.DONKEY, EntityType.FOX, EntityType.HOGLIN, EntityType.HORSE, EntityType.SKELETON_HORSE,
+            EntityType.ZOMBIE_HORSE, EntityType.IRON_GOLEM, EntityType.LLAMA, EntityType.TRADER_LLAMA, EntityType.MOOSHROOM,
+            EntityType.MULE, EntityType.OCELOT, EntityType.PARROT, EntityType.PIG, EntityType.POLAR_BEAR, EntityType.RABBIT,
+            EntityType.SHEEP, EntityType.SNOW_GOLEM, EntityType.STRIDER, EntityType.WOLF, EntityType.ZOGLIN);
+
+    private static final Set<EntityType> SADDLEABLE_WHEN_TAMED_MOB_TYPES = EnumSet.of(EntityType.DONKEY, EntityType.HORSE,
+            EntityType.ZOMBIE_HORSE, EntityType.MULE);
+
+    /**
+     * A list of all foods a wolf can eat on Java Edition.
+     * Used to display interactive tag if needed.
+     */
+    private static final Set<String> WOLF_FOODS = ImmutableSet.of("pufferfish", "tropical_fish", "chicken", "cooked_chicken",
+            "porkchop", "beef", "rabbit", "cooked_porkchop", "cooked_beef", "rotten_flesh", "mutton", "cooked_mutton",
+            "cooked_rabbit");
+
+    /**
+     * Update the suggestion that the client currently has on their screen for this entity (for example, "Feed" or "Ride")
+     *
+     * @param session the Bedrock client session
+     * @param interactEntity the entity that the client is currently facing.
+     */
+    public static void updateTag(GeyserSession session, Entity interactEntity) {
+        EntityDataMap entityMetadata = interactEntity.getMetadata();
+        ItemEntry itemEntry = session.getPlayerInventory().getItemInHand().getItemEntry();
+        String javaIdentifierStripped = itemEntry.getJavaIdentifier().replace("minecraft:", "");
+
+        // TODO - in the future, update these in the metadata? So the client doesn't have to wiggle their cursor around for it to happen
+        // TODO - also, might be good to abstract out the eating thing. I know there will need to be food tracked for https://github.com/GeyserMC/Geyser/issues/1005 but not all food is breeding food
+        InteractiveTag interactiveTag = InteractiveTag.NONE;
+        if (entityMetadata.getLong(EntityData.LEASH_HOLDER_EID) == session.getPlayerEntity().getGeyserId()) {
+            // Unleash the entity
+            interactiveTag = InteractiveTag.REMOVE_LEASH;
+        } else if (javaIdentifierStripped.equals("saddle") && !entityMetadata.getFlags().getFlag(EntityFlag.SADDLED) &&
+                ((SADDLEABLE_WHEN_TAMED_MOB_TYPES.contains(interactEntity.getEntityType()) && entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && !session.isSneaking()) ||
+                        interactEntity.getEntityType() == EntityType.PIG || interactEntity.getEntityType() == EntityType.STRIDER)) {
+            // Entity can be saddled and the conditions meet (entity can be saddled and, if needed, is tamed)
+            interactiveTag = InteractiveTag.SADDLE;
+        } else if (javaIdentifierStripped.equals("name_tag") && session.getPlayerInventory().getItemInHand().getNbt() != null &&
+                session.getPlayerInventory().getItemInHand().getNbt().contains("display")) {
+            // Holding a named name tag
+            interactiveTag = InteractiveTag.NAME;
+        } else if (javaIdentifierStripped.equals("lead") && LEASHABLE_MOB_TYPES.contains(interactEntity.getEntityType()) &&
+                entityMetadata.getLong(EntityData.LEASH_HOLDER_EID, -1L) == -1L) {
+            // Holding a leash and the mob is leashable for sure
+            // (Plugins can change this behavior so that's something to look into in the far far future)
+            interactiveTag = InteractiveTag.LEASH;
+        } else {
+            switch (interactEntity.getEntityType()) {
+                case BEE:
+                    if (FLOWERS.contains(javaIdentifierStripped)) {
+                        interactiveTag = InteractiveTag.FEED;
+                    }
+                    break;
+                case BOAT:
+                    interactiveTag = InteractiveTag.BOARD_BOAT;
+                    break;
+                case CAT:
+                    if (javaIdentifierStripped.equals("cod") || javaIdentifierStripped.equals("salmon")) {
+                        interactiveTag = InteractiveTag.FEED;
+                    } else if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) &&
+                            entityMetadata.getLong(EntityData.OWNER_EID) == session.getPlayerEntity().getGeyserId()) {
+                        // Tamed and owned by player - can sit/stand
+                        interactiveTag = entityMetadata.getFlags().getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT;
+                        break;
+                    }
+                    break;
+                case CHICKEN:
+                    if (javaIdentifierStripped.contains("seeds")) {
+                        interactiveTag = InteractiveTag.FEED;
+                    }
+                    break;
+                case MOOSHROOM:
+                    // Shear the mooshroom
+                    if (javaIdentifierStripped.equals("shears")) {
+                        interactiveTag = InteractiveTag.MOOSHROOM_SHEAR;
+                        break;
+                    }
+                    // Bowls are acceptable here
+                    else if (javaIdentifierStripped.equals("bowl")) {
+                        interactiveTag = InteractiveTag.MOOSHROOM_MILK_STEW;
+                        break;
+                    }
+                    // Fall down to COW as this works on mooshrooms
+                case COW:
+                    if (javaIdentifierStripped.equals("wheat")) {
+                        interactiveTag = InteractiveTag.FEED;
+                    } else if (javaIdentifierStripped.equals("bucket")) {
+                        // Milk the cow
+                        interactiveTag = InteractiveTag.MILK;
+                    }
+                    break;
+                case CREEPER:
+                    if (javaIdentifierStripped.equals("flint_and_steel")) {
+                        // Today I learned that you can ignite a creeper with flint and steel! Huh.
+                        interactiveTag = InteractiveTag.IGNITE_CREEPER;
+                    }
+                    break;
+                case DONKEY:
+                case LLAMA:
+                case MULE:
+                    if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) && !entityMetadata.getFlags().getFlag(EntityFlag.CHESTED)
+                            && javaIdentifierStripped.equals("chest")) {
+                        // Can attach a chest
+                        interactiveTag = InteractiveTag.ATTACH_CHEST;
+                        break;
+                    }
+                    // Intentional fall-through
+                case HORSE:
+                case SKELETON_HORSE:
+                case TRADER_LLAMA:
+                case ZOMBIE_HORSE:
+                    boolean tamed = entityMetadata.getFlags().getFlag(EntityFlag.TAMED);
+                    if (session.isSneaking() && tamed && (interactEntity instanceof HorseEntity || entityMetadata.getFlags().getFlag(EntityFlag.CHESTED))) {
+                        interactiveTag = InteractiveTag.OPEN_CONTAINER;
+                        break;
+                    }
+                    // have another switch statement as, while these share mount attributes they don't share food
+                    switch (interactEntity.getEntityType()) {
+                        case LLAMA:
+                        case TRADER_LLAMA:
+                            if (javaIdentifierStripped.equals("wheat") || javaIdentifierStripped.equals("hay_block")) {
+                                interactiveTag = InteractiveTag.FEED;
+                                break;
+                            }
+                        case DONKEY:
+                        case HORSE:
+                            // Undead can't eat
+                            if (DONKEY_AND_HORSE_FOODS.contains(javaIdentifierStripped)) {
+                                interactiveTag = InteractiveTag.FEED;
+                                break;
+                            }
+                    }
+                    if (!entityMetadata.getFlags().getFlag(EntityFlag.BABY)) {
+                        // Can't ride a baby
+                        if (tamed) {
+                            interactiveTag = InteractiveTag.RIDE_HORSE;
+                        } else if (itemEntry.equals(ItemEntry.AIR)) {
+                            // Can't hide an untamed entity without having your hand empty
+                            interactiveTag = InteractiveTag.MOUNT;
+                        }
+                    }
+                    break;
+                case FOX:
+                    if (javaIdentifierStripped.equals("sweet_berries")) {
+                        interactiveTag = InteractiveTag.FEED;
+                    }
+                    break;
+                case HOGLIN:
+                    if (javaIdentifierStripped.equals("crimson_fungus")) {
+                        interactiveTag = InteractiveTag.FEED;
+                    }
+                    break;
+                case MINECART:
+                    interactiveTag = InteractiveTag.RIDE_MINECART;
+                    break;
+                case MINECART_CHEST:
+                case MINECART_COMMAND_BLOCK:
+                case MINECART_HOPPER:
+                    interactiveTag = InteractiveTag.OPEN_CONTAINER;
+                    break;
+                case OCELOT:
+                    if (javaIdentifierStripped.equals("cod") || javaIdentifierStripped.equals("salmon")) {
+                        interactiveTag = InteractiveTag.FEED;
+                    }
+                    break;
+                case PANDA:
+                    if (javaIdentifierStripped.equals("bamboo")) {
+                        interactiveTag = InteractiveTag.FEED;
+                    }
+                    break;
+                case PARROT:
+                    if (javaIdentifierStripped.contains("seeds") || javaIdentifierStripped.equals("cookie")) {
+                        interactiveTag = InteractiveTag.FEED;
+                    }
+                    break;
+                case PIG:
+                    if (javaIdentifierStripped.equals("carrot") || javaIdentifierStripped.equals("potato") || javaIdentifierStripped.equals("beetroot")) {
+                        interactiveTag = InteractiveTag.FEED;
+                    } else if (entityMetadata.getFlags().getFlag(EntityFlag.SADDLED)) {
+                        interactiveTag = InteractiveTag.MOUNT;
+                    }
+                    break;
+                case PIGLIN:
+                    if (!entityMetadata.getFlags().getFlag(EntityFlag.BABY) && javaIdentifierStripped.equals("gold_ingot")) {
+                        interactiveTag = InteractiveTag.BARTER;
+                    }
+                    break;
+                case RABBIT:
+                    if (javaIdentifierStripped.equals("dandelion") || javaIdentifierStripped.equals("carrot") || javaIdentifierStripped.equals("golden_carrot")) {
+                        interactiveTag = InteractiveTag.FEED;
+                    }
+                    break;
+                case SHEEP:
+                    if (javaIdentifierStripped.equals("wheat")) {
+                        interactiveTag = InteractiveTag.FEED;
+                    } else if (!entityMetadata.getFlags().getFlag(EntityFlag.SHEARED)) {
+                        if (javaIdentifierStripped.equals("shears")) {
+                            // Shear the sheep
+                            interactiveTag = InteractiveTag.SHEAR;
+                        } else if (javaIdentifierStripped.contains("_dye")) {
+                            // Dye the sheep
+                            interactiveTag = InteractiveTag.DYE;
+                        }
+                    }
+                    break;
+                case STRIDER:
+                    if (javaIdentifierStripped.equals("warped_fungus")) {
+                        interactiveTag = InteractiveTag.FEED;
+                    } else if (entityMetadata.getFlags().getFlag(EntityFlag.SADDLED)) {
+                        interactiveTag = InteractiveTag.RIDE_STRIDER;
+                    }
+                    break;
+                case TURTLE:
+                    if (javaIdentifierStripped.equals("seagrass")) {
+                        interactiveTag = InteractiveTag.FEED;
+                    }
+                    break;
+                case VILLAGER:
+                    if (entityMetadata.getInt(EntityData.VARIANT) != 14 && entityMetadata.getInt(EntityData.VARIANT) != 0
+                            && entityMetadata.getFloat(EntityData.SCALE) >= 0.75f) { // Not a nitwit, has a profession and is not a baby
+                        interactiveTag = InteractiveTag.TRADE;
+                    }
+                    break;
+                case WANDERING_TRADER:
+                    interactiveTag = InteractiveTag.TRADE; // Since you can always trade with a wandering villager, presumably.
+                    break;
+                case WOLF:
+                    if (javaIdentifierStripped.equals("bone") && !entityMetadata.getFlags().getFlag(EntityFlag.TAMED)) {
+                        // Bone and untamed - can tame
+                        interactiveTag = InteractiveTag.TAME;
+                    } else if (WOLF_FOODS.contains(javaIdentifierStripped)) {
+                        // Compatible food in hand - feed
+                        // Sometimes just sits/stands when the wolf isn't hungry - there doesn't appear to be a way to fix this
+                        interactiveTag = InteractiveTag.FEED;
+                    } else if (entityMetadata.getFlags().getFlag(EntityFlag.TAMED) &&
+                            entityMetadata.getLong(EntityData.OWNER_EID) == session.getPlayerEntity().getGeyserId()) {
+                        // Tamed and owned by player - can sit/stand
+                        interactiveTag = entityMetadata.getFlags().getFlag(EntityFlag.SITTING) ? InteractiveTag.STAND : InteractiveTag.SIT;
+                    }
+                    break;
+                case ZOMBIE_VILLAGER:
+                    // We can't guarantee the existence of the weakness effect so we just always show it.
+                    if (javaIdentifierStripped.equals("golden_apple")) {
+                        interactiveTag = InteractiveTag.CURE;
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+        session.getPlayerEntity().getMetadata().put(EntityData.INTERACTIVE_TAG, interactiveTag.getValue());
+        session.getPlayerEntity().updateBedrockMetadata(session);
+    }
+
+    /**
+     * All interactive tags in enum form. For potential API usage.
+     */
+    public enum InteractiveTag {
+        NONE(true),
+        IGNITE_CREEPER("creeper"),
+        EDIT,
+        LEAVE_BOAT("exit.boat"),
+        FEED,
+        FISH("fishing"),
+        MILK,
+        MOOSHROOM_SHEAR("mooshear"),
+        MOOSHROOM_MILK_STEW("moostew"),
+        BOARD_BOAT("ride.boat"),
+        RIDE_MINECART("ride.minecart"),
+        RIDE_HORSE("ride.horse"),
+        RIDE_STRIDER("ride.strider"),
+        SHEAR,
+        SIT,
+        STAND,
+        TALK,
+        TAME,
+        DYE,
+        CURE,
+        OPEN_CONTAINER("opencontainer"),
+        CREATE_MAP("createMap"),
+        TAKE_PICTURE("takepicture"),
+        SADDLE,
+        MOUNT,
+        BOOST,
+        WRITE,
+        LEASH,
+        REMOVE_LEASH("unleash"),
+        NAME,
+        ATTACH_CHEST("attachchest"),
+        TRADE,
+        POSE_ARMOR_STAND("armorstand.pose"),
+        EQUIP_ARMOR_STAND("armorstand.equip"),
+        READ,
+        WAKE_VILLAGER("wakevillager"),
+        BARTER;
+
+        /**
+         * The full string that should be passed on to the client.
+         */
+        @Getter
+        private final String value;
+
+        InteractiveTag(boolean isNone) {
+            this.value = "";
+        }
+
+        InteractiveTag(String value) {
+            this.value = "action.interact." + value;
+        }
+
+        InteractiveTag() {
+            this.value = "action.interact." + name().toLowerCase();
+        }
+    }
+}
diff --git a/connector/src/test/java/org/geysermc/connector/network/translators/chat/MessageTranslatorTest.java b/connector/src/test/java/org/geysermc/connector/network/translators/chat/MessageTranslatorTest.java
index bbad2394d..7052123fe 100644
--- a/connector/src/test/java/org/geysermc/connector/network/translators/chat/MessageTranslatorTest.java
+++ b/connector/src/test/java/org/geysermc/connector/network/translators/chat/MessageTranslatorTest.java
@@ -46,8 +46,8 @@ public class MessageTranslatorTest {
 
         // RGB downgrade test
         messages.put("{\"extra\":[{\"text\":\"          \"},{\"color\":\"gold\",\"text\":\"The \"},{\"color\":\"#E14248\",\"obfuscated\":true,\"text\":\"||\"},{\"color\":\"#3AA9FF\",\"bold\":true,\"text\":\"CubeCraft\"},{\"color\":\"#E14248\",\"obfuscated\":true,\"text\":\"||\"},{\"color\":\"gold\",\"text\":\" Network \"},{\"color\":\"green\",\"text\":\"[1.8/1.9+]\\n         \"},{\"color\":\"#f5e342\",\"text\":\"✦ \"},{\"color\":\"#b042f5\",\"bold\":true,\"text\":\"N\"},{\"color\":\"#c142f5\",\"bold\":true,\"text\":\"E\"},{\"color\":\"#d342f5\",\"bold\":true,\"text\":\"W\"},{\"color\":\"#e442f5\",\"bold\":true,\"text\":\":\"},{\"color\":\"#f542f5\",\"bold\":true,\"text\":\" \"},{\"color\":\"#bcf542\",\"bold\":true,\"text\":\"A\"},{\"color\":\"#acee3f\",\"bold\":true,\"text\":\"M\"},{\"color\":\"#9ce73c\",\"bold\":true,\"text\":\"O\"},{\"color\":\"#8ce039\",\"bold\":true,\"text\":\"N\"},{\"color\":\"#7cd936\",\"bold\":true,\"text\":\"G\"},{\"color\":\"#6cd233\",\"bold\":true,\"text\":\" \"},{\"color\":\"#5ccb30\",\"bold\":true,\"text\":\"S\"},{\"color\":\"#4cc42d\",\"bold\":true,\"text\":\"L\"},{\"color\":\"#3cbd2a\",\"bold\":true,\"text\":\"I\"},{\"color\":\"#2cb627\",\"bold\":true,\"text\":\"M\"},{\"color\":\"#1caf24\",\"bold\":true,\"text\":\"E\"},{\"color\":\"#0ca821\",\"bold\":true,\"text\":\"S\"},{\"color\":\"#f5e342\",\"text\":\" \"},{\"color\":\"#6d7c87\",\"text\":\"(kinda sus) \"},{\"color\":\"#f5e342\",\"text\":\"✦\"}],\"text\":\"\"}",
-                "          §r§6The §r§c§k||§r§3§lCubeCraft§r§c§k||§r§6 Network §r§a[1.8/1.9+]\n" +
-                        "         §r§e✦ §r§d§lN§r§d§lE§r§d§lW§r§d§l:§r§d§l §r§e§lA§r§e§lM§r§a§lO§r§a§lN§r§a§lG§r§a§l §r§a§lS§r§a§lL§r§2§lI§r§2§lM§r§2§lE§r§2§lS§r§e §r§8(kinda sus) §r§e✦");
+                "          §r§6The §r§d§k||§r§b§lCubeCraft§r§d§k||§r§6 Network §r§a[1.8/1.9+]\n" +
+                        "         §r§e✦ §r§d§lN§r§d§lE§r§d§lW§r§d§l:§r§d§l §r§e§lA§r§e§lM§r§e§lO§r§a§lN§r§a§lG§r§a§l §r§a§lS§r§2§lL§r§2§lI§r§2§lM§r§2§lE§r§2§lS§r§e §r§b(kinda sus) §r§e✦");
 
         // Color code format resetting
         messages.put("{\"text\":\"\",\"extra\":[{\"text\":\"\",\"extra\":[{\"text\":\"[\",\"color\":\"gray\"},{\"text\":\"H\",\"color\":\"yellow\"},{\"text\":\"]\",\"color\":\"gray\"},{\"text\":\" \",\"color\":\"white\"},{\"text\":\"GUEST\",\"color\":\"#b7b7b7\",\"bold\":true}]},{\"text\":\"\",\"extra\":[{\"text\":\" \",\"bold\":true},{\"text\":\"»\",\"color\":\"blue\"},{\"text\":\" \",\"color\":\"gray\"}]},{\"text\":\"\",\"extra\":[{\"text\":\"rtm516\",\"color\":\"white\"},{\"text\":\": \",\"color\":\"gray\"},{\"text\":\"\",\"color\":\"white\"}]},{\"text\":\"\",\"extra\":[{\"text\":\"This is an amazing bedrock test message\",\"color\":\"white\"}]}]}\n",