diff --git a/paper-api/src/main/java/org/bukkit/inventory/CookingRecipe.java b/paper-api/src/main/java/org/bukkit/inventory/CookingRecipe.java index f7fa79393a..07906ca1a9 100644 --- a/paper-api/src/main/java/org/bukkit/inventory/CookingRecipe.java +++ b/paper-api/src/main/java/org/bukkit/inventory/CookingRecipe.java @@ -44,10 +44,10 @@ public abstract class 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 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/paper-api/src/main/java/org/bukkit/inventory/CraftingRecipe.java b/paper-api/src/main/java/org/bukkit/inventory/CraftingRecipe.java index 1b7b077150..5bf55b40fb 100644 --- a/paper-api/src/main/java/org/bukkit/inventory/CraftingRecipe.java +++ b/paper-api/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/paper-api/src/main/java/org/bukkit/inventory/EmptyRecipeChoice.java b/paper-api/src/main/java/org/bukkit/inventory/EmptyRecipeChoice.java new file mode 100644 index 0000000000..a95ecc97fd --- /dev/null +++ b/paper-api/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; + +@ApiStatus.Internal +@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/paper-api/src/main/java/org/bukkit/inventory/MerchantRecipe.java b/paper-api/src/main/java/org/bukkit/inventory/MerchantRecipe.java index 39f9766a03..4e59f5176f 100644 --- a/paper-api/src/main/java/org/bukkit/inventory/MerchantRecipe.java +++ b/paper-api/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(); for (ItemStack item : ingredients) { + Preconditions.checkArgument(!item.isEmpty(), "Recipe cannot have an empty itemstack ingredient."); // Paper this.ingredients.add(item.clone()); } } diff --git a/paper-api/src/main/java/org/bukkit/inventory/RecipeChoice.java b/paper-api/src/main/java/org/bukkit/inventory/RecipeChoice.java index 653837b5f3..922bb69b5f 100644 --- a/paper-api/src/main/java/org/bukkit/inventory/RecipeChoice.java +++ b/paper-api/src/main/java/org/bukkit/inventory/RecipeChoice.java @@ -22,6 +22,19 @@ import org.jetbrains.annotations.NotNull; */ public interface RecipeChoice extends Predicate, 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, 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, Cloneable { * @param choices the tag */ public MaterialChoice(@NotNull Tag 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 choices) { @@ -78,6 +97,7 @@ public interface RecipeChoice extends Predicate, 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, 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, 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, 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/paper-api/src/main/java/org/bukkit/inventory/ShapedRecipe.java b/paper-api/src/main/java/org/bukkit/inventory/ShapedRecipe.java index af540a218d..f0268829c7 100644 --- a/paper-api/src/main/java/org/bukkit/inventory/ShapedRecipe.java +++ b/paper-api/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/paper-api/src/main/java/org/bukkit/inventory/ShapelessRecipe.java b/paper-api/src/main/java/org/bukkit/inventory/ShapelessRecipe.java index 449f1ea47f..e653110876 100644 --- a/paper-api/src/main/java/org/bukkit/inventory/ShapelessRecipe.java +++ b/paper-api/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/paper-api/src/main/java/org/bukkit/inventory/SmithingRecipe.java b/paper-api/src/main/java/org/bukkit/inventory/SmithingRecipe.java index eb1c83455e..75dc0b05d3 100644 --- a/paper-api/src/main/java/org/bukkit/inventory/SmithingRecipe.java +++ b/paper-api/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/paper-api/src/main/java/org/bukkit/inventory/SmithingTransformRecipe.java b/paper-api/src/main/java/org/bukkit/inventory/SmithingTransformRecipe.java index e5726da050..1ca5058ee3 100644 --- a/paper-api/src/main/java/org/bukkit/inventory/SmithingTransformRecipe.java +++ b/paper-api/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/paper-api/src/main/java/org/bukkit/inventory/SmithingTrimRecipe.java b/paper-api/src/main/java/org/bukkit/inventory/SmithingTrimRecipe.java index 232aa8aeef..a8e125ef6d 100644 --- a/paper-api/src/main/java/org/bukkit/inventory/SmithingTrimRecipe.java +++ b/paper-api/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/paper-api/src/main/java/org/bukkit/inventory/StonecuttingRecipe.java b/paper-api/src/main/java/org/bukkit/inventory/StonecuttingRecipe.java index bc3440eb72..17b33f8e6e 100644 --- a/paper-api/src/main/java/org/bukkit/inventory/StonecuttingRecipe.java +++ b/paper-api/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; } diff --git a/paper-api/src/main/java/org/bukkit/inventory/TransmuteRecipe.java b/paper-api/src/main/java/org/bukkit/inventory/TransmuteRecipe.java index 5a8616792a..8484b045ce 100644 --- a/paper-api/src/main/java/org/bukkit/inventory/TransmuteRecipe.java +++ b/paper-api/src/main/java/org/bukkit/inventory/TransmuteRecipe.java @@ -26,8 +26,8 @@ public class TransmuteRecipe extends CraftingRecipe implements ComplexRecipe { */ public TransmuteRecipe(@NotNull NamespacedKey key, @NotNull Material result, @NotNull RecipeChoice input, @NotNull RecipeChoice material) { super(key, checkResult(new ItemStack(result))); - this.input = input; - this.material = material; + this.input = input.validate(false).clone(); // Paper + this.material = material.validate(false).clone(); // Paper } /**