--- a/net/minecraft/world/item/crafting/ShapedRecipe.java
+++ b/net/minecraft/world/item/crafting/ShapedRecipe.java
@@ -5,6 +5,7 @@
 import com.mojang.serialization.Codec;
 import com.mojang.serialization.DataResult;
 import com.mojang.serialization.codecs.RecordCodecBuilder;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -17,7 +18,16 @@
 import net.minecraft.world.level.Level;
 import org.apache.commons.lang3.NotImplementedException;
 
-public class ShapedRecipe implements CraftingRecipe {
+// CraftBukkit start
+import org.bukkit.NamespacedKey;
+import org.bukkit.craftbukkit.v1_20_R2.inventory.CraftItemStack;
+import org.bukkit.craftbukkit.v1_20_R2.inventory.CraftRecipe;
+import org.bukkit.craftbukkit.v1_20_R2.inventory.CraftShapedRecipe;
+import org.bukkit.inventory.RecipeChoice;
+// CraftBukkit end
+
+public class ShapedRecipe extends io.papermc.paper.inventory.recipe.RecipeBookExactChoiceRecipe<CraftingContainer> implements CraftingRecipe { // Paper - improve exact recipe choices
+
     final int width;
     final int height;
     final NonNullList<Ingredient> recipeItems;
@@ -34,12 +44,75 @@
         this.recipeItems = ingredients;
         this.result = result;
         this.showNotification = showNotification;
+        this.checkExactIngredients(); // Paper - improve exact recipe choices
     }
 
     public ShapedRecipe(String group, CraftingBookCategory category, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result) {
         this(group, category, width, height, ingredients, 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.height) {
+        case 1:
+            switch (this.width) {
+            case 1:
+                recipe.shape("a");
+                break;
+            case 2:
+                recipe.shape("ab");
+                break;
+            case 3:
+                recipe.shape("abc");
+                break;
+            }
+            break;
+        case 2:
+            switch (this.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.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.recipeItems) {
+            RecipeChoice choice = CraftRecipe.toBukkit(list);
+            if (choice != null) {
+                recipe.setIngredient(c, choice);
+            }
+
+            c++;
+        }
+        return recipe;
+    }
+    // CraftBukkit end
+
     @Override
     public RecipeSerializer<?> getSerializer() {
         return RecipeSerializer.SHAPED_RECIPE;
@@ -75,10 +148,9 @@
         return width >= this.width && height >= this.height;
     }
 
-    @Override
     public boolean matches(CraftingContainer inventory, Level world) {
-        for(int i = 0; i <= inventory.getWidth() - this.width; ++i) {
-            for(int j = 0; j <= inventory.getHeight() - this.height; ++j) {
+        for (int i = 0; i <= inventory.getWidth() - this.width; ++i) {
+            for (int j = 0; j <= inventory.getHeight() - this.height; ++j) {
                 if (this.matches(inventory, i, j, true)) {
                     return true;
                 }
@@ -93,20 +165,21 @@
     }
 
     private boolean matches(CraftingContainer inv, int offsetX, int offsetY, boolean flipped) {
-        for(int i = 0; i < inv.getWidth(); ++i) {
-            for(int j = 0; j < inv.getHeight(); ++j) {
-                int k = i - offsetX;
-                int l = j - offsetY;
-                Ingredient ingredient = Ingredient.EMPTY;
-                if (k >= 0 && l >= 0 && k < this.width && l < this.height) {
+        for (int k = 0; k < inv.getWidth(); ++k) {
+            for (int l = 0; l < inv.getHeight(); ++l) {
+                int i1 = k - offsetX;
+                int j1 = l - offsetY;
+                Ingredient recipeitemstack = Ingredient.EMPTY;
+
+                if (i1 >= 0 && j1 >= 0 && i1 < this.width && j1 < this.height) {
                     if (flipped) {
-                        ingredient = this.recipeItems.get(this.width - k - 1 + l * this.width);
+                        recipeitemstack = (Ingredient) this.recipeItems.get(this.width - i1 - 1 + j1 * this.width);
                     } else {
-                        ingredient = this.recipeItems.get(k + l * this.width);
+                        recipeitemstack = (Ingredient) this.recipeItems.get(i1 + j1 * this.width);
                     }
                 }
 
-                if (!ingredient.test(inv.getItem(i + j * inv.getWidth()))) {
+                if (!recipeitemstack.test(inv.getItem(k + l * inv.getWidth()))) {
                     return false;
                 }
             }
@@ -115,7 +188,6 @@
         return true;
     }
 
-    @Override
     public ItemStack assemble(CraftingContainer inventory, RegistryAccess registryManager) {
         return this.getResultItem(registryManager).copy();
     }
@@ -135,13 +207,15 @@
         int k = 0;
         int l = 0;
 
-        for(int m = 0; m < pattern.size(); ++m) {
-            String string = pattern.get(m);
-            i = Math.min(i, firstNonSpace(string));
-            int n = lastNonSpace(string);
-            j = Math.max(j, n);
-            if (n < 0) {
-                if (k == m) {
+        for (int i1 = 0; i1 < pattern.size(); ++i1) {
+            String s = (String) pattern.get(i1);
+
+            i = Math.min(i, ShapedRecipe.firstNonSpace(s));
+            int j1 = ShapedRecipe.lastNonSpace(s);
+
+            j = Math.max(j, j1);
+            if (j1 < 0) {
+                if (k == i1) {
                     ++k;
                 }
 
@@ -154,29 +228,32 @@
         if (pattern.size() == l) {
             return new String[0];
         } else {
-            String[] strings = new String[pattern.size() - l - k];
+            String[] astring = new String[pattern.size() - l - k];
 
-            for(int o = 0; o < strings.length; ++o) {
-                strings[o] = pattern.get(o + k).substring(i, j + 1);
+            for (int k1 = 0; k1 < astring.length; ++k1) {
+                astring[k1] = ((String) pattern.get(k1 + k)).substring(i, j + 1);
             }
 
-            return strings;
+            return astring;
         }
     }
 
     @Override
     public boolean isIncomplete() {
-        NonNullList<Ingredient> nonNullList = this.getIngredients();
-        return nonNullList.isEmpty() || nonNullList.stream().filter((ingredient) -> {
-            return !ingredient.isEmpty();
-        }).anyMatch((ingredient) -> {
-            return 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;
         });
     }
 
     private static int firstNonSpace(String line) {
         int i;
-        for(i = 0; i < line.length() && line.charAt(i) == ' '; ++i) {
+
+        for (i = 0; i < line.length() && line.charAt(i) == ' '; ++i) {
+            ;
         }
 
         return i;
@@ -184,74 +261,78 @@
 
     private static int lastNonSpace(String pattern) {
         int i;
-        for(i = pattern.length() - 1; i >= 0 && pattern.charAt(i) == ' '; --i) {
+
+        for (i = pattern.length() - 1; i >= 0 && pattern.charAt(i) == ' '; --i) {
+            ;
         }
 
         return i;
     }
 
     public static class Serializer implements RecipeSerializer<ShapedRecipe> {
-        static final Codec<List<String>> PATTERN_CODEC = Codec.STRING.listOf().flatXmap((rows) -> {
-            if (rows.size() > 3) {
+
+        static final Codec<List<String>> PATTERN_CODEC = Codec.STRING.listOf().flatXmap((list) -> {
+            if (list.size() > 3) {
                 return DataResult.error(() -> {
                     return "Invalid pattern: too many rows, 3 is maximum";
                 });
-            } else if (rows.isEmpty()) {
+            } else if (list.isEmpty()) {
                 return DataResult.error(() -> {
                     return "Invalid pattern: empty pattern not allowed";
                 });
             } else {
-                int i = rows.get(0).length();
+                int i = ((String) list.get(0)).length();
+                Iterator iterator = list.iterator();
 
-                for(String string : rows) {
-                    if (string.length() > 3) {
-                        return DataResult.error(() -> {
-                            return "Invalid pattern: too many columns, 3 is maximum";
-                        });
+                String s;
+
+                do {
+                    if (!iterator.hasNext()) {
+                        return DataResult.success(list);
                     }
 
-                    if (i != string.length()) {
+                    s = (String) iterator.next();
+                    if (s.length() > 3) {
                         return DataResult.error(() -> {
-                            return "Invalid pattern: each row must be the same width";
+                            return "Invalid pattern: too many columns, 3 is maximum";
                         });
                     }
-                }
+                } while (i == s.length());
 
-                return DataResult.success(rows);
-            }
-        }, DataResult::success);
-        static final Codec<String> SINGLE_CHARACTER_STRING_CODEC = Codec.STRING.flatXmap((keyEntry) -> {
-            if (keyEntry.length() != 1) {
                 return DataResult.error(() -> {
-                    return "Invalid key entry: '" + keyEntry + "' is an invalid symbol (must be 1 character only).";
+                    return "Invalid pattern: each row must be the same width";
                 });
-            } else {
-                return " ".equals(keyEntry) ? DataResult.error(() -> {
-                    return "Invalid key entry: ' ' is a reserved symbol.";
-                }) : DataResult.success(keyEntry);
             }
         }, DataResult::success);
-        private static final Codec<ShapedRecipe> CODEC = ShapedRecipe.Serializer.RawShapedRecipe.CODEC.flatXmap((recipe) -> {
-            String[] strings = ShapedRecipe.shrink(recipe.pattern);
-            int i = strings[0].length();
-            int j = strings.length;
-            NonNullList<Ingredient> nonNullList = NonNullList.withSize(i * j, Ingredient.EMPTY);
-            Set<String> set = Sets.newHashSet(recipe.key.keySet());
+        static final Codec<String> SINGLE_CHARACTER_STRING_CODEC = Codec.STRING.flatXmap((s) -> {
+            return s.length() != 1 ? DataResult.error(() -> {
+                return "Invalid key entry: '" + s + "' is an invalid symbol (must be 1 character only).";
+            }) : (" ".equals(s) ? DataResult.error(() -> {
+                return "Invalid key entry: ' ' is a reserved symbol.";
+            }) : DataResult.success(s));
+        }, DataResult::success);
+        private static final Codec<ShapedRecipe> CODEC = ShapedRecipe.Serializer.RawShapedRecipe.CODEC.flatXmap((shapedrecipes_serializer_rawshapedrecipe) -> {
+            String[] astring = ShapedRecipe.shrink(shapedrecipes_serializer_rawshapedrecipe.pattern);
+            int i = astring[0].length();
+            int j = astring.length;
+            NonNullList<Ingredient> nonnulllist = NonNullList.withSize(i * j, Ingredient.EMPTY);
+            Set<String> set = Sets.newHashSet(shapedrecipes_serializer_rawshapedrecipe.key.keySet());
 
-            for(int k = 0; k < strings.length; ++k) {
-                String string = strings[k];
+            for (int k = 0; k < astring.length; ++k) {
+                String s = astring[k];
 
-                for(int l = 0; l < string.length(); ++l) {
-                    String string2 = string.substring(l, l + 1);
-                    Ingredient ingredient = string2.equals(" ") ? Ingredient.EMPTY : recipe.key.get(string2);
-                    if (ingredient == null) {
+                for (int l = 0; l < s.length(); ++l) {
+                    String s1 = s.substring(l, l + 1);
+                    Ingredient recipeitemstack = s1.equals(" ") ? Ingredient.EMPTY : (Ingredient) shapedrecipes_serializer_rawshapedrecipe.key.get(s1);
+
+                    if (recipeitemstack == null) {
                         return DataResult.error(() -> {
-                            return "Pattern references symbol '" + string2 + "' but it's not defined in the key";
+                            return "Pattern references symbol '" + s1 + "' but it's not defined in the key";
                         });
                     }
 
-                    set.remove(string2);
-                    nonNullList.set(l + i * k, ingredient);
+                    set.remove(s1);
+                    nonnulllist.set(l + i * k, recipeitemstack);
                 }
             }
 
@@ -260,64 +341,71 @@
                     return "Key defines symbols that aren't used in pattern: " + set;
                 });
             } else {
-                ShapedRecipe shapedRecipe = new ShapedRecipe(recipe.group, recipe.category, i, j, nonNullList, recipe.result, recipe.showNotification);
-                return DataResult.success(shapedRecipe);
+                ShapedRecipe shapedrecipes = new ShapedRecipe(shapedrecipes_serializer_rawshapedrecipe.group, shapedrecipes_serializer_rawshapedrecipe.category, i, j, nonnulllist, shapedrecipes_serializer_rawshapedrecipe.result, shapedrecipes_serializer_rawshapedrecipe.showNotification);
+
+                return DataResult.success(shapedrecipes);
             }
-        }, (recipe) -> {
+        }, (shapedrecipes) -> {
             throw new NotImplementedException("Serializing ShapedRecipe is not implemented yet.");
         });
 
+        public Serializer() {}
+
         @Override
         public Codec<ShapedRecipe> codec() {
-            return CODEC;
+            return ShapedRecipe.Serializer.CODEC;
         }
 
         @Override
-        public ShapedRecipe fromNetwork(FriendlyByteBuf friendlyByteBuf) {
-            int i = friendlyByteBuf.readVarInt();
-            int j = friendlyByteBuf.readVarInt();
-            String string = friendlyByteBuf.readUtf();
-            CraftingBookCategory craftingBookCategory = friendlyByteBuf.readEnum(CraftingBookCategory.class);
-            NonNullList<Ingredient> nonNullList = NonNullList.withSize(i * j, Ingredient.EMPTY);
+        public ShapedRecipe fromNetwork(FriendlyByteBuf buf) {
+            int i = buf.readVarInt();
+            int j = buf.readVarInt();
+            String s = buf.readUtf();
+            CraftingBookCategory craftingbookcategory = (CraftingBookCategory) buf.readEnum(CraftingBookCategory.class);
+            NonNullList<Ingredient> nonnulllist = NonNullList.withSize(i * j, Ingredient.EMPTY);
 
-            for(int k = 0; k < nonNullList.size(); ++k) {
-                nonNullList.set(k, Ingredient.fromNetwork(friendlyByteBuf));
+            for (int k = 0; k < nonnulllist.size(); ++k) {
+                nonnulllist.set(k, Ingredient.fromNetwork(buf));
             }
 
-            ItemStack itemStack = friendlyByteBuf.readItem();
-            boolean bl = friendlyByteBuf.readBoolean();
-            return new ShapedRecipe(string, craftingBookCategory, i, j, nonNullList, itemStack, bl);
+            ItemStack itemstack = buf.readItem();
+            boolean flag = buf.readBoolean();
+
+            return new ShapedRecipe(s, craftingbookcategory, i, j, nonnulllist, itemstack, flag);
         }
 
-        @Override
         public void toNetwork(FriendlyByteBuf buf, ShapedRecipe recipe) {
             buf.writeVarInt(recipe.width);
             buf.writeVarInt(recipe.height);
             buf.writeUtf(recipe.group);
             buf.writeEnum(recipe.category);
+            Iterator iterator = recipe.recipeItems.iterator();
 
-            for(Ingredient ingredient : recipe.recipeItems) {
-                ingredient.toNetwork(buf);
+            while (iterator.hasNext()) {
+                Ingredient recipeitemstack = (Ingredient) iterator.next();
+
+                recipeitemstack.toNetwork(buf);
             }
 
             buf.writeItem(recipe.result);
             buf.writeBoolean(recipe.showNotification);
         }
 
-        static record RawShapedRecipe(String group, CraftingBookCategory category, Map<String, Ingredient> key, List<String> pattern, ItemStack result, boolean showNotification) {
+        private static record RawShapedRecipe(String group, CraftingBookCategory category, Map<String, Ingredient> key, List<String> pattern, ItemStack result, boolean showNotification) {
+
             public static final Codec<ShapedRecipe.Serializer.RawShapedRecipe> CODEC = RecordCodecBuilder.create((instance) -> {
-                return instance.group(ExtraCodecs.strictOptionalField(Codec.STRING, "group", "").forGetter((recipe) -> {
-                    return recipe.group;
-                }), CraftingBookCategory.CODEC.fieldOf("category").orElse(CraftingBookCategory.MISC).forGetter((recipe) -> {
-                    return recipe.category;
-                }), ExtraCodecs.strictUnboundedMap(ShapedRecipe.Serializer.SINGLE_CHARACTER_STRING_CODEC, Ingredient.CODEC_NONEMPTY).fieldOf("key").forGetter((recipe) -> {
-                    return recipe.key;
-                }), ShapedRecipe.Serializer.PATTERN_CODEC.fieldOf("pattern").forGetter((recipe) -> {
-                    return recipe.pattern;
-                }), CraftingRecipeCodecs.ITEMSTACK_OBJECT_CODEC.fieldOf("result").forGetter((recipe) -> {
-                    return recipe.result;
-                }), ExtraCodecs.strictOptionalField(Codec.BOOL, "show_notification", true).forGetter((recipe) -> {
-                    return recipe.showNotification;
+                return instance.group(ExtraCodecs.strictOptionalField(Codec.STRING, "group", "").forGetter((shapedrecipes_serializer_rawshapedrecipe) -> {
+                    return shapedrecipes_serializer_rawshapedrecipe.group;
+                }), CraftingBookCategory.CODEC.fieldOf("category").orElse(CraftingBookCategory.MISC).forGetter((shapedrecipes_serializer_rawshapedrecipe) -> {
+                    return shapedrecipes_serializer_rawshapedrecipe.category;
+                }), ExtraCodecs.strictUnboundedMap(ShapedRecipe.Serializer.SINGLE_CHARACTER_STRING_CODEC, Ingredient.CODEC_NONEMPTY).fieldOf("key").forGetter((shapedrecipes_serializer_rawshapedrecipe) -> {
+                    return shapedrecipes_serializer_rawshapedrecipe.key;
+                }), ShapedRecipe.Serializer.PATTERN_CODEC.fieldOf("pattern").forGetter((shapedrecipes_serializer_rawshapedrecipe) -> {
+                    return shapedrecipes_serializer_rawshapedrecipe.pattern;
+                }), CraftingRecipeCodecs.ITEMSTACK_OBJECT_CODEC.fieldOf("result").forGetter((shapedrecipes_serializer_rawshapedrecipe) -> {
+                    return shapedrecipes_serializer_rawshapedrecipe.result;
+                }), ExtraCodecs.strictOptionalField(Codec.BOOL, "show_notification", true).forGetter((shapedrecipes_serializer_rawshapedrecipe) -> {
+                    return shapedrecipes_serializer_rawshapedrecipe.showNotification;
                 })).apply(instance, ShapedRecipe.Serializer.RawShapedRecipe::new);
             });
         }
