From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Jake Potrebic Date: Sun, 12 May 2024 10:42:42 -0700 Subject: [PATCH] Improve Recipe validation diff --git a/src/main/java/org/bukkit/inventory/CookingRecipe.java b/src/main/java/org/bukkit/inventory/CookingRecipe.java index f7fa79393aef40027446b78bac8e9490cfafd8bc..a64dae15db89038f3eb599f2a41edf45990f4fda 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 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().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().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 e4bf772f7e06f38215bee68f089b15a4fcb12817..37024b4736dd3897490ca51d08cf07901b01d59f 100644 --- a/src/main/java/org/bukkit/inventory/CraftingRecipe.java +++ b/src/main/java/org/bukkit/inventory/CraftingRecipe.java @@ -18,7 +18,7 @@ public abstract class CraftingRecipe implements Recipe, Keyed { protected CraftingRecipe(@NotNull NamespacedKey key, @NotNull ItemStack result) { Preconditions.checkArgument(key != null, "key cannot be null"); - 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); } 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(); 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 db8bcc66bdc4bedfffb4705db6338eda4c0ad29a..c143b0d8b97d514566bac8413d0346cf50822aeb 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, Cloneable { @Override boolean test(@NotNull ItemStack itemStack); + // Paper start - check valid ingredients + @org.jetbrains.annotations.ApiStatus.Internal + default @NotNull RecipeChoice validate() { + return this; + } + // Paper end - check valid ingredients + /** * Represents a choice of multiple matching Materials. */ @@ -185,7 +192,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); @@ -232,5 +244,15 @@ public interface RecipeChoice extends Predicate, Cloneable { public String toString() { return "ExactChoice{" + "choices=" + choices + '}'; } + + // Paper start - check valid ingredients + @Override + public @NotNull RecipeChoice validate() { + 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 da878c6d4928ddbc16b50ace86d992685a2b7873..0547505987ee954adbac0cbe5a6ada3dfaedf034 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 { public ShapedRecipe setIngredient(char key, @NotNull RecipeChoice ingredient) { Preconditions.checkArgument(ingredients.containsKey(key), "Symbol does not appear in the shape:", key); - ingredients.put(key, ingredient); + ingredients.put(key, ingredient.validate().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 beb798482479c58a8628c314b510ab6349576ce8..6a8195ca224790e3130ec9923188e7b585797263 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 { 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().clone()); // Paper return this; } @@ -144,6 +144,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 1ef9a715a2736e88a16083c6873803a8bd6bcf29..a8f451dc1d8d49c2080c3ed3d8348020c3d470cf 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 { */ @Deprecated public SmithingRecipe(@NotNull NamespacedKey key, @NotNull ItemStack result, @NotNull RecipeChoice base, @NotNull 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.validate().clone(); // Paper + this.addition = addition.validate().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 --- 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 { */ 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 } // Paper start /** diff --git a/src/main/java/org/bukkit/inventory/SmithingTrimRecipe.java b/src/main/java/org/bukkit/inventory/SmithingTrimRecipe.java index ce36bb5b030f17e11f74e987235be143c1925aa7..6316112074a0708734106ca9de5ef968df52ce0e 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 */ 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 } // Paper start /** @@ -35,7 +35,7 @@ public class SmithingTrimRecipe extends SmithingRecipe implements ComplexRecipe */ 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 } // 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 --- 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().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().clone(); // Paper return (StonecuttingRecipe) this; }