Skip to content

Commit 1797fbc

Browse files
Add PredicateChoice (#12017)
Co-authored-by: Jake Potrebic <jake.m.potrebic@gmail.com>
1 parent bb09b43 commit 1797fbc

12 files changed

Lines changed: 1389 additions & 27 deletions

File tree

paper-api/src/main/java/io/papermc/paper/potion/PotionMix.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.bukkit.Keyed;
66
import org.bukkit.NamespacedKey;
77
import org.bukkit.inventory.ItemStack;
8+
import org.bukkit.inventory.ItemType;
89
import org.bukkit.inventory.RecipeChoice;
910
import org.jetbrains.annotations.Contract;
1011
import org.jspecify.annotations.NullMarked;
@@ -41,10 +42,12 @@ public PotionMix(final NamespacedKey key, final ItemStack result, final RecipeCh
4142
*
4243
* @param stackPredicate a predicate for an itemstack.
4344
* @return a new RecipeChoice
45+
* @deprecated use {@link RecipeChoice#predicateChoice(Predicate, ItemStack)}
4446
*/
4547
@Contract(value = "_ -> new", pure = true)
48+
@Deprecated(since = "26.2")
4649
public static RecipeChoice createPredicateChoice(final Predicate<? super ItemStack> stackPredicate) {
47-
return new PredicateRecipeChoice(stackPredicate);
50+
return RecipeChoice.predicateChoice(stackPredicate, ItemType.STONE.createItemStack());
4851
}
4952

5053
@Override
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.bukkit.inventory;
2+
3+
import com.google.common.base.Preconditions;
4+
import java.util.function.Predicate;
5+
import org.jspecify.annotations.NullMarked;
6+
7+
@NullMarked
8+
record PredicateRecipeChoiceImpl(Predicate<? super ItemStack> stackPredicate, ItemStack exampleStack) implements RecipeChoice.PredicateChoice {
9+
10+
public PredicateRecipeChoiceImpl {
11+
Preconditions.checkArgument(stackPredicate != null, "The item predicate cannot be null");
12+
Preconditions.checkArgument(exampleStack != null, "The example stack cannot be null");
13+
Preconditions.checkArgument(!exampleStack.isEmpty(), "Cannot have empty/air example stack");
14+
15+
exampleStack = exampleStack.clone();
16+
}
17+
18+
@Override
19+
public ItemStack getItemStack() {
20+
return this.exampleStack.clone();
21+
}
22+
23+
@SuppressWarnings({"MethodDoesntCallSuperMethod", "FunctionalExpressionCanBeFolded"})
24+
@Override
25+
public PredicateRecipeChoiceImpl clone() {
26+
return new PredicateRecipeChoiceImpl(this.stackPredicate::test, this.exampleStack);
27+
}
28+
29+
@Override
30+
public boolean test(final ItemStack itemStack) {
31+
return this.stackPredicate.test(itemStack);
32+
}
33+
34+
@Override
35+
public RecipeChoice validate(final boolean allowEmptyRecipes) {
36+
return this;
37+
}
38+
}

paper-api/src/main/java/org/bukkit/inventory/RecipeChoice.java

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,53 @@ static ItemTypeChoice itemType(final RegistryKeySet<ItemType> itemTypes) {
7070
return new ItemTypeRecipeChoiceImpl(itemTypes);
7171
}
7272

73+
/**
74+
* Creates a choice that will be valid only if one of the stacks is
75+
* exactly matched (aside from stack size).
76+
*
77+
* @param first an ItemStack to match against.
78+
* Cannot be null or empty/air.
79+
* @param others additional ItemStacks to match against.
80+
* @return a new ExactChoice
81+
*/
82+
@Contract(value = "_, _ -> new", pure = true)
83+
static ExactChoice exactChoice(ItemStack first, ItemStack... others) {
84+
List<ItemStack> stacks = new ArrayList<>(others.length + 1);
85+
stacks.add(first);
86+
Collections.addAll(stacks, others);
87+
return new ExactChoice(stacks);
88+
}
89+
90+
/**
91+
* Creates a choice that will be valid only if one of the stacks is
92+
* exactly matched (aside from stack size).
93+
*
94+
* @param stacks the ItemStacks to match against.
95+
* Cannot be empty or contain empty/air stacks.
96+
* @return a new ExactChoice
97+
*/
98+
@Contract(value = "_ -> new", pure = true)
99+
static ExactChoice exactChoice(List<ItemStack> stacks) {
100+
return new ExactChoice(stacks);
101+
}
102+
103+
/**
104+
* Creates a recipe choice that will be valid only if an item matches the
105+
* given predicate.
106+
* <p>
107+
* <b>Note:</b> Mutating the {@link ItemStack} within the predicate is not
108+
* supported.
109+
*
110+
* @param stackPredicate the predicate to match against.
111+
* @param exampleStack an example {@link ItemStack} to be shown in the
112+
* recipe book. Cannot be empty or air.
113+
* @return a new PredicateChoice
114+
*/
115+
@Contract(value = "_, _ -> new", pure = true)
116+
static PredicateChoice predicateChoice(Predicate<? super ItemStack> stackPredicate, ItemStack exampleStack) {
117+
return new PredicateRecipeChoiceImpl(stackPredicate, exampleStack);
118+
}
119+
73120
/**
74121
* Gets a single item stack representative of this stack choice.
75122
*
@@ -228,14 +275,26 @@ final class ExactChoice implements RecipeChoice {
228275

229276
private List<ItemStack> choices;
230277

278+
/**
279+
* @deprecated Use {@link RecipeChoice#exactChoice(ItemStack, ItemStack...)} instead
280+
*/
281+
@Deprecated(since = "26.2", forRemoval = true)
231282
public ExactChoice(ItemStack stack) {
232283
this(Arrays.asList(stack));
233284
}
234285

286+
/**
287+
* @deprecated Use {@link RecipeChoice#exactChoice(ItemStack, ItemStack...)} instead
288+
*/
289+
@Deprecated(since = "26.2", forRemoval = true)
235290
public ExactChoice(ItemStack... stacks) {
236291
this(Arrays.asList(stacks));
237292
}
238293

294+
/**
295+
* @deprecated Use {@link RecipeChoice#exactChoice(List)} instead
296+
*/
297+
@Deprecated(since = "26.2", forRemoval = true)
239298
public ExactChoice(List<ItemStack> choices) {
240299
Preconditions.checkArgument(choices != null, "choices");
241300
Preconditions.checkArgument(!choices.isEmpty(), "Must have at least one choice");
@@ -341,4 +400,13 @@ sealed interface ItemTypeChoice extends RecipeChoice permits ItemTypeRecipeChoic
341400
*/
342401
RegistryKeySet<ItemType> itemTypes();
343402
}
403+
404+
/**
405+
* Represents a choice that will be valid only if an item matches the
406+
* given predicate.
407+
*/
408+
@ApiStatus.NonExtendable
409+
interface PredicateChoice extends RecipeChoice {
410+
411+
}
344412
}

paper-api/src/main/java/org/bukkit/inventory/ShapedRecipe.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ public ShapedRecipe setIngredient(char key, @NotNull RecipeChoice ingredient) {
180180
@NotNull
181181
public ShapedRecipe setIngredient(char key, @NotNull ItemStack item) {
182182
Preconditions.checkArgument(!item.getType().isAir(), "Item cannot be air"); // Paper
183-
return setIngredient(key, new RecipeChoice.ExactChoice(item.clone())); // Paper
183+
return setIngredient(key, RecipeChoice.exactChoice(item.clone())); // Paper
184184
}
185185

186186
/**

paper-api/src/main/java/org/bukkit/inventory/ShapelessRecipe.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ public ShapelessRecipe addIngredient(int count, @NotNull ItemStack item) {
143143
Preconditions.checkArgument(!item.getType().isAir(), "Item cannot be air"); // Paper
144144
item = item.clone(); // Paper
145145
while (count-- > 0) {
146-
this.ingredients.add(new RecipeChoice.ExactChoice(item));
146+
this.ingredients.add(RecipeChoice.exactChoice(item));
147147
}
148148
return this;
149149
}

paper-server/patches/features/0020-Improve-exact-choice-recipe-ingredients.patch

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,10 @@ index 0000000000000000000000000000000000000000..9e621873454cc2edc2e59e6c7554d811
7878
+}
7979
diff --git a/io/papermc/paper/inventory/recipe/StackedContentsExtrasMap.java b/io/papermc/paper/inventory/recipe/StackedContentsExtrasMap.java
8080
new file mode 100644
81-
index 0000000000000000000000000000000000000000..f47c12e9dd6cfa857ca07a764edc22de372e25b6
81+
index 0000000000000000000000000000000000000000..a9dcccefb19e6610076e1153547d2f9156042313
8282
--- /dev/null
8383
+++ b/io/papermc/paper/inventory/recipe/StackedContentsExtrasMap.java
84-
@@ -0,0 +1,68 @@
84+
@@ -0,0 +1,81 @@
8585
+package io.papermc.paper.inventory.recipe;
8686
+
8787
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
@@ -94,21 +94,28 @@ index 0000000000000000000000000000000000000000..f47c12e9dd6cfa857ca07a764edc22de
9494
+import net.minecraft.world.item.crafting.CraftingInput;
9595
+import net.minecraft.world.item.crafting.Ingredient;
9696
+import net.minecraft.world.item.crafting.Recipe;
97+
+import java.util.ArrayList;
98+
+import java.util.List;
99+
+import java.util.function.Predicate;
97100
+
98101
+public final class StackedContentsExtrasMap {
99102
+
100103
+ private final StackedContents<ItemOrExact> contents;
101104
+ public Object2IntMap<ItemOrExact.Item> regularRemoved = new Object2IntOpenHashMap<>(); // needed for re-using the regular contents (for ShapelessRecipe)
102105
+ public final ObjectSet<ItemStack> exactIngredients = new ObjectOpenCustomHashSet<>(ItemStackLinkedSet.TYPE_AND_TAG);
106+
+ public final List<Predicate<ItemStack>> predicateIngredients = new ArrayList<>();
103107
+
104108
+ public StackedContentsExtrasMap(final StackedContents<ItemOrExact> contents) {
105109
+ this.contents = contents;
106110
+ }
107111
+
108112
+ public void initialize(final Recipe<?> recipe) {
109113
+ this.exactIngredients.clear();
114+
+ this.predicateIngredients.clear();
110115
+ for (final Ingredient ingredient : recipe.placementInfo().ingredients()) {
111-
+ if (ingredient.isExact()) {
116+
+ if (ingredient.stackPredicate != null) {
117+
+ this.predicateIngredients.add(ingredient.stackPredicate);
118+
+ } else if (ingredient.isExact()) {
112119
+ this.exactIngredients.addAll(ingredient.itemStacks());
113120
+ }
114121
+ }
@@ -138,11 +145,17 @@ index 0000000000000000000000000000000000000000..f47c12e9dd6cfa857ca07a764edc22de
138145
+ for (final Object2IntMap.Entry<ItemOrExact.Item> entry : this.regularRemoved.object2IntEntrySet()) {
139146
+ this.contents.amounts.addTo(entry.getKey(), entry.getIntValue());
140147
+ }
148+
+ this.predicateIngredients.clear();
141149
+ this.exactIngredients.clear();
142150
+ this.regularRemoved.clear();
143151
+ }
144152
+
145153
+ public boolean accountStack(final ItemStack stack, final int count) {
154+
+ for (Predicate<ItemStack> stackPredicate : this.predicateIngredients) {
155+
+ if (!stackPredicate.test(stack)) continue;
156+
+ this.contents.account(new ItemOrExact.Exact(stack), count);
157+
+ return true;
158+
+ }
146159
+ if (this.exactIngredients.contains(stack)) {
147160
+ this.contents.account(new ItemOrExact.Exact(stack), count);
148161
+ return true;
@@ -344,7 +357,7 @@ index ffaa8e450a11a01060b6847d1b66a76323f9d8ba..1ace17d8fc7e7b8b4722a1889d49233c
344357
}
345358

346359
diff --git a/net/minecraft/world/item/crafting/Ingredient.java b/net/minecraft/world/item/crafting/Ingredient.java
347-
index 1a6870fd5dd02022d6b0df88e8f55f8ba9cf07c9..4f3697573fbfec3865751037de00a562ad81cddb 100644
360+
index 218d56fa066513fd8d9761af36bcc2873a34406b..778b38911b0d244738fd3db3b275b0c78a17339f 100644
348361
--- a/net/minecraft/world/item/crafting/Ingredient.java
349362
+++ b/net/minecraft/world/item/crafting/Ingredient.java
350363
@@ -22,7 +22,7 @@ import net.minecraft.world.item.Items;
@@ -356,9 +369,9 @@ index 1a6870fd5dd02022d6b0df88e8f55f8ba9cf07c9..4f3697573fbfec3865751037de00a562
356369
public static final StreamCodec<RegistryFriendlyByteBuf, Ingredient> CONTENTS_STREAM_CODEC = ByteBufCodecs.holderSet(Registries.ITEM)
357370
.map(Ingredient::new, i -> i.values);
358371
public static final StreamCodec<RegistryFriendlyByteBuf, Optional<Ingredient>> OPTIONAL_CONTENTS_STREAM_CODEC = ByteBufCodecs.holderSet(Registries.ITEM)
359-
@@ -34,19 +34,23 @@ public final class Ingredient implements Predicate<ItemStack>, StackedContents.I
360-
public static final Codec<Ingredient> CODEC = ExtraCodecs.nonEmptyHolderSet(NON_AIR_HOLDER_SET_CODEC).xmap(Ingredient::new, i -> i.values);
372+
@@ -35,19 +35,23 @@ public final class Ingredient implements Predicate<ItemStack>, StackedContents.I
361373
public final HolderSet<Item> values;
374+
public java.util.function.@org.jspecify.annotations.Nullable Predicate<ItemStack> stackPredicate; // Paper - add PredicateChoice
362375
// CraftBukkit start
363376
- private java.util.@org.jspecify.annotations.Nullable List<ItemStack> itemStacks;
364377
+ private java.util.@org.jspecify.annotations.Nullable Set<ItemStack> itemStacks; // Paper - Improve exact choice recipe ingredients
@@ -383,8 +396,8 @@ index 1a6870fd5dd02022d6b0df88e8f55f8ba9cf07c9..4f3697573fbfec3865751037de00a562
383396
return ingredient;
384397
}
385398
// CraftBukkit end
386-
@@ -81,21 +85,22 @@ public final class Ingredient implements Predicate<ItemStack>, StackedContents.I
387-
public boolean test(final ItemStack input) {
399+
@@ -87,21 +91,32 @@ public final class Ingredient implements Predicate<ItemStack>, StackedContents.I
400+
// Paper end - add PredicateChoice
388401
// CraftBukkit start
389402
if (this.isExact()) {
390403
- for (ItemStack item : this.itemStacks()) {
@@ -408,14 +421,24 @@ index 1a6870fd5dd02022d6b0df88e8f55f8ba9cf07c9..4f3697573fbfec3865751037de00a562
408421
+ return switch (itemOrExact) {
409422
+ case io.papermc.paper.inventory.recipe.ItemOrExact.Item(final Holder<Item> item) ->
410423
+ !this.isExact() && this.values.contains(item);
411-
+ case io.papermc.paper.inventory.recipe.ItemOrExact.Exact(final ItemStack exact) ->
412-
+ this.isExact() && this.itemStacks.contains(exact);
424+
+ case io.papermc.paper.inventory.recipe.ItemOrExact.Exact(final ItemStack exact) -> {
425+
+ if (this.stackPredicate != null) {
426+
+ yield this.stackPredicate.test(exact);
427+
+ }
428+
+ if (this.isExact()) {
429+
+ yield this.itemStacks.contains(exact);
430+
+ }
431+
+ // A plain, usable item can be routed to the Exact pool because it also matched a
432+
+ // predicate/exact ingredient in the same recipe; a regular ingredient must still be
433+
+ // able to draw from that shared pool (the picker's per-key count keeps it double-count free).
434+
+ yield net.minecraft.world.entity.player.Inventory.isUsableForCrafting(exact) && exact.is(this.values);
435+
+ }
413436
+ };
414437
+ // Paper end - Improve exact choice recipe ingredients
415438
}
416439

417440
@Override
418-
@@ -125,6 +130,11 @@ public final class Ingredient implements Predicate<ItemStack>, StackedContents.I
441+
@@ -131,6 +146,11 @@ public final class Ingredient implements Predicate<ItemStack>, StackedContents.I
419442
}
420443

421444
public SlotDisplay display() {

paper-server/patches/sources/net/minecraft/world/item/crafting/Ingredient.java.patch

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
--- a/net/minecraft/world/item/crafting/Ingredient.java
22
+++ b/net/minecraft/world/item/crafting/Ingredient.java
3-
@@ -33,6 +_,23 @@
3+
@@ -33,6 +_,24 @@
44
public static final Codec<HolderSet<Item>> NON_AIR_HOLDER_SET_CODEC = HolderSetCodec.create(Registries.ITEM, Item.CODEC, false);
55
public static final Codec<Ingredient> CODEC = ExtraCodecs.nonEmptyHolderSet(NON_AIR_HOLDER_SET_CODEC).xmap(Ingredient::new, i -> i.values);
66
public final HolderSet<Item> values;
7+
+ public java.util.function.@org.jspecify.annotations.Nullable Predicate<ItemStack> stackPredicate; // Paper - add PredicateChoice
78
+ // CraftBukkit start
89
+ private java.util.@org.jspecify.annotations.Nullable List<ItemStack> itemStacks;
910
+
@@ -24,10 +25,15 @@
2425

2526
private Ingredient(final HolderSet<Item> values) {
2627
values.unwrap().ifRight(directValues -> {
27-
@@ -62,6 +_,17 @@
28+
@@ -62,6 +_,22 @@
2829

2930
@Override
3031
public boolean test(final ItemStack input) {
32+
+ // Paper start - add PredicateChoice
33+
+ if (this.stackPredicate != null) {
34+
+ return this.stackPredicate.test(input);
35+
+ }
36+
+ // Paper end - add PredicateChoice
3137
+ // CraftBukkit start
3238
+ if (this.isExact()) {
3339
+ for (ItemStack item : this.itemStacks()) {

paper-server/src/main/java/io/papermc/paper/potion/PaperPotionMix.java

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,12 @@
44
import net.minecraft.world.item.ItemStack;
55
import org.bukkit.craftbukkit.inventory.CraftItemStack;
66
import org.bukkit.craftbukkit.inventory.CraftRecipe;
7-
import org.bukkit.inventory.RecipeChoice;
7+
import org.jspecify.annotations.NullMarked;
88

9+
@NullMarked
910
public record PaperPotionMix(ItemStack result, Predicate<ItemStack> input, Predicate<ItemStack> ingredient) {
1011

11-
public PaperPotionMix(PotionMix potionMix) {
12-
this(CraftItemStack.asNMSCopy(potionMix.getResult()), convert(potionMix.getInput()), convert(potionMix.getIngredient()));
13-
}
14-
15-
static Predicate<ItemStack> convert(final RecipeChoice choice) {
16-
if (choice instanceof PredicateRecipeChoice predicateRecipeChoice) {
17-
return stack -> predicateRecipeChoice.test(CraftItemStack.asBukkitCopy(stack));
18-
}
19-
return CraftRecipe.toIngredient(choice, true);
12+
public PaperPotionMix(final PotionMix potionMix) {
13+
this(CraftItemStack.asNMSCopy(potionMix.getResult()), CraftRecipe.toIngredient(potionMix.getInput(), true), CraftRecipe.toIngredient(potionMix.getIngredient(), true));
2014
}
2115
}

paper-server/src/main/java/org/bukkit/craftbukkit/inventory/CraftRecipe.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
import io.papermc.paper.registry.set.PaperRegistrySets;
77
import io.papermc.paper.registry.set.RegistryKeySet;
88
import java.util.ArrayList;
9+
import java.util.Collections;
910
import java.util.List;
1011
import java.util.Optional;
12+
import java.util.function.Predicate;
1113
import net.minecraft.core.registries.Registries;
1214
import net.minecraft.world.item.crafting.Ingredient;
1315
import org.bukkit.inventory.ItemType;
@@ -32,6 +34,9 @@ static Ingredient toIngredient(RecipeChoice bukkit, boolean requireNotEmpty) {
3234
stack = Ingredient.of();
3335
} else if (bukkit instanceof final RecipeChoice.ItemTypeChoice itemTypeChoice) {
3436
stack = Ingredient.of(PaperRegistrySets.convertToNms(Registries.ITEM, Conversions.global().lookup(), itemTypeChoice.itemTypes()));
37+
} else if (bukkit instanceof final RecipeChoice.PredicateChoice predicateChoice) {
38+
stack = Ingredient.ofStacks(Collections.singletonList(CraftItemStack.asNMSCopy(predicateChoice.getItemStack())));
39+
stack.stackPredicate = nmsStack -> predicateChoice.test(CraftItemStack.asBukkitCopy(nmsStack));
3540
} else if (bukkit instanceof RecipeChoice.MaterialChoice) {
3641
stack = Ingredient.of(((RecipeChoice.MaterialChoice) bukkit).getChoices().stream().map(CraftItemType::bukkitToMinecraft));
3742
} else if (bukkit instanceof RecipeChoice.ExactChoice) {
@@ -63,13 +68,19 @@ static RecipeChoice toChoice(Ingredient ingredient) {
6368
return RecipeChoice.empty(); // Paper - null breaks API contracts
6469
}
6570

71+
if (ingredient.stackPredicate != null) {
72+
net.minecraft.world.item.ItemStack stack = ingredient.itemStacks().iterator().next();
73+
Predicate<org.bukkit.inventory.ItemStack> predicate = bukkitStack -> ingredient.stackPredicate.test(CraftItemStack.asNMSCopy(bukkitStack));
74+
return RecipeChoice.predicateChoice(predicate, CraftItemStack.asBukkitCopy(stack));
75+
}
76+
6677
if (ingredient.isExact()) {
6778
List<org.bukkit.inventory.ItemStack> choices = new ArrayList<>(ingredient.itemStacks().size());
6879
for (net.minecraft.world.item.ItemStack i : ingredient.itemStacks()) {
6980
choices.add(CraftItemStack.asBukkitCopy(i));
7081
}
7182

72-
return new RecipeChoice.ExactChoice(choices);
83+
return RecipeChoice.exactChoice(choices);
7384
} else {
7485
final RegistryKeySet<ItemType> itemTypes = PaperRegistrySets.convertToApi(RegistryKey.ITEM, ingredient.values);
7586
return RecipeChoice.itemType(itemTypes);

0 commit comments

Comments
 (0)