--- a/net/minecraft/world/item/crafting/ShapedRecipe.java
+++ b/net/minecraft/world/item/crafting/ShapedRecipe.java
@@ -9,8 +9,16 @@
 import net.minecraft.network.codec.StreamCodec;
 import net.minecraft.world.item.ItemStack;
 import net.minecraft.world.level.Level;
+// CraftBukkit start
+import org.bukkit.NamespacedKey;
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.craftbukkit.inventory.CraftRecipe;
+import org.bukkit.craftbukkit.inventory.CraftShapedRecipe;
+import org.bukkit.inventory.RecipeChoice;
+// CraftBukkit end
+
+public class ShapedRecipe extends io.papermc.paper.inventory.recipe.RecipeBookExactChoiceRecipe<CraftingInput> implements CraftingRecipe { // Paper - improve exact recipe choices
 
-public class ShapedRecipe implements CraftingRecipe {
     final ShapedRecipePattern pattern;
     final ItemStack result;
     final String group;
@@ -23,12 +31,75 @@
         this.pattern = raw;
         this.result = result;
         this.showNotification = showNotification;
+        this.checkExactIngredients(); // Paper - improve exact recipe choices
     }
 
     public ShapedRecipe(String group, CraftingBookCategory category, ShapedRecipePattern raw, ItemStack result) {
         this(group, category, raw, result, true);
     }
 
+    // CraftBukkit start
+    @Override
+    public org.bukkit.inventory.ShapedRecipe toBukkitRecipe(NamespacedKey id) {
+        CraftItemStack result = CraftItemStack.asCraftMirror(this.result);
+        CraftShapedRecipe recipe = new CraftShapedRecipe(id, result, this);
+        recipe.setGroup(this.group);
+        recipe.setCategory(CraftRecipe.getCategory(this.category()));
+
+        switch (this.pattern.height()) {
+        case 1:
+            switch (this.pattern.width()) {
+            case 1:
+                recipe.shape("a");
+                break;
+            case 2:
+                recipe.shape("ab");
+                break;
+            case 3:
+                recipe.shape("abc");
+                break;
+            }
+            break;
+        case 2:
+            switch (this.pattern.width()) {
+            case 1:
+                recipe.shape("a","b");
+                break;
+            case 2:
+                recipe.shape("ab","cd");
+                break;
+            case 3:
+                recipe.shape("abc","def");
+                break;
+            }
+            break;
+        case 3:
+            switch (this.pattern.width()) {
+            case 1:
+                recipe.shape("a","b","c");
+                break;
+            case 2:
+                recipe.shape("ab","cd","ef");
+                break;
+            case 3:
+                recipe.shape("abc","def","ghi");
+                break;
+            }
+            break;
+        }
+        char c = 'a';
+        for (Ingredient list : this.pattern.ingredients()) {
+            RecipeChoice choice = CraftRecipe.toBukkit(list);
+            if (choice != RecipeChoice.empty()) { // Paper
+                recipe.setIngredient(c, choice);
+            }
+
+            c++;
+        }
+        return recipe;
+    }
+    // CraftBukkit end
+
     @Override
     public RecipeSerializer<?> getSerializer() {
         return RecipeSerializer.SHAPED_RECIPE;
@@ -64,12 +135,10 @@
         return width >= this.pattern.width() && height >= this.pattern.height();
     }
 
-    @Override
     public boolean matches(CraftingInput input, Level world) {
         return this.pattern.matches(input);
     }
 
-    @Override
     public ItemStack assemble(CraftingInput input, HolderLookup.Provider lookup) {
         return this.getResultItem(lookup).copy();
     }
@@ -84,43 +153,52 @@
 
     @Override
     public boolean isIncomplete() {
-        NonNullList<Ingredient> nonNullList = this.getIngredients();
-        return nonNullList.isEmpty()
-            || nonNullList.stream().filter(ingredient -> !ingredient.isEmpty()).anyMatch(ingredient -> ingredient.getItems().length == 0);
+        NonNullList<Ingredient> nonnulllist = this.getIngredients();
+
+        return nonnulllist.isEmpty() || nonnulllist.stream().filter((recipeitemstack) -> {
+            return !recipeitemstack.isEmpty();
+        }).anyMatch((recipeitemstack) -> {
+            return recipeitemstack.getItems().length == 0;
+        });
     }
 
     public static class Serializer implements RecipeSerializer<ShapedRecipe> {
-        public static final MapCodec<ShapedRecipe> CODEC = RecordCodecBuilder.mapCodec(
-            instance -> instance.group(
-                        Codec.STRING.optionalFieldOf("group", "").forGetter(recipe -> recipe.group),
-                        CraftingBookCategory.CODEC.fieldOf("category").orElse(CraftingBookCategory.MISC).forGetter(recipe -> recipe.category),
-                        ShapedRecipePattern.MAP_CODEC.forGetter(recipe -> recipe.pattern),
-                        ItemStack.STRICT_CODEC.fieldOf("result").forGetter(recipe -> recipe.result),
-                        Codec.BOOL.optionalFieldOf("show_notification", Boolean.valueOf(true)).forGetter(recipe -> recipe.showNotification)
-                    )
-                    .apply(instance, ShapedRecipe::new)
-        );
-        public static final StreamCodec<RegistryFriendlyByteBuf, ShapedRecipe> STREAM_CODEC = StreamCodec.of(
-            ShapedRecipe.Serializer::toNetwork, ShapedRecipe.Serializer::fromNetwork
-        );
+
+        public static final MapCodec<ShapedRecipe> CODEC = RecordCodecBuilder.mapCodec((instance) -> {
+            return instance.group(Codec.STRING.optionalFieldOf("group", "").forGetter((shapedrecipes) -> {
+                return shapedrecipes.group;
+            }), CraftingBookCategory.CODEC.fieldOf("category").orElse(CraftingBookCategory.MISC).forGetter((shapedrecipes) -> {
+                return shapedrecipes.category;
+            }), ShapedRecipePattern.MAP_CODEC.forGetter((shapedrecipes) -> {
+                return shapedrecipes.pattern;
+            }), ItemStack.STRICT_CODEC.fieldOf("result").forGetter((shapedrecipes) -> {
+                return shapedrecipes.result;
+            }), Codec.BOOL.optionalFieldOf("show_notification", true).forGetter((shapedrecipes) -> {
+                return shapedrecipes.showNotification;
+            })).apply(instance, ShapedRecipe::new);
+        });
+        public static final StreamCodec<RegistryFriendlyByteBuf, ShapedRecipe> STREAM_CODEC = StreamCodec.of(ShapedRecipe.Serializer::toNetwork, ShapedRecipe.Serializer::fromNetwork);
+
+        public Serializer() {}
 
         @Override
         public MapCodec<ShapedRecipe> codec() {
-            return CODEC;
+            return ShapedRecipe.Serializer.CODEC;
         }
 
         @Override
         public StreamCodec<RegistryFriendlyByteBuf, ShapedRecipe> streamCodec() {
-            return STREAM_CODEC;
+            return ShapedRecipe.Serializer.STREAM_CODEC;
         }
 
         private static ShapedRecipe fromNetwork(RegistryFriendlyByteBuf buf) {
-            String string = buf.readUtf();
-            CraftingBookCategory craftingBookCategory = buf.readEnum(CraftingBookCategory.class);
-            ShapedRecipePattern shapedRecipePattern = ShapedRecipePattern.STREAM_CODEC.decode(buf);
-            ItemStack itemStack = ItemStack.STREAM_CODEC.decode(buf);
-            boolean bl = buf.readBoolean();
-            return new ShapedRecipe(string, craftingBookCategory, shapedRecipePattern, itemStack, bl);
+            String s = buf.readUtf();
+            CraftingBookCategory craftingbookcategory = (CraftingBookCategory) buf.readEnum(CraftingBookCategory.class);
+            ShapedRecipePattern shapedrecipepattern = (ShapedRecipePattern) ShapedRecipePattern.STREAM_CODEC.decode(buf);
+            ItemStack itemstack = (ItemStack) ItemStack.STREAM_CODEC.decode(buf);
+            boolean flag = buf.readBoolean();
+
+            return new ShapedRecipe(s, craftingbookcategory, shapedrecipepattern, itemstack, flag);
         }
 
         private static void toNetwork(RegistryFriendlyByteBuf buf, ShapedRecipe recipe) {
