diff --git a/paper-api/src/main/java/io/papermc/paper/potion/PotionMix.java b/paper-api/src/main/java/io/papermc/paper/potion/PotionMix.java index 41152c98c0a8..0bdc3acc04eb 100644 --- a/paper-api/src/main/java/io/papermc/paper/potion/PotionMix.java +++ b/paper-api/src/main/java/io/papermc/paper/potion/PotionMix.java @@ -3,6 +3,7 @@ import java.util.Objects; import java.util.function.Predicate; import org.bukkit.Keyed; +import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.RecipeChoice; @@ -41,10 +42,12 @@ public PotionMix(final NamespacedKey key, final ItemStack result, final RecipeCh * * @param stackPredicate a predicate for an itemstack. * @return a new RecipeChoice + * @deprecated use {@link RecipeChoice#predicateChoice(Predicate, ItemStack)} */ @Contract(value = "_ -> new", pure = true) + @Deprecated(since = "1.21.4") public static RecipeChoice createPredicateChoice(final Predicate stackPredicate) { - return new PredicateRecipeChoice(stackPredicate); + return RecipeChoice.predicateChoice(stackPredicate, ItemStack.of(Material.STONE, 1)); } @Override diff --git a/paper-api/src/main/java/org/bukkit/inventory/PredicateRecipeChoiceImpl.java b/paper-api/src/main/java/org/bukkit/inventory/PredicateRecipeChoiceImpl.java new file mode 100644 index 000000000000..e8969f26ff01 --- /dev/null +++ b/paper-api/src/main/java/org/bukkit/inventory/PredicateRecipeChoiceImpl.java @@ -0,0 +1,41 @@ +package org.bukkit.inventory; + +import com.google.common.base.Preconditions; +import java.util.function.Predicate; +import org.jspecify.annotations.NullMarked; + +@NullMarked +record PredicateRecipeChoiceImpl(Predicate stackPredicate, ItemStack exampleStack) implements RecipeChoice.PredicateChoice { + + public PredicateRecipeChoiceImpl { + Preconditions.checkArgument(stackPredicate != null, "The item predicate cannot be null"); + Preconditions.checkArgument(exampleStack != null, "The example stack cannot be null"); + Preconditions.checkArgument(!exampleStack.isEmpty(), "Cannot have empty/air example stack"); + + exampleStack = exampleStack.clone(); + } + + @Override + public ItemStack getItemStack() { + return this.exampleStack.clone(); + } + + @SuppressWarnings({"MethodDoesntCallSuperMethod", "FunctionalExpressionCanBeFolded"}) + @Override + public PredicateRecipeChoiceImpl clone() { + return new PredicateRecipeChoiceImpl(this.stackPredicate::test, this.exampleStack.clone()); + } + + @Override + public boolean test(final ItemStack itemStack) { + return this.stackPredicate.test(itemStack); + } + + @Override + public RecipeChoice validate(final boolean allowEmptyRecipes) { + if (this.exampleStack.getType().isAir()) { + throw new IllegalArgumentException("RecipeChoice.ExactChoice cannot contain air"); + } + return this; + } +} 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 922bb69b5f21..79b71c630a2d 100644 --- a/paper-api/src/main/java/org/bukkit/inventory/RecipeChoice.java +++ b/paper-api/src/main/java/org/bukkit/inventory/RecipeChoice.java @@ -11,6 +11,8 @@ import org.bukkit.Material; import org.bukkit.Tag; import org.bukkit.material.MaterialData; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; /** @@ -23,6 +25,7 @@ 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 @@ -35,6 +38,53 @@ public interface RecipeChoice extends Predicate, Cloneable { } // Paper end + /** + * Creates a choice that will be valid only if one of the stacks is + * exactly matched (aside from stack size). + * + * @param first an ItemStack to match against. + * Cannot be null or empty/air. + * @param others additional ItemStacks to match against. + * @return a new ExactChoice + */ + @Contract(value = "_, _ -> new", pure = true) + static @NotNull ExactChoice exactChoice(@NotNull ItemStack first, @NotNull ItemStack... others) { + List stacks = new ArrayList<>(others.length + 1); + stacks.add(first); + Collections.addAll(stacks, others); + return new ExactChoice(stacks); + } + + /** + * Creates a choice that will be valid only if one of the stacks is + * exactly matched (aside from stack size). + * + * @param stacks the ItemStacks to match against. + * Cannot be empty or contain empty/air stacks. + * @return a new ExactChoice + */ + @Contract(value = "_ -> new", pure = true) + static @NotNull ExactChoice exactChoice(@NotNull List stacks) { + return new ExactChoice(stacks); + } + + /** + * Creates a recipe choice that will be valid only if an item matches the + * given predicate. + *

+ * Note: Mutating the {@link ItemStack} within the predicate is not + * supported. + * + * @param stackPredicate the predicate to match against. + * @param exampleStack an example {@link ItemStack} to be shown in the + * recipe book. Cannot be empty or air. + * @return a new PredicateChoice + */ + @Contract(value = "_, _ -> new", pure = true) + static @NotNull PredicateChoice predicateChoice(@NotNull Predicate stackPredicate, @NotNull ItemStack exampleStack) { + return new PredicateRecipeChoiceImpl(stackPredicate, exampleStack); + } + /** * Gets a single item stack representative of this stack choice. * @@ -192,14 +242,26 @@ public static class ExactChoice implements RecipeChoice { private List choices; + /** + * @deprecated Use {@link RecipeChoice#exactChoice(ItemStack, ItemStack...)} instead + */ + @Deprecated(since = "1.21.4", forRemoval = true) public ExactChoice(@NotNull ItemStack stack) { this(Arrays.asList(stack)); } + /** + * @deprecated Use {@link RecipeChoice#exactChoice(ItemStack, ItemStack...)} instead + */ + @Deprecated(since = "1.21.4", forRemoval = true) public ExactChoice(@NotNull ItemStack... stacks) { this(Arrays.asList(stacks)); } + /** + * @deprecated Use {@link RecipeChoice#exactChoice(List)} instead + */ + @Deprecated(since = "1.21.4", forRemoval = true) public ExactChoice(@NotNull List choices) { Preconditions.checkArgument(choices != null, "choices"); Preconditions.checkArgument(!choices.isEmpty(), "Must have at least one choice"); @@ -290,4 +352,13 @@ public String toString() { } // Paper end - check valid ingredients } + + /** + * Represents a choice that will be valid only if an item matches the + * given predicate. + */ + @ApiStatus.NonExtendable + interface PredicateChoice extends RecipeChoice { + + } } 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 462a6d1da59b..add3df52f6a6 100644 --- a/paper-api/src/main/java/org/bukkit/inventory/ShapedRecipe.java +++ b/paper-api/src/main/java/org/bukkit/inventory/ShapedRecipe.java @@ -186,7 +186,7 @@ public ShapedRecipe setIngredient(char key, @NotNull RecipeChoice ingredient) { @NotNull public ShapedRecipe setIngredient(char key, @NotNull ItemStack item) { Preconditions.checkArgument(!item.getType().isAir(), "Item cannot be air"); // Paper - return setIngredient(key, new RecipeChoice.ExactChoice(item.clone())); // Paper + return setIngredient(key, 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 e65311087668..7eb26323f497 100644 --- a/paper-api/src/main/java/org/bukkit/inventory/ShapelessRecipe.java +++ b/paper-api/src/main/java/org/bukkit/inventory/ShapelessRecipe.java @@ -148,7 +148,7 @@ public ShapelessRecipe addIngredient(int count, @NotNull ItemStack item) { Preconditions.checkArgument(!item.getType().isAir(), "Item cannot be air"); // Paper item = item.clone(); // Paper while (count-- > 0) { - ingredients.add(new RecipeChoice.ExactChoice(item)); + ingredients.add(RecipeChoice.exactChoice(item)); } return this; } diff --git a/paper-server/patches/features/0020-Improve-exact-choice-recipe-ingredients.patch b/paper-server/patches/features/0020-Improve-exact-choice-recipe-ingredients.patch index 19c90b00174a..36bb34e206d8 100644 --- a/paper-server/patches/features/0020-Improve-exact-choice-recipe-ingredients.patch +++ b/paper-server/patches/features/0020-Improve-exact-choice-recipe-ingredients.patch @@ -77,10 +77,10 @@ index 0000000000000000000000000000000000000000..ce745e49cd54fe3ae187785563a1bd31 +} diff --git a/io/papermc/paper/inventory/recipe/StackedContentsExtrasMap.java b/io/papermc/paper/inventory/recipe/StackedContentsExtrasMap.java new file mode 100644 -index 0000000000000000000000000000000000000000..f47c12e9dd6cfa857ca07a764edc22de372e25b6 +index 0000000000000000000000000000000000000000..12da5da5b6a9732d74f2a9acedc7dec84eb3bfee --- /dev/null +++ b/io/papermc/paper/inventory/recipe/StackedContentsExtrasMap.java -@@ -0,0 +1,68 @@ +@@ -0,0 +1,80 @@ +package io.papermc.paper.inventory.recipe; + +import it.unimi.dsi.fastutil.objects.Object2IntMap; @@ -93,12 +93,16 @@ index 0000000000000000000000000000000000000000..f47c12e9dd6cfa857ca07a764edc22de +import net.minecraft.world.item.crafting.CraftingInput; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.function.Predicate; + +public final class StackedContentsExtrasMap { + + private final StackedContents contents; + public Object2IntMap regularRemoved = new Object2IntOpenHashMap<>(); // needed for re-using the regular contents (for ShapelessRecipe) + public final ObjectSet exactIngredients = new ObjectOpenCustomHashSet<>(ItemStackLinkedSet.TYPE_AND_TAG); ++ public final List> predicateIngredients = new ArrayList<>(); + + public StackedContentsExtrasMap(final StackedContents contents) { + this.contents = contents; @@ -107,7 +111,9 @@ index 0000000000000000000000000000000000000000..f47c12e9dd6cfa857ca07a764edc22de + public void initialize(final Recipe recipe) { + this.exactIngredients.clear(); + for (final Ingredient ingredient : recipe.placementInfo().ingredients()) { -+ if (ingredient.isExact()) { ++ if (ingredient.stackPredicate != null) { ++ this.predicateIngredients.add(ingredient.stackPredicate); ++ } else if (ingredient.isExact()) { + this.exactIngredients.addAll(ingredient.itemStacks()); + } + } @@ -137,11 +143,17 @@ index 0000000000000000000000000000000000000000..f47c12e9dd6cfa857ca07a764edc22de + for (final Object2IntMap.Entry entry : this.regularRemoved.object2IntEntrySet()) { + this.contents.amounts.addTo(entry.getKey(), entry.getIntValue()); + } ++ this.predicateIngredients.clear(); + this.exactIngredients.clear(); + this.regularRemoved.clear(); + } + + public boolean accountStack(final ItemStack stack, final int count) { ++ for (Predicate stackPredicate : this.predicateIngredients) { ++ if (!stackPredicate.test(stack)) continue; ++ this.contents.account(new ItemOrExact.Exact(stack), count); ++ return true; ++ } + if (this.exactIngredients.contains(stack)) { + this.contents.account(new ItemOrExact.Exact(stack), count); + return true; @@ -341,7 +353,7 @@ index 6bbe2e51ef71d193e0a5d3cace2b0ad1760ce759..83ccde54c625d40dc595e000c533f60a } diff --git a/net/minecraft/world/item/crafting/Ingredient.java b/net/minecraft/world/item/crafting/Ingredient.java -index e43641650d66a62b5b7b58c43833ce504970ab1e..879c8fe1f20decc793cfa39e686b61d521bd76ba 100644 +index 4ed4f4a3ee7df461ccedcf3506ccadcd27d45f94..7a3f2e45ae5dabd0e4ca65b9edf7b5df83b998fc 100644 --- a/net/minecraft/world/item/crafting/Ingredient.java +++ b/net/minecraft/world/item/crafting/Ingredient.java @@ -21,7 +21,7 @@ import net.minecraft.world.item.Items; @@ -353,8 +365,8 @@ index e43641650d66a62b5b7b58c43833ce504970ab1e..879c8fe1f20decc793cfa39e686b61d5 public static final StreamCodec CONTENTS_STREAM_CODEC = ByteBufCodecs.holderSet(Registries.ITEM) .map(Ingredient::new, ingredient -> ingredient.values); public static final StreamCodec> OPTIONAL_CONTENTS_STREAM_CODEC = ByteBufCodecs.holderSet(Registries.ITEM) -@@ -35,20 +35,24 @@ public final class Ingredient implements StackedContents.IngredientInfo values; +@@ -37,20 +37,24 @@ public final class Ingredient implements StackedContents.IngredientInfo stackPredicate; // Paper - add PredicateChoice // CraftBukkit start @javax.annotation.Nullable - private java.util.List itemStacks; @@ -381,8 +393,8 @@ index e43641650d66a62b5b7b58c43833ce504970ab1e..879c8fe1f20decc793cfa39e686b61d5 return recipe; } // CraftBukkit end -@@ -81,21 +85,22 @@ public final class Ingredient implements StackedContents.IngredientInfo item) -> + !this.isExact() && this.values.contains(item); -+ case io.papermc.paper.inventory.recipe.ItemOrExact.Exact(final ItemStack exact) -> -+ this.isExact() && this.itemStacks.contains(exact); ++ case io.papermc.paper.inventory.recipe.ItemOrExact.Exact(final ItemStack exact) -> { ++ if (this.stackPredicate != null) { ++ yield this.stackPredicate.test(exact); ++ } ++ yield this.isExact() && this.itemStacks.contains(exact); ++ } + }; + // Paper end - Improve exact choice recipe ingredients } @Override -@@ -120,6 +125,11 @@ public final class Ingredient implements StackedContents.IngredientInfo CODEC = ExtraCodecs.nonEmptyHolderSet(NON_AIR_HOLDER_SET_CODEC) .xmap(Ingredient::new, ingredient -> ingredient.values); private final HolderSet values; ++ @org.jetbrains.annotations.Nullable ++ public Predicate stackPredicate; // Paper - add PredicateChoice + // CraftBukkit start + @javax.annotation.Nullable + private java.util.List itemStacks; @@ -26,10 +28,15 @@ private Ingredient(HolderSet values) { values.unwrap().ifRight(list -> { -@@ -60,6 +_,17 @@ +@@ -60,6 +_,22 @@ @Override public boolean test(ItemStack stack) { ++ // Paper start - add PredicateChoice ++ if (this.stackPredicate != null) { ++ return this.stackPredicate.test(stack); ++ } ++ // Paper end - add PredicateChoice + // CraftBukkit start + if (this.isExact()) { + for (ItemStack itemstack1 : this.itemStacks()) { diff --git a/paper-server/src/main/java/io/papermc/paper/potion/PaperPotionMix.java b/paper-server/src/main/java/io/papermc/paper/potion/PaperPotionMix.java index 7ea357ac2f3a..6b709a3d40a5 100644 --- a/paper-server/src/main/java/io/papermc/paper/potion/PaperPotionMix.java +++ b/paper-server/src/main/java/io/papermc/paper/potion/PaperPotionMix.java @@ -4,18 +4,12 @@ import net.minecraft.world.item.ItemStack; import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.craftbukkit.inventory.CraftRecipe; -import org.bukkit.inventory.RecipeChoice; +import org.jspecify.annotations.NullMarked; +@NullMarked public record PaperPotionMix(ItemStack result, Predicate input, Predicate ingredient) { - public PaperPotionMix(PotionMix potionMix) { - this(CraftItemStack.asNMSCopy(potionMix.getResult()), convert(potionMix.getInput()), convert(potionMix.getIngredient())); - } - - static Predicate convert(final RecipeChoice choice) { - if (choice instanceof PredicateRecipeChoice predicateRecipeChoice) { - return stack -> predicateRecipeChoice.test(CraftItemStack.asBukkitCopy(stack)); - } - return CraftRecipe.toIngredient(choice, true); + public PaperPotionMix(final PotionMix potionMix) { + this(CraftItemStack.asNMSCopy(potionMix.getResult()), CraftRecipe.toIngredient(potionMix.getInput(), true), CraftRecipe.toIngredient(potionMix.getIngredient(), true)); } } diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java index 4864e2016cb1..6db0ad5d8c19 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java @@ -2,10 +2,13 @@ import com.google.common.base.Preconditions; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Predicate; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceKey; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; import org.bukkit.NamespacedKey; import org.bukkit.craftbukkit.util.CraftNamespacedKey; @@ -32,6 +35,9 @@ static Ingredient toIngredient(RecipeChoice bukkit, boolean requireNotEmpty) { if (bukkit == null) { stack = Ingredient.of(); + } else if (bukkit instanceof RecipeChoice.PredicateChoice predicateChoice) { + stack = Ingredient.ofStacks(Collections.singletonList(CraftItemStack.asNMSCopy(predicateChoice.getItemStack()))); + stack.stackPredicate = nmsStack -> predicateChoice.test(CraftItemStack.asBukkitCopy(nmsStack)); } else if (bukkit instanceof RecipeChoice.MaterialChoice) { stack = Ingredient.of(((RecipeChoice.MaterialChoice) bukkit).getChoices().stream().map((mat) -> CraftItemType.bukkitToMinecraft(mat))); } else if (bukkit instanceof RecipeChoice.ExactChoice) { @@ -63,13 +69,19 @@ public static RecipeChoice toBukkit(Ingredient list) { return RecipeChoice.empty(); // Paper - null breaks API contracts } + if (list.stackPredicate != null) { + ItemStack stack = list.itemStacks().iterator().next(); + Predicate predicate = bukkitStack -> list.stackPredicate.test(CraftItemStack.asNMSCopy(bukkitStack)); + return RecipeChoice.predicateChoice(predicate, CraftItemStack.asBukkitCopy(stack)); + } + if (list.isExact()) { List choices = new ArrayList<>(list.itemStacks().size()); for (net.minecraft.world.item.ItemStack i : list.itemStacks()) { choices.add(CraftItemStack.asBukkitCopy(i)); } - return new RecipeChoice.ExactChoice(choices); + return RecipeChoice.exactChoice(choices); } else { List choices = list.items().map((i) -> CraftItemType.minecraftToBukkit(i.value())).toList();