--- a/net/minecraft/world/item/crafting/Ingredient.java
+++ b/net/minecraft/world/item/crafting/Ingredient.java
@@ -13,6 +13,7 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 import java.util.function.Predicate;
 import java.util.stream.Stream;
@@ -30,17 +31,19 @@
 import net.minecraft.world.level.ItemLike;
 
 public final class Ingredient implements Predicate<ItemStack> {
+
     public static final Ingredient EMPTY = new Ingredient(Stream.empty());
     private final Ingredient.Value[] values;
     @Nullable
     public ItemStack[] itemStacks;
     @Nullable
     private IntList stackingIds;
-    public static final Codec<Ingredient> CODEC = codec(true);
-    public static final Codec<Ingredient> CODEC_NONEMPTY = codec(false);
+    public boolean exact; // CraftBukkit
+    public static final Codec<Ingredient> CODEC = Ingredient.codec(true);
+    public static final Codec<Ingredient> CODEC_NONEMPTY = Ingredient.codec(false);
 
     public Ingredient(Stream<? extends Ingredient.Value> entries) {
-        this.values = entries.toArray((i) -> {
+        this.values = (Ingredient.Value[]) entries.toArray((i) -> {
             return new Ingredient.Value[i];
         });
     }
@@ -51,8 +54,8 @@
 
     public ItemStack[] getItems() {
         if (this.itemStacks == null) {
-            this.itemStacks = Arrays.stream(this.values).flatMap((entry) -> {
-                return entry.getItems().stream();
+            this.itemStacks = (ItemStack[]) Arrays.stream(this.values).flatMap((recipeitemstack_provider) -> {
+                return recipeitemstack_provider.getItems().stream();
             }).distinct().toArray((i) -> {
                 return new ItemStack[i];
             });
@@ -61,15 +64,28 @@
         return this.itemStacks;
     }
 
-    @Override
-    public boolean test(@Nullable ItemStack itemStack) {
-        if (itemStack == null) {
+    public boolean test(@Nullable ItemStack itemstack) {
+        if (itemstack == null) {
             return false;
         } else if (this.isEmpty()) {
-            return itemStack.isEmpty();
+            return itemstack.isEmpty();
         } else {
-            for(ItemStack itemStack2 : this.getItems()) {
-                if (itemStack2.is(itemStack.getItem())) {
+            ItemStack[] aitemstack = this.getItems();
+            int i = aitemstack.length;
+
+            for (int j = 0; j < i; ++j) {
+                ItemStack itemstack1 = aitemstack[j];
+
+                // CraftBukkit start
+                if (this.exact) {
+                    if (itemstack1.getItem() == itemstack.getItem() && ItemStack.isSameItemSameTags(itemstack, itemstack1)) {
+                        return true;
+                    }
+
+                    continue;
+                }
+                // CraftBukkit end
+                if (itemstack1.is(itemstack.getItem())) {
                     return true;
                 }
             }
@@ -80,11 +96,16 @@
 
     public IntList getStackingIds() {
         if (this.stackingIds == null) {
-            ItemStack[] itemStacks = this.getItems();
-            this.stackingIds = new IntArrayList(itemStacks.length);
+            ItemStack[] aitemstack = this.getItems();
 
-            for(ItemStack itemStack : itemStacks) {
-                this.stackingIds.add(StackedContents.getStackingIndex(itemStack));
+            this.stackingIds = new IntArrayList(aitemstack.length);
+            ItemStack[] aitemstack1 = aitemstack;
+            int i = aitemstack.length;
+
+            for (int j = 0; j < i; ++j) {
+                ItemStack itemstack = aitemstack1[j];
+
+                this.stackingIds.add(StackedContents.getStackingIndex(itemstack));
             }
 
             this.stackingIds.sort(IntComparators.NATURAL_COMPARATOR);
@@ -98,108 +119,113 @@
     }
 
     public JsonElement toJson(boolean allowEmpty) {
-        Codec<Ingredient> codec = allowEmpty ? CODEC : CODEC_NONEMPTY;
-        return Util.getOrThrow(codec.encodeStart(JsonOps.INSTANCE, this), IllegalStateException::new);
+        Codec<Ingredient> codec = allowEmpty ? Ingredient.CODEC : Ingredient.CODEC_NONEMPTY;
+
+        return (JsonElement) Util.getOrThrow(codec.encodeStart(JsonOps.INSTANCE, this), IllegalStateException::new);
     }
 
     public boolean isEmpty() {
         return this.values.length == 0;
     }
 
-    @Override
     public boolean equals(Object object) {
-        if (object instanceof Ingredient ingredient) {
-            return Arrays.equals((Object[])this.values, (Object[])ingredient.values);
+        if (object instanceof Ingredient) {
+            Ingredient recipeitemstack = (Ingredient) object;
+
+            return Arrays.equals(this.values, recipeitemstack.values);
         } else {
             return false;
         }
     }
 
     private static Ingredient fromValues(Stream<? extends Ingredient.Value> entries) {
-        Ingredient ingredient = new Ingredient(entries);
-        return ingredient.isEmpty() ? EMPTY : ingredient;
+        Ingredient recipeitemstack = new Ingredient(entries);
+
+        return recipeitemstack.isEmpty() ? Ingredient.EMPTY : recipeitemstack;
     }
 
     public static Ingredient of() {
-        return EMPTY;
+        return Ingredient.EMPTY;
     }
 
     public static Ingredient of(ItemLike... items) {
-        return of(Arrays.stream(items).map(ItemStack::new));
+        return Ingredient.of(Arrays.stream(items).map(ItemStack::new));
     }
 
     public static Ingredient of(ItemStack... stacks) {
-        return of(Arrays.stream(stacks));
+        return Ingredient.of(Arrays.stream(stacks));
     }
 
     public static Ingredient of(Stream<ItemStack> stacks) {
-        return fromValues(stacks.filter((stack) -> {
-            return !stack.isEmpty();
+        return Ingredient.fromValues(stacks.filter((itemstack) -> {
+            return !itemstack.isEmpty();
         }).map(Ingredient.ItemValue::new));
     }
 
     public static Ingredient of(TagKey<Item> tag) {
-        return fromValues(Stream.of(new Ingredient.TagValue(tag)));
+        return Ingredient.fromValues(Stream.of(new Ingredient.TagValue(tag)));
     }
 
     public static Ingredient fromNetwork(FriendlyByteBuf buf) {
-        return fromValues(buf.<ItemStack>readList(FriendlyByteBuf::readItem).stream().map(Ingredient.ItemValue::new));
+        return Ingredient.fromValues(buf.readList(FriendlyByteBuf::readItem).stream().map(Ingredient.ItemValue::new));
     }
 
     private static Codec<Ingredient> codec(boolean allowEmpty) {
-        Codec<Ingredient.Value[]> codec = Codec.list(Ingredient.Value.CODEC).comapFlatMap((entries) -> {
-            return !allowEmpty && entries.size() < 1 ? DataResult.error(() -> {
+        Codec<Ingredient.Value[]> codec = Codec.list(Ingredient.Value.CODEC).comapFlatMap((list) -> {
+            return !allowEmpty && list.size() < 1 ? DataResult.error(() -> {
                 return "Item array cannot be empty, at least one item must be defined";
-            }) : DataResult.success(entries.toArray(new Ingredient.Value[0]));
+            }) : DataResult.success((Ingredient.Value[]) list.toArray(new Ingredient.Value[0]));
         }, List::of);
+
         return ExtraCodecs.either(codec, Ingredient.Value.CODEC).flatComapMap((either) -> {
-            return either.map(Ingredient::new, (entry) -> {
-                return new Ingredient(new Ingredient.Value[]{entry});
+            return (Ingredient) either.map(Ingredient::new, (recipeitemstack_provider) -> {
+                return new Ingredient(new Ingredient.Value[]{recipeitemstack_provider});
             });
-        }, (ingredient) -> {
-            if (ingredient.values.length == 1) {
-                return DataResult.success(Either.right(ingredient.values[0]));
-            } else {
-                return ingredient.values.length == 0 && !allowEmpty ? DataResult.error(() -> {
-                    return "Item array cannot be empty, at least one item must be defined";
-                }) : DataResult.success(Either.left(ingredient.values));
-            }
+        }, (recipeitemstack) -> {
+            return recipeitemstack.values.length == 1 ? DataResult.success(Either.right(recipeitemstack.values[0])) : (recipeitemstack.values.length == 0 && !allowEmpty ? DataResult.error(() -> {
+                return "Item array cannot be empty, at least one item must be defined";
+            }) : DataResult.success(Either.left(recipeitemstack.values)));
         });
     }
 
-    public static record ItemValue(ItemStack item) implements Ingredient.Value {
-        static final Codec<Ingredient.ItemValue> CODEC = RecordCodecBuilder.create((instance) -> {
-            return instance.group(CraftingRecipeCodecs.ITEMSTACK_NONAIR_CODEC.fieldOf("item").forGetter((entry) -> {
-                return entry.item;
-            })).apply(instance, Ingredient.ItemValue::new);
-        });
+    public interface Value {
 
-        @Override
-        public boolean equals(Object object) {
-            if (!(object instanceof Ingredient.ItemValue itemValue)) {
-                return false;
+        Codec<Ingredient.Value> CODEC = ExtraCodecs.xor(Ingredient.ItemValue.CODEC, Ingredient.TagValue.CODEC).xmap((either) -> {
+            return (Ingredient.Value) either.map((recipeitemstack_stackprovider) -> {
+                return recipeitemstack_stackprovider;
+            }, (recipeitemstack_b) -> {
+                return recipeitemstack_b;
+            });
+        }, (recipeitemstack_provider) -> {
+            if (recipeitemstack_provider instanceof Ingredient.TagValue) {
+                Ingredient.TagValue recipeitemstack_b = (Ingredient.TagValue) recipeitemstack_provider;
+
+                return Either.right(recipeitemstack_b);
+            } else if (recipeitemstack_provider instanceof Ingredient.ItemValue) {
+                Ingredient.ItemValue recipeitemstack_stackprovider = (Ingredient.ItemValue) recipeitemstack_provider;
+
+                return Either.left(recipeitemstack_stackprovider);
             } else {
-                return itemValue.item.getItem().equals(this.item.getItem()) && itemValue.item.getCount() == this.item.getCount();
+                throw new UnsupportedOperationException("This is neither an item value nor a tag value.");
             }
-        }
+        });
 
-        @Override
-        public Collection<ItemStack> getItems() {
-            return Collections.singleton(this.item);
-        }
+        Collection<ItemStack> getItems();
     }
 
-    static record TagValue(TagKey<Item> tag) implements Ingredient.Value {
+    private static record TagValue(TagKey<Item> tag) implements Ingredient.Value {
+
         static final Codec<Ingredient.TagValue> CODEC = RecordCodecBuilder.create((instance) -> {
-            return instance.group(TagKey.codec(Registries.ITEM).fieldOf("tag").forGetter((entry) -> {
-                return entry.tag;
+            return instance.group(TagKey.codec(Registries.ITEM).fieldOf("tag").forGetter((recipeitemstack_b) -> {
+                return recipeitemstack_b.tag;
             })).apply(instance, Ingredient.TagValue::new);
         });
 
-        @Override
         public boolean equals(Object object) {
-            if (object instanceof Ingredient.TagValue tagValue) {
-                return tagValue.tag.location().equals(this.tag.location());
+            if (object instanceof Ingredient.TagValue) {
+                Ingredient.TagValue recipeitemstack_b = (Ingredient.TagValue) object;
+
+                return recipeitemstack_b.tag.location().equals(this.tag.location());
             } else {
                 return false;
             }
@@ -208,8 +234,11 @@
         @Override
         public Collection<ItemStack> getItems() {
             List<ItemStack> list = Lists.newArrayList();
+            Iterator iterator = BuiltInRegistries.ITEM.getTagOrEmpty(this.tag).iterator();
+
+            while (iterator.hasNext()) {
+                Holder<Item> holder = (Holder) iterator.next();
 
-            for(Holder<Item> holder : BuiltInRegistries.ITEM.getTagOrEmpty(this.tag)) {
                 list.add(new ItemStack(holder));
             }
 
@@ -217,23 +246,27 @@
         }
     }
 
-    public interface Value {
-        Codec<Ingredient.Value> CODEC = ExtraCodecs.xor(Ingredient.ItemValue.CODEC, Ingredient.TagValue.CODEC).xmap((either) -> {
-            return either.map((stackEntry) -> {
-                return stackEntry;
-            }, (tagEntry) -> {
-                return tagEntry;
-            });
-        }, (entry) -> {
-            if (entry instanceof Ingredient.TagValue tagValue) {
-                return Either.right(tagValue);
-            } else if (entry instanceof Ingredient.ItemValue itemValue) {
-                return Either.left(itemValue);
+    public static record ItemValue(ItemStack item) implements Ingredient.Value {
+
+        static final Codec<Ingredient.ItemValue> CODEC = RecordCodecBuilder.create((instance) -> {
+            return instance.group(CraftingRecipeCodecs.ITEMSTACK_NONAIR_CODEC.fieldOf("item").forGetter((recipeitemstack_stackprovider) -> {
+                return recipeitemstack_stackprovider.item;
+            })).apply(instance, Ingredient.ItemValue::new);
+        });
+
+        public boolean equals(Object object) {
+            if (!(object instanceof Ingredient.ItemValue)) {
+                return false;
             } else {
-                throw new UnsupportedOperationException("This is neither an item value nor a tag value.");
+                Ingredient.ItemValue recipeitemstack_stackprovider = (Ingredient.ItemValue) object;
+
+                return recipeitemstack_stackprovider.item.getItem().equals(this.item.getItem()) && recipeitemstack_stackprovider.item.getCount() == this.item.getCount();
             }
-        });
+        }
 
-        Collection<ItemStack> getItems();
+        @Override
+        public Collection<ItemStack> getItems() {
+            return Collections.singleton(this.item);
+        }
     }
 }
