diff --git a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java
index a61c5db25..b47801cb5 100644
--- a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java
+++ b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java
@@ -25,6 +25,8 @@
 
 package org.geysermc.geyser;
 
+import javax.annotation.Nullable;
+
 public interface GeyserLogger {
 
     /**
@@ -78,6 +80,15 @@ public interface GeyserLogger {
      */
     void debug(String message);
 
+    /**
+     * Logs an object to console if debug mode is enabled
+     *
+     * @param object the object to log
+     */
+    default void debug(@Nullable Object object) {
+        debug(String.valueOf(object));
+    }
+
     /**
      * Sets if the logger should print debug messages
      *
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/Container.java b/core/src/main/java/org/geysermc/geyser/inventory/Container.java
index 073887a64..569802a5a 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/Container.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/Container.java
@@ -27,11 +27,12 @@ package org.geysermc.geyser.inventory;
 
 import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType;
 import lombok.Getter;
-import lombok.NonNull;
 import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.translator.inventory.InventoryTranslator;
 import org.jetbrains.annotations.Range;
 
+import javax.annotation.Nonnull;
+
 /**
  * Combination of {@link Inventory} and {@link PlayerInventory}
  */
@@ -66,7 +67,7 @@ public class Container extends Inventory {
     }
 
     @Override
-    public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession session) {
+    public void setItem(int slot, @Nonnull GeyserItemStack newItem, GeyserSession session) {
         if (slot < this.size) {
             super.setItem(slot, newItem, session);
         } else {
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java b/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java
index 26dc261a0..ca7e90a25 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/Inventory.java
@@ -31,7 +31,6 @@ import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
 import com.github.steveice10.opennbt.tag.builtin.Tag;
 import com.nukkitx.math.vector.Vector3i;
 import lombok.Getter;
-import lombok.NonNull;
 import lombok.Setter;
 import lombok.ToString;
 import org.geysermc.geyser.GeyserImpl;
@@ -40,11 +39,11 @@ import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.translator.inventory.item.ItemTranslator;
 import org.jetbrains.annotations.Range;
 
+import javax.annotation.Nonnull;
 import java.util.Arrays;
 
 @ToString
 public abstract class Inventory {
-
     @Getter
     protected final int id;
 
@@ -72,8 +71,7 @@ public abstract class Inventory {
     protected final ContainerType containerType;
 
     @Getter
-    @Setter
-    protected String title;
+    protected final String title;
 
     protected final GeyserItemStack[] items;
 
@@ -115,7 +113,7 @@ public abstract class Inventory {
 
     public abstract int getOffsetForHotbar(@Range(from = 0, to = 8) int slot);
 
-    public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession session) {
+    public void setItem(int slot, @Nonnull GeyserItemStack newItem, GeyserSession session) {
         if (slot > this.size) {
             session.getGeyser().getLogger().debug("Tried to set an item out of bounds! " + this);
             return;
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java
index e973beadc..e6eeea689 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java
@@ -28,7 +28,6 @@ package org.geysermc.geyser.inventory.click;
 import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
 import com.github.steveice10.mc.protocol.data.game.inventory.ContainerActionType;
 import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType;
-import com.github.steveice10.mc.protocol.data.game.inventory.MoveToHotbarAction;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
@@ -40,20 +39,22 @@ import org.geysermc.geyser.inventory.SlotType;
 import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.translator.inventory.CraftingInventoryTranslator;
 import org.geysermc.geyser.translator.inventory.InventoryTranslator;
-import org.geysermc.geyser.translator.inventory.PlayerInventoryTranslator;
 import org.geysermc.geyser.util.InventoryUtils;
 import org.jetbrains.annotations.Contract;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.ListIterator;
 
-public class ClickPlan {
+public final class ClickPlan {
     private final List<ClickAction> plan = new ArrayList<>();
     private final Int2ObjectMap<GeyserItemStack> simulatedItems;
+    /**
+     * Used for 1.17.1+ proper packet translation - any non-cursor item that is changed in a single transaction gets sent here.
+     */
+    private Int2ObjectMap<ItemStack> changedItems;
     private GeyserItemStack simulatedCursor;
-    private boolean simulating;
+    private boolean finished;
 
     private final GeyserSession session;
     private final InventoryTranslator translator;
@@ -66,21 +67,11 @@ public class ClickPlan {
         this.inventory = inventory;
 
         this.simulatedItems = new Int2ObjectOpenHashMap<>(inventory.getSize());
+        this.changedItems = null;
         this.simulatedCursor = session.getPlayerInventory().getCursor().copy();
-        this.simulating = true;
+        this.finished = false;
 
-        if (translator instanceof PlayerInventoryTranslator) {
-            gridSize = 4;
-        } else if (translator instanceof CraftingInventoryTranslator) {
-            gridSize = 9;
-        } else {
-            gridSize = -1;
-        }
-    }
-
-    private void resetSimulation() {
-        this.simulatedItems.clear();
-        this.simulatedCursor = session.getPlayerInventory().getCursor().copy();
+        gridSize = translator.getGridSize();
     }
 
     public void add(Click click, int slot) {
@@ -88,7 +79,7 @@ public class ClickPlan {
     }
 
     public void add(Click click, int slot, boolean force) {
-        if (!simulating)
+        if (finished)
             throw new UnsupportedOperationException("ClickPlan already executed");
 
         if (click == Click.LEFT_OUTSIDE || click == Click.RIGHT_OUTSIDE) {
@@ -97,12 +88,10 @@ public class ClickPlan {
 
         ClickAction action = new ClickAction(click, slot, force);
         plan.add(action);
-        simulateAction(action);
     }
 
     public void execute(boolean refresh) {
         //update geyser inventory after simulation to avoid net id desync
-        resetSimulation();
         ListIterator<ClickAction> planIter = plan.listIterator();
         while (planIter.hasNext()) {
             ClickAction action = planIter.next();
@@ -112,33 +101,48 @@ public class ClickPlan {
                 refresh = true;
             }
 
-            //int stateId = stateIdHack(action);
+            changedItems = new Int2ObjectOpenHashMap<>();
 
-            //simulateAction(action);
+            boolean emulatePost1_16Logic = session.isEmulatePost1_16Logic();
+
+            int stateId;
+            if (emulatePost1_16Logic) {
+                stateId = stateIdHack(action);
+                simulateAction(action);
+            } else {
+                stateId = inventory.getStateId();
+            }
 
             ItemStack clickedItemStack;
             if (!planIter.hasNext() && refresh) {
                 clickedItemStack = InventoryUtils.REFRESH_ITEM;
-            } else if (action.click.actionType == ContainerActionType.DROP_ITEM || action.slot == Click.OUTSIDE_SLOT) {
-                clickedItemStack = null;
             } else {
-                //// The action must be simulated first as Java expects the new contents of the cursor (as of 1.18.1)
-                //clickedItemStack = simulatedCursor.getItemStack(); TODO fix - this is the proper behavior but it terribly breaks 1.16.5
-                clickedItemStack = getItem(action.slot).getItemStack();
+                if (emulatePost1_16Logic) {
+                    // The action must be simulated first as Java expects the new contents of the cursor (as of 1.18.1)
+                    clickedItemStack = simulatedCursor.getItemStack();
+                } else {
+                    if (action.click.actionType == ContainerActionType.DROP_ITEM || action.slot == Click.OUTSIDE_SLOT) {
+                        clickedItemStack = null;
+                    } else {
+                        clickedItemStack = getItem(action.slot).getItemStack();
+                    }
+                }
+            }
+
+            if (!emulatePost1_16Logic) {
+                simulateAction(action);
             }
 
             ServerboundContainerClickPacket clickPacket = new ServerboundContainerClickPacket(
                     inventory.getId(),
-                    inventory.getStateId(),
+                    stateId,
                     action.slot,
                     action.click.actionType,
                     action.click.action,
                     clickedItemStack,
-                    Collections.emptyMap() // Anything else we change, at this time, should have a packet sent to address
+                    changedItems
             );
 
-            simulateAction(action);
-
             session.sendDownstreamPacket(clickPacket);
         }
 
@@ -146,19 +150,11 @@ public class ClickPlan {
         for (Int2ObjectMap.Entry<GeyserItemStack> simulatedSlot : simulatedItems.int2ObjectEntrySet()) {
             inventory.setItem(simulatedSlot.getIntKey(), simulatedSlot.getValue(), session);
         }
-        simulating = false;
+        finished = true;
     }
 
     public GeyserItemStack getItem(int slot) {
-        return getItem(slot, true);
-    }
-
-    public GeyserItemStack getItem(int slot, boolean generate) {
-        if (generate) {
-            return simulatedItems.computeIfAbsent(slot, k -> inventory.getItem(slot).copy());
-        } else {
-            return simulatedItems.getOrDefault(slot, inventory.getItem(slot));
-        }
+        return simulatedItems.computeIfAbsent(slot, k -> inventory.getItem(slot).copy());
     }
 
     public GeyserItemStack getCursor() {
@@ -166,23 +162,38 @@ public class ClickPlan {
     }
 
     private void setItem(int slot, GeyserItemStack item) {
-        if (simulating) {
-            simulatedItems.put(slot, item);
-        } else {
-            inventory.setItem(slot, item, session);
-        }
+        simulatedItems.put(slot, item);
+        onSlotItemChange(slot, item);
     }
 
     private void setCursor(GeyserItemStack item) {
-        if (simulating) {
-            simulatedCursor = item;
-        } else {
-            session.getPlayerInventory().setCursor(item, session);
-        }
+        simulatedCursor = item;
+    }
+
+    private void add(int slot, GeyserItemStack itemStack, int amount) {
+        itemStack.add(amount);
+        onSlotItemChange(slot, itemStack);
+    }
+
+    private void sub(int slot, GeyserItemStack itemStack, int amount) {
+        itemStack.sub(amount);
+        onSlotItemChange(slot, itemStack);
+    }
+
+    private void setAmount(int slot, GeyserItemStack itemStack, int amount) {
+        itemStack.setAmount(amount);
+        onSlotItemChange(slot, itemStack);
+    }
+
+    /**
+     * Does not need to be called for the cursor
+     */
+    private void onSlotItemChange(int slot, GeyserItemStack itemStack) {
+        changedItems.put(slot, itemStack.getItemStack());
     }
 
     private void simulateAction(ClickAction action) {
-        GeyserItemStack cursor = simulating ? getCursor() : session.getPlayerInventory().getCursor();
+        GeyserItemStack cursor = getCursor();
         switch (action.click) {
             case LEFT_OUTSIDE -> {
                 setCursor(GeyserItemStack.EMPTY);
@@ -196,7 +207,7 @@ public class ClickPlan {
             }
         }
 
-        GeyserItemStack clicked = simulating ? getItem(action.slot) : inventory.getItem(action.slot);
+        GeyserItemStack clicked = getItem(action.slot);
         if (translator.getSlotType(action.slot) == SlotType.OUTPUT) {
             switch (action.click) {
                 case LEFT, RIGHT -> {
@@ -206,6 +217,7 @@ public class ClickPlan {
                         cursor.add(clicked.getAmount());
                     }
                     reduceCraftingGrid(false);
+                    setItem(action.slot, GeyserItemStack.EMPTY); // Matches Java Edition 1.18.1
                 }
                 case LEFT_SHIFT -> reduceCraftingGrid(true);
             }
@@ -217,20 +229,20 @@ public class ClickPlan {
                         setItem(action.slot, cursor);
                     } else {
                         setCursor(GeyserItemStack.EMPTY);
-                        clicked.add(cursor.getAmount());
+                        add(action.slot, clicked, cursor.getAmount());
                     }
                     break;
                 case RIGHT:
                     if (cursor.isEmpty() && !clicked.isEmpty()) {
                         int half = clicked.getAmount() / 2; //smaller half
                         setCursor(clicked.copy(clicked.getAmount() - half)); //larger half
-                        clicked.setAmount(half);
+                        setAmount(action.slot, clicked, half);
                     } else if (!cursor.isEmpty() && clicked.isEmpty()) {
                         cursor.sub(1);
                         setItem(action.slot, cursor.copy(1));
                     } else if (InventoryUtils.canStack(cursor, clicked)) {
                         cursor.sub(1);
-                        clicked.add(1);
+                        add(action.slot, clicked, 1);
                     }
                     break;
                 case SWAP_TO_HOTBAR_1:
@@ -265,7 +277,7 @@ public class ClickPlan {
                     break;
                 case DROP_ONE:
                     if (!clicked.isEmpty()) {
-                        clicked.sub(1);
+                        sub(action.slot, clicked, 1);
                     }
                     break;
                 case DROP_ALL:
@@ -279,7 +291,7 @@ public class ClickPlan {
      * Swap between two inventory slots without a cursor. This should only be used with {@link ContainerActionType#MOVE_TO_HOTBAR_SLOT}
      */
     private void swap(int sourceSlot, int destSlot, GeyserItemStack sourceItem) {
-        GeyserItemStack destinationItem = simulating ? getItem(destSlot) : inventory.getItem(destSlot);
+        GeyserItemStack destinationItem = getItem(destSlot);
         setItem(sourceSlot, destinationItem);
         setItem(destSlot, sourceItem);
     }
@@ -292,63 +304,44 @@ public class ClickPlan {
             stateId = inventory.getStateId();
         }
 
-        // This is a hack.
-        // Java will never ever send more than one container click packet per set of actions.
+        // Java will never ever send more than one container click packet per set of actions*.
+        // *(exception being Java's "quick craft"/painting feature)
         // Bedrock might, and this would generally fall into one of two categories:
         // - Bedrock is sending an item directly from one slot to another, without picking it up, that cannot
         //   be expressed with a shift click
         // - Bedrock wants to pick up or place an arbitrary amount of items that cannot be expressed from
         //   one left/right click action.
-        // When Bedrock does one of these actions and sends multiple packets, a 1.17.1+ server will
-        // increment the state ID on each confirmation packet it sends back (I.E. set slot). Then when it
-        // reads our next packet, because we kept the same state ID but the server incremented it, it'll be
-        // desynced and send the entire inventory contents back at us.
-        // This hack therefore increments the state ID to what the server will presumably send back to us.
-        // (This won't be perfect, but should get us through most vanilla situations, and if this is wrong the
-        // server will just send a set content packet back at us)
+        // Java typically doesn't increment the state ID if you send a vanilla-accurate container click packet,
+        // but it will increment the state ID with a vanilla client in at least the crafting table
         if (inventory.getContainerType() == ContainerType.CRAFTING && CraftingInventoryTranslator.isCraftingGrid(action.slot)) {
             // 1.18.1 sends a second set slot update for any action in the crafting grid
             // And an additional packet if something is removed (Mojmap: CraftingContainer#removeItem)
-            //TODO this code kind of really sucks; it's potentially possible to see what Bedrock sends us and send a PlaceRecipePacket
             int stateIdIncrements;
             GeyserItemStack clicked = getItem(action.slot);
             if (action.click == Click.LEFT) {
                 if (!clicked.isEmpty() && !InventoryUtils.canStack(simulatedCursor, clicked)) {
                     // An item is removed from the crafting table; yes deletion
-                    stateIdIncrements = 3;
+                    stateIdIncrements = 2;
                 } else {
                     // We can stack and we add all the items to the crafting slot; no deletion
-                    stateIdIncrements = 2;
+                    stateIdIncrements = 1;
                 }
             } else if (action.click == Click.RIGHT) {
-                if (simulatedCursor.isEmpty() && !clicked.isEmpty()) {
-                    // Items are taken; yes deletion
-                    stateIdIncrements = 3;
-                } else if ((!simulatedCursor.isEmpty() && clicked.isEmpty()) || InventoryUtils.canStack(simulatedCursor, clicked)) {
-                    // Adding our cursor item to the slot; no deletion
-                    stateIdIncrements = 2;
-                } else {
-                    // ?? nothing I guess
-                    stateIdIncrements = 2;
-                }
+                stateIdIncrements = 1;
+            } else if (action.click.actionType == ContainerActionType.MOVE_TO_HOTBAR_SLOT) {
+                stateIdIncrements = 1;
             } else {
                 if (session.getGeyser().getConfig().isDebugMode()) {
                     session.getGeyser().getLogger().debug("Not sure how to handle state ID hack in crafting table: " + plan);
                 }
-                stateIdIncrements = 2;
+                stateIdIncrements = 1;
             }
             inventory.incrementStateId(stateIdIncrements);
-        } else if (action.click.action instanceof MoveToHotbarAction) {
-            // Two slot changes sent
-            inventory.incrementStateId(2);
-        } else {
-            inventory.incrementStateId(1);
         }
 
         return stateId;
     }
 
-    //TODO
     private void reduceCraftingGrid(boolean makeAll) {
         if (gridSize == -1)
             return;
@@ -370,9 +363,12 @@ public class ClickPlan {
         }
 
         for (int i = 0; i < gridSize; i++) {
-            GeyserItemStack item = getItem(i + 1);
-            if (!item.isEmpty())
-                item.sub(crafted);
+            final int slot = i + 1;
+            GeyserItemStack item = getItem(slot);
+            if (!item.isEmpty()) {
+                // These changes should be broadcasted to the server
+                sub(slot, item, crafted);
+            }
         }
     }
 
diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
index 99c8c5cc4..3a097f732 100644
--- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
+++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
@@ -361,6 +361,15 @@ public class GeyserSession implements GeyserConnection, CommandSender {
     @Setter
     private Int2ObjectMap<IntList> stonecutterRecipes;
 
+    /**
+     * Starting in 1.17, Java servers expect the <code>carriedItem</code> parameter of the serverbound click container
+     * packet to be the current contents of the mouse after the transaction has been done. 1.16 expects the clicked slot
+     * contents before any transaction is done. With the current ViaVersion structure, if we do not send what 1.16 expects
+     * and send multiple click container packets, then successive transactions will be rejected.
+     */
+    @Setter
+    private boolean emulatePost1_16Logic = true;
+
     /**
      * The current attack speed of the player. Used for sending proper cooldown timings.
      * Setting a default fixes cooldowns not showing up on a fresh world.
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java
index 19d9d6de5..f194d0d3f 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java
@@ -38,17 +38,16 @@ import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequ
 import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionType;
 import com.nukkitx.protocol.bedrock.packet.BlockEntityDataPacket;
 import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket;
+import it.unimi.dsi.fastutil.ints.IntSets;
 import org.geysermc.geyser.inventory.BeaconContainer;
+import org.geysermc.geyser.inventory.BedrockContainerSlot;
 import org.geysermc.geyser.inventory.Inventory;
 import org.geysermc.geyser.inventory.PlayerInventory;
-import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.inventory.BedrockContainerSlot;
 import org.geysermc.geyser.inventory.holder.BlockInventoryHolder;
 import org.geysermc.geyser.inventory.updater.UIInventoryUpdater;
+import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.util.InventoryUtils;
 
-import java.util.Collections;
-
 public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator {
     public BeaconInventoryTranslator() {
         super(1, new BlockInventoryHolder("minecraft:beacon", com.nukkitx.protocol.bedrock.data.inventory.ContainerType.BEACON) {
@@ -114,7 +113,7 @@ public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator
         BeaconPaymentStackRequestActionData beaconPayment = (BeaconPaymentStackRequestActionData) request.getActions()[0];
         ServerboundSetBeaconPacket packet = new ServerboundSetBeaconPacket(beaconPayment.getPrimaryEffect(), beaconPayment.getSecondaryEffect());
         session.sendDownstreamPacket(packet);
-        return acceptRequest(request, makeContainerEntries(session, inventory, Collections.emptySet()));
+        return acceptRequest(request, makeContainerEntries(session, inventory, IntSets.emptySet()));
     }
 
     @Override
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/CraftingInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/CraftingInventoryTranslator.java
index ec3335f3c..61e2258b6 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/CraftingInventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/CraftingInventoryTranslator.java
@@ -37,6 +37,11 @@ public class CraftingInventoryTranslator extends AbstractBlockInventoryTranslato
         super(10, "minecraft:crafting_table", ContainerType.WORKBENCH, UIInventoryUpdater.INSTANCE);
     }
 
+    @Override
+    public int getGridSize() {
+        return 9;
+    }
+
     @Override
     public SlotType getSlotType(int javaSlot) {
         if (javaSlot == 0) {
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/EnchantingInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/EnchantingInventoryTranslator.java
index 97ece79d8..800b35901 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/EnchantingInventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/EnchantingInventoryTranslator.java
@@ -27,23 +27,22 @@ package org.geysermc.geyser.translator.inventory;
 
 import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerButtonClickPacket;
-import com.nukkitx.protocol.bedrock.data.inventory.*;
+import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType;
+import com.nukkitx.protocol.bedrock.data.inventory.EnchantOptionData;
+import com.nukkitx.protocol.bedrock.data.inventory.ItemStackRequest;
+import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData;
 import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.CraftRecipeStackRequestActionData;
 import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionData;
 import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.StackRequestActionType;
 import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket;
 import com.nukkitx.protocol.bedrock.packet.PlayerEnchantOptionsPacket;
-import org.geysermc.geyser.inventory.EnchantingContainer;
-import org.geysermc.geyser.inventory.GeyserEnchantOption;
-import org.geysermc.geyser.inventory.Inventory;
-import org.geysermc.geyser.inventory.PlayerInventory;
-import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.inventory.BedrockContainerSlot;
-import org.geysermc.geyser.inventory.updater.UIInventoryUpdater;
+import it.unimi.dsi.fastutil.ints.IntSets;
+import org.geysermc.geyser.inventory.*;
 import org.geysermc.geyser.inventory.item.Enchantment;
+import org.geysermc.geyser.inventory.updater.UIInventoryUpdater;
+import org.geysermc.geyser.session.GeyserSession;
 
 import java.util.Arrays;
-import java.util.Collections;
 
 public class EnchantingInventoryTranslator extends AbstractBlockInventoryTranslator {
     public EnchantingInventoryTranslator() {
@@ -130,7 +129,7 @@ public class EnchantingInventoryTranslator extends AbstractBlockInventoryTransla
         }
         ServerboundContainerButtonClickPacket packet = new ServerboundContainerButtonClickPacket(inventory.getId(), javaSlot);
         session.sendDownstreamPacket(packet);
-        return acceptRequest(request, makeContainerEntries(session, inventory, Collections.emptySet()));
+        return acceptRequest(request, makeContainerEntries(session, inventory, IntSets.emptySet()));
     }
 
     @Override
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java
index e0b90db02..e6a9faf74 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java
@@ -26,12 +26,11 @@
 package org.geysermc.geyser.translator.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.data.game.inventory.ContainerType;
 import com.github.steveice10.mc.protocol.data.game.recipe.Ingredient;
 import com.github.steveice10.mc.protocol.data.game.recipe.Recipe;
 import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapedRecipeData;
 import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData;
-import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType;
 import com.github.steveice10.opennbt.tag.builtin.IntTag;
 import com.github.steveice10.opennbt.tag.builtin.Tag;
 import com.nukkitx.protocol.bedrock.data.inventory.ContainerSlotType;
@@ -43,15 +42,10 @@ import it.unimi.dsi.fastutil.ints.*;
 import lombok.AllArgsConstructor;
 import org.checkerframework.checker.nullness.qual.Nullable;
 import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.inventory.CartographyContainer;
-import org.geysermc.geyser.inventory.GeyserItemStack;
-import org.geysermc.geyser.inventory.Inventory;
-import org.geysermc.geyser.inventory.PlayerInventory;
-import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.inventory.BedrockContainerSlot;
-import org.geysermc.geyser.inventory.SlotType;
+import org.geysermc.geyser.inventory.*;
 import org.geysermc.geyser.inventory.click.Click;
 import org.geysermc.geyser.inventory.click.ClickPlan;
+import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.translator.inventory.chest.DoubleChestInventoryTranslator;
 import org.geysermc.geyser.translator.inventory.chest.SingleChestInventoryTranslator;
 import org.geysermc.geyser.translator.inventory.furnace.BlastFurnaceInventoryTranslator;
@@ -119,6 +113,13 @@ public abstract class InventoryTranslator {
     public abstract SlotType getSlotType(int javaSlot);
     public abstract Inventory createInventory(String name, int windowId, ContainerType containerType, PlayerInventory playerInventory);
 
+    /**
+     * Used for crafting-related transactions. Will override in PlayerInventoryTranslator and CraftingInventoryTranslator.
+     */
+    public int getGridSize() {
+        return -1;
+    }
+
     /**
      * Should be overwritten in cases where specific inventories should reject an item being in a specific spot.
      * For examples, looms use this to reject items that are dyes in Bedrock but not in Java.
@@ -147,7 +148,7 @@ public abstract class InventoryTranslator {
         return rejectRequest(request);
     }
 
-    public void translateRequests(GeyserSession session, Inventory inventory, List<ItemStackRequest> requests) {
+    public final void translateRequests(GeyserSession session, Inventory inventory, List<ItemStackRequest> requests) {
         boolean refresh = false;
         ItemStackResponsePacket responsePacket = new ItemStackResponsePacket();
         for (ItemStackRequest request : requests) {
@@ -199,10 +200,6 @@ public abstract class InventoryTranslator {
                 case PLACE: {
                     TransferStackRequestActionData transferAction = (TransferStackRequestActionData) action;
                     if (!(checkNetId(session, inventory, transferAction.getSource()) && checkNetId(session, inventory, transferAction.getDestination()))) {
-                        if (session.getGameMode().equals(GameMode.CREATIVE) && transferAction.getSource().getContainer() == ContainerSlotType.CRAFTING_INPUT &&
-                                transferAction.getSource().getSlot() >= 28 && transferAction.getSource().getSlot() <= 31) {
-                            return rejectRequest(request, false);
-                        }
                         if (session.getGeyser().getConfig().isDebugMode()) {
                             session.getGeyser().getLogger().error("DEBUG: About to reject TAKE/PLACE request made by " + session.name());
                             dumpStackRequestDetails(session, inventory, transferAction.getSource(), transferAction.getDestination());
@@ -212,17 +209,19 @@ public abstract class InventoryTranslator {
 
                     int sourceSlot = bedrockSlotToJava(transferAction.getSource());
                     int destSlot = bedrockSlotToJava(transferAction.getDestination());
+                    boolean isSourceCursor = isCursor(transferAction.getSource());
+                    boolean isDestCursor = isCursor(transferAction.getDestination());
 
                     if (shouldRejectItemPlace(session, inventory, transferAction.getSource().getContainer(),
-                            isCursor(transferAction.getSource()) ? -1 : sourceSlot,
-                            transferAction.getDestination().getContainer(), isCursor(transferAction.getDestination()) ? -1 : destSlot)) {
+                            isSourceCursor ? -1 : sourceSlot,
+                            transferAction.getDestination().getContainer(), isDestCursor ? -1 : destSlot)) {
                         // This item would not be here in Java
                         return rejectRequest(request, false);
                     }
 
-                    if (isCursor(transferAction.getSource()) && isCursor(transferAction.getDestination())) { //???
+                    if (isSourceCursor && isDestCursor) { //???
                         return rejectRequest(request);
-                    } else if (isCursor(transferAction.getSource())) { //releasing cursor
+                    } else if (isSourceCursor) { //releasing cursor
                         int sourceAmount = cursor.getAmount();
                         if (transferAction.getCount() == sourceAmount) { //release all
                             plan.add(Click.LEFT, destSlot);
@@ -231,7 +230,7 @@ public abstract class InventoryTranslator {
                                 plan.add(Click.RIGHT, destSlot);
                             }
                         }
-                    } else if (isCursor(transferAction.getDestination())) { //picking up into cursor
+                    } else if (isDestCursor) { //picking up into cursor
                         GeyserItemStack sourceItem = plan.getItem(sourceSlot);
                         int sourceAmount = sourceItem.getAmount();
                         if (cursor.isEmpty()) { //picking up into empty cursor
@@ -431,6 +430,8 @@ public abstract class InventoryTranslator {
 
         int leftover = 0;
         ClickPlan plan = new ClickPlan(session, this, inventory);
+        // Track all the crafting table slots to report back the contents of the slots after crafting
+        IntSet affectedSlots = new IntOpenHashSet();
         for (StackRequestActionData action : request.getActions()) {
             switch (action.getType()) {
                 case CRAFT_RECIPE: {
@@ -462,6 +463,7 @@ public abstract class InventoryTranslator {
                         return rejectRequest(request);
                     }
                     craftState = CraftState.INGREDIENTS;
+                    affectedSlots.add(bedrockSlotToJava(((ConsumeStackRequestActionData) action).getSource()));
                     break;
                 }
                 case TAKE:
@@ -522,21 +524,16 @@ public abstract class InventoryTranslator {
             }
         }
         plan.execute(false);
-        return acceptRequest(request, makeContainerEntries(session, inventory, plan.getAffectedSlots()));
+        affectedSlots.addAll(plan.getAffectedSlots());
+        return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots));
     }
 
     public ItemStackResponsePacket.Response translateAutoCraftingRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
-        int gridSize;
-        int gridDimensions;
-        if (this instanceof PlayerInventoryTranslator) {
-            gridSize = 4;
-            gridDimensions = 2;
-        } else if (this instanceof CraftingInventoryTranslator) {
-            gridSize = 9;
-            gridDimensions = 3;
-        } else {
+        final int gridSize = getGridSize();
+        if (gridSize == -1) {
             return rejectRequest(request);
         }
+        int gridDimensions = gridSize == 4 ? 2 : 3;
 
         Recipe recipe;
         Ingredient[] ingredients = new Ingredient[0];
@@ -722,7 +719,7 @@ public abstract class InventoryTranslator {
     /**
      * Handled in {@link PlayerInventoryTranslator}
      */
-    public ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
+    protected ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
         return rejectRequest(request);
     }
 
@@ -757,14 +754,14 @@ public abstract class InventoryTranslator {
         }
     }
 
-    public static ItemStackResponsePacket.Response acceptRequest(ItemStackRequest request, List<ItemStackResponsePacket.ContainerEntry> containerEntries) {
+    protected static ItemStackResponsePacket.Response acceptRequest(ItemStackRequest request, List<ItemStackResponsePacket.ContainerEntry> containerEntries) {
         return new ItemStackResponsePacket.Response(ItemStackResponsePacket.ResponseStatus.OK, request.getRequestId(), containerEntries);
     }
 
     /**
      * Reject an incorrect ItemStackRequest.
      */
-    public static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request) {
+    protected static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request) {
         return rejectRequest(request, true);
     }
 
@@ -774,7 +771,7 @@ public abstract class InventoryTranslator {
      * @param throwError whether this request was truly erroneous (true), or known as an outcome and should not be treated
      *                   as bad (false).
      */
-    public static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request, boolean throwError) {
+    protected static ItemStackResponsePacket.Response rejectRequest(ItemStackRequest request, boolean throwError) {
         if (throwError && GeyserImpl.getInstance().getConfig().isDebugMode()) {
             new Throwable("DEBUGGING: ItemStackRequest rejected " + request.toString()).printStackTrace();
         }
@@ -849,9 +846,12 @@ public abstract class InventoryTranslator {
         return -1;
     }
 
-    public List<ItemStackResponsePacket.ContainerEntry> makeContainerEntries(GeyserSession session, Inventory inventory, Set<Integer> affectedSlots) {
+    protected final List<ItemStackResponsePacket.ContainerEntry> makeContainerEntries(GeyserSession session, Inventory inventory, IntSet affectedSlots) {
         Map<ContainerSlotType, List<ItemStackResponsePacket.ItemEntry>> containerMap = new HashMap<>();
-        for (int slot : affectedSlots) {
+        // Manually call iterator to prevent Integer boxing
+        IntIterator it = affectedSlots.iterator();
+        while (it.hasNext()) {
+            int slot = it.nextInt();
             BedrockContainerSlot bedrockSlot = javaSlotToBedrockContainer(slot);
             List<ItemStackResponsePacket.ItemEntry> list = containerMap.computeIfAbsent(bedrockSlot.container(), k -> new ArrayList<>());
             list.add(makeItemEntry(session, bedrockSlot.slot(), inventory.getItem(slot)));
@@ -868,7 +868,7 @@ public abstract class InventoryTranslator {
         return containerEntries;
     }
 
-    public static ItemStackResponsePacket.ItemEntry makeItemEntry(GeyserSession session, int bedrockSlot, GeyserItemStack itemStack) {
+    private static ItemStackResponsePacket.ItemEntry makeItemEntry(GeyserSession session, int bedrockSlot, GeyserItemStack itemStack) {
         ItemStackResponsePacket.ItemEntry itemEntry;
         if (!itemStack.isEmpty()) {
             // As of 1.16.210: Bedrock needs confirmation on what the current item durability is.
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java
index 04de68a1e..e2349e5a5 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java
@@ -35,6 +35,7 @@ import com.nukkitx.protocol.bedrock.data.inventory.stackrequestactions.*;
 import com.nukkitx.protocol.bedrock.packet.InventoryContentPacket;
 import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
 import com.nukkitx.protocol.bedrock.packet.ItemStackResponsePacket;
+import it.unimi.dsi.fastutil.ints.IntIterator;
 import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
 import it.unimi.dsi.fastutil.ints.IntSet;
 import org.geysermc.geyser.inventory.*;
@@ -55,6 +56,11 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
         super(46);
     }
 
+    @Override
+    public int getGridSize() {
+        return 4;
+    }
+
     @Override
     public void updateInventory(GeyserSession session, Inventory inventory) {
         updateCraftingGrid(session, inventory);
@@ -370,14 +376,17 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
                 }
             }
         }
-        for (int slot : affectedSlots) {
+        // Manually call iterator to prevent Integer boxing
+        IntIterator it = affectedSlots.iterator();
+        while (it.hasNext()) {
+            int slot = it.nextInt();
             sendCreativeAction(session, inventory, slot);
         }
         return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots));
     }
 
     @Override
-    public ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
+    protected ItemStackResponsePacket.Response translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
         ItemStack javaCreativeItem = null;
         IntSet affectedSlots = new IntOpenHashSet();
         CraftState craftState = CraftState.START;
@@ -478,7 +487,10 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
                     return rejectRequest(request);
             }
         }
-        for (int slot : affectedSlots) {
+        // Manually call iterator to prevent Integer boxing
+        IntIterator it = affectedSlots.iterator();
+        while (it.hasNext()) {
+            int slot = it.nextInt();
             sendCreativeAction(session, inventory, slot);
         }
         return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots));
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java
index be10452f4..869062da2 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java
@@ -25,6 +25,7 @@
 
 package org.geysermc.geyser.translator.protocol.bedrock;
 
+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.object.Direction;
 import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
@@ -41,6 +42,8 @@ import com.nukkitx.math.vector.Vector3i;
 import com.nukkitx.protocol.bedrock.data.LevelEventType;
 import com.nukkitx.protocol.bedrock.data.inventory.*;
 import com.nukkitx.protocol.bedrock.packet.*;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
 import org.geysermc.geyser.entity.EntityDefinitions;
 import org.geysermc.geyser.entity.type.CommandBlockMinecartEntity;
 import org.geysermc.geyser.entity.type.Entity;
@@ -59,7 +62,6 @@ import org.geysermc.geyser.translator.sound.EntitySoundInteractionTranslator;
 import org.geysermc.geyser.util.BlockUtils;
 import org.geysermc.geyser.util.InventoryUtils;
 
-import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
@@ -316,9 +318,13 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                                         playerInventory.setItem(armorSlot, hotbarItem, session);
                                         playerInventory.setItem(bedrockHotbarSlot, armorSlotItem, session);
 
+                                        Int2ObjectMap<ItemStack> changedSlots = new Int2ObjectOpenHashMap<>(2);
+                                        changedSlots.put(armorSlot, hotbarItem.getItemStack());
+                                        changedSlots.put(bedrockHotbarSlot, armorSlotItem.getItemStack());
+
                                         ServerboundContainerClickPacket clickPacket = new ServerboundContainerClickPacket(
                                                 playerInventory.getId(), playerInventory.getStateId(), armorSlot,
-                                                click.actionType, click.action, null, Collections.emptyMap());
+                                                click.actionType, click.action, null, changedSlots);
                                         session.sendDownstreamPacket(clickPacket);
                                     }
                                 } else {
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeTranslator.java
index b3a04e163..da35da60e 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeTranslator.java
@@ -31,7 +31,7 @@ import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.translator.protocol.PacketTranslator;
 import org.geysermc.geyser.translator.protocol.Translator;
 
-import java.util.Arrays;
+import java.util.Collections;
 
 /**
  * Used to list recipes that we can definitely use the recipe book for (and therefore save on packet usage)
@@ -42,9 +42,11 @@ public class JavaRecipeTranslator extends PacketTranslator<ClientboundRecipePack
     @Override
     public void translate(GeyserSession session, ClientboundRecipePacket packet) {
         if (packet.getAction() == UnlockRecipesAction.REMOVE) {
-            session.getUnlockedRecipes().removeAll(Arrays.asList(packet.getRecipes()));
+            for (String identifier : packet.getRecipes()) {
+                session.getUnlockedRecipes().remove(identifier);
+            }
         } else {
-            session.getUnlockedRecipes().addAll(Arrays.asList(packet.getRecipes()));
+            Collections.addAll(session.getUnlockedRecipes(), packet.getRecipes());
         }
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetContentTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetContentTranslator.java
index 0670090d4..f905cd661 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetContentTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetContentTranslator.java
@@ -26,6 +26,7 @@
 package org.geysermc.geyser.translator.protocol.java.inventory;
 
 import com.github.steveice10.mc.protocol.packet.ingame.clientbound.inventory.ClientboundContainerSetContentPacket;
+import org.geysermc.geyser.GeyserImpl;
 import org.geysermc.geyser.inventory.GeyserItemStack;
 import org.geysermc.geyser.inventory.Inventory;
 import org.geysermc.geyser.session.GeyserSession;
@@ -43,9 +44,26 @@ public class JavaContainerSetContentTranslator extends PacketTranslator<Clientbo
         if (inventory == null)
             return;
 
-        inventory.setStateId(packet.getStateId());
-
+        int inventorySize = inventory.getSize();
         for (int i = 0; i < packet.getItems().length; i++) {
+            if (i > inventorySize) {
+                GeyserImpl geyser = session.getGeyser();
+                geyser.getLogger().warning("ClientboundContainerSetContentPacket sent to " + session.name()
+                        + " that exceeds inventory size!");
+                if (geyser.getConfig().isDebugMode()) {
+                    geyser.getLogger().debug(packet);
+                    geyser.getLogger().debug(inventory);
+                }
+                InventoryTranslator translator = session.getInventoryTranslator();
+                if (translator != null) {
+                    translator.updateInventory(session, inventory);
+                }
+                // 1.18.1 behavior: the previous items will be correctly set, but the state ID and carried item will not
+                // as this produces a stack trace on the client.
+                // If Java processes this correctly in the future, we can revert this behavior
+                return;
+            }
+
             GeyserItemStack newItem = GeyserItemStack.from(packet.getItems()[i]);
             inventory.setItem(i, newItem, session);
         }
@@ -55,6 +73,10 @@ public class JavaContainerSetContentTranslator extends PacketTranslator<Clientbo
             translator.updateInventory(session, inventory);
         }
 
+        int stateId = packet.getStateId();
+        session.setEmulatePost1_16Logic(stateId > 0 || stateId != inventory.getStateId());
+        inventory.setStateId(stateId);
+
         session.getPlayerInventory().setCursor(GeyserItemStack.from(packet.getCarriedItem()), session);
         InventoryUtils.updateCursor(session);
     }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java
index 283d95fc4..4bb2a8e60 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java
@@ -30,7 +30,6 @@ import com.github.steveice10.mc.protocol.data.game.recipe.Ingredient;
 import com.github.steveice10.mc.protocol.data.game.recipe.Recipe;
 import com.github.steveice10.mc.protocol.data.game.recipe.RecipeType;
 import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapedRecipeData;
-import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData;
 import com.github.steveice10.mc.protocol.packet.ingame.clientbound.inventory.ClientboundContainerSetSlotPacket;
 import com.nukkitx.protocol.bedrock.data.inventory.ContainerId;
 import com.nukkitx.protocol.bedrock.data.inventory.CraftingData;
@@ -40,17 +39,15 @@ import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
 import org.geysermc.geyser.inventory.GeyserItemStack;
 import org.geysermc.geyser.inventory.Inventory;
 import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.translator.protocol.PacketTranslator;
-import org.geysermc.geyser.translator.protocol.Translator;
 import org.geysermc.geyser.translator.inventory.InventoryTranslator;
-import org.geysermc.geyser.translator.inventory.CraftingInventoryTranslator;
 import org.geysermc.geyser.translator.inventory.PlayerInventoryTranslator;
 import org.geysermc.geyser.translator.inventory.item.ItemTranslator;
+import org.geysermc.geyser.translator.protocol.PacketTranslator;
+import org.geysermc.geyser.translator.protocol.Translator;
 import org.geysermc.geyser.util.InventoryUtils;
 
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.Objects;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
@@ -72,14 +69,16 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
             return;
 
         // Intentional behavior here below the cursor; Minecraft 1.18.1 also does this.
-        inventory.setStateId(packet.getStateId());
+        int stateId = packet.getStateId();
+        session.setEmulatePost1_16Logic(stateId > 0 || stateId != inventory.getStateId());
+        inventory.setStateId(stateId);
 
         InventoryTranslator translator = session.getInventoryTranslator();
         if (translator != null) {
             if (session.getCraftingGridFuture() != null) {
                 session.getCraftingGridFuture().cancel(false);
             }
-            session.setCraftingGridFuture(session.scheduleInEventLoop(() -> updateCraftingGrid(session, packet, inventory, translator), 150, TimeUnit.MILLISECONDS));
+            updateCraftingGrid(session, packet.getSlot(), packet.getItem(), inventory, translator);
 
             GeyserItemStack newItem = GeyserItemStack.from(packet.getItem());
             if (packet.getContainerId() == 0 && !(translator instanceof PlayerInventoryTranslator)) {
@@ -93,21 +92,23 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
         }
     }
 
-    private static void updateCraftingGrid(GeyserSession session, ClientboundContainerSetSlotPacket packet, Inventory inventory, InventoryTranslator translator) {
-        if (packet.getSlot() == 0) {
-            int gridSize;
-            if (translator instanceof PlayerInventoryTranslator) {
-                gridSize = 4;
-            } else if (translator instanceof CraftingInventoryTranslator) {
-                gridSize = 9;
-            } else {
-                return;
-            }
+    /**
+     * Checks for a changed output slot in the crafting grid, and ensures Bedrock sees the recipe.
+     */
+    private static void updateCraftingGrid(GeyserSession session, int slot, ItemStack item, Inventory inventory, InventoryTranslator translator) {
+        if (slot != 0) {
+            return;
+        }
+        int gridSize = translator.getGridSize();
+        if (gridSize == -1) {
+            return;
+        }
 
-            if (packet.getItem() == null || packet.getItem().getId() == 0) {
-                return;
-            }
+        if (item == null || item.getId() == 0) {
+            return;
+        }
 
+        session.setCraftingGridFuture(session.scheduleInEventLoop(() -> {
             int offset = gridSize == 4 ? 28 : 32;
             int gridDimensions = gridSize == 4 ? 2 : 3;
             int firstRow = -1, height = -1;
@@ -135,62 +136,10 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
             height += -firstRow + 1;
             width += -firstCol + 1;
 
-            recipes:
-            for (Recipe recipe : session.getCraftingRecipes().values()) {
-                if (recipe.getType() == RecipeType.CRAFTING_SHAPED) {
-                    ShapedRecipeData data = (ShapedRecipeData) recipe.getData();
-                    if (!data.getResult().equals(packet.getItem())) {
-                        continue;
-                    }
-                    if (data.getWidth() != width || data.getHeight() != height || width * height != data.getIngredients().length) {
-                        continue;
-                    }
-
-                    Ingredient[] ingredients = data.getIngredients();
-                    if (!testShapedRecipe(ingredients, inventory, gridDimensions, firstRow, height, firstCol, width)) {
-                        Ingredient[] mirroredIngredients = new Ingredient[data.getIngredients().length];
-                        for (int row = 0; row < height; row++) {
-                            for (int col = 0; col < width; col++) {
-                                mirroredIngredients[col + (row * width)] = ingredients[(width - 1 - col) + (row * width)];
-                            }
-                        }
-
-                        if (Arrays.equals(ingredients, mirroredIngredients) ||
-                                !testShapedRecipe(mirroredIngredients, inventory, gridDimensions, firstRow, height, firstCol, width)) {
-                            continue;
-                        }
-                    }
-                    // Recipe is had, don't sent packet
-                    return;
-                } else if (recipe.getType() == RecipeType.CRAFTING_SHAPELESS) {
-                    ShapelessRecipeData data = (ShapelessRecipeData) recipe.getData();
-                    if (!data.getResult().equals(packet.getItem())) {
-                        continue;
-                    }
-                    for (int i = 0; i < data.getIngredients().length; i++) {
-                        Ingredient ingredient = data.getIngredients()[i];
-                        for (ItemStack itemStack : ingredient.getOptions()) {
-                            boolean inventoryHasItem = false;
-                            for (int j = 0; j < inventory.getSize(); j++) {
-                                GeyserItemStack geyserItemStack = inventory.getItem(j);
-                                if (geyserItemStack.isEmpty()) {
-                                    inventoryHasItem = itemStack == null || itemStack.getId() == 0;
-                                    if (inventoryHasItem) {
-                                        break;
-                                    }
-                                } else if (itemStack.equals(geyserItemStack.getItemStack(1))) {
-                                    inventoryHasItem = true;
-                                    break;
-                                }
-                            }
-                            if (!inventoryHasItem) {
-                                continue recipes;
-                            }
-                        }
-                    }
-                    // Recipe is had, don't sent packet
-                    return;
-                }
+            if (InventoryUtils.getValidRecipe(session, item, inventory::getItem, gridDimensions, firstRow,
+                    height, firstCol, width) != null) {
+                // Recipe is already present on the client; don't send packet
+                return;
             }
 
             UUID uuid = UUID.randomUUID();
@@ -216,7 +165,7 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
                 }
             }
 
-            ShapedRecipeData data = new ShapedRecipeData(width, height, "", javaIngredients, packet.getItem());
+            ShapedRecipeData data = new ShapedRecipeData(width, height, "", javaIngredients, item);
             // Cache this recipe so we know the client has received it
             session.getCraftingRecipes().put(newRecipeId, new Recipe(RecipeType.CRAFTING_SHAPED, uuid.toString(), data));
 
@@ -226,7 +175,7 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
                     width,
                     height,
                     Arrays.asList(ingredients),
-                    Collections.singletonList(ItemTranslator.translateToBedrock(session, packet.getItem())),
+                    Collections.singletonList(ItemTranslator.translateToBedrock(session, item)),
                     uuid,
                     "crafting_table",
                     0,
@@ -246,33 +195,6 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
                     index++;
                 }
             }
-        }
-    }
-
-    private static boolean testShapedRecipe(Ingredient[] ingredients, Inventory inventory, int gridDimensions, int firstRow, int height, int firstCol, int width) {
-        int ingredientIndex = 0;
-        for (int row = firstRow; row < height + firstRow; row++) {
-            for (int col = firstCol; col < width + firstCol; col++) {
-                GeyserItemStack geyserItemStack = inventory.getItem(col + (row * gridDimensions) + 1);
-                Ingredient ingredient = ingredients[ingredientIndex++];
-                if (ingredient.getOptions().length == 0) {
-                    if (!geyserItemStack.isEmpty()) {
-                        return false;
-                    }
-                } else {
-                    boolean inventoryHasItem = false;
-                    for (ItemStack item : ingredient.getOptions()) {
-                        if (Objects.equals(geyserItemStack.getItemStack(1), item)) {
-                            inventoryHasItem = true;
-                            break;
-                        }
-                    }
-                    if (!inventoryHasItem) {
-                        return false;
-                    }
-                }
-            }
-        }
-        return true;
+        }, 150, TimeUnit.MILLISECONDS));
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java b/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java
index 72f20797e..4210ee6f8 100644
--- a/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java
+++ b/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java
@@ -27,6 +27,11 @@ package org.geysermc.geyser.util;
 
 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.data.game.recipe.Ingredient;
+import com.github.steveice10.mc.protocol.data.game.recipe.Recipe;
+import com.github.steveice10.mc.protocol.data.game.recipe.RecipeType;
+import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapedRecipeData;
+import com.github.steveice10.mc.protocol.data.game.recipe.data.ShapelessRecipeData;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundPickItemPacket;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundSetCreativeModeSlotPacket;
 import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
@@ -52,6 +57,7 @@ import org.geysermc.geyser.registry.Registries;
 import org.geysermc.geyser.registry.type.ItemMapping;
 
 import javax.annotation.Nullable;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Objects;
 import java.util.concurrent.TimeUnit;
@@ -348,4 +354,115 @@ public class InventoryUtils {
             default -> null;
         };
     }
+
+    /**
+     * Test all known recipes to find a valid match
+     *
+     * @param output if not null, the recipe has to output this item
+     */
+    @Nullable
+    public static Recipe getValidRecipe(final GeyserSession session, final @Nullable ItemStack output, final IntFunction<GeyserItemStack> inventoryGetter,
+                                        final int gridDimensions, final int firstRow, final int height, final int firstCol, final int width) {
+        int nonAirCount = 0; // Used for shapeless recipes for amount of items needed in recipe
+        for (int row = firstRow; row < height + firstRow; row++) {
+            for (int col = firstCol; col < width + firstCol; col++) {
+                if (!inventoryGetter.apply(col + (row * gridDimensions) + 1).isEmpty()) {
+                    nonAirCount++;
+                }
+            }
+        }
+
+        recipes:
+        for (Recipe recipe : session.getCraftingRecipes().values()) {
+            if (recipe.getType() == RecipeType.CRAFTING_SHAPED) {
+                ShapedRecipeData data = (ShapedRecipeData) recipe.getData();
+                if (output != null && !data.getResult().equals(output)) {
+                    continue;
+                }
+                Ingredient[] ingredients = data.getIngredients();
+                if (data.getWidth() != width || data.getHeight() != height || width * height != ingredients.length) {
+                    continue;
+                }
+
+                if (!testShapedRecipe(ingredients, inventoryGetter, gridDimensions, firstRow, height, firstCol, width)) {
+                    Ingredient[] mirroredIngredients = new Ingredient[ingredients.length];
+                    for (int row = 0; row < height; row++) {
+                        for (int col = 0; col < width; col++) {
+                            mirroredIngredients[col + (row * width)] = ingredients[(width - 1 - col) + (row * width)];
+                        }
+                    }
+
+                    if (Arrays.equals(ingredients, mirroredIngredients) ||
+                            !testShapedRecipe(mirroredIngredients, inventoryGetter, gridDimensions, firstRow, height, firstCol, width)) {
+                        continue;
+                    }
+                }
+                return recipe;
+            } else if (recipe.getType() == RecipeType.CRAFTING_SHAPELESS) {
+                ShapelessRecipeData data = (ShapelessRecipeData) recipe.getData();
+                if (output != null && !data.getResult().equals(output)) {
+                    continue;
+                }
+                if (nonAirCount != data.getIngredients().length) {
+                    // There is an amount of items on the crafting table that is not the same as the ingredient count so this is invalid
+                    continue;
+                }
+                for (int i = 0; i < data.getIngredients().length; i++) {
+                    Ingredient ingredient = data.getIngredients()[i];
+                    for (ItemStack itemStack : ingredient.getOptions()) {
+                        boolean inventoryHasItem = false;
+                        // Iterate only over the crafting table to find this item
+                        crafting:
+                        for (int row = firstRow; row < height + firstRow; row++) {
+                            for (int col = firstCol; col < width + firstCol; col++) {
+                                GeyserItemStack geyserItemStack = inventoryGetter.apply(col + (row * gridDimensions) + 1);
+                                if (geyserItemStack.isEmpty()) {
+                                    inventoryHasItem = itemStack == null || itemStack.getId() == 0;
+                                    if (inventoryHasItem) {
+                                        break crafting;
+                                    }
+                                } else if (itemStack.equals(geyserItemStack.getItemStack(1))) {
+                                    inventoryHasItem = true;
+                                    break crafting;
+                                }
+                            }
+                        }
+                        if (!inventoryHasItem) {
+                            continue recipes;
+                        }
+                    }
+                }
+                return recipe;
+            }
+        }
+        return null;
+    }
+
+    private static boolean testShapedRecipe(final Ingredient[] ingredients, final IntFunction<GeyserItemStack> inventoryGetter,
+                                            final int gridDimensions, final int firstRow, final int height, final int firstCol, final int width) {
+        int ingredientIndex = 0;
+        for (int row = firstRow; row < height + firstRow; row++) {
+            for (int col = firstCol; col < width + firstCol; col++) {
+                GeyserItemStack geyserItemStack = inventoryGetter.apply(col + (row * gridDimensions) + 1);
+                Ingredient ingredient = ingredients[ingredientIndex++];
+                if (ingredient.getOptions().length == 0) {
+                    if (!geyserItemStack.isEmpty()) {
+                        return false;
+                    }
+                } else {
+                    boolean inventoryHasItem = false;
+                    for (ItemStack item : ingredient.getOptions()) {
+                        if (Objects.equals(geyserItemStack.getItemStack(1), item)) {
+                            inventoryHasItem = true;
+                            break;
+                        }
+                    }
+                    if (!inventoryHasItem) {
+                        return false;
+                    }
+                }
+            }
+        }
+        return true;
+    }
 }