diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/TranslatorsInit.java b/connector/src/main/java/org/geysermc/connector/network/translators/TranslatorsInit.java
index 538500097..6b3adf15d 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/TranslatorsInit.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/TranslatorsInit.java
@@ -46,6 +46,8 @@ import lombok.Getter;
 import org.geysermc.connector.network.translators.bedrock.*;
 import org.geysermc.connector.network.translators.block.BlockTranslator;
 import org.geysermc.connector.network.translators.inventory.*;
+import org.geysermc.connector.network.translators.inventory.updater.ContainerInventoryUpdater;
+import org.geysermc.connector.network.translators.inventory.updater.InventoryUpdater;
 import org.geysermc.connector.network.translators.item.ItemTranslator;
 import org.geysermc.connector.network.translators.java.*;
 import org.geysermc.connector.network.translators.java.entity.*;
@@ -72,7 +74,7 @@ public class TranslatorsInit {
     private static BlockTranslator blockTranslator;
 
     @Getter
-    private static Map<WindowType, InventoryTranslator> inventoryTranslators = new HashMap<WindowType, InventoryTranslator>();
+    private static Map<WindowType, InventoryTranslator> inventoryTranslators = new HashMap<>();
 
     private static final CompoundTag EMPTY_TAG = CompoundTagBuilder.builder().buildRootTag();
     public static final byte[] EMPTY_LEVEL_CHUNK_DATA;
@@ -127,6 +129,7 @@ public class TranslatorsInit {
         Registry.registerJava(ServerPlayerSetExperiencePacket.class, new JavaPlayerSetExperienceTranslator());
         Registry.registerJava(ServerPlayerHealthPacket.class, new JavaPlayerHealthTranslator());
         Registry.registerJava(ServerPlayerActionAckPacket.class, new JavaPlayerActionAckTranslator());
+        Registry.registerJava(ServerPlayerChangeHeldItemPacket.class, new JavaPlayerChangeHeldItemTranslator());
 
         // FIXME: This translator messes with allowing flight in creative mode. Will need to be addressed later
         // Registry.registerJava(ServerPlayerAbilitiesPacket.class, new JavaPlayerAbilitiesTranslator());
@@ -176,9 +179,9 @@ public class TranslatorsInit {
         inventoryTranslators.put(WindowType.GENERIC_9X4, new DoubleChestInventoryTranslator(36));
         inventoryTranslators.put(WindowType.GENERIC_9X5, new DoubleChestInventoryTranslator(45));
         inventoryTranslators.put(WindowType.GENERIC_9X6, new DoubleChestInventoryTranslator(54));
-        inventoryTranslators.put(WindowType.BREWING_STAND, new BrewingStandInventoryTranslator());
+        inventoryTranslators.put(WindowType.BREWING_STAND, new BrewingInventoryTranslator());
         inventoryTranslators.put(WindowType.ANVIL, new AnvilInventoryTranslator());
-        inventoryTranslators.put(WindowType.CRAFTING, new CraftingTableInventoryTranslator());
+        inventoryTranslators.put(WindowType.CRAFTING, new CraftingInventoryTranslator());
         //inventoryTranslators.put(WindowType.ENCHANTMENT, new EnchantmentInventoryTranslator()); //TODO
 
         InventoryTranslator furnace = new FurnaceInventoryTranslator();
@@ -186,9 +189,10 @@ public class TranslatorsInit {
         inventoryTranslators.put(WindowType.BLAST_FURNACE, furnace);
         inventoryTranslators.put(WindowType.SMOKER, furnace);
 
-        inventoryTranslators.put(WindowType.GENERIC_3X3, new BlockInventoryTranslator(9, "minecraft:dispenser[facing=north,triggered=false]", ContainerType.DISPENSER));
-        inventoryTranslators.put(WindowType.HOPPER, new BlockInventoryTranslator(5, "minecraft:hopper[enabled=false,facing=down]", ContainerType.HOPPER));
-        inventoryTranslators.put(WindowType.SHULKER_BOX, new BlockInventoryTranslator(27, "minecraft:shulker_box[facing=north]", ContainerType.CONTAINER));
+        InventoryUpdater containerUpdater = new ContainerInventoryUpdater();
+        inventoryTranslators.put(WindowType.GENERIC_3X3, new BlockInventoryTranslator(9, "minecraft:dispenser[facing=north,triggered=false]", ContainerType.DISPENSER, containerUpdater));
+        inventoryTranslators.put(WindowType.HOPPER, new BlockInventoryTranslator(5, "minecraft:hopper[enabled=false,facing=down]", ContainerType.HOPPER, containerUpdater));
+        inventoryTranslators.put(WindowType.SHULKER_BOX, new BlockInventoryTranslator(27, "minecraft:shulker_box[facing=north]", ContainerType.CONTAINER, containerUpdater));
         //inventoryTranslators.put(WindowType.BEACON, new BlockInventoryTranslator(1, "minecraft:beacon", ContainerType.BEACON)); //TODO
     }
 }
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 ca6729b96..ee12b5aad 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
@@ -25,13 +25,7 @@
 
 package org.geysermc.connector.network.translators.bedrock;
 
-import com.github.steveice10.mc.protocol.data.game.window.*;
-import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientConfirmTransactionPacket;
-import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientCreativeInventoryActionPacket;
-import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientRenameItemPacket;
-import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
 import com.nukkitx.math.vector.Vector3f;
-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.mc.protocol.data.game.entity.player.GameMode;
 import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
@@ -41,382 +35,31 @@ 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.ClientPlayerInteractEntityPacket;
 import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlayerUseItemPacket;
-import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientWindowActionPacket;
-import com.nukkitx.protocol.bedrock.data.ContainerId;
-import com.nukkitx.protocol.bedrock.data.InventoryAction;
-import com.nukkitx.protocol.bedrock.data.InventorySource;
-import com.nukkitx.protocol.bedrock.data.ItemData;
-import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
 import com.nukkitx.protocol.bedrock.packet.InventoryTransactionPacket;
 import org.geysermc.connector.entity.Entity;
 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.PacketTranslator;
 import org.geysermc.connector.network.translators.TranslatorsInit;
-import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
-import org.geysermc.connector.network.translators.inventory.SlotType;
+import org.geysermc.connector.utils.InventoryUtils;
 
 import java.util.*;
 
 public class BedrockInventoryTransactionTranslator extends PacketTranslator<InventoryTransactionPacket> {
-    private final ItemStack refreshItem = new ItemStack(1, 127, new CompoundTag(""));
 
     @Override
     public void translate(InventoryTransactionPacket packet, GeyserSession session) {
         switch (packet.getTransactionType()) {
             case NORMAL:
-                for (InventoryAction action : packet.getActions()) {
-                    if (action.getSource().getContainerId() == ContainerId.CRAFTING_USE_INGREDIENT ||
-                            action.getSource().getContainerId() == ContainerId.CRAFTING_RESULT) {
-                        return;
-                    }
-                }
-
                 Inventory inventory = session.getInventoryCache().getOpenInventory();
-                if (inventory == null)
-                    inventory = session.getInventory();
-                InventoryTranslator translator = TranslatorsInit.getInventoryTranslators().get(inventory.getWindowType());
-
-                int craftSlot = session.getCraftSlot();
-                session.setCraftSlot(0);
-
-                if (session.getGameMode() == GameMode.CREATIVE && inventory.getId() == 0) {
-                    ItemStack javaItem;
-                    for (InventoryAction action : packet.getActions()) {
-                        switch (action.getSource().getContainerId()) {
-                            case ContainerId.INVENTORY:
-                            case ContainerId.ARMOR:
-                            case ContainerId.OFFHAND:
-                                int javaSlot = translator.bedrockSlotToJava(action);
-                                if (action.getToItem().getId() == 0) {
-                                    javaItem = new ItemStack(-1, 0, null);
-                                } else {
-                                    javaItem = TranslatorsInit.getItemTranslator().translateToJava(action.getToItem());
-                                    if (javaItem.getId() == 0) { //item missing mapping
-                                        translator.updateInventory(session, inventory);
-                                        break;
-                                    }
-                                }
-                                ClientCreativeInventoryActionPacket creativePacket = new ClientCreativeInventoryActionPacket(javaSlot, fixStack(javaItem));
-                                session.getDownstream().getSession().send(creativePacket);
-                                inventory.setItem(javaSlot, javaItem);
-                                break;
-                            case ContainerId.NONE:
-                                if (action.getSource().getType() == InventorySource.Type.WORLD_INTERACTION &&
-                                        action.getSource().getFlag() == InventorySource.Flag.DROP_ITEM) {
-                                    javaItem = TranslatorsInit.getItemTranslator().translateToJava(action.getToItem());
-                                    if (javaItem.getId() == 0) { //item missing mapping
-                                        break;
-                                    }
-                                    ClientCreativeInventoryActionPacket creativeDropPacket = new ClientCreativeInventoryActionPacket(-1, fixStack(javaItem));
-                                    session.getDownstream().getSession().send(creativeDropPacket);
-                                }
-                                break;
-                        }
-                    }
-                    return;
-                }
-
-                InventoryAction worldAction = null;
-                InventoryAction cursorAction = null;
-                for (InventoryAction action : packet.getActions()) {
-                    if (action.getSource().getType() == InventorySource.Type.WORLD_INTERACTION) {
-                        worldAction = action;
-                    } else if (action.getSource().getContainerId() == ContainerId.CURSOR && action.getSlot() == 0) {
-                        cursorAction = action;
-                    }
-                }
-                List<InventoryAction> actions = packet.getActions();
-                if (inventory.getWindowType() == WindowType.ANVIL) {
-                    InventoryAction anvilResult = null;
-                    InventoryAction anvilInput = null;
-                    for (InventoryAction action : packet.getActions()) {
-                        if (action.getSource().getContainerId() == ContainerId.ANVIL_MATERIAL) {
-                            //useless packet
-                            return;
-                        } else if (action.getSource().getContainerId() == ContainerId.ANVIL_RESULT) {
-                            anvilResult = action;
-                        } else if (translator.bedrockSlotToJava(action) == 0) {
-                            anvilInput = action;
-                        }
-                    }
-                    ItemData itemName = null;
-                    if (anvilResult != null) {
-                        itemName = anvilResult.getFromItem();
-                    } else if (anvilInput != null) {
-                        itemName = anvilInput.getToItem();
-                    }
-                    if (itemName != null) {
-                        String rename;
-                        com.nukkitx.nbt.tag.CompoundTag tag = itemName.getTag();
-                        if (tag != null) {
-                            rename = tag.getAsCompound("display").getAsString("Name");
-                        } else {
-                            rename = "";
-                        }
-                        ClientRenameItemPacket renameItemPacket = new ClientRenameItemPacket(rename);
-                        session.getDownstream().getSession().send(renameItemPacket);
-                    }
-                    if (anvilResult != null) {
-                        //client will send another packet to grab anvil output
-                        //this packet was only used to send rename packet
-                        return;
-                    }
-                }
-
-                if (actions.size() == 2) {
-                    if (worldAction != null) {
-                        //find container action
-                        InventoryAction containerAction = null;
-                        for (InventoryAction action : actions) {
-                            if (action != worldAction) {
-                                containerAction = action;
-                                break;
-                            }
-                        }
-                        if (containerAction != null && worldAction.getSource().getFlag() == InventorySource.Flag.DROP_ITEM) {
-                            //quick dropping from hotbar?
-                            if (session.getInventoryCache().getOpenInventory() == null && containerAction.getSource().getContainerId() == ContainerId.INVENTORY) {
-                                int heldSlot = session.getInventory().getHeldItemSlot();
-                                if (containerAction.getSlot() == heldSlot) {
-                                    ClientPlayerActionPacket actionPacket = new ClientPlayerActionPacket(
-                                            containerAction.getToItem().getCount() == 0 ? PlayerAction.DROP_ITEM_STACK : PlayerAction.DROP_ITEM,
-                                            new Position(0, 0, 0), BlockFace.DOWN);
-                                    session.getDownstream().getSession().send(actionPacket);
-                                    ItemStack item = session.getInventory().getItem(heldSlot);
-                                    if (item != null) {
-                                        session.getInventory().setItem(heldSlot, new ItemStack(item.getId(), item.getAmount() - 1, item.getNbt()));
-                                    }
-                                    return;
-                                }
-                            }
-                            int dropAmount = containerAction.getFromItem().getCount() - containerAction.getToItem().getCount();
-                            if (containerAction != cursorAction) { //dropping directly from inventory
-                                int javaSlot = translator.bedrockSlotToJava(containerAction);
-                                if (dropAmount == containerAction.getFromItem().getCount()) {
-                                    ClientWindowActionPacket dropPacket = new ClientWindowActionPacket(inventory.getId(),
-                                            inventory.getTransactionId().getAndIncrement(),
-                                            javaSlot, null, WindowAction.DROP_ITEM,
-                                            DropItemParam.DROP_SELECTED_STACK);
-                                    session.getDownstream().getSession().send(dropPacket);
-                                } else {
-                                    for (int i = 0; i < dropAmount; i++) {
-                                        ClientWindowActionPacket dropPacket = new ClientWindowActionPacket(inventory.getId(),
-                                                inventory.getTransactionId().getAndIncrement(),
-                                                javaSlot, null, WindowAction.DROP_ITEM,
-                                                DropItemParam.DROP_FROM_SELECTED);
-                                        session.getDownstream().getSession().send(dropPacket);
-                                    }
-                                }
-                                ItemStack item = session.getInventory().getItem(javaSlot);
-                                if (item != null) {
-                                    session.getInventory().setItem(javaSlot, new ItemStack(item.getId(), item.getAmount() - dropAmount, item.getNbt()));
-                                }
-                                return;
-                            } else { //clicking outside of inventory
-                                ClientWindowActionPacket dropPacket = new ClientWindowActionPacket(inventory.getId(), inventory.getTransactionId().getAndIncrement(),
-                                        -999, null, WindowAction.CLICK_ITEM,
-                                        dropAmount > 1 ? ClickItemParam.LEFT_CLICK : ClickItemParam.RIGHT_CLICK);
-                                session.getDownstream().getSession().send(dropPacket);
-                                ItemStack cursor = session.getInventory().getCursor();
-                                if (cursor != null) {
-                                    session.getInventory().setCursor(new ItemStack(cursor.getId(), dropAmount > 1 ? 0 : cursor.getAmount() - 1, cursor.getNbt()));
-                                }
-                                return;
-                            }
-                        }
-                    } else if (cursorAction != null) {
-                        //find container action
-                        InventoryAction containerAction = null;
-                        for (InventoryAction action : actions) {
-                            if (action != cursorAction) {
-                                containerAction = action;
-                                break;
-                            }
-                        }
-                        if (containerAction != null) {
-                            //left/right click
-                            List<ClickAction> plan = new ArrayList<>();
-                            ItemStack translatedCursor = cursorAction.getFromItem().isValid() ?
-                                    TranslatorsInit.getItemTranslator().translateToJava(cursorAction.getFromItem()) : null;
-                            ItemStack currentCursor = session.getInventory().getCursor();
-                            boolean refresh = false;
-                            if (currentCursor != null) {
-                                if (translatedCursor != null) {
-                                    refresh = !(currentCursor.getId() == translatedCursor.getId() &&
-                                            currentCursor.getAmount() == translatedCursor.getAmount());
-                                } else {
-                                    refresh = true;
-                                }
-                            }
-
-                            int javaSlot = translator.bedrockSlotToJava(containerAction);
-                            if (cursorAction.getFromItem().equals(containerAction.getToItem()) &&
-                                    containerAction.getFromItem().equals(cursorAction.getToItem()) &&
-                                    !canStack(cursorAction.getFromItem(), containerAction.getFromItem())) { //simple swap
-                                Click.LEFT.onSlot(javaSlot, plan);
-                            } else if (cursorAction.getFromItem().getCount() > cursorAction.getToItem().getCount()) { //release
-                                if (cursorAction.getToItem().getCount() == 0) {
-                                    Click.LEFT.onSlot(javaSlot, plan);
-                                } else {
-                                    int difference = cursorAction.getFromItem().getCount() - cursorAction.getToItem().getCount();
-                                    for (int i = 0; i < difference; i++) {
-                                        Click.RIGHT.onSlot(javaSlot, plan);
-                                    }
-                                }
-                            } else { //pickup
-                                if (cursorAction.getFromItem().getCount() == 0) {
-                                    if (containerAction.getToItem().getCount() == 0) { //pickup all
-                                        Click.LEFT.onSlot(javaSlot, plan);
-                                    } else { //pickup some
-                                        if (translator.getSlotType(javaSlot) == SlotType.FURNACE_OUTPUT ||
-                                                containerAction.getToItem().getCount() == containerAction.getFromItem().getCount() / 2) { //right click
-                                            Click.RIGHT.onSlot(javaSlot, plan);
-                                        } else {
-                                            Click.LEFT.onSlot(javaSlot, plan);
-                                            int difference = containerAction.getFromItem().getCount() - cursorAction.getToItem().getCount();
-                                            for (int i = 0; i < difference; i++) {
-                                                Click.RIGHT.onSlot(javaSlot, plan);
-                                            }
-                                        }
-                                    }
-                                } else { //pickup into non-empty cursor
-                                    if (translator.getSlotType(javaSlot) == SlotType.FURNACE_OUTPUT) {
-                                        if (containerAction.getToItem().getCount() == 0) {
-                                            Click.LEFT.onSlot(javaSlot, plan);
-                                        } else {
-                                            ClientWindowActionPacket shiftClickPacket = new ClientWindowActionPacket(inventory.getId(),
-                                                    inventory.getTransactionId().getAndIncrement(),
-                                                    javaSlot, refreshItem, WindowAction.SHIFT_CLICK_ITEM,
-                                                    ShiftClickItemParam.LEFT_CLICK);
-                                            session.getDownstream().getSession().send(shiftClickPacket);
-                                            translator.updateInventory(session, inventory);
-                                            return;
-                                        }
-                                    } else if (translator.getSlotType(javaSlot) == SlotType.OUTPUT) {
-                                        Click.LEFT.onSlot(javaSlot, plan);
-                                    } else {
-                                        int cursorSlot = findTempSlot(inventory, session.getInventory().getCursor(), Collections.singletonList(javaSlot));
-                                        if (cursorSlot != -1) {
-                                            Click.LEFT.onSlot(cursorSlot, plan);
-                                        } else {
-                                            translator.updateInventory(session, inventory);
-                                            return;
-                                        }
-                                        Click.LEFT.onSlot(javaSlot, plan);
-                                        int difference = cursorAction.getToItem().getCount() - cursorAction.getFromItem().getCount();
-                                        for (int i = 0; i < difference; i++) {
-                                            Click.RIGHT.onSlot(cursorSlot, plan);
-                                        }
-                                        Click.LEFT.onSlot(javaSlot, plan);
-                                        Click.LEFT.onSlot(cursorSlot, plan);
-                                    }
-                                }
-                            }
-                            executePlan(session, inventory, translator, plan, refresh);
-                            return;
-                        }
-                    } else {
-                        List<ClickAction> plan = new ArrayList<>();
-                        InventoryAction fromAction;
-                        InventoryAction toAction;
-                        if (actions.get(0).getFromItem().getCount() >= actions.get(0).getToItem().getCount()) {
-                            fromAction = actions.get(0);
-                            toAction = actions.get(1);
-                        } else {
-                            fromAction = actions.get(1);
-                            toAction = actions.get(0);
-                        }
-                        int fromSlot = translator.bedrockSlotToJava(fromAction);
-                        int toSlot = translator.bedrockSlotToJava(toAction);
-
-                        if (translator.getSlotType(fromSlot) == SlotType.OUTPUT) {
-                            if ((craftSlot != 0 && craftSlot != -2) && (inventory.getItem(toSlot) == null ||
-                                    canStack(session.getInventory().getCursor(), inventory.getItem(toSlot)))) {
-                                boolean refresh = false;
-                                if (fromAction.getToItem().getCount() == 0) {
-                                    refresh = true;
-                                    Click.LEFT.onSlot(toSlot, plan);
-                                    if (craftSlot != -1) {
-                                        Click.LEFT.onSlot(craftSlot, plan);
-                                    }
-                                } else {
-                                    int difference = toAction.getToItem().getCount() - toAction.getFromItem().getCount();
-                                    for (int i = 0; i < difference; i++) {
-                                        Click.RIGHT.onSlot(toSlot, plan);
-                                    }
-                                    session.setCraftSlot(craftSlot);
-                                }
-                                executePlan(session, inventory, translator, plan, refresh);
-                                return;
-                            } else {
-                                session.setCraftSlot(-2);
-                            }
-                        }
-
-                        int cursorSlot = -1;
-                        if (session.getInventory().getCursor() != null) { //move cursor contents to a temporary slot
-                            cursorSlot = findTempSlot(inventory, session.getInventory().getCursor(), Arrays.asList(fromSlot, toSlot));
-                            if (cursorSlot != -1) {
-                                Click.LEFT.onSlot(cursorSlot, plan);
-                            } else {
-                                translator.updateInventory(session, inventory);
-                                return;
-                            }
-                        }
-                        if ((fromAction.getFromItem().equals(toAction.getToItem()) && !canStack(fromAction.getFromItem(), toAction.getFromItem())) || fromAction.getToItem().getId() == 0) { //slot swap
-                            Click.LEFT.onSlot(fromSlot, plan);
-                            Click.LEFT.onSlot(toSlot, plan);
-                            if (fromAction.getToItem().getId() != 0) {
-                                Click.LEFT.onSlot(fromSlot, plan);
-                            }
-                        } else if (canStack(fromAction.getFromItem(), toAction.getToItem())) { //partial item move
-                            if (translator.getSlotType(fromSlot) == SlotType.FURNACE_OUTPUT) {
-                                ClientWindowActionPacket shiftClickPacket = new ClientWindowActionPacket(inventory.getId(),
-                                        inventory.getTransactionId().getAndIncrement(),
-                                        fromSlot, refreshItem, WindowAction.SHIFT_CLICK_ITEM,
-                                        ShiftClickItemParam.LEFT_CLICK);
-                                session.getDownstream().getSession().send(shiftClickPacket);
-                                translator.updateInventory(session, inventory);
-                                return;
-                            } else if (translator.getSlotType(fromSlot) == SlotType.OUTPUT) {
-                                session.setCraftSlot(cursorSlot);
-                                Click.LEFT.onSlot(fromSlot, plan);
-                                int difference = toAction.getToItem().getCount() - toAction.getFromItem().getCount();
-                                for (int i = 0; i < difference; i++) {
-                                    Click.RIGHT.onSlot(toSlot, plan);
-                                }
-                                //client will send additional packets later to finish transferring crafting output
-                                //translator will know how to handle this using the craftSlot variable
-                            } else {
-                                Click.LEFT.onSlot(fromSlot, plan);
-                                int difference = toAction.getToItem().getCount() - toAction.getFromItem().getCount();
-                                for (int i = 0; i < difference; i++) {
-                                    Click.RIGHT.onSlot(toSlot, plan);
-                                }
-                                Click.LEFT.onSlot(fromSlot, plan);
-                            }
-                        }
-                        if (cursorSlot != -1) {
-                            Click.LEFT.onSlot(cursorSlot, plan);
-                        }
-                        executePlan(session, inventory, translator, plan, false);
-                        return;
-                    }
-                }
-                translator.updateInventory(session, inventory);
+                if (inventory == null) inventory = session.getInventory();
+                TranslatorsInit.getInventoryTranslators().get(inventory.getWindowType()).translateActions(session, inventory, packet.getActions());
                 break;
             case INVENTORY_MISMATCH:
-                InventorySlotPacket cursorPacket = new InventorySlotPacket();
-                cursorPacket.setContainerId(ContainerId.CURSOR);
-                cursorPacket.setSlot(TranslatorsInit.getItemTranslator().translateToBedrock(session.getInventory().getCursor()));
-                //session.getUpstream().sendPacket(cursorPacket);
-
                 Inventory inv = session.getInventoryCache().getOpenInventory();
-                if (inv == null)
-                    inv = session.getInventory();
+                if (inv == null) inv = session.getInventory();
                 TranslatorsInit.getInventoryTranslators().get(inv.getWindowType()).updateInventory(session, inv);
+                InventoryUtils.updateCursor(session);
                 break;
             case ITEM_USE:
                 if (packet.getActionType() == 1) {
@@ -448,136 +91,4 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                 break;
         }
     }
-
-    private int findTempSlot(Inventory inventory, ItemStack item, List<Integer> slotBlacklist) {
-        /*try and find a slot that can temporarily store the given item
-        only look in the main inventory and hotbar
-        only slots that are empty or contain a different type of item are valid*/
-        int offset = inventory.getId() == 0 ? 1 : 0; //offhand is not a viable slot (some servers disable it)
-        List<ItemStack> itemBlacklist = new ArrayList<>(slotBlacklist.size() + 1);
-        itemBlacklist.add(item);
-        for (int slot : slotBlacklist) {
-            ItemStack blacklistItem = inventory.getItem(slot);
-            if (blacklistItem != null)
-                itemBlacklist.add(blacklistItem);
-        }
-        for (int i = inventory.getSize() - (36 + offset); i < inventory.getSize() - offset; i++) {
-            ItemStack testItem = inventory.getItem(i);
-            boolean acceptable = true;
-            if (testItem != null) {
-                for (ItemStack blacklistItem : itemBlacklist) {
-                    if (canStack(testItem, blacklistItem)) {
-                        acceptable = false;
-                        break;
-                    }
-                }
-            }
-            if (acceptable && !slotBlacklist.contains(i))
-                return i;
-        }
-        //could not find a viable temp slot
-        return -1;
-    }
-
-    //NPE if compound tag is null
-    private ItemStack fixStack(ItemStack stack) {
-        if (stack == null || stack.getId() == 0)
-            return null;
-        return new ItemStack(stack.getId(), stack.getAmount(), stack.getNbt() == null ? new CompoundTag("") : stack.getNbt());
-    }
-
-    private boolean canStack(ItemStack item1, ItemStack item2) {
-        if (item1 == null || item2 == null)
-            return false;
-        return item1.getId() == item2.getId() && Objects.equals(item1.getNbt(), item2.getNbt());
-    }
-
-    private boolean canStack(ItemData item1, ItemData item2) {
-        if (item1 == null || item2 == null)
-            return false;
-        return item1.equals(item2, false, true, true);
-    }
-
-    private void executePlan(GeyserSession session, Inventory inventory, InventoryTranslator translator, List<ClickAction> plan, boolean refresh) {
-        PlayerInventory playerInventory = session.getInventory();
-        ListIterator<ClickAction> planIter = plan.listIterator();
-        while (planIter.hasNext()) {
-            ClickAction action = planIter.next();
-            ItemStack cursorItem = playerInventory.getCursor();
-            ItemStack clickedItem = inventory.getItem(action.slot);
-            short actionId = (short) inventory.getTransactionId().getAndIncrement();
-            boolean isOutput = translator.getSlotType(action.slot) == SlotType.OUTPUT;
-
-            if (isOutput || translator.getSlotType(action.slot) == SlotType.FURNACE_OUTPUT)
-                refresh = true;
-            ClientWindowActionPacket clickPacket = new ClientWindowActionPacket(inventory.getId(),
-                    actionId, action.slot, !planIter.hasNext() && refresh ? refreshItem : fixStack(clickedItem),
-                    WindowAction.CLICK_ITEM, action.click.actionParam);
-
-            if (isOutput) {
-                if (cursorItem == null && clickedItem != null) {
-                    playerInventory.setCursor(clickedItem);
-                } else if (canStack(cursorItem, clickedItem)) {
-                    playerInventory.setCursor(new ItemStack(cursorItem.getId(),
-                            cursorItem.getAmount() + clickedItem.getAmount(), cursorItem.getNbt()));
-                }
-            } else {
-                switch (action.click) {
-                    case LEFT:
-                        if (!canStack(cursorItem, clickedItem)) {
-                            playerInventory.setCursor(clickedItem);
-                            inventory.setItem(action.slot, cursorItem);
-                        } else {
-                            playerInventory.setCursor(null);
-                            inventory.setItem(action.slot, new ItemStack(clickedItem.getId(),
-                                    clickedItem.getAmount() + cursorItem.getAmount(), clickedItem.getNbt()));
-                        }
-                        break;
-                    case RIGHT:
-                        if (cursorItem == null && clickedItem != null) {
-                            ItemStack halfItem = new ItemStack(clickedItem.getId(),
-                                    clickedItem.getAmount() / 2, clickedItem.getNbt());
-                            inventory.setItem(action.slot, halfItem);
-                            playerInventory.setCursor(new ItemStack(clickedItem.getId(),
-                                    clickedItem.getAmount() - halfItem.getAmount(), clickedItem.getNbt()));
-                        } else if (cursorItem != null && clickedItem == null) {
-                            playerInventory.setCursor(new ItemStack(cursorItem.getId(),
-                                    cursorItem.getAmount() - 1, cursorItem.getNbt()));
-                            inventory.setItem(action.slot, new ItemStack(cursorItem.getId(),
-                                    1, cursorItem.getNbt()));
-                        } else if (canStack(cursorItem, clickedItem)) {
-                            playerInventory.setCursor(new ItemStack(cursorItem.getId(),
-                                    cursorItem.getAmount() - 1, cursorItem.getNbt()));
-                            inventory.setItem(action.slot, new ItemStack(clickedItem.getId(),
-                                    clickedItem.getAmount() + 1, clickedItem.getNbt()));
-                        }
-                        break;
-                }
-            }
-            session.getDownstream().getSession().send(clickPacket);
-            session.getDownstream().getSession().send(new ClientConfirmTransactionPacket(inventory.getId(), actionId, true));
-        }
-    }
-
-    private enum Click {
-        LEFT(ClickItemParam.LEFT_CLICK),
-        RIGHT(ClickItemParam.RIGHT_CLICK);
-
-        final WindowActionParam actionParam;
-        Click(WindowActionParam actionParam) {
-            this.actionParam = actionParam;
-        }
-        void onSlot(int slot, List<ClickAction> plan) {
-            plan.add(new ClickAction(slot, this));
-        }
-    }
-
-    private static class ClickAction {
-        final int slot;
-        final Click click;
-        ClickAction(int slot, Click click) {
-            this.slot = slot;
-            this.click = click;
-        }
-    }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/AnvilInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/AnvilInventoryTranslator.java
index 115a7dbd7..395751888 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/AnvilInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/AnvilInventoryTranslator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2020 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
@@ -25,13 +25,20 @@
 
 package org.geysermc.connector.network.translators.inventory;
 
+import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientRenameItemPacket;
 import com.nukkitx.protocol.bedrock.data.ContainerId;
 import com.nukkitx.protocol.bedrock.data.ContainerType;
 import com.nukkitx.protocol.bedrock.data.InventoryAction;
+import com.nukkitx.protocol.bedrock.data.ItemData;
+import org.geysermc.connector.inventory.Inventory;
+import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.inventory.updater.CursorInventoryUpdater;
+
+import java.util.List;
 
 public class AnvilInventoryTranslator extends BlockInventoryTranslator {
     public AnvilInventoryTranslator() {
-        super(3, "minecraft:anvil[facing=north]", ContainerType.ANVIL);
+        super(3, "minecraft:anvil[facing=north]", ContainerType.ANVIL, new CursorInventoryUpdater());
     }
 
     @Override
@@ -49,10 +56,62 @@ public class AnvilInventoryTranslator extends BlockInventoryTranslator {
         return super.bedrockSlotToJava(action);
     }
 
+    @Override
+    public int javaSlotToBedrock(int slot) {
+        switch (slot) {
+            case 0:
+                return 1;
+            case 1:
+                return 2;
+            case 2:
+                return 50;
+        }
+        return super.javaSlotToBedrock(slot);
+    }
+
     @Override
     public SlotType getSlotType(int javaSlot) {
         if (javaSlot == 2)
             return SlotType.OUTPUT;
         return SlotType.NORMAL;
     }
+
+    @Override
+    public void translateActions(GeyserSession session, Inventory inventory, List<InventoryAction> actions) {
+        InventoryAction anvilResult = null;
+        InventoryAction anvilInput = null;
+        for (InventoryAction action : actions) {
+            if (action.getSource().getContainerId() == ContainerId.ANVIL_MATERIAL) {
+                //useless packet
+                return;
+            } else if (action.getSource().getContainerId() == ContainerId.ANVIL_RESULT) {
+                anvilResult = action;
+            } else if (bedrockSlotToJava(action) == 0) {
+                anvilInput = action;
+            }
+        }
+        ItemData itemName = null;
+        if (anvilResult != null) {
+            itemName = anvilResult.getFromItem();
+        } else if (anvilInput != null) {
+            itemName = anvilInput.getToItem();
+        }
+        if (itemName != null) {
+            String rename;
+            com.nukkitx.nbt.tag.CompoundTag tag = itemName.getTag();
+            if (tag != null) {
+                rename = tag.getAsCompound("display").getAsString("Name");
+            } else {
+                rename = "";
+            }
+            ClientRenameItemPacket renameItemPacket = new ClientRenameItemPacket(rename);
+            session.getDownstream().getSession().send(renameItemPacket);
+        }
+        if (anvilResult != null) {
+            //client will send another packet to grab anvil output
+            return;
+        }
+
+        super.translateActions(session, inventory, actions);
+    }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/ContainerInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BaseInventoryTranslator.java
similarity index 50%
rename from connector/src/main/java/org/geysermc/connector/network/translators/inventory/ContainerInventoryTranslator.java
rename to connector/src/main/java/org/geysermc/connector/network/translators/inventory/BaseInventoryTranslator.java
index 8f66675b1..d64c0e78e 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/ContainerInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BaseInventoryTranslator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2020 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
@@ -27,53 +27,20 @@ package org.geysermc.connector.network.translators.inventory;
 
 import com.nukkitx.protocol.bedrock.data.ContainerId;
 import com.nukkitx.protocol.bedrock.data.InventoryAction;
-import com.nukkitx.protocol.bedrock.data.ItemData;
-import com.nukkitx.protocol.bedrock.packet.InventoryContentPacket;
-import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
 import org.geysermc.connector.inventory.Inventory;
 import org.geysermc.connector.network.session.GeyserSession;
-import org.geysermc.connector.network.translators.TranslatorsInit;
+import org.geysermc.connector.network.translators.inventory.action.InventoryActionTranslator;
 
-public abstract class ContainerInventoryTranslator extends InventoryTranslator {
-    ContainerInventoryTranslator(int size) {
+import java.util.List;
+
+public abstract class BaseInventoryTranslator extends InventoryTranslator{
+    BaseInventoryTranslator(int size) {
         super(size);
     }
 
-    @Override
-    public void updateInventory(GeyserSession session, Inventory inventory) {
-        ItemData[] bedrockItems = new ItemData[this.size];
-        for (int i = 0; i < bedrockItems.length; i++) {
-            bedrockItems[javaSlotToBedrock(i)] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(i));
-        }
-        InventoryContentPacket contentPacket = new InventoryContentPacket();
-        contentPacket.setContainerId(inventory.getId());
-        contentPacket.setContents(bedrockItems);
-        session.getUpstream().sendPacket(contentPacket);
-
-        Inventory playerInventory = session.getInventory();
-        for (int i = 0; i < 36; i++) {
-            playerInventory.setItem(i + 9, inventory.getItem(i + this.size));
-        }
-        TranslatorsInit.getInventoryTranslators().get(playerInventory.getWindowType()).updateInventory(session, playerInventory);
-    }
-
-    @Override
-    public void updateSlot(GeyserSession session, Inventory inventory, int slot) {
-        if (slot >= this.size) {
-            Inventory playerInventory = session.getInventory();
-            playerInventory.setItem((slot + 9) - this.size, inventory.getItem(slot));
-            TranslatorsInit.getInventoryTranslators().get(playerInventory.getWindowType()).updateSlot(session, playerInventory, (slot + 9) - this.size);
-        } else {
-            InventorySlotPacket slotPacket = new InventorySlotPacket();
-            slotPacket.setContainerId(inventory.getId());
-            slotPacket.setInventorySlot(javaSlotToBedrock(slot));
-            slotPacket.setSlot(TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(slot)));
-            session.getUpstream().sendPacket(slotPacket);
-        }
-    }
-
     @Override
     public void updateProperty(GeyserSession session, Inventory inventory, int key, int value) {
+        //
     }
 
     @Override
@@ -86,13 +53,20 @@ public abstract class ContainerInventoryTranslator extends InventoryTranslator {
             } else {
                 return slotnum + this.size + 27;
             }
-        } else {
-            return slotnum;
         }
+        return slotnum;
     }
 
     @Override
     public int javaSlotToBedrock(int slot) {
+        if (slot >= this.size) {
+            final int tmp = slot - this.size;
+            if (tmp < 27) {
+                return tmp + 9;
+            } else {
+                return tmp - 27;
+            }
+        }
         return slot;
     }
 
@@ -100,4 +74,9 @@ public abstract class ContainerInventoryTranslator extends InventoryTranslator {
     public SlotType getSlotType(int javaSlot) {
         return SlotType.NORMAL;
     }
+
+    @Override
+    public void translateActions(GeyserSession session, Inventory inventory, List<InventoryAction> actions) {
+        InventoryActionTranslator.translate(this, session, inventory, actions);
+    }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BlockInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BlockInventoryTranslator.java
index 54ab9bcec..32dfc2a63 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BlockInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BlockInventoryTranslator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2020 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
@@ -25,70 +25,47 @@
 
 package org.geysermc.connector.network.translators.inventory;
 
-import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
-import com.nukkitx.math.vector.Vector3i;
-import com.nukkitx.nbt.tag.CompoundTag;
 import com.nukkitx.protocol.bedrock.data.ContainerType;
-import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket;
-import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket;
-import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket;
 import org.geysermc.connector.inventory.Inventory;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.TranslatorsInit;
-import org.geysermc.connector.network.translators.block.BlockEntry;
+import org.geysermc.connector.network.translators.inventory.holder.BlockInventoryHolder;
+import org.geysermc.connector.network.translators.inventory.holder.InventoryHolder;
+import org.geysermc.connector.network.translators.inventory.updater.InventoryUpdater;
 
-public class BlockInventoryTranslator extends ContainerInventoryTranslator {
-    final int blockId;
-    private final ContainerType containerType;
+public class BlockInventoryTranslator extends BaseInventoryTranslator {
+    private final InventoryHolder holder;
+    private final InventoryUpdater updater;
 
-    public BlockInventoryTranslator(int size, String javaBlockIdentifier, ContainerType containerType) {
+    public BlockInventoryTranslator(int size, String javaBlockIdentifier, ContainerType containerType, InventoryUpdater updater) {
         super(size);
-        this.blockId = TranslatorsInit.getBlockTranslator().getBlockEntry(javaBlockIdentifier).getBedrockRuntimeId();
-        this.containerType = containerType;
+        final int blockId = TranslatorsInit.getBlockTranslator().getBlockEntry(javaBlockIdentifier).getBedrockRuntimeId();
+        this.holder = new BlockInventoryHolder(blockId, containerType);
+        this.updater = updater;
     }
 
     @Override
     public void prepareInventory(GeyserSession session, Inventory inventory) {
-        Vector3i position = session.getPlayerEntity().getPosition().toInt();
-        position = position.add(Vector3i.UP);
-        UpdateBlockPacket blockPacket = new UpdateBlockPacket();
-        blockPacket.setDataLayer(0);
-        blockPacket.setBlockPosition(position);
-        blockPacket.setRuntimeId(blockId);
-        blockPacket.getFlags().add(UpdateBlockPacket.Flag.PRIORITY);
-        session.getUpstream().sendPacket(blockPacket);
-        inventory.setHolderPosition(position);
-
-        CompoundTag tag = CompoundTag.EMPTY.toBuilder()
-                .intTag("x", position.getX())
-                .intTag("y", position.getY())
-                .intTag("z", position.getZ())
-                .stringTag("CustomName", inventory.getTitle()).buildRootTag();
-        BlockEntityDataPacket dataPacket = new BlockEntityDataPacket();
-        dataPacket.setData(tag);
-        dataPacket.setBlockPosition(position);
-        session.getUpstream().sendPacket(dataPacket);
+        holder.prepareInventory(this, session, inventory);
     }
 
     @Override
     public void openInventory(GeyserSession session, Inventory inventory) {
-        ContainerOpenPacket containerOpenPacket = new ContainerOpenPacket();
-        containerOpenPacket.setWindowId((byte) inventory.getId());
-        containerOpenPacket.setType((byte) containerType.id());
-        containerOpenPacket.setBlockPosition(inventory.getHolderPosition());
-        containerOpenPacket.setUniqueEntityId(inventory.getHolderId());
-        session.getUpstream().sendPacket(containerOpenPacket);
+        holder.openInventory(this, session, inventory);
     }
 
     @Override
     public void closeInventory(GeyserSession session, Inventory inventory) {
-        Vector3i holderPos = inventory.getHolderPosition();
-        Position pos = new Position(holderPos.getX(), holderPos.getY(), holderPos.getZ());
-        BlockEntry realBlock = session.getChunkCache().getBlockAt(pos);
-        UpdateBlockPacket blockPacket = new UpdateBlockPacket();
-        blockPacket.setDataLayer(0);
-        blockPacket.setBlockPosition(holderPos);
-        blockPacket.setRuntimeId(realBlock.getBedrockRuntimeId());
-        session.getUpstream().sendPacket(blockPacket);
+        holder.closeInventory(this, session, inventory);
+    }
+
+    @Override
+    public void updateInventory(GeyserSession session, Inventory inventory) {
+        updater.updateInventory(this, session, inventory);
+    }
+
+    @Override
+    public void updateSlot(GeyserSession session, Inventory inventory, int slot) {
+        updater.updateSlot(this, session, inventory, slot);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BrewingStandInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BrewingInventoryTranslator.java
similarity index 86%
rename from connector/src/main/java/org/geysermc/connector/network/translators/inventory/BrewingStandInventoryTranslator.java
rename to connector/src/main/java/org/geysermc/connector/network/translators/inventory/BrewingInventoryTranslator.java
index 532928e64..c5f67a03a 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BrewingStandInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/BrewingInventoryTranslator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2020 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
@@ -30,10 +30,11 @@ import com.nukkitx.protocol.bedrock.data.InventoryAction;
 import com.nukkitx.protocol.bedrock.packet.ContainerSetDataPacket;
 import org.geysermc.connector.inventory.Inventory;
 import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.inventory.updater.ContainerInventoryUpdater;
 
-public class BrewingStandInventoryTranslator extends BlockInventoryTranslator {
-    public BrewingStandInventoryTranslator() {
-        super(5, "minecraft:brewing_stand[has_bottle_0=false,has_bottle_1=false,has_bottle_2=false]", ContainerType.BREWING_STAND);
+public class BrewingInventoryTranslator extends BlockInventoryTranslator {
+    public BrewingInventoryTranslator() {
+        super(5, "minecraft:brewing_stand[has_bottle_0=false,has_bottle_1=false,has_bottle_2=false]", ContainerType.BREWING_STAND, new ContainerInventoryUpdater());
     }
 
     @Override
@@ -66,8 +67,8 @@ public class BrewingStandInventoryTranslator extends BlockInventoryTranslator {
 
     @Override
     public int bedrockSlotToJava(InventoryAction action) {
-        int slotnum = super.bedrockSlotToJava(action);
-        switch (slotnum) {
+        final int slot = super.bedrockSlotToJava(action);
+        switch (slot) {
             case 0:
                 return 3;
             case 1:
@@ -77,13 +78,13 @@ public class BrewingStandInventoryTranslator extends BlockInventoryTranslator {
             case 3:
                 return 2;
             default:
-                return slotnum;
+                return slot;
         }
     }
 
     @Override
-    public int javaSlotToBedrock(int slotnum) {
-        switch (slotnum) {
+    public int javaSlotToBedrock(int slot) {
+        switch (slot) {
             case 0:
                 return 1;
             case 1:
@@ -92,8 +93,7 @@ public class BrewingStandInventoryTranslator extends BlockInventoryTranslator {
                 return 3;
             case 3:
                 return 0;
-            default:
-                return slotnum;
         }
+        return super.javaSlotToBedrock(slot);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/CraftingTableInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/CraftingInventoryTranslator.java
similarity index 61%
rename from connector/src/main/java/org/geysermc/connector/network/translators/inventory/CraftingTableInventoryTranslator.java
rename to connector/src/main/java/org/geysermc/connector/network/translators/inventory/CraftingInventoryTranslator.java
index 28ad0cb20..fe70609fe 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/CraftingTableInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/CraftingInventoryTranslator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2020 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
@@ -25,21 +25,33 @@
 
 package org.geysermc.connector.network.translators.inventory;
 
+import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
 import com.nukkitx.protocol.bedrock.data.ContainerId;
 import com.nukkitx.protocol.bedrock.data.ContainerType;
 import com.nukkitx.protocol.bedrock.data.InventoryAction;
+import com.nukkitx.protocol.bedrock.data.InventorySource;
 import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket;
+import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
 import org.geysermc.connector.inventory.Inventory;
 import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.TranslatorsInit;
+import org.geysermc.connector.network.translators.inventory.updater.CursorInventoryUpdater;
+import org.geysermc.connector.network.translators.inventory.updater.InventoryUpdater;
+import org.geysermc.connector.utils.InventoryUtils;
 
-public class CraftingTableInventoryTranslator extends ContainerInventoryTranslator {
-    public CraftingTableInventoryTranslator() {
+import java.util.List;
+
+public class CraftingInventoryTranslator extends BaseInventoryTranslator {
+    private final InventoryUpdater updater;
+
+    public CraftingInventoryTranslator() {
         super(10);
+        this.updater = new CursorInventoryUpdater();
     }
 
     @Override
     public void prepareInventory(GeyserSession session, Inventory inventory) {
-
+        //
     }
 
     @Override
@@ -54,7 +66,17 @@ public class CraftingTableInventoryTranslator extends ContainerInventoryTranslat
 
     @Override
     public void closeInventory(GeyserSession session, Inventory inventory) {
+        //
+    }
 
+    @Override
+    public void updateInventory(GeyserSession session, Inventory inventory) {
+        updater.updateInventory(this, session, inventory);
+    }
+
+    @Override
+    public void updateSlot(GeyserSession session, Inventory inventory, int slot) {
+        updater.updateSlot(this, session, inventory, slot);
     }
 
     @Override
@@ -70,10 +92,29 @@ public class CraftingTableInventoryTranslator extends ContainerInventoryTranslat
         return super.bedrockSlotToJava(action);
     }
 
+    @Override
+    public int javaSlotToBedrock(int slot) {
+        return slot == 0 ? 50 : slot + 31;
+    }
+
     @Override
     public SlotType getSlotType(int javaSlot) {
         if (javaSlot == 0)
             return SlotType.OUTPUT;
         return SlotType.NORMAL;
     }
+
+    @Override
+    public void translateActions(GeyserSession session, Inventory inventory, List<InventoryAction> actions) {
+        if (session.getGameMode() == GameMode.CREATIVE) {
+            for (InventoryAction action : actions) {
+                if (action.getSource().getType() == InventorySource.Type.CREATIVE) {
+                    updateInventory(session, inventory);
+                    InventoryUtils.updateCursor(session);
+                    return;
+                }
+            }
+        }
+        super.translateActions(session, inventory, actions);
+    }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/DoubleChestInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/DoubleChestInventoryTranslator.java
index e0825ed87..07d3a414e 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/DoubleChestInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/DoubleChestInventoryTranslator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2020 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
@@ -29,32 +29,39 @@ import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
 import com.nukkitx.math.vector.Vector3i;
 import com.nukkitx.nbt.tag.CompoundTag;
 import com.nukkitx.protocol.bedrock.data.ContainerType;
-import com.nukkitx.protocol.bedrock.data.ItemData;
 import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket;
-import com.nukkitx.protocol.bedrock.packet.InventoryContentPacket;
+import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket;
 import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket;
 import org.geysermc.connector.inventory.Inventory;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.TranslatorsInit;
 import org.geysermc.connector.network.translators.block.BlockEntry;
+import org.geysermc.connector.network.translators.inventory.updater.ChestInventoryUpdater;
+import org.geysermc.connector.network.translators.inventory.updater.InventoryUpdater;
+
+public class DoubleChestInventoryTranslator extends BaseInventoryTranslator {
+    private final int blockId;
+    private final InventoryUpdater updater;
 
-public class DoubleChestInventoryTranslator extends BlockInventoryTranslator {
     public DoubleChestInventoryTranslator(int size) {
-        super(size, "minecraft:chest[facing=north,type=single,waterlogged=false]", ContainerType.CONTAINER);
+        super(size);
+        this.blockId = TranslatorsInit.getBlockTranslator().getBlockEntry("minecraft:chest[facing=north,type=single,waterlogged=false]").getBedrockRuntimeId();
+        this.updater = new ChestInventoryUpdater(54);
     }
 
     @Override
     public void prepareInventory(GeyserSession session, Inventory inventory) {
         Vector3i position = session.getPlayerEntity().getPosition().toInt().add(Vector3i.UP);
         Vector3i pairPosition = position.add(Vector3i.UNIT_X);
+
         UpdateBlockPacket blockPacket = new UpdateBlockPacket();
         blockPacket.setDataLayer(0);
         blockPacket.setBlockPosition(position);
         blockPacket.setRuntimeId(blockId);
-        blockPacket.getFlags().add(UpdateBlockPacket.Flag.PRIORITY);
+        blockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY);
         session.getUpstream().sendPacket(blockPacket);
 
-        CompoundTag tag = CompoundTag.EMPTY.toBuilder()
+        CompoundTag tag = CompoundTag.builder()
                 .stringTag("id", "Chest")
                 .intTag("x", position.getX())
                 .intTag("y", position.getY())
@@ -71,10 +78,10 @@ public class DoubleChestInventoryTranslator extends BlockInventoryTranslator {
         blockPacket.setDataLayer(0);
         blockPacket.setBlockPosition(pairPosition);
         blockPacket.setRuntimeId(blockId);
-        blockPacket.getFlags().add(UpdateBlockPacket.Flag.PRIORITY);
+        blockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY);
         session.getUpstream().sendPacket(blockPacket);
 
-        tag = CompoundTag.EMPTY.toBuilder()
+        tag = CompoundTag.builder()
                 .stringTag("id", "Chest")
                 .intTag("x", pairPosition.getX())
                 .intTag("y", pairPosition.getY())
@@ -90,6 +97,16 @@ public class DoubleChestInventoryTranslator extends BlockInventoryTranslator {
         inventory.setHolderPosition(position);
     }
 
+    @Override
+    public void openInventory(GeyserSession session, Inventory inventory) {
+        ContainerOpenPacket containerOpenPacket = new ContainerOpenPacket();
+        containerOpenPacket.setWindowId((byte) inventory.getId());
+        containerOpenPacket.setType((byte) ContainerType.CONTAINER.id());
+        containerOpenPacket.setBlockPosition(inventory.getHolderPosition());
+        containerOpenPacket.setUniqueEntityId(inventory.getHolderId());
+        session.getUpstream().sendPacket(containerOpenPacket);
+    }
+
     @Override
     public void closeInventory(GeyserSession session, Inventory inventory) {
         Vector3i holderPos = inventory.getHolderPosition();
@@ -113,24 +130,11 @@ public class DoubleChestInventoryTranslator extends BlockInventoryTranslator {
 
     @Override
     public void updateInventory(GeyserSession session, Inventory inventory) {
-        //need to pad empty slots for 4x9 and 5x9
-        ItemData[] bedrockItems = new ItemData[54];
-        for (int i = 0; i < bedrockItems.length; i++) {
-            if (i <= this.size) {
-                bedrockItems[i] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(i));
-            } else {
-                bedrockItems[i] = ItemData.AIR;
-            }
-        }
-        InventoryContentPacket contentPacket = new InventoryContentPacket();
-        contentPacket.setContainerId(inventory.getId());
-        contentPacket.setContents(bedrockItems);
-        session.getUpstream().sendPacket(contentPacket);
+        updater.updateInventory(this, session, inventory);
+    }
 
-        Inventory playerInventory = session.getInventory();
-        for (int i = 0; i < 36; i++) {
-            playerInventory.setItem(i + 9, inventory.getItem(i + this.size));
-        }
-        TranslatorsInit.getInventoryTranslators().get(playerInventory.getWindowType()).updateInventory(session, playerInventory);
+    @Override
+    public void updateSlot(GeyserSession session, Inventory inventory, int slot) {
+        updater.updateSlot(this, session, inventory, slot);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/EnchantmentInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/EnchantmentInventoryTranslator.java
index 0b4ed4ba9..ba7f8cc7a 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/EnchantmentInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/EnchantmentInventoryTranslator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2020 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
@@ -28,10 +28,11 @@ package org.geysermc.connector.network.translators.inventory;
 import com.nukkitx.protocol.bedrock.data.ContainerType;
 import org.geysermc.connector.inventory.Inventory;
 import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.inventory.updater.ContainerInventoryUpdater;
 
 public class EnchantmentInventoryTranslator extends BlockInventoryTranslator {
     public EnchantmentInventoryTranslator() {
-        super(2, "minecraft:enchanting_table", ContainerType.ENCHANTMENT);
+        super(2, "minecraft:enchanting_table", ContainerType.ENCHANTMENT, new ContainerInventoryUpdater());
     }
 
     @Override
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/FurnaceInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/FurnaceInventoryTranslator.java
index a2eefac40..9b45201ed 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/FurnaceInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/FurnaceInventoryTranslator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2020 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
@@ -30,10 +30,11 @@ import com.nukkitx.protocol.bedrock.data.ContainerType;
 import com.nukkitx.protocol.bedrock.packet.ContainerSetDataPacket;
 import org.geysermc.connector.inventory.Inventory;
 import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.inventory.updater.ContainerInventoryUpdater;
 
 public class FurnaceInventoryTranslator extends BlockInventoryTranslator {
     public FurnaceInventoryTranslator() {
-        super(3, "minecraft:furnace[facing=north,lit=false]", ContainerType.FURNACE);
+        super(3, "minecraft:furnace[facing=north,lit=false]", ContainerType.FURNACE, new ContainerInventoryUpdater());
     }
 
     @Override
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 915621940..7baef61a5 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
@@ -26,16 +26,16 @@
 package org.geysermc.connector.network.translators.inventory;
 
 import com.nukkitx.protocol.bedrock.data.InventoryAction;
+import lombok.AllArgsConstructor;
 import org.geysermc.connector.inventory.Inventory;
 import org.geysermc.connector.network.session.GeyserSession;
 
+import java.util.List;
+
+@AllArgsConstructor
 public abstract class InventoryTranslator {
     public final int size;
 
-    InventoryTranslator(int size) {
-        this.size = size;
-    }
-
     public abstract void prepareInventory(GeyserSession session, Inventory inventory);
     public abstract void openInventory(GeyserSession session, Inventory inventory);
     public abstract void closeInventory(GeyserSession session, Inventory inventory);
@@ -45,4 +45,5 @@ public abstract class InventoryTranslator {
     public abstract int bedrockSlotToJava(InventoryAction action);
     public abstract int javaSlotToBedrock(int slot);
     public abstract SlotType getSlotType(int javaSlot);
+    public abstract void translateActions(GeyserSession session, Inventory inventory, List<InventoryAction> actions);
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/PlayerInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/PlayerInventoryTranslator.java
index 6ba25bc52..acad709f5 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/PlayerInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/PlayerInventoryTranslator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2020 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
@@ -25,12 +25,19 @@
 
 package org.geysermc.connector.network.translators.inventory;
 
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
+import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
+import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientCreativeInventoryActionPacket;
 import com.nukkitx.protocol.bedrock.data.*;
 import com.nukkitx.protocol.bedrock.packet.InventoryContentPacket;
 import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
 import org.geysermc.connector.inventory.Inventory;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.TranslatorsInit;
+import org.geysermc.connector.network.translators.inventory.action.InventoryActionTranslator;
+import org.geysermc.connector.utils.InventoryUtils;
+
+import java.util.List;
 
 public class PlayerInventoryTranslator extends InventoryTranslator {
     public PlayerInventoryTranslator() {
@@ -39,20 +46,26 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
 
     @Override
     public void updateInventory(GeyserSession session, Inventory inventory) {
+        // Crafting grid
+        for (int i = 1; i < 5; i++) {
+            InventorySlotPacket slotPacket = new InventorySlotPacket();
+            slotPacket.setContainerId(ContainerId.CURSOR);
+            slotPacket.setInventorySlot(i + 27);
+            slotPacket.setSlot(TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(i)));
+            session.getUpstream().sendPacket(slotPacket);
+        }
+
         InventoryContentPacket inventoryContentPacket = new InventoryContentPacket();
         inventoryContentPacket.setContainerId(ContainerId.INVENTORY);
-
         ItemData[] contents = new ItemData[36];
         // Inventory
         for (int i = 9; i < 36; i++) {
             contents[i] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(i));
         }
-
         // Hotbar
         for (int i = 36; i < 45; i++) {
             contents[i - 36] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(i));
         }
-
         inventoryContentPacket.setContents(contents);
         session.getUpstream().sendPacket(inventoryContentPacket);
 
@@ -75,7 +88,7 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
 
     @Override
     public void updateSlot(GeyserSession session, Inventory inventory, int slot) {
-        if (slot >= 5 && slot <= 44) {
+        if (slot >= 1 && slot <= 44) {
             InventorySlotPacket slotPacket = new InventorySlotPacket();
             if (slot >= 9) {
                 slotPacket.setContainerId(ContainerId.INVENTORY);
@@ -84,9 +97,12 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
                 } else {
                     slotPacket.setInventorySlot(slot);
                 }
-            } else {
+            } else if (slot >= 5) {
                 slotPacket.setContainerId(ContainerId.ARMOR);
                 slotPacket.setInventorySlot(slot - 5);
+            } else {
+                slotPacket.setContainerId(ContainerId.CURSOR);
+                slotPacket.setInventorySlot(slot + 27);
             }
             slotPacket.setSlot(TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(slot)));
             session.getUpstream().sendPacket(slotPacket);
@@ -142,6 +158,55 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
         return SlotType.NORMAL;
     }
 
+    @Override
+    public void translateActions(GeyserSession session, Inventory inventory, List<InventoryAction> actions) {
+        if (session.getGameMode() == GameMode.CREATIVE) {
+            //crafting grid is not visible in creative mode in java edition
+            for (InventoryAction action : actions) {
+                if (action.getSource().getContainerId() == ContainerId.CURSOR && (action.getSlot() >= 28 && 31 >= action.getSlot())) {
+                    updateInventory(session, inventory);
+                    InventoryUtils.updateCursor(session);
+                    return;
+                }
+            }
+
+            ItemStack javaItem;
+            for (InventoryAction action : actions) {
+                switch (action.getSource().getContainerId()) {
+                    case ContainerId.INVENTORY:
+                    case ContainerId.ARMOR:
+                    case ContainerId.OFFHAND:
+                        int javaSlot = bedrockSlotToJava(action);
+                        if (action.getToItem().getId() == 0) {
+                            javaItem = new ItemStack(-1, 0, null);
+                        } else {
+                            javaItem = TranslatorsInit.getItemTranslator().translateToJava(action.getToItem());
+                        }
+                        ClientCreativeInventoryActionPacket creativePacket = new ClientCreativeInventoryActionPacket(javaSlot, InventoryUtils.fixStack(javaItem));
+                        session.getDownstream().getSession().send(creativePacket);
+                        inventory.setItem(javaSlot, javaItem);
+                        break;
+                    case ContainerId.CURSOR:
+                        if (action.getSlot() == 0) {
+                            session.getInventory().setCursor(TranslatorsInit.getItemTranslator().translateToJava(action.getToItem()));
+                        }
+                        break;
+                    case ContainerId.NONE:
+                        if (action.getSource().getType() == InventorySource.Type.WORLD_INTERACTION
+                                && action.getSource().getFlag() == InventorySource.Flag.DROP_ITEM) {
+                            javaItem = TranslatorsInit.getItemTranslator().translateToJava(action.getToItem());
+                            ClientCreativeInventoryActionPacket creativeDropPacket = new ClientCreativeInventoryActionPacket(-1, InventoryUtils.fixStack(javaItem));
+                            session.getDownstream().getSession().send(creativeDropPacket);
+                        }
+                        break;
+                }
+            }
+            return;
+        }
+
+        InventoryActionTranslator.translate(this, session, inventory, actions);
+    }
+
     @Override
     public void prepareInventory(GeyserSession session, Inventory inventory) {
     }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/SingleChestInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/SingleChestInventoryTranslator.java
index 84426eaa0..5c99b0126 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/SingleChestInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/SingleChestInventoryTranslator.java
@@ -26,37 +26,10 @@
 package org.geysermc.connector.network.translators.inventory;
 
 import com.nukkitx.protocol.bedrock.data.ContainerType;
-import com.nukkitx.protocol.bedrock.data.ItemData;
-import com.nukkitx.protocol.bedrock.packet.*;
-import org.geysermc.connector.inventory.Inventory;
-import org.geysermc.connector.network.session.GeyserSession;
-import org.geysermc.connector.network.translators.TranslatorsInit;
+import org.geysermc.connector.network.translators.inventory.updater.ChestInventoryUpdater;
 
 public class SingleChestInventoryTranslator extends BlockInventoryTranslator {
     public SingleChestInventoryTranslator(int size) {
-        super(size, "minecraft:chest[facing=north,type=single,waterlogged=false]", ContainerType.CONTAINER);
-    }
-
-    @Override
-    public void updateInventory(GeyserSession session, Inventory inventory) {
-        //need to pad empty slots for 1x9 and 2x9
-        ItemData[] bedrockItems = new ItemData[27];
-        for (int i = 0; i < bedrockItems.length; i++) {
-            if (i <= this.size) {
-                bedrockItems[i] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(i));
-            } else {
-                bedrockItems[i] = ItemData.AIR;
-            }
-        }
-        InventoryContentPacket contentPacket = new InventoryContentPacket();
-        contentPacket.setContainerId(inventory.getId());
-        contentPacket.setContents(bedrockItems);
-        session.getUpstream().sendPacket(contentPacket);
-
-        Inventory playerInventory = session.getInventory();
-        for (int i = 0; i < 36; i++) {
-            playerInventory.setItem(i + 9, inventory.getItem(i + this.size));
-        }
-        TranslatorsInit.getInventoryTranslators().get(playerInventory.getWindowType()).updateInventory(session, playerInventory);
+        super(size, "minecraft:chest[facing=north,type=single,waterlogged=false]", ContainerType.CONTAINER, new ChestInventoryUpdater(27));
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/SlotType.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/SlotType.java
index b5657af46..045adbd32 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/SlotType.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/SlotType.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2020 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
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/action/Click.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/action/Click.java
new file mode 100644
index 000000000..1fdfa3640
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/action/Click.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2019-2020 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.translators.inventory.action;
+
+import com.github.steveice10.mc.protocol.data.game.window.ClickItemParam;
+import com.github.steveice10.mc.protocol.data.game.window.WindowActionParam;
+import lombok.AllArgsConstructor;
+
+@AllArgsConstructor
+enum Click {
+    LEFT(ClickItemParam.LEFT_CLICK),
+    RIGHT(ClickItemParam.RIGHT_CLICK);
+
+    public final WindowActionParam actionParam;
+}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/action/ClickPlan.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/action/ClickPlan.java
new file mode 100644
index 000000000..3abdd2843
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/action/ClickPlan.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2019-2020 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.translators.inventory.action;
+
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
+import com.github.steveice10.mc.protocol.data.game.window.WindowAction;
+import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientConfirmTransactionPacket;
+import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientWindowActionPacket;
+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.InventoryTranslator;
+import org.geysermc.connector.network.translators.inventory.SlotType;
+import org.geysermc.connector.utils.InventoryUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ListIterator;
+
+class ClickPlan {
+    private final List<ClickAction> plan = new ArrayList<>();
+
+    public void add(Click click, int slot) {
+        plan.add(new ClickAction(click, slot));
+    }
+
+    public void execute(GeyserSession session, InventoryTranslator translator, Inventory inventory, boolean refresh) {
+        PlayerInventory playerInventory = session.getInventory();
+        ListIterator<ClickAction> planIter = plan.listIterator();
+        while (planIter.hasNext()) {
+            final ClickAction action = planIter.next();
+            final ItemStack cursorItem = playerInventory.getCursor();
+            final ItemStack clickedItem = inventory.getItem(action.slot);
+            final short actionId = (short) inventory.getTransactionId().getAndIncrement();
+
+            //TODO: stop relying on refreshing the inventory for crafting to work properly
+            if (translator.getSlotType(action.slot) != SlotType.NORMAL)
+                refresh = true;
+
+            ClientWindowActionPacket clickPacket = new ClientWindowActionPacket(inventory.getId(),
+                    actionId, action.slot, !planIter.hasNext() && refresh ? InventoryUtils.REFRESH_ITEM : InventoryUtils.fixStack(clickedItem),
+                    WindowAction.CLICK_ITEM, action.click.actionParam);
+
+            if (translator.getSlotType(action.slot) == SlotType.OUTPUT) {
+                if (cursorItem == null && clickedItem != null) {
+                    playerInventory.setCursor(clickedItem);
+                } else if (InventoryUtils.canStack(cursorItem, clickedItem)) {
+                    playerInventory.setCursor(new ItemStack(cursorItem.getId(),
+                            cursorItem.getAmount() + clickedItem.getAmount(), cursorItem.getNbt()));
+                }
+            } else {
+                switch (action.click) {
+                    case LEFT:
+                        if (!InventoryUtils.canStack(cursorItem, clickedItem)) {
+                            playerInventory.setCursor(clickedItem);
+                            inventory.setItem(action.slot, cursorItem);
+                        } else {
+                            playerInventory.setCursor(null);
+                            inventory.setItem(action.slot, new ItemStack(clickedItem.getId(),
+                                    clickedItem.getAmount() + cursorItem.getAmount(), clickedItem.getNbt()));
+                        }
+                        break;
+                    case RIGHT:
+                        if (cursorItem == null && clickedItem != null) {
+                            ItemStack halfItem = new ItemStack(clickedItem.getId(),
+                                    clickedItem.getAmount() / 2, clickedItem.getNbt());
+                            inventory.setItem(action.slot, halfItem);
+                            playerInventory.setCursor(new ItemStack(clickedItem.getId(),
+                                    clickedItem.getAmount() - halfItem.getAmount(), clickedItem.getNbt()));
+                        } else if (cursorItem != null && clickedItem == null) {
+                            playerInventory.setCursor(new ItemStack(cursorItem.getId(),
+                                    cursorItem.getAmount() - 1, cursorItem.getNbt()));
+                            inventory.setItem(action.slot, new ItemStack(cursorItem.getId(),
+                                    1, cursorItem.getNbt()));
+                        } else if (InventoryUtils.canStack(cursorItem, clickedItem)) {
+                            playerInventory.setCursor(new ItemStack(cursorItem.getId(),
+                                    cursorItem.getAmount() - 1, cursorItem.getNbt()));
+                            inventory.setItem(action.slot, new ItemStack(clickedItem.getId(),
+                                    clickedItem.getAmount() + 1, clickedItem.getNbt()));
+                        }
+                        break;
+                }
+            }
+            session.getDownstream().getSession().send(clickPacket);
+            session.getDownstream().getSession().send(new ClientConfirmTransactionPacket(inventory.getId(), actionId, true));
+        }
+
+        /*if (refresh) {
+            translator.updateInventory(session, inventory);
+            InventoryUtils.updateCursor(session);
+        }*/
+    }
+
+    private static class ClickAction {
+        final Click click;
+        final int slot;
+        ClickAction(Click click, int slot) {
+            this.click = click;
+            this.slot = slot;
+        }
+    }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/action/InventoryActionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/action/InventoryActionTranslator.java
new file mode 100644
index 000000000..586c2c023
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/action/InventoryActionTranslator.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (c) 2019-2020 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.translators.inventory.action;
+
+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.mc.protocol.data.game.entity.player.PlayerAction;
+import com.github.steveice10.mc.protocol.data.game.window.*;
+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.window.ClientWindowActionPacket;
+import com.nukkitx.protocol.bedrock.data.ContainerId;
+import com.nukkitx.protocol.bedrock.data.InventoryAction;
+import com.nukkitx.protocol.bedrock.data.InventorySource;
+import com.nukkitx.protocol.bedrock.data.ItemData;
+import org.geysermc.connector.inventory.Inventory;
+import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.TranslatorsInit;
+import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
+import org.geysermc.connector.network.translators.inventory.SlotType;
+import org.geysermc.connector.utils.InventoryUtils;
+
+import java.util.*;
+
+public class InventoryActionTranslator {
+    public static void translate(InventoryTranslator translator, GeyserSession session, Inventory inventory, List<InventoryAction> actions) {
+        if (actions.size() != 2)
+            return;
+
+        InventoryAction worldAction = null;
+        InventoryAction cursorAction = null;
+        InventoryAction containerAction = null;
+        boolean refresh = false;
+        for (InventoryAction action : actions) {
+            if (action.getSource().getContainerId() == ContainerId.CRAFTING_USE_INGREDIENT || action.getSource().getContainerId() == ContainerId.CRAFTING_RESULT) {
+                return;
+            } else if (action.getSource().getType() == InventorySource.Type.WORLD_INTERACTION) {
+                worldAction = action;
+            } else if (action.getSource().getContainerId() == ContainerId.CURSOR && action.getSlot() == 0) {
+                cursorAction = action;
+                ItemData translatedCursor = TranslatorsInit.getItemTranslator().translateToBedrock(session.getInventory().getCursor());
+                if (!translatedCursor.equals(action.getFromItem())) {
+                    refresh = true;
+                }
+            } else {
+                containerAction = action;
+                ItemData translatedItem = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(translator.bedrockSlotToJava(action)));
+                if (!translatedItem.equals(action.getFromItem())) {
+                    refresh = true;
+                }
+            }
+        }
+
+        final int craftSlot = session.getCraftSlot();
+        session.setCraftSlot(0);
+
+        if (worldAction != null) {
+            InventoryAction sourceAction;
+            if (cursorAction != null) {
+                sourceAction = cursorAction;
+            } else {
+                sourceAction = containerAction;
+            }
+
+            if (sourceAction != null) {
+                if (worldAction.getSource().getFlag() == InventorySource.Flag.DROP_ITEM) {
+                    //quick dropping from hotbar?
+                    if (session.getInventoryCache().getOpenInventory() == null && sourceAction.getSource().getContainerId() == ContainerId.INVENTORY) {
+                        int heldSlot = session.getInventory().getHeldItemSlot();
+                        if (sourceAction.getSlot() == heldSlot) {
+                            ClientPlayerActionPacket actionPacket = new ClientPlayerActionPacket(
+                                    sourceAction.getToItem().getCount() == 0 ? PlayerAction.DROP_ITEM_STACK : PlayerAction.DROP_ITEM,
+                                    new Position(0, 0, 0), BlockFace.DOWN);
+                            session.getDownstream().getSession().send(actionPacket);
+                            ItemStack item = session.getInventory().getItem(heldSlot);
+                            if (item != null) {
+                                session.getInventory().setItem(heldSlot, new ItemStack(item.getId(), item.getAmount() - 1, item.getNbt()));
+                            }
+                            return;
+                        }
+                    }
+                    int dropAmount = sourceAction.getFromItem().getCount() - sourceAction.getToItem().getCount();
+                    if (sourceAction != cursorAction) { //dropping directly from inventory
+                        int javaSlot = translator.bedrockSlotToJava(sourceAction);
+                        if (dropAmount == sourceAction.getFromItem().getCount()) {
+                            ClientWindowActionPacket dropPacket = new ClientWindowActionPacket(inventory.getId(),
+                                    inventory.getTransactionId().getAndIncrement(),
+                                    javaSlot, null, WindowAction.DROP_ITEM,
+                                    DropItemParam.DROP_SELECTED_STACK);
+                            session.getDownstream().getSession().send(dropPacket);
+                        } else {
+                            for (int i = 0; i < dropAmount; i++) {
+                                ClientWindowActionPacket dropPacket = new ClientWindowActionPacket(inventory.getId(),
+                                        inventory.getTransactionId().getAndIncrement(),
+                                        javaSlot, null, WindowAction.DROP_ITEM,
+                                        DropItemParam.DROP_FROM_SELECTED);
+                                session.getDownstream().getSession().send(dropPacket);
+                            }
+                        }
+                        ItemStack item = session.getInventory().getItem(javaSlot);
+                        if (item != null) {
+                            session.getInventory().setItem(javaSlot, new ItemStack(item.getId(), item.getAmount() - dropAmount, item.getNbt()));
+                        }
+                        return;
+                    } else { //clicking outside of inventory
+                        ClientWindowActionPacket dropPacket = new ClientWindowActionPacket(inventory.getId(), inventory.getTransactionId().getAndIncrement(),
+                                -999, null, WindowAction.CLICK_ITEM,
+                                dropAmount > 1 ? ClickItemParam.LEFT_CLICK : ClickItemParam.RIGHT_CLICK);
+                        session.getDownstream().getSession().send(dropPacket);
+                        ItemStack cursor = session.getInventory().getCursor();
+                        if (cursor != null) {
+                            session.getInventory().setCursor(new ItemStack(cursor.getId(), dropAmount > 1 ? 0 : cursor.getAmount() - 1, cursor.getNbt()));
+                        }
+                        return;
+                    }
+                }
+            }
+        } else if (cursorAction != null && containerAction != null) {
+            //left/right click
+            ClickPlan plan = new ClickPlan();
+            int javaSlot = translator.bedrockSlotToJava(containerAction);
+            if (cursorAction.getFromItem().equals(containerAction.getToItem())
+                    && containerAction.getFromItem().equals(cursorAction.getToItem())
+                    && !InventoryUtils.canStack(cursorAction.getFromItem(), containerAction.getFromItem())) { //simple swap
+                plan.add(Click.LEFT, javaSlot);
+            } else if (cursorAction.getFromItem().getCount() > cursorAction.getToItem().getCount()) { //release
+                if (cursorAction.getToItem().getCount() == 0) {
+                    plan.add(Click.LEFT, javaSlot);
+                } else {
+                    int difference = cursorAction.getFromItem().getCount() - cursorAction.getToItem().getCount();
+                    for (int i = 0; i < difference; i++) {
+                        plan.add(Click.RIGHT, javaSlot);
+                    }
+                }
+            } else { //pickup
+                if (cursorAction.getFromItem().getCount() == 0) {
+                    if (containerAction.getToItem().getCount() == 0) { //pickup all
+                        plan.add(Click.LEFT, javaSlot);
+                    } else { //pickup some
+                        if (translator.getSlotType(javaSlot) == SlotType.FURNACE_OUTPUT
+                                || containerAction.getToItem().getCount() == containerAction.getFromItem().getCount() / 2) { //right click
+                            plan.add(Click.RIGHT, javaSlot);
+                        } else {
+                            plan.add(Click.LEFT, javaSlot);
+                            int difference = containerAction.getFromItem().getCount() - cursorAction.getToItem().getCount();
+                            for (int i = 0; i < difference; i++) {
+                                plan.add(Click.RIGHT, javaSlot);
+                            }
+                        }
+                    }
+                } else { //pickup into non-empty cursor
+                    if (translator.getSlotType(javaSlot) == SlotType.FURNACE_OUTPUT) {
+                        if (containerAction.getToItem().getCount() == 0) {
+                            plan.add(Click.LEFT, javaSlot);
+                        } else {
+                            ClientWindowActionPacket shiftClickPacket = new ClientWindowActionPacket(inventory.getId(),
+                                    inventory.getTransactionId().getAndIncrement(),
+                                    javaSlot, InventoryUtils.REFRESH_ITEM, WindowAction.SHIFT_CLICK_ITEM,
+                                    ShiftClickItemParam.LEFT_CLICK);
+                            session.getDownstream().getSession().send(shiftClickPacket);
+                            translator.updateInventory(session, inventory);
+                            return;
+                        }
+                    } else if (translator.getSlotType(javaSlot) == SlotType.OUTPUT) {
+                        plan.add(Click.LEFT, javaSlot);
+                    } else {
+                        int cursorSlot = findTempSlot(inventory, session.getInventory().getCursor(), Collections.singletonList(javaSlot));
+                        if (cursorSlot != -1) {
+                            plan.add(Click.LEFT, cursorSlot);
+                        } else {
+                            translator.updateInventory(session, inventory);
+                            return;
+                        }
+                        plan.add(Click.LEFT, javaSlot);
+                        int difference = cursorAction.getToItem().getCount() - cursorAction.getFromItem().getCount();
+                        for (int i = 0; i < difference; i++) {
+                            plan.add(Click.RIGHT, cursorSlot);
+                        }
+                        plan.add(Click.LEFT, javaSlot);
+                        plan.add(Click.LEFT, cursorSlot);
+                    }
+                }
+            }
+            plan.execute(session, translator, inventory, refresh);
+            return;
+        } else {
+            ClickPlan plan = new ClickPlan();
+            InventoryAction fromAction;
+            InventoryAction toAction;
+            if (actions.get(0).getFromItem().getCount() >= actions.get(0).getToItem().getCount()) {
+                fromAction = actions.get(0);
+                toAction = actions.get(1);
+            } else {
+                fromAction = actions.get(1);
+                toAction = actions.get(0);
+            }
+            int fromSlot = translator.bedrockSlotToJava(fromAction);
+            int toSlot = translator.bedrockSlotToJava(toAction);
+
+            if (translator.getSlotType(fromSlot) == SlotType.OUTPUT) {
+                if ((craftSlot != 0 && craftSlot != -2) && (inventory.getItem(toSlot) == null
+                        || InventoryUtils.canStack(session.getInventory().getCursor(), inventory.getItem(toSlot)))) {
+                    if (fromAction.getToItem().getCount() == 0) {
+                        refresh = true;
+                        plan.add(Click.LEFT, toSlot);
+                        if (craftSlot != -1) {
+                            plan.add(Click.LEFT, craftSlot);
+                        }
+                    } else {
+                        int difference = toAction.getToItem().getCount() - toAction.getFromItem().getCount();
+                        for (int i = 0; i < difference; i++) {
+                            plan.add(Click.RIGHT, toSlot);
+                        }
+                        session.setCraftSlot(craftSlot);
+                    }
+                    plan.execute(session, translator, inventory, refresh);
+                    return;
+                } else {
+                    session.setCraftSlot(-2);
+                }
+            }
+
+            int cursorSlot = -1;
+            if (session.getInventory().getCursor() != null) { //move cursor contents to a temporary slot
+                cursorSlot = findTempSlot(inventory, session.getInventory().getCursor(), Arrays.asList(fromSlot, toSlot));
+                if (cursorSlot != -1) {
+                    plan.add(Click.LEFT, cursorSlot);
+                } else {
+                    translator.updateInventory(session, inventory);
+                    return;
+                }
+            }
+            if ((fromAction.getFromItem().equals(toAction.getToItem()) && !InventoryUtils.canStack(fromAction.getFromItem(), toAction.getFromItem()))
+                    || fromAction.getToItem().getId() == 0) { //slot swap
+                plan.add(Click.LEFT, fromSlot);
+                plan.add(Click.LEFT, toSlot);
+                if (fromAction.getToItem().getId() != 0) {
+                    plan.add(Click.LEFT, fromSlot);
+                }
+            } else if (InventoryUtils.canStack(fromAction.getFromItem(), toAction.getToItem())) { //partial item move
+                if (translator.getSlotType(fromSlot) == SlotType.FURNACE_OUTPUT) {
+                    ClientWindowActionPacket shiftClickPacket = new ClientWindowActionPacket(inventory.getId(),
+                            inventory.getTransactionId().getAndIncrement(),
+                            fromSlot, InventoryUtils.REFRESH_ITEM, WindowAction.SHIFT_CLICK_ITEM,
+                            ShiftClickItemParam.LEFT_CLICK);
+                    session.getDownstream().getSession().send(shiftClickPacket);
+                    translator.updateInventory(session, inventory);
+                    return;
+                } else if (translator.getSlotType(fromSlot) == SlotType.OUTPUT) {
+                    session.setCraftSlot(cursorSlot);
+                    plan.add(Click.LEFT, fromSlot);
+                    int difference = toAction.getToItem().getCount() - toAction.getFromItem().getCount();
+                    for (int i = 0; i < difference; i++) {
+                        plan.add(Click.RIGHT, toSlot);
+                    }
+                    //client will send additional packets later to finish transferring crafting output
+                    //translator will know how to handle this using the craftSlot variable
+                } else {
+                    plan.add(Click.LEFT, fromSlot);
+                    int difference = toAction.getToItem().getCount() - toAction.getFromItem().getCount();
+                    for (int i = 0; i < difference; i++) {
+                        plan.add(Click.RIGHT, toSlot);
+                    }
+                    plan.add(Click.LEFT, fromSlot);
+                }
+            }
+            if (cursorSlot != -1) {
+                plan.add(Click.LEFT, cursorSlot);
+            }
+            plan.execute(session, translator, inventory, refresh);
+            return;
+        }
+
+        translator.updateInventory(session, inventory);
+        InventoryUtils.updateCursor(session);
+    }
+
+    private static int findTempSlot(Inventory inventory, ItemStack item, List<Integer> slotBlacklist) {
+        /*try and find a slot that can temporarily store the given item
+        only look in the main inventory and hotbar
+        only slots that are empty or contain a different type of item are valid*/
+        int offset = inventory.getId() == 0 ? 1 : 0; //offhand is not a viable slot (some servers disable it)
+        List<ItemStack> itemBlacklist = new ArrayList<>(slotBlacklist.size() + 1);
+        itemBlacklist.add(item);
+        for (int slot : slotBlacklist) {
+            ItemStack blacklistItem = inventory.getItem(slot);
+            if (blacklistItem != null)
+                itemBlacklist.add(blacklistItem);
+        }
+        for (int i = inventory.getSize() - (36 + offset); i < inventory.getSize() - offset; i++) {
+            ItemStack testItem = inventory.getItem(i);
+            boolean acceptable = true;
+            if (testItem != null) {
+                for (ItemStack blacklistItem : itemBlacklist) {
+                    if (InventoryUtils.canStack(testItem, blacklistItem)) {
+                        acceptable = false;
+                        break;
+                    }
+                }
+            }
+            if (acceptable && !slotBlacklist.contains(i))
+                return i;
+        }
+        //could not find a viable temp slot
+        return -1;
+    }
+}
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
new file mode 100644
index 000000000..6c0db853d
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/holder/BlockInventoryHolder.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2019-2020 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.translators.inventory.holder;
+
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
+import com.nukkitx.math.vector.Vector3i;
+import com.nukkitx.nbt.tag.CompoundTag;
+import com.nukkitx.protocol.bedrock.data.ContainerType;
+import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket;
+import com.nukkitx.protocol.bedrock.packet.ContainerOpenPacket;
+import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket;
+import lombok.AllArgsConstructor;
+import org.geysermc.connector.inventory.Inventory;
+import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.block.BlockEntry;
+import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
+
+@AllArgsConstructor
+public class BlockInventoryHolder extends InventoryHolder {
+    private final int blockId;
+    private final ContainerType containerType;
+
+    @Override
+    public void prepareInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) {
+        Vector3i position = session.getPlayerEntity().getPosition().toInt();
+        position = position.add(Vector3i.UP);
+        UpdateBlockPacket blockPacket = new UpdateBlockPacket();
+        blockPacket.setDataLayer(0);
+        blockPacket.setBlockPosition(position);
+        blockPacket.setRuntimeId(blockId);
+        blockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY);
+        session.getUpstream().sendPacket(blockPacket);
+        inventory.setHolderPosition(position);
+
+        CompoundTag tag = CompoundTag.builder()
+                .intTag("x", position.getX())
+                .intTag("y", position.getY())
+                .intTag("z", position.getZ())
+                .stringTag("CustomName", inventory.getTitle()).buildRootTag();
+        BlockEntityDataPacket dataPacket = new BlockEntityDataPacket();
+        dataPacket.setData(tag);
+        dataPacket.setBlockPosition(position);
+        session.getUpstream().sendPacket(dataPacket);
+    }
+
+    @Override
+    public void openInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) {
+        ContainerOpenPacket containerOpenPacket = new ContainerOpenPacket();
+        containerOpenPacket.setWindowId((byte) inventory.getId());
+        containerOpenPacket.setType((byte) containerType.id());
+        containerOpenPacket.setBlockPosition(inventory.getHolderPosition());
+        containerOpenPacket.setUniqueEntityId(inventory.getHolderId());
+        session.getUpstream().sendPacket(containerOpenPacket);
+    }
+
+    @Override
+    public void closeInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) {
+        Vector3i holderPos = inventory.getHolderPosition();
+        Position pos = new Position(holderPos.getX(), holderPos.getY(), holderPos.getZ());
+        BlockEntry realBlock = session.getChunkCache().getBlockAt(pos);
+        UpdateBlockPacket blockPacket = new UpdateBlockPacket();
+        blockPacket.setDataLayer(0);
+        blockPacket.setBlockPosition(holderPos);
+        blockPacket.setRuntimeId(realBlock.getBedrockRuntimeId());
+        session.getUpstream().sendPacket(blockPacket);
+    }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/holder/InventoryHolder.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/holder/InventoryHolder.java
new file mode 100644
index 000000000..5a9e736e9
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/holder/InventoryHolder.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2019-2020 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.translators.inventory.holder;
+
+import org.geysermc.connector.inventory.Inventory;
+import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
+
+public abstract class InventoryHolder {
+    public abstract void prepareInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory);
+    public abstract void openInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory);
+    public abstract void closeInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory);
+}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/ChestInventoryUpdater.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/ChestInventoryUpdater.java
new file mode 100644
index 000000000..4af1fba13
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/ChestInventoryUpdater.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2019-2020 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.translators.inventory.updater;
+
+import com.nukkitx.protocol.bedrock.data.ItemData;
+import com.nukkitx.protocol.bedrock.packet.InventoryContentPacket;
+import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
+import lombok.AllArgsConstructor;
+import org.geysermc.connector.inventory.Inventory;
+import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.TranslatorsInit;
+import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
+
+@AllArgsConstructor
+public class ChestInventoryUpdater extends InventoryUpdater {
+    private final int paddedSize;
+
+    @Override
+    public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) {
+        super.updateInventory(translator, session, inventory);
+
+        ItemData[] bedrockItems = new ItemData[paddedSize];
+        for (int i = 0; i < bedrockItems.length; i++) {
+            if (i <= translator.size) {
+                bedrockItems[i] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(i));
+            } else {
+                bedrockItems[i] = ItemData.AIR;
+            }
+        }
+
+        InventoryContentPacket contentPacket = new InventoryContentPacket();
+        contentPacket.setContainerId(inventory.getId());
+        contentPacket.setContents(bedrockItems);
+        session.getUpstream().sendPacket(contentPacket);
+    }
+
+    @Override
+    public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) {
+        if (super.updateSlot(translator, session, inventory, javaSlot))
+            return true;
+
+        InventorySlotPacket slotPacket = new InventorySlotPacket();
+        slotPacket.setContainerId(inventory.getId());
+        slotPacket.setInventorySlot(translator.javaSlotToBedrock(javaSlot));
+        slotPacket.setSlot(TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(javaSlot)));
+        session.getUpstream().sendPacket(slotPacket);
+        return true;
+    }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/ContainerInventoryUpdater.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/ContainerInventoryUpdater.java
new file mode 100644
index 000000000..7169311ed
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/ContainerInventoryUpdater.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2019-2020 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.translators.inventory.updater;
+
+import com.nukkitx.protocol.bedrock.data.ItemData;
+import com.nukkitx.protocol.bedrock.packet.InventoryContentPacket;
+import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
+import org.geysermc.connector.inventory.Inventory;
+import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.TranslatorsInit;
+import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
+
+public class ContainerInventoryUpdater extends InventoryUpdater {
+    @Override
+    public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) {
+        super.updateInventory(translator, session, inventory);
+
+        ItemData[] bedrockItems = new ItemData[translator.size];
+        for (int i = 0; i < bedrockItems.length; i++) {
+            bedrockItems[translator.javaSlotToBedrock(i)] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(i));
+        }
+
+        InventoryContentPacket contentPacket = new InventoryContentPacket();
+        contentPacket.setContainerId(inventory.getId());
+        contentPacket.setContents(bedrockItems);
+        session.getUpstream().sendPacket(contentPacket);
+    }
+
+    @Override
+    public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) {
+        if (super.updateSlot(translator, session, inventory, javaSlot))
+            return true;
+
+        InventorySlotPacket slotPacket = new InventorySlotPacket();
+        slotPacket.setContainerId(inventory.getId());
+        slotPacket.setInventorySlot(translator.javaSlotToBedrock(javaSlot));
+        slotPacket.setSlot(TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(javaSlot)));
+        session.getUpstream().sendPacket(slotPacket);
+        return true;
+    }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/CursorInventoryUpdater.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/CursorInventoryUpdater.java
new file mode 100644
index 000000000..3df8d7662
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/CursorInventoryUpdater.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2019-2020 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.translators.inventory.updater;
+
+import com.nukkitx.protocol.bedrock.data.ContainerId;
+import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
+import org.geysermc.connector.inventory.Inventory;
+import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.TranslatorsInit;
+import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
+
+public class CursorInventoryUpdater extends InventoryUpdater {
+    @Override
+    public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) {
+        super.updateInventory(translator, session, inventory);
+
+        for (int i = 0; i < translator.size; i++) {
+            final int bedrockSlot = translator.javaSlotToBedrock(i);
+            if (bedrockSlot == 50)
+                continue;
+            InventorySlotPacket slotPacket = new InventorySlotPacket();
+            slotPacket.setContainerId(ContainerId.CURSOR);
+            slotPacket.setInventorySlot(bedrockSlot);
+            slotPacket.setSlot(TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(i)));
+            session.getUpstream().sendPacket(slotPacket);
+        }
+    }
+
+    @Override
+    public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) {
+        if (super.updateSlot(translator, session, inventory, javaSlot))
+            return true;
+
+        InventorySlotPacket slotPacket = new InventorySlotPacket();
+        slotPacket.setContainerId(ContainerId.CURSOR);
+        slotPacket.setInventorySlot(translator.javaSlotToBedrock(javaSlot));
+        slotPacket.setSlot(TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(javaSlot)));
+        session.getUpstream().sendPacket(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
new file mode 100644
index 000000000..e5b6f4c56
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/InventoryUpdater.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2019-2020 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.translators.inventory.updater;
+
+import com.nukkitx.protocol.bedrock.data.ContainerId;
+import com.nukkitx.protocol.bedrock.data.ItemData;
+import com.nukkitx.protocol.bedrock.packet.InventoryContentPacket;
+import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
+import org.geysermc.connector.inventory.Inventory;
+import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.TranslatorsInit;
+import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
+
+public abstract class InventoryUpdater {
+    public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) {
+        ItemData[] bedrockItems = new ItemData[36];
+        for (int i = 0; i < 36; i++) {
+            final int offset = i < 9 ? 27 : -9;
+            bedrockItems[i] = TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(translator.size + i + offset));
+        }
+        InventoryContentPacket contentPacket = new InventoryContentPacket();
+        contentPacket.setContainerId(ContainerId.INVENTORY);
+        contentPacket.setContents(bedrockItems);
+        session.getUpstream().sendPacket(contentPacket);
+    }
+
+    public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) {
+        if (javaSlot >= translator.size) {
+            InventorySlotPacket slotPacket = new InventorySlotPacket();
+            slotPacket.setContainerId(ContainerId.INVENTORY);
+            slotPacket.setInventorySlot(translator.javaSlotToBedrock(javaSlot));
+            slotPacket.setSlot(TranslatorsInit.getItemTranslator().translateToBedrock(inventory.getItem(javaSlot)));
+            session.getUpstream().sendPacket(slotPacket);
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java
index 596ef2153..c5c152a2f 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2020 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
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java
index ed61e4912..aaf00169d 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/ItemTranslator.java
@@ -74,9 +74,8 @@ public class ItemTranslator {
     }
 
     public ItemData translateToBedrock(ItemStack stack) {
-        // Most likely dirt if null
         if (stack == null) {
-            return ItemData.of(3, (short)0, 0);
+            return ItemData.AIR;
         }
 
         ItemEntry bedrockItem = getItem(stack);
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/Potion.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/Potion.java
index ca5cd5c0f..f711d3ea2 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/item/Potion.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/Potion.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2020 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
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java
index 9015fc28f..9399d1dd1 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaDeclareRecipesTranslator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2020 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
@@ -49,8 +49,10 @@ import java.util.*;
 import java.util.stream.Collectors;
 
 public class JavaDeclareRecipesTranslator extends PacketTranslator<ServerDeclareRecipesPacket> {
-
-    private final int[] brewingIngredients = new int[]{372, 331, 348, 376, 289, 437, 353, 414, 382, 375, 462, 378, 396, 377, 370, 469, 470};
+    private static final Collection<PotionMixData> POTION_MIXES =
+            Arrays.stream(new int[]{372, 331, 348, 376, 289, 437, 353, 414, 382, 375, 462, 378, 396, 377, 370, 469, 470})
+            .mapToObj(ingredient -> new PotionMixData(0, ingredient, 0))
+            .collect(Collectors.toList());
 
     @Override
     public void translate(ServerDeclareRecipesPacket packet, GeyserSession session) {
@@ -61,6 +63,7 @@ public class JavaDeclareRecipesTranslator extends PacketTranslator<ServerDeclare
                 case CRAFTING_SHAPELESS: {
                     ShapelessRecipeData shapelessRecipeData = (ShapelessRecipeData) recipe.getData();
                     ItemData output = TranslatorsInit.getItemTranslator().translateToBedrock(shapelessRecipeData.getResult());
+                    output = ItemData.of(output.getId(), output.getDamage(), output.getCount()); //strip NBT
                     ItemData[][] inputCombinations = combinations(shapelessRecipeData.getIngredients());
                     for (ItemData[] inputs : inputCombinations) {
                         UUID uuid = UUID.randomUUID();
@@ -72,6 +75,7 @@ public class JavaDeclareRecipesTranslator extends PacketTranslator<ServerDeclare
                 case CRAFTING_SHAPED: {
                     ShapedRecipeData shapedRecipeData = (ShapedRecipeData) recipe.getData();
                     ItemData output = TranslatorsInit.getItemTranslator().translateToBedrock(shapedRecipeData.getResult());
+                    output = ItemData.of(output.getId(), output.getDamage(), output.getCount()); //strip NBT
                     ItemData[][] inputCombinations = combinations(shapedRecipeData.getIngredients());
                     for (ItemData[] inputs : inputCombinations) {
                         UUID uuid = UUID.randomUUID();
@@ -83,12 +87,11 @@ public class JavaDeclareRecipesTranslator extends PacketTranslator<ServerDeclare
                 }
             }
         }
-        for (int brewingIngredient : brewingIngredients) {
-            craftingDataPacket.getPotionMixData().add(new PotionMixData(0, brewingIngredient, 0));
-        }
+        craftingDataPacket.getPotionMixData().addAll(POTION_MIXES);
         session.getUpstream().sendPacket(craftingDataPacket);
     }
 
+    //TODO: rewrite
     private ItemData[][] combinations(Ingredient[] ingredients) {
         Map<Set<ItemData>, IntSet> squashedOptions = new HashMap<>();
         for (int i = 0; i < ingredients.length; i++) {
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerChangeHeldItemTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerChangeHeldItemTranslator.java
new file mode 100644
index 000000000..90042e32f
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/entity/player/JavaPlayerChangeHeldItemTranslator.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2019-2020 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.translators.java.entity.player;
+
+import com.github.steveice10.mc.protocol.packet.ingame.server.entity.player.ServerPlayerChangeHeldItemPacket;
+import com.nukkitx.protocol.bedrock.packet.PlayerHotbarPacket;
+import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.PacketTranslator;
+
+public class JavaPlayerChangeHeldItemTranslator extends PacketTranslator<ServerPlayerChangeHeldItemPacket> {
+
+    @Override
+    public void translate(ServerPlayerChangeHeldItemPacket packet, GeyserSession session) {
+        PlayerHotbarPacket hotbarPacket = new PlayerHotbarPacket();
+        hotbarPacket.setContainerId(0);
+        hotbarPacket.setSelectedHotbarSlot(packet.getSlot());
+        hotbarPacket.setSelectHotbarSlot(true);
+        session.getUpstream().sendPacket(hotbarPacket);
+
+        session.getInventory().setHeldItemSlot(packet.getSlot());
+    }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaCloseWindowTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaCloseWindowTranslator.java
index dcb0c9feb..cb532e608 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaCloseWindowTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaCloseWindowTranslator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2020 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
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaConfirmTransactionTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaConfirmTransactionTranslator.java
index 09718dbd5..10c85de4a 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaConfirmTransactionTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaConfirmTransactionTranslator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2020 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
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaOpenWindowTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaOpenWindowTranslator.java
index 43f3b6d15..4190ac5a4 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaOpenWindowTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaOpenWindowTranslator.java
@@ -80,7 +80,7 @@ public class JavaOpenWindowTranslator extends PacketTranslator<ServerOpenWindowP
             InventoryTranslator openTranslator = TranslatorsInit.getInventoryTranslators().get(openInventory.getWindowType());
             if (!openTranslator.getClass().equals(newTranslator.getClass())) {
                 InventoryUtils.closeInventory(session, openInventory.getId());
-                Geyser.getGeneralThreadPool().schedule(() -> InventoryUtils.openInventory(session, newInventory), 350, TimeUnit.MILLISECONDS);
+                Geyser.getGeneralThreadPool().schedule(() -> InventoryUtils.openInventory(session, newInventory), 500, TimeUnit.MILLISECONDS);
                 return;
             }
         }
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 9443f86b4..21c7217ae 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
@@ -31,6 +31,7 @@ import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
 import org.geysermc.connector.network.translators.TranslatorsInit;
 import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
+import org.geysermc.connector.utils.InventoryUtils;
 
 import java.util.Objects;
 
@@ -43,8 +44,9 @@ public class JavaSetSlotTranslator extends PacketTranslator<ServerSetSlotPacket>
                 return;
             if (session.getCraftSlot() != 0)
                 return;
-            //bedrock client is bugged when changing the cursor. do not send slot update packet
+
             session.getInventory().setCursor(packet.getItem());
+            InventoryUtils.updateCursor(session);
             return;
         }
 
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaWindowPropertyTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaWindowPropertyTranslator.java
index c8e04d377..d7e2292e1 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaWindowPropertyTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/window/JavaWindowPropertyTranslator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2020 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
diff --git a/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java b/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java
index 60779f521..f55f28bbd 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/InventoryUtils.java
@@ -25,6 +25,11 @@
 
 package org.geysermc.connector.utils;
 
+import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
+import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
+import com.nukkitx.protocol.bedrock.data.ContainerId;
+import com.nukkitx.protocol.bedrock.data.ItemData;
+import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
 import org.geysermc.api.Geyser;
 import org.geysermc.connector.inventory.Inventory;
 import org.geysermc.connector.network.session.GeyserSession;
@@ -32,9 +37,11 @@ import org.geysermc.connector.network.translators.TranslatorsInit;
 import org.geysermc.connector.network.translators.inventory.DoubleChestInventoryTranslator;
 import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
 
+import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 
 public class InventoryUtils {
+    public static final ItemStack REFRESH_ITEM = new ItemStack(1, 127, new CompoundTag("")); //TODO: stop using this
 
     public static void openInventory(GeyserSession session, Inventory inventory) {
         InventoryTranslator translator = TranslatorsInit.getInventoryTranslators().get(inventory.getWindowType());
@@ -71,4 +78,31 @@ public class InventoryUtils {
         session.setCraftSlot(0);
         session.getInventory().setCursor(null);
     }
+
+    public static void updateCursor(GeyserSession session) {
+        InventorySlotPacket cursorPacket = new InventorySlotPacket();
+        cursorPacket.setContainerId(ContainerId.CURSOR);
+        cursorPacket.setInventorySlot(0);
+        cursorPacket.setSlot(TranslatorsInit.getItemTranslator().translateToBedrock(session.getInventory().getCursor()));
+        session.getUpstream().sendPacket(cursorPacket);
+    }
+
+    //NPE if compound tag is null
+    public static ItemStack fixStack(ItemStack stack) {
+        if (stack == null || stack.getId() == 0)
+            return null;
+        return new ItemStack(stack.getId(), stack.getAmount(), stack.getNbt() == null ? new CompoundTag("") : stack.getNbt());
+    }
+
+    public static boolean canStack(ItemStack item1, ItemStack item2) {
+        if (item1 == null || item2 == null)
+            return false;
+        return item1.getId() == item2.getId() && Objects.equals(item1.getNbt(), item2.getNbt());
+    }
+
+    public static boolean canStack(ItemData item1, ItemData item2) {
+        if (item1 == null || item2 == null)
+            return false;
+        return item1.equals(item2, false, true, true);
+    }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/utils/Toolbox.java b/connector/src/main/java/org/geysermc/connector/utils/Toolbox.java
index cc2f03b50..00bc86c29 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/Toolbox.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/Toolbox.java
@@ -25,6 +25,7 @@
 
 package org.geysermc.connector.utils;
 
+import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nukkitx.nbt.NbtUtils;
 import com.nukkitx.nbt.stream.NBTInputStream;
@@ -150,31 +151,31 @@ public class Toolbox {
 
         InputStream creativeItemStream = Toolbox.class.getClassLoader().getResourceAsStream("bedrock/creative_items.json");
         ObjectMapper creativeItemMapper = new ObjectMapper();
-        List<LinkedHashMap<String, Object>> creativeItemEntries = new ArrayList<>();
+        JsonNode creativeItemEntries;
 
         try {
-            creativeItemEntries = (ArrayList<LinkedHashMap<String, Object>>) creativeItemMapper.readValue(creativeItemStream, HashMap.class).get("items");
+            creativeItemEntries = creativeItemMapper.readTree(creativeItemStream).get("items");
         } catch (Exception e) {
-            e.printStackTrace();
+            throw new AssertionError("Unable to load creative items", e);
         }
 
         List<ItemData> creativeItems = new ArrayList<>();
-        for (Map<String, Object> map : creativeItemEntries) {
+        for (JsonNode itemNode : creativeItemEntries) {
             short damage = 0;
-            if (map.containsKey("damage")) {
-                damage = (short)(int) map.get("damage");
+            if (itemNode.has("damage")) {
+                damage = itemNode.get("damage").numberValue().shortValue();
             }
-            if (map.containsKey("nbt_b64")) {
-                byte[] bytes = Base64.getDecoder().decode((String) map.get("nbt_b64"));
+            if (itemNode.has("nbt_b64")) {
+                byte[] bytes = Base64.getDecoder().decode(itemNode.get("nbt_b64").asText());
                 ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
                 try {
                     com.nukkitx.nbt.tag.CompoundTag tag = (com.nukkitx.nbt.tag.CompoundTag) NbtUtils.createReaderLE(bais).readTag();
-                    creativeItems.add(ItemData.of((int) map.get("id"), damage, 1, tag));
+                    creativeItems.add(ItemData.of(itemNode.get("id").asInt(), damage, 1, tag));
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             } else {
-                creativeItems.add(ItemData.of((int) map.get("id"), damage, 1));
+                creativeItems.add(ItemData.of(itemNode.get("id").asInt(), damage, 1));
             }
         }