aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJake Potrebic <[email protected]>2024-05-20 07:20:47 -0700
committerGitHub <[email protected]>2024-05-20 07:20:47 -0700
commit7d2e5c33ead3f4040597b2200f00fbd0e5b1e502 (patch)
tree8d085f512d5768b97aade83fc1e07b8e800edaa2
parent591521e697a619d5d81323c5488b154e3cd14ba3 (diff)
downloadPaper-7d2e5c33ead3f4040597b2200f00fbd0e5b1e502.tar.gz
Paper-7d2e5c33ead3f4040597b2200f00fbd0e5b1e502.zip
Add an 'empty' RecipeChoice for certain ingredient slots (#10710)
-rw-r--r--patches/api/0477-Fix-issues-with-recipe-API.patch (renamed from patches/api/0477-Improve-Recipe-validation.patch)187
-rw-r--r--patches/server/1046-Fix-issues-with-Recipe-API.patch116
2 files changed, 275 insertions, 28 deletions
diff --git a/patches/api/0477-Improve-Recipe-validation.patch b/patches/api/0477-Fix-issues-with-recipe-API.patch
index 1989238381..d9f853e3e4 100644
--- a/patches/api/0477-Improve-Recipe-validation.patch
+++ b/patches/api/0477-Fix-issues-with-recipe-API.patch
@@ -1,11 +1,20 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jake Potrebic <[email protected]>
Date: Sun, 12 May 2024 10:42:42 -0700
-Subject: [PATCH] Improve Recipe validation
+Subject: [PATCH] Fix issues with recipe API
+Improves the validation when creating recipes
+and RecipeChoices to closer match what is
+allowed by the Codecs and StreamCodecs internally.
+
+Adds RecipeChoice#empty which is allowed in specific
+recipes and ingredient slots.
+
+Also fixes some issues regarding mutability of both ItemStack
+and implementations of RecipeChoice.
diff --git a/src/main/java/org/bukkit/inventory/CookingRecipe.java b/src/main/java/org/bukkit/inventory/CookingRecipe.java
-index f7fa79393aef40027446b78bac8e9490cfafd8bc..a64dae15db89038f3eb599f2a41edf45990f4fda 100644
+index f7fa79393aef40027446b78bac8e9490cfafd8bc..07906ca1a9b39fcc6774870daa498402f7f37917 100644
--- a/src/main/java/org/bukkit/inventory/CookingRecipe.java
+++ b/src/main/java/org/bukkit/inventory/CookingRecipe.java
@@ -44,10 +44,10 @@ public abstract class CookingRecipe<T extends CookingRecipe> implements Recipe,
@@ -17,7 +26,7 @@ index f7fa79393aef40027446b78bac8e9490cfafd8bc..a64dae15db89038f3eb599f2a41edf45
this.key = key;
this.output = new ItemStack(result);
- this.ingredient = input;
-+ this.ingredient = input.validate().clone(); // Paper
++ this.ingredient = input.validate(false).clone(); // Paper
this.experience = experience;
this.cookingTime = cookingTime;
}
@@ -26,7 +35,7 @@ index f7fa79393aef40027446b78bac8e9490cfafd8bc..a64dae15db89038f3eb599f2a41edf45
@NotNull
public T setInputChoice(@NotNull RecipeChoice input) {
- this.ingredient = input;
-+ this.ingredient = input.validate().clone(); // Paper
++ this.ingredient = input.validate(false).clone(); // Paper
return (T) this;
}
@@ -43,6 +52,45 @@ index e4bf772f7e06f38215bee68f089b15a4fcb12817..37024b4736dd3897490ca51d08cf0790
this.key = key;
this.output = new ItemStack(result);
}
+diff --git a/src/main/java/org/bukkit/inventory/EmptyRecipeChoice.java b/src/main/java/org/bukkit/inventory/EmptyRecipeChoice.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ed0ab6163f47ec843ba4f7ea4a98bb2fa315eaa1
+--- /dev/null
++++ b/src/main/java/org/bukkit/inventory/EmptyRecipeChoice.java
+@@ -0,0 +1,33 @@
++package org.bukkit.inventory;
++
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import org.jetbrains.annotations.ApiStatus;
++
++@DefaultQualifier(NonNull.class)
++record EmptyRecipeChoice() implements RecipeChoice {
++
++ static final RecipeChoice INSTANCE = new EmptyRecipeChoice();
++ @Override
++ public ItemStack getItemStack() {
++ throw new UnsupportedOperationException("This is an empty RecipeChoice");
++ }
++
++ @SuppressWarnings("MethodDoesntCallSuperMethod")
++ @Override
++ public RecipeChoice clone() {
++ return this;
++ }
++
++ @Override
++ public boolean test(final ItemStack itemStack) {
++ return false;
++ }
++
++ @Override
++ public RecipeChoice validate(final boolean allowEmptyRecipes) {
++ if (allowEmptyRecipes) return this;
++ throw new IllegalArgumentException("empty RecipeChoice isn't allowed here");
++ }
++}
diff --git a/src/main/java/org/bukkit/inventory/MerchantRecipe.java b/src/main/java/org/bukkit/inventory/MerchantRecipe.java
index 39f9766a03d420340d79841197f75c8b1dd49f4a..4e59f5176fd6cf92457ad750081c253a58790b61 100644
--- a/src/main/java/org/bukkit/inventory/MerchantRecipe.java
@@ -78,16 +126,36 @@ index 39f9766a03d420340d79841197f75c8b1dd49f4a..4e59f5176fd6cf92457ad750081c253a
}
}
diff --git a/src/main/java/org/bukkit/inventory/RecipeChoice.java b/src/main/java/org/bukkit/inventory/RecipeChoice.java
-index db8bcc66bdc4bedfffb4705db6338eda4c0ad29a..c143b0d8b97d514566bac8413d0346cf50822aeb 100644
+index db8bcc66bdc4bedfffb4705db6338eda4c0ad29a..1cd6601c9d2e86ef850011fcb9e59811207b4af7 100644
--- a/src/main/java/org/bukkit/inventory/RecipeChoice.java
+++ b/src/main/java/org/bukkit/inventory/RecipeChoice.java
-@@ -35,6 +35,13 @@ public interface RecipeChoice extends Predicate<ItemStack>, Cloneable {
+@@ -19,6 +19,19 @@ import org.jetbrains.annotations.NotNull;
+ */
+ public interface RecipeChoice extends Predicate<ItemStack>, Cloneable {
+
++ // Paper start - add "empty" choice
++ /**
++ * An "empty" recipe choice. Only valid as a recipe choice in
++ * specific places. Check the javadocs of a method before using it
++ * to be sure it's valid for that recipe and ingredient type.
++ *
++ * @return the empty recipe choice
++ */
++ static @NotNull RecipeChoice empty() {
++ return EmptyRecipeChoice.INSTANCE;
++ }
++ // Paper end
++
+ /**
+ * Gets a single item stack representative of this stack choice.
+ *
+@@ -35,6 +48,13 @@ public interface RecipeChoice extends Predicate<ItemStack>, Cloneable {
@Override
boolean test(@NotNull ItemStack itemStack);
+ // Paper start - check valid ingredients
+ @org.jetbrains.annotations.ApiStatus.Internal
-+ default @NotNull RecipeChoice validate() {
++ default @NotNull RecipeChoice validate(final boolean allowEmptyRecipes) {
+ return this;
+ }
+ // Paper end - check valid ingredients
@@ -95,7 +163,24 @@ index db8bcc66bdc4bedfffb4705db6338eda4c0ad29a..c143b0d8b97d514566bac8413d0346cf
/**
* Represents a choice of multiple matching Materials.
*/
-@@ -185,7 +192,12 @@ public interface RecipeChoice extends Predicate<ItemStack>, Cloneable {
+@@ -141,6 +161,16 @@ public interface RecipeChoice extends Predicate<ItemStack>, Cloneable {
+ public String toString() {
+ return "MaterialChoice{" + "choices=" + choices + '}';
+ }
++
++ // Paper start - check valid ingredients
++ @Override
++ public @NotNull RecipeChoice validate(final boolean allowEmptyRecipes) {
++ if (this.choices.stream().anyMatch(Material::isAir)) {
++ throw new IllegalArgumentException("RecipeChoice.MaterialChoice cannot contain air");
++ }
++ return this;
++ }
++ // Paper end - check valid ingredients
+ }
+
+ /**
+@@ -185,7 +215,12 @@ public interface RecipeChoice extends Predicate<ItemStack>, Cloneable {
public ExactChoice clone() {
try {
ExactChoice clone = (ExactChoice) super.clone();
@@ -109,14 +194,14 @@ index db8bcc66bdc4bedfffb4705db6338eda4c0ad29a..c143b0d8b97d514566bac8413d0346cf
return clone;
} catch (CloneNotSupportedException ex) {
throw new AssertionError(ex);
-@@ -232,5 +244,15 @@ public interface RecipeChoice extends Predicate<ItemStack>, Cloneable {
+@@ -232,5 +267,15 @@ public interface RecipeChoice extends Predicate<ItemStack>, Cloneable {
public String toString() {
return "ExactChoice{" + "choices=" + choices + '}';
}
+
+ // Paper start - check valid ingredients
+ @Override
-+ public @NotNull RecipeChoice validate() {
++ public @NotNull RecipeChoice validate(final boolean allowEmptyRecipes) {
+ if (this.choices.stream().anyMatch(s -> s.getType().isAir())) {
+ throw new IllegalArgumentException("RecipeChoice.ExactChoice cannot contain air");
+ }
@@ -126,7 +211,7 @@ index db8bcc66bdc4bedfffb4705db6338eda4c0ad29a..c143b0d8b97d514566bac8413d0346cf
}
}
diff --git a/src/main/java/org/bukkit/inventory/ShapedRecipe.java b/src/main/java/org/bukkit/inventory/ShapedRecipe.java
-index da878c6d4928ddbc16b50ace86d992685a2b7873..0547505987ee954adbac0cbe5a6ada3dfaedf034 100644
+index da878c6d4928ddbc16b50ace86d992685a2b7873..28e8d4e4f354b304f32cd8f14dcff7166a7bb37e 100644
--- a/src/main/java/org/bukkit/inventory/ShapedRecipe.java
+++ b/src/main/java/org/bukkit/inventory/ShapedRecipe.java
@@ -166,14 +166,15 @@ public class ShapedRecipe extends CraftingRecipe {
@@ -134,7 +219,7 @@ index da878c6d4928ddbc16b50ace86d992685a2b7873..0547505987ee954adbac0cbe5a6ada3d
Preconditions.checkArgument(ingredients.containsKey(key), "Symbol does not appear in the shape:", key);
- ingredients.put(key, ingredient);
-+ ingredients.put(key, ingredient.validate().clone()); // Paper
++ ingredients.put(key, ingredient.validate(false).clone()); // Paper
return this;
}
@@ -148,7 +233,7 @@ index da878c6d4928ddbc16b50ace86d992685a2b7873..0547505987ee954adbac0cbe5a6ada3d
// Paper end
diff --git a/src/main/java/org/bukkit/inventory/ShapelessRecipe.java b/src/main/java/org/bukkit/inventory/ShapelessRecipe.java
-index beb798482479c58a8628c314b510ab6349576ce8..6a8195ca224790e3130ec9923188e7b585797263 100644
+index beb798482479c58a8628c314b510ab6349576ce8..8251170314ab25c26270208e453b4e3909435754 100644
--- a/src/main/java/org/bukkit/inventory/ShapelessRecipe.java
+++ b/src/main/java/org/bukkit/inventory/ShapelessRecipe.java
@@ -131,7 +131,7 @@ public class ShapelessRecipe extends CraftingRecipe {
@@ -156,7 +241,7 @@ index beb798482479c58a8628c314b510ab6349576ce8..6a8195ca224790e3130ec9923188e7b5
Preconditions.checkArgument(ingredients.size() + 1 <= 9, "Shapeless recipes cannot have more than 9 ingredients");
- ingredients.add(ingredient);
-+ ingredients.add(ingredient.validate().clone()); // Paper
++ ingredients.add(ingredient.validate(false).clone()); // Paper
return this;
}
@@ -170,7 +255,7 @@ index beb798482479c58a8628c314b510ab6349576ce8..6a8195ca224790e3130ec9923188e7b5
ingredients.add(new RecipeChoice.ExactChoice(item));
}
diff --git a/src/main/java/org/bukkit/inventory/SmithingRecipe.java b/src/main/java/org/bukkit/inventory/SmithingRecipe.java
-index 1ef9a715a2736e88a16083c6873803a8bd6bcf29..a8f451dc1d8d49c2080c3ed3d8348020c3d470cf 100644
+index 1ef9a715a2736e88a16083c6873803a8bd6bcf29..3072858dd4413129ec1737572838c2ea5ffd84bc 100644
--- a/src/main/java/org/bukkit/inventory/SmithingRecipe.java
+++ b/src/main/java/org/bukkit/inventory/SmithingRecipe.java
@@ -44,12 +44,13 @@ public class SmithingRecipe implements Recipe, Keyed {
@@ -184,48 +269,94 @@ index 1ef9a715a2736e88a16083c6873803a8bd6bcf29..a8f451dc1d8d49c2080c3ed3d8348020
this.result = result;
- this.base = base;
- this.addition = addition;
-+ this.base = base.validate().clone(); // Paper
-+ this.addition = addition.validate().clone(); // Paper
++ this.base = base.validate(true).clone(); // Paper
++ this.addition = addition.validate(true).clone(); // Paper
}
/**
diff --git a/src/main/java/org/bukkit/inventory/SmithingTransformRecipe.java b/src/main/java/org/bukkit/inventory/SmithingTransformRecipe.java
-index 68e7132d77151b7b8312638d8bb79ea59e2fa5a6..d3a7070a51d15531fb6f917ca87196dfa08f83aa 100644
+index 68e7132d77151b7b8312638d8bb79ea59e2fa5a6..60bfdd6b8814be8e3ffdfaef8a5ac7eeff9a5830 100644
--- a/src/main/java/org/bukkit/inventory/SmithingTransformRecipe.java
+++ b/src/main/java/org/bukkit/inventory/SmithingTransformRecipe.java
-@@ -21,7 +21,7 @@ public class SmithingTransformRecipe extends SmithingRecipe {
+@@ -15,13 +15,13 @@ public class SmithingTransformRecipe extends SmithingRecipe {
+ *
+ * @param key The unique recipe key
+ * @param result The item you want the recipe to create.
+- * @param template The template item.
+- * @param base The base ingredient
+- * @param addition The addition ingredient
++ * @param template The template item ({@link RecipeChoice#empty()} can be used)
++ * @param base The base ingredient ({@link RecipeChoice#empty()} can be used)
++ * @param addition The addition ingredient ({@link RecipeChoice#empty()} can be used)
*/
public SmithingTransformRecipe(@NotNull NamespacedKey key, @NotNull ItemStack result, @NotNull RecipeChoice template, @NotNull RecipeChoice base, @NotNull RecipeChoice addition) {
super(key, result, base, addition);
- this.template = template;
-+ this.template = template.validate().clone(); // Paper
++ this.template = template.validate(true).clone(); // Paper
}
// Paper start
/**
+@@ -29,14 +29,14 @@ public class SmithingTransformRecipe extends SmithingRecipe {
+ *
+ * @param key The unique recipe key
+ * @param result The item you want the recipe to create.
+- * @param template The template item.
+- * @param base The base ingredient
+- * @param addition The addition ingredient
++ * @param template The template item ({@link RecipeChoice#empty()} can be used)
++ * @param base The base ingredient ({@link RecipeChoice#empty()} can be used)
++ * @param addition The addition ingredient ({@link RecipeChoice#empty()} can be used)
+ * @param copyDataComponents whether to copy the data components from the input base item to the output
+ */
+ public SmithingTransformRecipe(@NotNull NamespacedKey key, @NotNull ItemStack result, @NotNull RecipeChoice template, @NotNull RecipeChoice base, @NotNull RecipeChoice addition, boolean copyDataComponents) {
+ super(key, result, base, addition, copyDataComponents);
+- this.template = template;
++ this.template = template.validate(true).clone();
+ }
+ // Paper end
+
diff --git a/src/main/java/org/bukkit/inventory/SmithingTrimRecipe.java b/src/main/java/org/bukkit/inventory/SmithingTrimRecipe.java
-index ce36bb5b030f17e11f74e987235be143c1925aa7..6316112074a0708734106ca9de5ef968df52ce0e 100644
+index ce36bb5b030f17e11f74e987235be143c1925aa7..9ac27cbe7dcff0403ef34727d0461b8201aca6f1 100644
--- a/src/main/java/org/bukkit/inventory/SmithingTrimRecipe.java
+++ b/src/main/java/org/bukkit/inventory/SmithingTrimRecipe.java
-@@ -21,7 +21,7 @@ public class SmithingTrimRecipe extends SmithingRecipe implements ComplexRecipe
+@@ -15,27 +15,27 @@ public class SmithingTrimRecipe extends SmithingRecipe implements ComplexRecipe
+ * Create a smithing recipe to produce the specified result ItemStack.
+ *
+ * @param key The unique recipe key
+- * @param template The template item.
+- * @param base The base ingredient
+- * @param addition The addition ingredient
++ * @param template The template item ({@link RecipeChoice#empty()} can be used)
++ * @param base The base ingredient ({@link RecipeChoice#empty()} can be used)
++ * @param addition The addition ingredient ({@link RecipeChoice#empty()} can be used)
*/
public SmithingTrimRecipe(@NotNull NamespacedKey key, @NotNull RecipeChoice template, @NotNull RecipeChoice base, @NotNull RecipeChoice addition) {
super(key, new ItemStack(Material.AIR), base, addition);
- this.template = template;
-+ this.template = template.validate().clone(); // Paper
++ this.template = template.validate(true).clone(); // Paper
}
// Paper start
/**
-@@ -35,7 +35,7 @@ public class SmithingTrimRecipe extends SmithingRecipe implements ComplexRecipe
+ * Create a smithing recipe to produce the specified result ItemStack.
+ *
+ * @param key The unique recipe key
+- * @param template The template item.
+- * @param base The base ingredient
+- * @param addition The addition ingredient
++ * @param template The template item. ({@link RecipeChoice#empty()} can be used)
++ * @param base The base ingredient ({@link RecipeChoice#empty()} can be used)
++ * @param addition The addition ingredient ({@link RecipeChoice#empty()} can be used)
+ * @param copyDataComponents whether to copy the data components from the input base item to the output
*/
public SmithingTrimRecipe(@NotNull NamespacedKey key, @NotNull RecipeChoice template, @NotNull RecipeChoice base, @NotNull RecipeChoice addition, boolean copyDataComponents) {
super(key, new ItemStack(Material.AIR), base, addition, copyDataComponents);
- this.template = template;
-+ this.template = template.validate().clone(); // Paper
++ this.template = template.validate(true).clone(); // Paper
}
// Paper end
diff --git a/src/main/java/org/bukkit/inventory/StonecuttingRecipe.java b/src/main/java/org/bukkit/inventory/StonecuttingRecipe.java
-index bc3440eb72127824b3961fbdae583bb61385f65e..802c23a86d301a5336a97a8256182da7d792ec1d 100644
+index bc3440eb72127824b3961fbdae583bb61385f65e..17b33f8e6e3dc6a22686a498fa944382e8767077 100644
--- a/src/main/java/org/bukkit/inventory/StonecuttingRecipe.java
+++ b/src/main/java/org/bukkit/inventory/StonecuttingRecipe.java
@@ -35,10 +35,10 @@ public class StonecuttingRecipe implements Recipe, Keyed {
@@ -237,7 +368,7 @@ index bc3440eb72127824b3961fbdae583bb61385f65e..802c23a86d301a5336a97a8256182da7
this.key = key;
this.output = new ItemStack(result);
- this.ingredient = input;
-+ this.ingredient = input.validate().clone(); // Paper
++ this.ingredient = input.validate(false).clone(); // Paper
}
/**
@@ -246,7 +377,7 @@ index bc3440eb72127824b3961fbdae583bb61385f65e..802c23a86d301a5336a97a8256182da7
@NotNull
public StonecuttingRecipe setInputChoice(@NotNull RecipeChoice input) {
- this.ingredient = input;
-+ this.ingredient = input.validate().clone(); // Paper
++ this.ingredient = input.validate(false).clone(); // Paper
return (StonecuttingRecipe) this;
}
diff --git a/patches/server/1046-Fix-issues-with-Recipe-API.patch b/patches/server/1046-Fix-issues-with-Recipe-API.patch
new file mode 100644
index 0000000000..a5312e5dc7
--- /dev/null
+++ b/patches/server/1046-Fix-issues-with-Recipe-API.patch
@@ -0,0 +1,116 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <[email protected]>
+Date: Sun, 12 May 2024 15:49:36 -0700
+Subject: [PATCH] Fix issues with Recipe API
+
+
+diff --git a/src/main/java/net/minecraft/world/item/crafting/ShapedRecipe.java b/src/main/java/net/minecraft/world/item/crafting/ShapedRecipe.java
+index 482d7b12b80328fba97a01bcfeb974b7ac4bcdb7..fb4c8a2a15e8e3e26454b7da920454e9861336c6 100644
+--- a/src/main/java/net/minecraft/world/item/crafting/ShapedRecipe.java
++++ b/src/main/java/net/minecraft/world/item/crafting/ShapedRecipe.java
+@@ -91,7 +91,7 @@ public class ShapedRecipe extends io.papermc.paper.inventory.recipe.RecipeBookEx
+ char c = 'a';
+ for (Ingredient list : this.pattern.ingredients()) {
+ RecipeChoice choice = CraftRecipe.toBukkit(list);
+- if (choice != null) {
++ if (choice != RecipeChoice.empty()) { // Paper
+ recipe.setIngredient(c, choice);
+ }
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java
+index 6ba29875d78ede4aa7978ff689e588f7fed11528..c76c78bb7757d407102271463e14716a1b012deb 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java
+@@ -29,6 +29,10 @@ public interface CraftRecipe extends Recipe {
+ } else if (bukkit instanceof RecipeChoice.ExactChoice) {
+ stack = new Ingredient(((RecipeChoice.ExactChoice) bukkit).getChoices().stream().map((mat) -> new net.minecraft.world.item.crafting.Ingredient.ItemValue(CraftItemStack.asNMSCopy(mat))));
+ stack.exact = true;
++ // Paper start - support "empty" choices
++ } else if (bukkit == RecipeChoice.empty()) {
++ stack = Ingredient.EMPTY;
++ // Paper end
+ } else {
+ throw new IllegalArgumentException("Unknown recipe stack instance " + bukkit);
+ }
+@@ -45,7 +49,7 @@ public interface CraftRecipe extends Recipe {
+ list.getItems();
+
+ if (list.itemStacks.length == 0) {
+- return null;
++ return RecipeChoice.empty(); // Paper - null breaks API contracts
+ }
+
+ if (list.exact) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftSmithingTransformRecipe.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftSmithingTransformRecipe.java
+index 38690b28b6f67624d68877c1e89ebe30b402b233..3aec771478a6b17353d57e82baac53dd24779e7b 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftSmithingTransformRecipe.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftSmithingTransformRecipe.java
+@@ -30,6 +30,6 @@ public class CraftSmithingTransformRecipe extends SmithingTransformRecipe implem
+ public void addToCraftingManager() {
+ ItemStack result = this.getResult();
+
+- MinecraftServer.getServer().getRecipeManager().addRecipe(new RecipeHolder<>(CraftNamespacedKey.toMinecraft(this.getKey()), new net.minecraft.world.item.crafting.SmithingTransformRecipe(this.toNMS(this.getTemplate(), true), this.toNMS(this.getBase(), true), this.toNMS(this.getAddition(), true), CraftItemStack.asNMSCopy(result), this.willCopyDataComponents()))); // Paper - Option to prevent data components copy
++ MinecraftServer.getServer().getRecipeManager().addRecipe(new RecipeHolder<>(CraftNamespacedKey.toMinecraft(this.getKey()), new net.minecraft.world.item.crafting.SmithingTransformRecipe(this.toNMS(this.getTemplate(), false), this.toNMS(this.getBase(), false), this.toNMS(this.getAddition(), false), CraftItemStack.asNMSCopy(result), this.willCopyDataComponents()))); // Paper - Option to prevent data components copy & support empty RecipeChoice
+ }
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftSmithingTrimRecipe.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftSmithingTrimRecipe.java
+index 5d7782b168138383c606a2c52fbdebe1732364ac..61af2fe3534ff67f10310c6c7dec39cff0f93ee3 100644
+--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftSmithingTrimRecipe.java
++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftSmithingTrimRecipe.java
+@@ -28,6 +28,6 @@ public class CraftSmithingTrimRecipe extends SmithingTrimRecipe implements Craft
+
+ @Override
+ public void addToCraftingManager() {
+- MinecraftServer.getServer().getRecipeManager().addRecipe(new RecipeHolder<>(CraftNamespacedKey.toMinecraft(this.getKey()), new net.minecraft.world.item.crafting.SmithingTrimRecipe(this.toNMS(this.getTemplate(), true), this.toNMS(this.getBase(), true), this.toNMS(this.getAddition(), true), this.willCopyDataComponents()))); // Paper - Option to prevent data components copy
++ MinecraftServer.getServer().getRecipeManager().addRecipe(new RecipeHolder<>(CraftNamespacedKey.toMinecraft(this.getKey()), new net.minecraft.world.item.crafting.SmithingTrimRecipe(this.toNMS(this.getTemplate(), false), this.toNMS(this.getBase(), false), this.toNMS(this.getAddition(), false), this.willCopyDataComponents()))); // Paper - Option to prevent data components copy & support empty RecipeChoice
+ }
+ }
+diff --git a/src/test/java/io/papermc/paper/inventory/recipe/TestRecipeChoice.java b/src/test/java/io/papermc/paper/inventory/recipe/TestRecipeChoice.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b6816485a2360b936c049b398183658ee18813ec
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/inventory/recipe/TestRecipeChoice.java
+@@ -0,0 +1,24 @@
++package io.papermc.paper.inventory.recipe;
++
++import java.util.Iterator;
++import org.bukkit.Bukkit;
++import org.bukkit.inventory.Recipe;
++import org.bukkit.support.AbstractTestingBase;
++import org.junit.jupiter.api.Test;
++
++import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
++import static org.junit.jupiter.api.Assertions.assertTrue;
++
++class TestRecipeChoice extends AbstractTestingBase {
++
++ @Test
++ void testRecipeChoices() {
++ final Iterator<Recipe> iter = Bukkit.recipeIterator();
++ boolean foundRecipes = false;
++ while (iter.hasNext()) {
++ foundRecipes = true;
++ assertDoesNotThrow(iter::next, "Failed to convert a recipe to Bukkit recipe!");
++ }
++ assertTrue(foundRecipes, "No recipes found!");
++ }
++}
+diff --git a/src/test/java/org/bukkit/support/DummyServer.java b/src/test/java/org/bukkit/support/DummyServer.java
+index d7e24766f383f75ed46123fff1bd0ec926a635b4..a73c16bb7923957113e688fa6fe46cbd68837d3e 100644
+--- a/src/test/java/org/bukkit/support/DummyServer.java
++++ b/src/test/java/org/bukkit/support/DummyServer.java
+@@ -56,6 +56,14 @@ public final class DummyServer {
+ when(instance.getTag(anyString(), any(org.bukkit.NamespacedKey.class), any())).thenAnswer(ignored -> new io.papermc.paper.util.EmptyTag());
+ // paper end - testing additions
+
++ // Paper start - add test for recipe conversion
++ when(instance.recipeIterator()).thenAnswer(ignored -> {
++ return com.google.common.collect.Iterators.transform(
++ AbstractTestingBase.DATA_PACK.getRecipeManager().byType.entries().iterator(),
++ input -> input.getValue().toBukkitRecipe());
++ });
++ // Paper end - add test for recipe conversion
++
+ Bukkit.setServer(instance);
+ } catch (Throwable t) {
+ throw new Error(t);