aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/api/0465-Fix-issues-with-recipe-API.patch
diff options
context:
space:
mode:
Diffstat (limited to 'patches/api/0465-Fix-issues-with-recipe-API.patch')
-rw-r--r--patches/api/0465-Fix-issues-with-recipe-API.patch443
1 files changed, 443 insertions, 0 deletions
diff --git a/patches/api/0465-Fix-issues-with-recipe-API.patch b/patches/api/0465-Fix-issues-with-recipe-API.patch
new file mode 100644
index 0000000000..646143b638
--- /dev/null
+++ b/patches/api/0465-Fix-issues-with-recipe-API.patch
@@ -0,0 +1,443 @@
+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] 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.
+
+Also adds some validation regarding Materials passed to RecipeChoice
+being items.
+
+diff --git a/src/main/java/org/bukkit/inventory/CookingRecipe.java b/src/main/java/org/bukkit/inventory/CookingRecipe.java
+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,
+ * @param cookingTime The cooking time (in ticks)
+ */
+ public CookingRecipe(@NotNull NamespacedKey key, @NotNull ItemStack result, @NotNull RecipeChoice input, float experience, int cookingTime) {
+- Preconditions.checkArgument(result.getType() != Material.AIR, "Recipe must have non-AIR result.");
++ Preconditions.checkArgument(!result.isEmpty(), "Recipe cannot have an empty result."); // Paper
+ this.key = key;
+ this.output = new ItemStack(result);
+- this.ingredient = input;
++ this.ingredient = input.validate(false).clone(); // Paper
+ this.experience = experience;
+ this.cookingTime = cookingTime;
+ }
+@@ -84,7 +84,7 @@ public abstract class CookingRecipe<T extends CookingRecipe> implements Recipe,
+ */
+ @NotNull
+ public T setInputChoice(@NotNull RecipeChoice input) {
+- this.ingredient = input;
++ this.ingredient = input.validate(false).clone(); // Paper
+ return (T) this;
+ }
+
+diff --git a/src/main/java/org/bukkit/inventory/CraftingRecipe.java b/src/main/java/org/bukkit/inventory/CraftingRecipe.java
+index 1b7b07715067014bf3d35002ae1655793248b426..5bf55b40fbf6ec708f37d90bd0853fe7dd8fffd9 100644
+--- a/src/main/java/org/bukkit/inventory/CraftingRecipe.java
++++ b/src/main/java/org/bukkit/inventory/CraftingRecipe.java
+@@ -99,7 +99,7 @@ public abstract class CraftingRecipe implements Recipe, Keyed {
+ @ApiStatus.Internal
+ @NotNull
+ protected static ItemStack checkResult(@NotNull ItemStack result) {
+- Preconditions.checkArgument(result.getType() != Material.AIR, "Recipe must have non-AIR result.");
++ Preconditions.checkArgument(!result.isEmpty(), "Recipe cannot have an empty result."); // Paper
+ return 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..a95ecc97fd6507103f9264a686ec1c061fa725b1
+--- /dev/null
++++ b/src/main/java/org/bukkit/inventory/EmptyRecipeChoice.java
+@@ -0,0 +1,32 @@
++package org.bukkit.inventory;
++
++import org.jetbrains.annotations.ApiStatus;
++import org.jspecify.annotations.NullMarked;
++
++@NullMarked
++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
++++ b/src/main/java/org/bukkit/inventory/MerchantRecipe.java
+@@ -79,6 +79,7 @@ public class MerchantRecipe implements Recipe {
+ this(result, uses, maxUses, experienceReward, villagerExperience, priceMultiplier, 0, 0, ignoreDiscounts);
+ }
+ public MerchantRecipe(@NotNull ItemStack result, int uses, int maxUses, boolean experienceReward, int villagerExperience, float priceMultiplier, int demand, int specialPrice, boolean ignoreDiscounts) {
++ Preconditions.checkArgument(!result.isEmpty(), "Recipe cannot have an empty result."); // Paper
+ this.ignoreDiscounts = ignoreDiscounts;
+ // Paper end
+ this.result = result;
+@@ -101,11 +102,12 @@ public class MerchantRecipe implements Recipe {
+ @NotNull
+ @Override
+ public ItemStack getResult() {
+- return result;
++ return result.clone(); // Paper
+ }
+
+ public void addIngredient(@NotNull ItemStack item) {
+ Preconditions.checkState(ingredients.size() < 2, "MerchantRecipe can only have maximum 2 ingredients");
++ Preconditions.checkArgument(!item.isEmpty(), "Recipe cannot have an empty itemstack ingredient."); // Paper
+ ingredients.add(item.clone());
+ }
+
+@@ -117,6 +119,7 @@ public class MerchantRecipe implements Recipe {
+ Preconditions.checkState(ingredients.size() <= 2, "MerchantRecipe can only have maximum 2 ingredients");
+ this.ingredients = new ArrayList<ItemStack>();
+ for (ItemStack item : ingredients) {
++ Preconditions.checkArgument(!item.isEmpty(), "Recipe cannot have an empty itemstack ingredient."); // Paper
+ this.ingredients.add(item.clone());
+ }
+ }
+diff --git a/src/main/java/org/bukkit/inventory/RecipeChoice.java b/src/main/java/org/bukkit/inventory/RecipeChoice.java
+index 91bfeffcdbe47208c7d0ddbe013cd0f11fddfa32..f1aa67997f904953742e8895e49341c2f73d44a2 100644
+--- a/src/main/java/org/bukkit/inventory/RecipeChoice.java
++++ b/src/main/java/org/bukkit/inventory/RecipeChoice.java
+@@ -22,6 +22,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.
+ *
+@@ -38,6 +51,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(final boolean allowEmptyRecipes) {
++ return this;
++ }
++ // Paper end - check valid ingredients
++
+ /**
+ * Represents a choice of multiple matching Materials.
+ */
+@@ -60,8 +80,7 @@ public interface RecipeChoice extends Predicate<ItemStack>, Cloneable {
+ * @param choices the tag
+ */
+ public MaterialChoice(@NotNull Tag<Material> choices) {
+- Preconditions.checkArgument(choices != null, "choices");
+- this.choices = new ArrayList<>(choices.getValues());
++ this(new ArrayList<>(java.util.Objects.requireNonNull(choices, "Cannot create a material choice with null tag").getValues())); // Paper - delegate to list ctor to make sure all checks are called
+ }
+
+ public MaterialChoice(@NotNull List<Material> choices) {
+@@ -78,6 +97,7 @@ public interface RecipeChoice extends Predicate<ItemStack>, Cloneable {
+ }
+
+ Preconditions.checkArgument(!choice.isAir(), "Cannot have empty/air choice");
++ Preconditions.checkArgument(choice.isItem(), "Cannot have non-item choice %s", choice); // Paper - validate material choice input to items
+ this.choices.add(choice);
+ }
+ }
+@@ -152,6 +172,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
+ }
+
+ /**
+@@ -197,7 +227,12 @@ public interface RecipeChoice extends Predicate<ItemStack>, Cloneable {
+ public ExactChoice clone() {
+ try {
+ ExactChoice clone = (ExactChoice) super.clone();
+- clone.choices = new ArrayList<>(choices);
++ // Paper start - properly clone
++ clone.choices = new ArrayList<>(this.choices.size());
++ for (ItemStack choice : this.choices) {
++ clone.choices.add(choice.clone());
++ }
++ // Paper end - properly clone
+ return clone;
+ } catch (CloneNotSupportedException ex) {
+ throw new AssertionError(ex);
+@@ -244,5 +279,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(final boolean allowEmptyRecipes) {
++ if (this.choices.stream().anyMatch(s -> s.getType().isAir())) {
++ throw new IllegalArgumentException("RecipeChoice.ExactChoice cannot contain air");
++ }
++ return this;
++ }
++ // Paper end - check valid ingredients
+ }
+ }
+diff --git a/src/main/java/org/bukkit/inventory/ShapedRecipe.java b/src/main/java/org/bukkit/inventory/ShapedRecipe.java
+index 295d82dd73b600e9436d2bbec0e11dbeaf78bbf4..c0105d716985acef497d60b5c631a56b4ca5847b 100644
+--- a/src/main/java/org/bukkit/inventory/ShapedRecipe.java
++++ b/src/main/java/org/bukkit/inventory/ShapedRecipe.java
+@@ -178,14 +178,15 @@ public class ShapedRecipe extends CraftingRecipe {
+ Preconditions.checkArgument(key != ' ', "Space in recipe shape must represent no ingredient");
+ Preconditions.checkArgument(ingredients.containsKey(key), "Symbol does not appear in the shape:", key);
+
+- ingredients.put(key, ingredient);
++ ingredients.put(key, ingredient.validate(false).clone()); // Paper
+ return this;
+ }
+
+ // Paper start
+ @NotNull
+ public ShapedRecipe setIngredient(char key, @NotNull ItemStack item) {
+- return setIngredient(key, new RecipeChoice.ExactChoice(item));
++ Preconditions.checkArgument(!item.getType().isAir(), "Item cannot be air"); // Paper
++ return setIngredient(key, new RecipeChoice.ExactChoice(item.clone())); // Paper
+ }
+ // Paper end
+
+diff --git a/src/main/java/org/bukkit/inventory/ShapelessRecipe.java b/src/main/java/org/bukkit/inventory/ShapelessRecipe.java
+index 3bf5064cd6ceb05ea98b18993da46c67be140115..79db6dbc0367de2eaa397674624c765d5aeb8fa5 100644
+--- a/src/main/java/org/bukkit/inventory/ShapelessRecipe.java
++++ b/src/main/java/org/bukkit/inventory/ShapelessRecipe.java
+@@ -132,7 +132,7 @@ public class ShapelessRecipe extends CraftingRecipe {
+ public ShapelessRecipe addIngredient(@NotNull RecipeChoice ingredient) {
+ Preconditions.checkArgument(ingredients.size() + 1 <= 9, "Shapeless recipes cannot have more than 9 ingredients");
+
+- ingredients.add(ingredient);
++ ingredients.add(ingredient.validate(false).clone()); // Paper
+ return this;
+ }
+
+@@ -145,6 +145,8 @@ public class ShapelessRecipe extends CraftingRecipe {
+ @NotNull
+ public ShapelessRecipe addIngredient(int count, @NotNull ItemStack item) {
+ Preconditions.checkArgument(ingredients.size() + count <= 9, "Shapeless recipes cannot have more than 9 ingredients");
++ Preconditions.checkArgument(!item.getType().isAir(), "Item cannot be air"); // Paper
++ item = item.clone(); // Paper
+ while (count-- > 0) {
+ 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 ee462ca9fd3e0ddcdb0fffd5dba91d82fa6ad08f..0fb110a995bddcdf09b1902784e43cbe67510fba 100644
+--- a/src/main/java/org/bukkit/inventory/SmithingRecipe.java
++++ b/src/main/java/org/bukkit/inventory/SmithingRecipe.java
+@@ -45,12 +45,13 @@ public class SmithingRecipe implements Recipe, Keyed {
+ */
+ @Deprecated
+ public SmithingRecipe(@NotNull NamespacedKey key, @NotNull ItemStack result, @Nullable RecipeChoice base, @Nullable RecipeChoice addition, boolean copyDataComponents) {
++ com.google.common.base.Preconditions.checkArgument(!result.isEmpty() || this instanceof ComplexRecipe, "Recipe cannot have an empty result."); // Paper
+ this.copyDataComponents = copyDataComponents;
+ // Paper end
+ this.key = key;
+ this.result = result;
+- this.base = base;
+- this.addition = addition;
++ this.base = base == null ? RecipeChoice.empty() : base.validate(true).clone(); // Paper
++ this.addition = addition == null ? RecipeChoice.empty() : addition.validate(true).clone(); // Paper
+ }
+
+ /**
+@@ -58,7 +59,7 @@ public class SmithingRecipe implements Recipe, Keyed {
+ *
+ * @return base choice
+ */
+- @Nullable
++ @NotNull // Paper - fix issues with recipe api
+ public RecipeChoice getBase() {
+ return (base != null) ? base.clone() : null;
+ }
+@@ -68,7 +69,7 @@ public class SmithingRecipe implements Recipe, Keyed {
+ *
+ * @return addition choice
+ */
+- @Nullable
++ @NotNull // Paper - fix issues with recipe api
+ public RecipeChoice getAddition() {
+ return (addition != null) ? addition.clone() : null;
+ }
+diff --git a/src/main/java/org/bukkit/inventory/SmithingTransformRecipe.java b/src/main/java/org/bukkit/inventory/SmithingTransformRecipe.java
+index e5726da0507ee70cb9dd76c57da6a8442e771307..1ca5058ee3bf75e1f70c8bc33842b466f65a6240 100644
+--- a/src/main/java/org/bukkit/inventory/SmithingTransformRecipe.java
++++ b/src/main/java/org/bukkit/inventory/SmithingTransformRecipe.java
+@@ -16,13 +16,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, @Nullable RecipeChoice template, @Nullable RecipeChoice base, @Nullable RecipeChoice addition) {
++ public SmithingTransformRecipe(@NotNull NamespacedKey key, @NotNull ItemStack result, @NotNull RecipeChoice template, @NotNull RecipeChoice base, @NotNull RecipeChoice addition) { // Paper - fix issues with recipe api - prevent null choices
+ super(key, result, base, addition);
+- this.template = template;
++ this.template = template == null ? RecipeChoice.empty() : template.validate(true).clone(); // Paper - fix issues with recipe api - prevent null choices
+ }
+ // Paper start
+ /**
+@@ -30,14 +30,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, @Nullable RecipeChoice template, @Nullable RecipeChoice base, @Nullable RecipeChoice addition, boolean copyDataComponents) {
++ 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 == null ? RecipeChoice.empty() : template.validate(true).clone(); // Paper - fix issues with recipe api - prevent null choices
+ }
+ // Paper end
+
+@@ -46,7 +46,7 @@ public class SmithingTransformRecipe extends SmithingRecipe {
+ *
+ * @return template choice
+ */
+- @Nullable
++ @NotNull // Paper - fix issues with recipe api - prevent null choices
+ public RecipeChoice getTemplate() {
+ return (template != null) ? template.clone() : null;
+ }
+diff --git a/src/main/java/org/bukkit/inventory/SmithingTrimRecipe.java b/src/main/java/org/bukkit/inventory/SmithingTrimRecipe.java
+index 232aa8aeef9e34146e413e56b3681919c8850687..a8e125ef6dd9e8d6af74b68088df91d4c2ea7a8a 100644
+--- a/src/main/java/org/bukkit/inventory/SmithingTrimRecipe.java
++++ b/src/main/java/org/bukkit/inventory/SmithingTrimRecipe.java
+@@ -16,27 +16,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, @Nullable RecipeChoice template, @Nullable RecipeChoice base, @Nullable RecipeChoice addition) {
++ public SmithingTrimRecipe(@NotNull NamespacedKey key, @NotNull RecipeChoice template, @NotNull RecipeChoice base, @NotNull RecipeChoice addition) { // Paper - fix issues with recipe api - prevent null choices
+ super(key, new ItemStack(Material.AIR), base, addition);
+- this.template = template;
++ this.template = template == null ? RecipeChoice.empty() : template.validate(true).clone(); // Paper
+ }
+ // Paper start
+ /**
+ * 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) {
++ public SmithingTrimRecipe(@NotNull NamespacedKey key, @NotNull RecipeChoice template, @NotNull RecipeChoice base, @NotNull RecipeChoice addition, boolean copyDataComponents) { // Paper - fix issues with recipe api - prevent null choices
+ super(key, new ItemStack(Material.AIR), base, addition, copyDataComponents);
+- this.template = template;
++ this.template = template == null ? RecipeChoice.empty() : template.validate(true).clone(); // Paper
+ }
+ // Paper end
+
+@@ -45,7 +45,7 @@ public class SmithingTrimRecipe extends SmithingRecipe implements ComplexRecipe
+ *
+ * @return template choice
+ */
+- @Nullable
++ @NotNull // Paper - fix issues with recipe api - prevent null choices
+ public RecipeChoice getTemplate() {
+ return (template != null) ? template.clone() : null;
+ }
+diff --git a/src/main/java/org/bukkit/inventory/StonecuttingRecipe.java b/src/main/java/org/bukkit/inventory/StonecuttingRecipe.java
+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 {
+ * @param input The input choices.
+ */
+ public StonecuttingRecipe(@NotNull NamespacedKey key, @NotNull ItemStack result, @NotNull RecipeChoice input) {
+- Preconditions.checkArgument(result.getType() != Material.AIR, "Recipe must have non-AIR result.");
++ Preconditions.checkArgument(!result.isEmpty(), "Recipe cannot have an empty result."); // Paper
+ this.key = key;
+ this.output = new ItemStack(result);
+- this.ingredient = input;
++ this.ingredient = input.validate(false).clone(); // Paper
+ }
+
+ /**
+@@ -73,7 +73,7 @@ public class StonecuttingRecipe implements Recipe, Keyed {
+ */
+ @NotNull
+ public StonecuttingRecipe setInputChoice(@NotNull RecipeChoice input) {
+- this.ingredient = input;
++ this.ingredient = input.validate(false).clone(); // Paper
+ return (StonecuttingRecipe) this;
+ }
+