Compare commits

..
Author SHA1 Message Date
Jake Potrebic fd5c62850a Bytecode Modification Framework 2026-08-10 18:20:44 +02:00
60 changed files with 4281 additions and 5692 deletions

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
Paper [![Version](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Fartifactory.papermc.io%2Fartifactory%2Funiverse%2Fio%2Fpapermc%2Fpaper%2Fpaper-api%2Fmaven-metadata.xml&strategy=highestVersion&filter=26.2.*&label=version&color=%23344ceb
Paper [![Version](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Fartifactory.papermc.io%2Fartifactory%2Funiverse%2Fio%2Fpapermc%2Fpaper%2Fpaper-api%2Fmaven-metadata.xml&strategy=highestVersion&filter=26.2*&label=version&color=%23344ceb
)](https://papermc.io/downloads/paper)
[![Paper Build Status](https://img.shields.io/github/actions/workflow/status/PaperMC/Paper/build.yml?branch=main)](https://github.com/PaperMC/Paper/actions)
[![Discord](https://img.shields.io/discord/289587909051416579.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/papermc)
@@ -5,7 +5,6 @@ import java.util.function.Predicate;
import org.bukkit.Keyed;
import org.bukkit.NamespacedKey;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.ItemType;
import org.bukkit.inventory.RecipeChoice;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.NullMarked;
@@ -42,12 +41,10 @@ public final class PotionMix implements Keyed {
*
* @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 = "26.2")
public static RecipeChoice createPredicateChoice(final Predicate<? super ItemStack> stackPredicate) {
return RecipeChoice.predicateChoice(stackPredicate, ItemType.STONE.createItemStack());
return new PredicateRecipeChoice(stackPredicate);
}
@Override
@@ -1367,7 +1367,11 @@ public final class Bukkit {
* Sets the radius, in blocks, around each worlds spawn point to protect.
*
* @param value new spawn radius, or 0 if none
* @deprecated has not functioned for a long time as the spawn radius is defined by the server.properties file.
* There is no API replacement for this method. It is generally recommended to implement "protection"-like behaviour
* via events or third-party plugin APIs.
*/
@Deprecated(since = "1.21.4", forRemoval = true)
public static void setSpawnRadius(int value) {
server.setSpawnRadius(value);
}
@@ -15,6 +15,7 @@ import org.jetbrains.annotations.NotNull;
*/
public abstract class Event {
private String name;
private final boolean isAsync;
/**
@@ -59,7 +60,10 @@ public abstract class Event {
*/
@NotNull
public String getEventName() {
return this.getClass().getSimpleName();
if (this.name == null) {
this.name = this.getClass().getSimpleName();
}
return this.name;
}
@NotNull
@@ -1,38 +0,0 @@
package org.bukkit.inventory;
import com.google.common.base.Preconditions;
import java.util.function.Predicate;
import org.jspecify.annotations.NullMarked;
@NullMarked
record PredicateRecipeChoiceImpl(Predicate<? super ItemStack> 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);
}
@Override
public boolean test(final ItemStack itemStack) {
return this.stackPredicate.test(itemStack);
}
@Override
public RecipeChoice validate(final boolean allowEmptyRecipes) {
return this;
}
}
@@ -70,53 +70,6 @@ public interface RecipeChoice extends Predicate<ItemStack>, Cloneable {
return new ItemTypeRecipeChoiceImpl(itemTypes);
}
/**
* 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 ExactChoice exactChoice(ItemStack first, ItemStack... others) {
List<ItemStack> 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 ExactChoice exactChoice(List<ItemStack> stacks) {
return new ExactChoice(stacks);
}
/**
* Creates a recipe choice that will be valid only if an item matches the
* given predicate.
* <p>
* <b>Note:</b> 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 PredicateChoice predicateChoice(Predicate<? super ItemStack> stackPredicate, ItemStack exampleStack) {
return new PredicateRecipeChoiceImpl(stackPredicate, exampleStack);
}
/**
* Gets a single item stack representative of this stack choice.
*
@@ -275,26 +228,14 @@ public interface RecipeChoice extends Predicate<ItemStack>, Cloneable {
private List<ItemStack> choices;
/**
* @deprecated Use {@link RecipeChoice#exactChoice(ItemStack, ItemStack...)} instead
*/
@Deprecated(since = "26.2", forRemoval = true)
public ExactChoice(ItemStack stack) {
this(Arrays.asList(stack));
}
/**
* @deprecated Use {@link RecipeChoice#exactChoice(ItemStack, ItemStack...)} instead
*/
@Deprecated(since = "26.2", forRemoval = true)
public ExactChoice(ItemStack... stacks) {
this(Arrays.asList(stacks));
}
/**
* @deprecated Use {@link RecipeChoice#exactChoice(List)} instead
*/
@Deprecated(since = "26.2", forRemoval = true)
public ExactChoice(List<ItemStack> choices) {
Preconditions.checkArgument(choices != null, "choices");
Preconditions.checkArgument(!choices.isEmpty(), "Must have at least one choice");
@@ -400,13 +341,4 @@ public interface RecipeChoice extends Predicate<ItemStack>, Cloneable {
*/
RegistryKeySet<ItemType> itemTypes();
}
/**
* Represents a choice that will be valid only if an item matches the
* given predicate.
*/
@ApiStatus.NonExtendable
interface PredicateChoice extends RecipeChoice {
}
}
@@ -180,7 +180,7 @@ public class ShapedRecipe extends CraftingRecipe {
@NotNull
public ShapedRecipe setIngredient(char key, @NotNull ItemStack item) {
Preconditions.checkArgument(!item.getType().isAir(), "Item cannot be air"); // Paper
return setIngredient(key, RecipeChoice.exactChoice(item.clone())); // Paper
return setIngredient(key, new RecipeChoice.ExactChoice(item.clone())); // Paper
}
/**
@@ -143,7 +143,7 @@ public class ShapelessRecipe extends CraftingRecipe {
Preconditions.checkArgument(!item.getType().isAir(), "Item cannot be air"); // Paper
item = item.clone(); // Paper
while (count-- > 0) {
this.ingredients.add(RecipeChoice.exactChoice(item));
this.ingredients.add(new RecipeChoice.ExactChoice(item));
}
return this;
}
+6 -3
View File
@@ -110,7 +110,7 @@ abstract class MockitoAgentProvider : CommandLineArgumentProvider {
dependencies {
implementation(project(":paper-api"))
implementation("ca.spottedleaf:leafpile:1.2.0")
implementation("ca.spottedleaf:leafpile:1.0.0")
implementation("org.jline:jline-terminal-ffm:3.27.1") // use ffm on java 22+
implementation("org.jline:jline-terminal-jni:3.27.1") // fall back to jni on java 21
implementation("net.minecrell:terminalconsoleappender:1.3.0")
@@ -127,7 +127,7 @@ dependencies {
runtimeOnly(log4jPlugins.output)
alsoShade(log4jPlugins.output)
implementation("com.velocitypowered:velocity-native:4.1.0") {
implementation("com.velocitypowered:velocity-native:3.4.0-SNAPSHOT") {
isTransitive = false
}
implementation("io.netty:netty-codec-haproxy:4.2.15.Final") // Add support for proxy protocol
@@ -154,9 +154,12 @@ dependencies {
testImplementation("org.ow2.asm:asm-tree:9.9.1")
testImplementation("org.junit-pioneer:junit-pioneer:2.3.0") // CartesianTest
// Bytecode framework
implementation("io.papermc:asm-utils:0.0.4-SNAPSHOT")
// Spark
implementation("me.lucko:spark-api:0.1-20240720.200737-2")
implementation("me.lucko:spark-paper:1.10.180")
implementation("me.lucko:spark-paper:1.10.172")
}
tasks.jar {
@@ -15996,10 +15996,10 @@ index 0000000000000000000000000000000000000000..1414f25ef770b0fe73ea618a450ade2f
+}
diff --git a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..54b2492c3ab6bf9ff52cec53dc4635f725f9eb4f
index 0000000000000000000000000000000000000000..15568f598b8c61cf92563b6a7f8cae526b84484e
--- /dev/null
+++ b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
@@ -0,0 +1,2200 @@
@@ -0,0 +1,2199 @@
+package ca.spottedleaf.moonrise.patches.collisions;
+
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
@@ -18048,7 +18048,6 @@ index 0000000000000000000000000000000000000000..54b2492c3ab6bf9ff52cec53dc4635f7
+ continue;
+ }
+
+ // TODO perform intersection check using moved aabb instead of moved blockCollision so we can avoid the call?
+ final VoxelShape blockCollisionOffset = blockCollision.move((double)blockX, (double)blockY, (double)blockZ);
+
+ if (!voxelShapeIntersectNoEmpty(blockCollisionOffset, aabb)) {
@@ -27403,7 +27402,7 @@ index f11c5ea1ee9381e7bf65305b959bd7da67423ece..26822e1211194b411dd8ea75ce523133
List<Entity> passengers = this.entity.getPassengers();
if (!passengers.equals(this.lastPassengers)) {
diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
index 473feb1aa6ccf0f03ce0bc895de367b3e554e1bc..4fce291cb7cd6eb2f4e47237fe247a5d666da0ad 100644
index 473feb1aa6ccf0f03ce0bc895de367b3e554e1bc..e5328f08d6a323aa1307ed791a88916b4588939c 100644
--- a/net/minecraft/server/level/ServerLevel.java
+++ b/net/minecraft/server/level/ServerLevel.java
@@ -182,7 +182,7 @@ import net.minecraft.world.waypoints.WaypointTransmitter;
@@ -27626,13 +27625,7 @@ index 473feb1aa6ccf0f03ce0bc895de367b3e554e1bc..4fce291cb7cd6eb2f4e47237fe247a5d
}
}
}
@@ -335,21 +471,140 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
ca.spottedleaf.moonrise.common.PlatformHooks.get().scheduleChunkLoad(
- this, cx, cz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true, priority, consumer
+ this, cx, cz, chunkStatus, true, priority, consumer
);
@@ -340,16 +476,135 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
}
}
}
File diff suppressed because it is too large. Load diff
@@ -28,7 +28,7 @@ and then catch exceptions and close if they fire.
Part of this commit was authored by: Spottedleaf, sandtechnology
diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java
index f46c55fd212ea68386342bc9ce71c04108758a8e..0b65c06e43fd22b2c30d9b8c5ef5b5e99301cec1 100644
index 709f2c6b47f78920cd9432da4c75e0d1ab2bffbc..3f10ea62d2ef70e7e4f01c83942cd9051249954c 100644
--- a/net/minecraft/network/Connection.java
+++ b/net/minecraft/network/Connection.java
@@ -65,7 +65,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
@@ -111,7 +111,7 @@ index f46c55fd212ea68386342bc9ce71c04108758a8e..0b65c06e43fd22b2c30d9b8c5ef5b5e9
this.flushQueue();
}
}
@@ -461,21 +492,42 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
@@ -444,21 +475,42 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
}
private void doSendPacket(final Packet<?> packet, final @Nullable ChannelFutureListener listener, final boolean flush) {
@@ -158,7 +158,7 @@ index f46c55fd212ea68386342bc9ce71c04108758a8e..0b65c06e43fd22b2c30d9b8c5ef5b5e9
}
}
@@ -487,16 +539,57 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
@@ -470,16 +522,57 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
}
}
@@ -221,7 +221,7 @@ index f46c55fd212ea68386342bc9ce71c04108758a8e..0b65c06e43fd22b2c30d9b8c5ef5b5e9
private static final int MAX_PER_TICK = io.papermc.paper.configuration.GlobalConfiguration.get().misc.maxJoinsPerTick; // Paper - Buffer joins to world
private static int joinAttemptsThisTick; // Paper - Buffer joins to world
@@ -566,6 +659,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
@@ -549,6 +642,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
public void disconnect(final DisconnectionDetails details) {
this.preparing = false; // Spigot
@@ -229,7 +229,7 @@ index f46c55fd212ea68386342bc9ce71c04108758a8e..0b65c06e43fd22b2c30d9b8c5ef5b5e9
if (this.channel == null) {
this.delayedDisconnect = details;
}
@@ -755,7 +849,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
@@ -738,7 +832,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
public void handleDisconnection() {
if (this.channel != null && !this.channel.isOpen()) {
if (this.disconnectionHandled) {
@@ -238,7 +238,7 @@ index f46c55fd212ea68386342bc9ce71c04108758a8e..0b65c06e43fd22b2c30d9b8c5ef5b5e9
} else {
this.disconnectionHandled = true;
PacketListener packetListener = this.getPacketListener();
@@ -766,7 +860,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
@@ -749,7 +843,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
);
disconnectListener.onDisconnect(details);
}
@@ -247,7 +247,7 @@ index f46c55fd212ea68386342bc9ce71c04108758a8e..0b65c06e43fd22b2c30d9b8c5ef5b5e9
// Paper start - Add PlayerConnectionCloseEvent
if (packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) {
/* Player was logged in, either game listener or configuration listener */
@@ -809,4 +903,96 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
@@ -792,4 +886,96 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
public @Nullable UUID getIntendedProfileId() {
return this.intendedProfileId;
}
@@ -267,10 +267,10 @@ index 595db570b0ade9cad7de77d2ab7b10d9e68699df..ac095f59d9f17d8239d6493d1c1e3942
+ // Paper end - Use Velocity cipher
}
diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java
index 0b65c06e43fd22b2c30d9b8c5ef5b5e99301cec1..9fbc7e93712672dbab67f931e2aee447d803607b 100644
index 3f10ea62d2ef70e7e4f01c83942cd9051249954c..9a04309584a5b9b55d0d383619113835190ad8f9 100644
--- a/net/minecraft/network/Connection.java
+++ b/net/minecraft/network/Connection.java
@@ -785,10 +785,24 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
@@ -768,10 +768,24 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
return connection;
}
@@ -298,7 +298,7 @@ index 0b65c06e43fd22b2c30d9b8c5ef5b5e99301cec1..9fbc7e93712672dbab67f931e2aee447
public boolean isConnected() {
return this.channel != null && this.channel.isOpen();
@@ -822,16 +836,17 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
@@ -805,16 +819,17 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
public void setupCompression(final int threshold, final boolean validateDecompressed) {
if (threshold >= 0) {
@@ -320,7 +320,7 @@ index 0b65c06e43fd22b2c30d9b8c5ef5b5e99301cec1..9fbc7e93712672dbab67f931e2aee447
this.channel.pipeline().fireUserEventTriggered(io.papermc.paper.network.ConnectionEvent.COMPRESSION_THRESHOLD_SET); // Paper - Add Channel initialization listeners
} else {
diff --git a/net/minecraft/server/network/ServerConnectionListener.java b/net/minecraft/server/network/ServerConnectionListener.java
index fa2467ff885e039f861bb9b1ae076a5a64b891ed..8a2f1ebcf06bb0c1e8b352652de69b6c1d74daf3 100644
index 1a60071f32a0c92c69f60c75035e5324aa144fb4..e84e243f35b09691d03e9a9cf3de0c093b1d5cfb 100644
--- a/net/minecraft/server/network/ServerConnectionListener.java
+++ b/net/minecraft/server/network/ServerConnectionListener.java
@@ -80,6 +80,10 @@ public class ServerConnectionListener {
@@ -335,10 +335,10 @@ index fa2467ff885e039f861bb9b1ae076a5a64b891ed..8a2f1ebcf06bb0c1e8b352652de69b6c
.add(
new ServerBootstrap()
diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
index 24bcd1067db20fc09b87b6e300615e3fa590049a..1d892a8c905496af9d56cec86941e82ec48505db 100644
index d6b6c6e7dce618c3f4aecc0969b7423771eab3ba..b94e01aa54b932b29370d1742b00ed73254fe4dc 100644
--- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
@@ -259,9 +259,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
@@ -257,9 +257,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
SecretKey secretKey = packet.getSecretKey(serverPrivateKey);
digest = new BigInteger(Crypt.digestData("", this.server.getKeyPair().getPublic(), secretKey)).toString(16);
this.state = ServerLoginPacketListenerImpl.State.AUTHENTICATING;
@@ -78,10 +78,10 @@ index 0000000000000000000000000000000000000000..9e621873454cc2edc2e59e6c7554d811
+}
diff --git a/io/papermc/paper/inventory/recipe/StackedContentsExtrasMap.java b/io/papermc/paper/inventory/recipe/StackedContentsExtrasMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..a9dcccefb19e6610076e1153547d2f9156042313
index 0000000000000000000000000000000000000000..f47c12e9dd6cfa857ca07a764edc22de372e25b6
--- /dev/null
+++ b/io/papermc/paper/inventory/recipe/StackedContentsExtrasMap.java
@@ -0,0 +1,81 @@
@@ -0,0 +1,68 @@
+package io.papermc.paper.inventory.recipe;
+
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
@@ -94,16 +94,12 @@ index 0000000000000000000000000000000000000000..a9dcccefb19e6610076e1153547d2f91
+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<ItemOrExact> contents;
+ public Object2IntMap<ItemOrExact.Item> regularRemoved = new Object2IntOpenHashMap<>(); // needed for re-using the regular contents (for ShapelessRecipe)
+ public final ObjectSet<ItemStack> exactIngredients = new ObjectOpenCustomHashSet<>(ItemStackLinkedSet.TYPE_AND_TAG);
+ public final List<Predicate<ItemStack>> predicateIngredients = new ArrayList<>();
+
+ public StackedContentsExtrasMap(final StackedContents<ItemOrExact> contents) {
+ this.contents = contents;
@@ -111,11 +107,8 @@ index 0000000000000000000000000000000000000000..a9dcccefb19e6610076e1153547d2f91
+
+ public void initialize(final Recipe<?> recipe) {
+ this.exactIngredients.clear();
+ this.predicateIngredients.clear();
+ for (final Ingredient ingredient : recipe.placementInfo().ingredients()) {
+ if (ingredient.stackPredicate != null) {
+ this.predicateIngredients.add(ingredient.stackPredicate);
+ } else if (ingredient.isExact()) {
+ if (ingredient.isExact()) {
+ this.exactIngredients.addAll(ingredient.itemStacks());
+ }
+ }
@@ -145,17 +138,11 @@ index 0000000000000000000000000000000000000000..a9dcccefb19e6610076e1153547d2f91
+ for (final Object2IntMap.Entry<ItemOrExact.Item> 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<ItemStack> 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;
@@ -357,7 +344,7 @@ index ffaa8e450a11a01060b6847d1b66a76323f9d8ba..1ace17d8fc7e7b8b4722a1889d49233c
}
diff --git a/net/minecraft/world/item/crafting/Ingredient.java b/net/minecraft/world/item/crafting/Ingredient.java
index 218d56fa066513fd8d9761af36bcc2873a34406b..778b38911b0d244738fd3db3b275b0c78a17339f 100644
index 1a6870fd5dd02022d6b0df88e8f55f8ba9cf07c9..4f3697573fbfec3865751037de00a562ad81cddb 100644
--- a/net/minecraft/world/item/crafting/Ingredient.java
+++ b/net/minecraft/world/item/crafting/Ingredient.java
@@ -22,7 +22,7 @@ import net.minecraft.world.item.Items;
@@ -369,9 +356,9 @@ index 218d56fa066513fd8d9761af36bcc2873a34406b..778b38911b0d244738fd3db3b275b0c7
public static final StreamCodec<RegistryFriendlyByteBuf, Ingredient> CONTENTS_STREAM_CODEC = ByteBufCodecs.holderSet(Registries.ITEM)
.map(Ingredient::new, i -> i.values);
public static final StreamCodec<RegistryFriendlyByteBuf, Optional<Ingredient>> OPTIONAL_CONTENTS_STREAM_CODEC = ByteBufCodecs.holderSet(Registries.ITEM)
@@ -35,19 +35,23 @@ public final class Ingredient implements Predicate<ItemStack>, StackedContents.I
@@ -34,19 +34,23 @@ public final class Ingredient implements Predicate<ItemStack>, StackedContents.I
public static final Codec<Ingredient> CODEC = ExtraCodecs.nonEmptyHolderSet(NON_AIR_HOLDER_SET_CODEC).xmap(Ingredient::new, i -> i.values);
public final HolderSet<Item> values;
public java.util.function.@org.jspecify.annotations.Nullable Predicate<ItemStack> stackPredicate; // Paper - add PredicateChoice
// CraftBukkit start
- private java.util.@org.jspecify.annotations.Nullable List<ItemStack> itemStacks;
+ private java.util.@org.jspecify.annotations.Nullable Set<ItemStack> itemStacks; // Paper - Improve exact choice recipe ingredients
@@ -396,8 +383,8 @@ index 218d56fa066513fd8d9761af36bcc2873a34406b..778b38911b0d244738fd3db3b275b0c7
return ingredient;
}
// CraftBukkit end
@@ -87,21 +91,32 @@ public final class Ingredient implements Predicate<ItemStack>, StackedContents.I
// Paper end - add PredicateChoice
@@ -81,21 +85,22 @@ public final class Ingredient implements Predicate<ItemStack>, StackedContents.I
public boolean test(final ItemStack input) {
// CraftBukkit start
if (this.isExact()) {
- for (ItemStack item : this.itemStacks()) {
@@ -421,24 +408,14 @@ index 218d56fa066513fd8d9761af36bcc2873a34406b..778b38911b0d244738fd3db3b275b0c7
+ return switch (itemOrExact) {
+ case io.papermc.paper.inventory.recipe.ItemOrExact.Item(final Holder<Item> item) ->
+ !this.isExact() && this.values.contains(item);
+ case io.papermc.paper.inventory.recipe.ItemOrExact.Exact(final ItemStack exact) -> {
+ if (this.stackPredicate != null) {
+ yield this.stackPredicate.test(exact);
+ }
+ if (this.isExact()) {
+ yield this.itemStacks.contains(exact);
+ }
+ // A plain, usable item can be routed to the Exact pool because it also matched a
+ // predicate/exact ingredient in the same recipe; a regular ingredient must still be
+ // able to draw from that shared pool (the picker's per-key count keeps it double-count free).
+ yield net.minecraft.world.entity.player.Inventory.isUsableForCrafting(exact) && exact.is(this.values);
+ }
+ case io.papermc.paper.inventory.recipe.ItemOrExact.Exact(final ItemStack exact) ->
+ this.isExact() && this.itemStacks.contains(exact);
+ };
+ // Paper end - Improve exact choice recipe ingredients
}
@Override
@@ -131,6 +146,11 @@ public final class Ingredient implements Predicate<ItemStack>, StackedContents.I
@@ -125,6 +130,11 @@ public final class Ingredient implements Predicate<ItemStack>, StackedContents.I
}
public SlotDisplay display() {
@@ -4,6 +4,43 @@ Date: Sat, 7 Jun 2025 15:05:05 -0400
Subject: [PATCH] DataConverter, Moonrise & co fixes
diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractOldAttributesRename.java b/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractOldAttributesRename.java
index 1a6d02df13f3461402fed429f7fbdb5acee877c4..712cca21a6b4736a15d49dceaf7e6d641096b0a9 100644
--- a/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractOldAttributesRename.java
+++ b/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractOldAttributesRename.java
@@ -38,7 +38,13 @@ public final class ConverterAbstractOldAttributesRename {
MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(version, versionStep) {
@Override
public MapType convert(final MapType data, final long sourceVersion, final long toVersion) {
- final ListType attributes = data.getList("AttributeModifiers", ObjectType.MAP);
+ final MapType tag = data.getMap("tag");
+
+ if (tag == null) {
+ return null;
+ }
+
+ final ListType attributes = tag.getList("AttributeModifiers", ObjectType.MAP);
if (attributes == null) {
return null;
diff --git a/ca/spottedleaf/dataconverter/minecraft/versions/V4307.java b/ca/spottedleaf/dataconverter/minecraft/versions/V4307.java
index d2877c20f389d0131e1dd208b464f590671e5d82..27bdc70d861ca39487ad16cb3afb89d604b462c8 100644
--- a/ca/spottedleaf/dataconverter/minecraft/versions/V4307.java
+++ b/ca/spottedleaf/dataconverter/minecraft/versions/V4307.java
@@ -87,10 +87,10 @@ public final class V4307 {
@Override
public MapType convert(final MapType root, final long sourceVersion, final long toVersion) {
- final Set<String> hiddenComponents = new LinkedHashSet<>();
-
- unwrapBlockPredicates(root, "minecraft:can_break", hiddenComponents);
+ // Don't use a linked hash set, and ensure that it is added in the same (undefined) order as the vanilla datafixer
+ final Set<String> hiddenComponents = new java.util.HashSet<>();
unwrapBlockPredicates(root, "minecraft:can_place_on", hiddenComponents);
+ unwrapBlockPredicates(root, "minecraft:can_break", hiddenComponents);
updateComponent(root, "minecraft:trim", hiddenComponents);
updateComponent(root, "minecraft:unbreakable", hiddenComponents);
diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java
index 3b28b39ebf252d8de46f7d0ce27037cdb041f1b5..0b6d7f99edde802cb464baa5a4b2b4a4dcce684a 100644
--- a/net/minecraft/server/dedicated/DedicatedServer.java
@@ -5,16 +5,16 @@ Subject: [PATCH] Call AsyncServerDataFixerRemoveBlockEntityEvent from DC
diff --git a/ca/spottedleaf/dataconverter/minecraft/converters/tileentity/ConverterAbstractTileEntityRemove.java b/ca/spottedleaf/dataconverter/minecraft/converters/tileentity/ConverterAbstractTileEntityRemove.java
index 6aef3311ca59220e8d7c97a4a5c1f14e96d08117..e70408061c8a495af319c810ee93d51a2644e465 100644
index a74821562154c99ee3a8571ff8dbf8fe5eacf416..8d47addc3cf6d0c551809463992da8551c2835f6 100644
--- a/ca/spottedleaf/dataconverter/minecraft/converters/tileentity/ConverterAbstractTileEntityRemove.java
+++ b/ca/spottedleaf/dataconverter/minecraft/converters/tileentity/ConverterAbstractTileEntityRemove.java
@@ -63,8 +63,43 @@ public final class ConverterAbstractTileEntityRemove {
@@ -61,8 +61,43 @@ public final class ConverterAbstractTileEntityRemove {
return null;
}
+ // Paper start - call event on block entity removal
+ final java.util.function.Supplier<net.kyori.adventure.key.Key> worldKeySupplier = com.google.common.base.Suppliers.memoize(() -> {
+ final MapType context = data.getMap(net.minecraft.util.datafix.fixes.ChunkHeightAndBiomeFix.DATAFIXER_CONTEXT_TAG);
+ final ca.spottedleaf.dataconverter.types.MapType context = data.getMap(net.minecraft.util.datafix.fixes.ChunkHeightAndBiomeFix.DATAFIXER_CONTEXT_TAG);
+ if (context == null) return null;
+
+ final @org.intellij.lang.annotations.Subst("minecraft:overworld") String levelIdentifier = context.getString("level_identifier");
@@ -1,6 +1,6 @@
--- a/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java
+++ b/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java
@@ -27,17 +_,38 @@
@@ -27,17 +_,36 @@
ServerLevel level = source.level();
Direction direction = source.state().getValue(DispenserBlock.FACING);
Position position = this.dispenseConfig.positionFunction().getDispensePosition(source, direction);
@@ -40,10 +40,8 @@
+
+ // SPIGOT-7923: Avoid create projectiles with empty item
+ if (!singleItemStack.isEmpty()) {
+ Projectile projectile = this.projectileItem.asProjectile(level, position, org.bukkit.craftbukkit.inventory.CraftItemStack.unwrap(event.getItem()), direction); // Paper - track changed items in the dispense event; unwrap is safe here because all uses of the stack make their own copies
+ Projectile.Delayed<Projectile> delayedProjectile = Projectile.spawnProjectileUsingShootDelayed(projectile, level, singleItemStack, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), this.dispenseConfig.power(), this.dispenseConfig.uncertainty());
+ Projectile projectile = Projectile.spawnProjectileUsingShoot(this.projectileItem.asProjectile(level, position, org.bukkit.craftbukkit.inventory.CraftItemStack.unwrap(event.getItem()), direction), level, singleItemStack, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), this.dispenseConfig.power(), this.dispenseConfig.uncertainty()); // Paper - track changed items in the dispense event; unwrap is safe here because all uses of the stack make their own copies
+ projectile.projectileSource = new org.bukkit.craftbukkit.projectiles.CraftBlockProjectileSource(source.blockEntity());
+ delayedProjectile.spawn();
+ }
+ if (shrink) dispensed.shrink(1);
+ // CraftBukkit end
@@ -182,7 +182,7 @@
} catch (RejectedExecutionException ignored) {
this.disconnect(Component.translatable("multiplayer.disconnect.server_shutdown"));
} catch (ClassCastException exception) {
@@ -297,6 +_,47 @@
@@ -297,6 +_,30 @@
}
}
@@ -195,7 +195,7 @@
+ *
+ * @see <a href="https://mojira.dev/MC-308621">MC-308621</a>
+ */
+ private void scheduleNettyTask(final Runnable task) {
+ public void scheduleNettyTask(final Runnable task) {
+ final boolean connected = this.isConnected();
+ if (!connected && !this.preparing) {
+ return;
@@ -208,23 +208,6 @@
+ this.flushQueue();
+ }
+ }
+
+ /**
+ * Schedules a task on the channel's event loop after all currently queued packets have been submitted for writing.
+ * This allows the compression packet to be written and compression to be enabled in the same event-loop task.
+ * Vanilla enables compression from a write-future listener, which may run after another packet has already been
+ * queued without compression.
+ *
+ * @see <a href="https://mojira.dev/MC-308621">MC-308621</a>
+ */
+ public void sendPacketAndScheduleNettyTask(final Packet<?> packet, final boolean flush, final Runnable after) {
+ this.scheduleNettyTask(() -> {
+ Connection.this.sendPacket(packet, null, flush);
+ if (after != null) {
+ after.run();
+ }
+ });
+ }
+ // Paper end - Add netty task scheduling helper
+
private void sendPacket(final Packet<?> packet, final @Nullable ChannelFutureListener listener, final boolean flush) {
@@ -143,7 +143,7 @@
}
}
}
@@ -139,21 +_,25 @@
@@ -139,21 +_,23 @@
private void verifyLoginAndFinishConnectionSetup(final GameProfile profile) {
PlayerList playerList = this.server.getPlayerList();
@@ -164,12 +164,10 @@
- );
+ // Paper start - Keep compression packet write and setup in the same Netty task
+ final int compressionThreshold = this.server.getCompressionThreshold();
+ this.connection.sendPacketAndScheduleNettyTask(
+ new ClientboundLoginCompressionPacket(compressionThreshold), true,
+ () -> {
+ this.connection.setupCompression(compressionThreshold, true);
+ }
+ );
+ this.connection.scheduleNettyTask(() -> {
+ this.connection.send(new ClientboundLoginCompressionPacket(compressionThreshold));
+ this.connection.setupCompression(compressionThreshold, true);
+ });
+ // Paper end - Keep compression packet write and setup in the same Netty task
}
@@ -1,12 +1,11 @@
--- a/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java
+++ b/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java
@@ -101,6 +_,12 @@
@@ -101,6 +_,11 @@
}
if (this.ticksSinceReachedGoal > 60) {
+ // CraftBukkit start - Step on eggs
+ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityInteractEvent(this.removerMob, org.bukkit.craftbukkit.block.CraftBlock.at(level, eatPos))) {
+ this.ticksSinceReachedGoal++;
+ return;
+ }
+ // CraftBukkit end
@@ -1,10 +1,9 @@
--- a/net/minecraft/world/item/crafting/Ingredient.java
+++ b/net/minecraft/world/item/crafting/Ingredient.java
@@ -33,6 +_,24 @@
@@ -33,6 +_,23 @@
public static final Codec<HolderSet<Item>> NON_AIR_HOLDER_SET_CODEC = HolderSetCodec.create(Registries.ITEM, Item.CODEC, false);
public static final Codec<Ingredient> CODEC = ExtraCodecs.nonEmptyHolderSet(NON_AIR_HOLDER_SET_CODEC).xmap(Ingredient::new, i -> i.values);
public final HolderSet<Item> values;
+ public java.util.function.@org.jspecify.annotations.Nullable Predicate<ItemStack> stackPredicate; // Paper - add PredicateChoice
+ // CraftBukkit start
+ private java.util.@org.jspecify.annotations.Nullable List<ItemStack> itemStacks;
+
@@ -25,15 +24,10 @@
private Ingredient(final HolderSet<Item> values) {
values.unwrap().ifRight(directValues -> {
@@ -62,6 +_,22 @@
@@ -62,6 +_,17 @@
@Override
public boolean test(final ItemStack input) {
+ // Paper start - add PredicateChoice
+ if (this.stackPredicate != null) {
+ return this.stackPredicate.test(input);
+ }
+ // Paper end - add PredicateChoice
+ // CraftBukkit start
+ if (this.isExact()) {
+ for (ItemStack item : this.itemStacks()) {
@@ -0,0 +1,182 @@
package io.papermc.paper.plugin;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.AnnotatedType;
import java.util.Comparator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import org.bukkit.craftbukkit.util.Versioning;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.spongepowered.configurate.serialize.ScalarSerializer;
import org.spongepowered.configurate.serialize.SerializationException;
@NullMarked
public sealed interface ApiVersion extends io.papermc.asm.versioned.ApiVersion<ApiVersion> permits ApiVersion.Legacy, ApiVersion.Modern, ApiVersion.None {
Serializer SERIALIZER = new Serializer();
ApiVersion CURRENT = getOrCreateVersion(Versioning.getCurrentApiVersion());
ApiVersion FLATTENING = getOrCreateVersion("1.13");
ApiVersion FIELD_NAME_PARITY = getOrCreateVersion("1.20.5");
ApiVersion ABSTRACT_COW = getOrCreateVersion("1.21.5");
ApiVersion ABSTRACT_CUBE_MOB = getOrCreateVersion("26.2");
ApiVersion NONE = getOrCreateVersion("none");
String getVersionString();
@Override
default int compareTo(final ApiVersion o) {
return switch (o) {
case final Legacy oLegacy -> switch (this) {
case final Legacy thisLegacy -> thisLegacy.compareSameType(oLegacy);
case final Modern ignored -> 1;
case final None ignored -> -1;
};
case final Modern oModern -> switch (this) {
case final Legacy ignored -> -1;
case final Modern thisModern -> thisModern.compareSameType(oModern);
case final None ignored -> -1;
};
case final None ignored -> switch (this) {
case final Legacy ignored2 -> 1;
case final Modern ignored2 -> 1;
case final None ignored2 -> 0;
};
};
}
record Legacy(int major, int minor, int patch) implements ApiVersion {
private static final Comparator<Legacy> COMPARATOR = Comparator
.comparingInt(Legacy::major)
.thenComparingInt(Legacy::minor)
.thenComparingInt(Legacy::patch);
int compareSameType(final Legacy other) {
return COMPARATOR.compare(this, other);
}
@Override
public String getVersionString() {
return this.major() + "." + this.minor() + "." + this.patch();
}
@Override
public String toString() {
return this.getVersionString();
}
}
record Modern(int year, int drop, int hotfix) implements ApiVersion {
private static final Comparator<Modern> COMPARATOR = Comparator
.comparingInt(Modern::year)
.thenComparingInt(Modern::drop)
.thenComparingInt(Modern::hotfix);
int compareSameType(final Modern other) {
return COMPARATOR.compare(this, other);
}
@Override
public String getVersionString() {
return this.year() + "." + this.drop() + "." + this.hotfix();
}
@Override
public String toString() {
return this.getVersionString();
}
}
record None() implements ApiVersion {
private static final None INSTANCE = new None();
@Override
public String getVersionString() {
return "none";
}
@Override
public String toString() {
return this.getVersionString();
}
}
final class Serializer extends ScalarSerializer.Annotated<ApiVersion> {
Serializer() {
super(ApiVersion.class);
}
@Override
public ApiVersion deserialize(final AnnotatedType type, final Object obj) throws SerializationException {
try {
final ApiVersion version = getOrCreateVersion(obj.toString());
final Minimum min = type.getAnnotation(Minimum.class);
if (min != null) {
final ApiVersion minVersion = getOrCreateVersion(min.value());
if (version.isOlderThan(minVersion)) {
throw new SerializationException(ApiVersion.class, version + " is too old for a paper plugin!");
}
}
return version;
} catch (final IllegalArgumentException ex) {
throw new SerializationException(ApiVersion.class, "Could not parse version string", ex);
}
}
@Override
protected Object serialize(final AnnotatedType type, final ApiVersion item, final Predicate<Class<?>> typeSupported) {
return item.getVersionString();
}
}
static ApiVersion getOrCreateVersion(final @Nullable String versionString) {
class Holder {
private static final Map<String, ApiVersion> cache = new ConcurrentHashMap<>();
}
if (versionString == null || versionString.isBlank() || versionString.equalsIgnoreCase("none")) {
return None.INSTANCE;
}
final ApiVersion parsed = parse(versionString);
Holder.cache.putIfAbsent(parsed.getVersionString(), parsed);
return Holder.cache.get(parsed.getVersionString());
}
private static ApiVersion parse(final String versionString) {
final String[] parts = versionString.split("\\.");
if (parts.length < 2 || parts.length > 3) {
throw new IllegalArgumentException("Version string must have 2 or 3 numbers");
}
final int first = Integer.parseInt(parts[0]);
final int second = Integer.parseInt(parts[1]);
final int third = parts.length == 3 ? Integer.parseInt(parts[2]) : 0;
if (first == 1) {
if (second > 21) {
throw new IllegalArgumentException("Legacy version string must be 1.21.xx or lower");
}
return new Legacy(first, second, third);
} else {
if (first < 26) {
throw new IllegalArgumentException("Modern version string must be 26.x.x or higher");
}
return new Modern(first, second, third);
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Minimum {
String value();
}
}
@@ -1,12 +0,0 @@
package io.papermc.paper.plugin.entrypoint.classloader;
import io.papermc.paper.plugin.configuration.PluginMeta;
// Stub, implement in future.
public class PaperClassloaderBytecodeModifier implements ClassloaderBytecodeModifier {
@Override
public byte[] modify(PluginMeta configuration, byte[] bytecode) {
return bytecode;
}
}
@@ -0,0 +1,60 @@
package io.papermc.paper.plugin.entrypoint.classloader.bytecode;
import com.google.common.collect.Iterators;
import io.papermc.paper.plugin.ApiVersion;
import io.papermc.paper.plugin.configuration.PluginMeta;
import io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import net.minecraft.util.Util;
import org.objectweb.asm.Opcodes;
public class PaperClassloaderBytecodeModifier implements ClassloaderBytecodeModifier {
private static final Map<ApiVersion, List<ModifierFactory>> MODIFIERS = Util.make(new LinkedHashMap<>(), map -> {
});
private final Map<ApiVersion, List<VersionedClassloaderBytecodeModifier>> constructedModifiers = MODIFIERS.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> {
return entry.getValue().stream().map(factory -> factory.create(Opcodes.ASM9)).toList();
}));
@Override
public byte[] modify(final PluginMeta configuration, byte[] bytecode) {
int start = -1;
if (configuration.getAPIVersion() != null) {
int i = 0;
for (final Map.Entry<ApiVersion, List<VersionedClassloaderBytecodeModifier>> entry : this.constructedModifiers.entrySet()) {
final ApiVersion apiVersion = ApiVersion.getOrCreateVersion(configuration.getAPIVersion());
final ApiVersion modifierApiVersion = entry.getKey();
if (apiVersion.isOlderThanOrSameAs(modifierApiVersion)) {
start = i;
break;
}
i++;
}
} else {
start = 0;
}
if (start == -1) {
return bytecode; // no modification needed. The plugin version is newer than all versioned modifiers
}
final Iterator<Map.Entry<ApiVersion, List<VersionedClassloaderBytecodeModifier>>> it = this.constructedModifiers.entrySet().iterator();
Iterators.advance(it, start);
while (it.hasNext()) {
for (final VersionedClassloaderBytecodeModifier modifier : it.next().getValue()) {
bytecode = modifier.modify(configuration, bytecode);
}
}
return bytecode;
}
private interface ModifierFactory {
VersionedClassloaderBytecodeModifier create(int api);
}
}
@@ -0,0 +1,32 @@
package io.papermc.paper.plugin.entrypoint.classloader.bytecode;
import io.papermc.asm.AbstractRewriteRuleVisitorFactory;
import io.papermc.asm.ClassInfoProvider;
import io.papermc.asm.rules.builder.RuleFactoryConfiguration;
import io.papermc.paper.plugin.configuration.PluginMeta;
import io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import static io.papermc.asm.util.DescriptorUtils.desc;
public abstract class VersionedClassloaderBytecodeModifier extends AbstractRewriteRuleVisitorFactory implements ClassloaderBytecodeModifier, RuleFactoryConfiguration.Holder {
protected VersionedClassloaderBytecodeModifier(final int api) {
super(api, ClassInfoProvider.basic());
}
@Override
public final byte[] modify(final PluginMeta config, final byte[] bytecode) {
final ClassReader cr = new ClassReader(bytecode);
final ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS); // need to compute maxs because of instruction removal in ctor rewriting
cr.accept(this.createVisitor(cw), 0);
return cw.toByteArray();
}
@Override
public final RuleFactoryConfiguration configuration() {
return RuleFactoryConfiguration.create(desc(this.getClass()));
}
}
@@ -0,0 +1,4 @@
@NullMarked
package io.papermc.paper.plugin.entrypoint.classloader.bytecode;
import org.jspecify.annotations.NullMarked;
@@ -5,15 +5,13 @@ import com.google.common.collect.ImmutableList;
import io.papermc.paper.configuration.constraint.Constraint;
import io.papermc.paper.configuration.serializer.ComponentSerializer;
import io.papermc.paper.configuration.serializer.EnumValueSerializer;
import io.papermc.paper.plugin.ApiVersion;
import io.papermc.paper.plugin.configuration.PluginMeta;
import io.papermc.paper.plugin.provider.configuration.serializer.PermissionConfigurationSerializer;
import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints;
import io.papermc.paper.plugin.provider.configuration.type.DependencyConfiguration;
import io.papermc.paper.plugin.provider.configuration.type.PermissionConfiguration;
import io.papermc.paper.plugin.provider.configuration.type.PluginDependencyLifeCycle;
import java.lang.reflect.Type;
import java.util.function.Predicate;
import org.bukkit.craftbukkit.util.ApiVersion;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
import org.bukkit.plugin.PluginLoadOrder;
@@ -26,8 +24,6 @@ import org.spongepowered.configurate.loader.HeaderMode;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.ObjectMapper;
import org.spongepowered.configurate.objectmapping.meta.Required;
import org.spongepowered.configurate.serialize.ScalarSerializer;
import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.yaml.NodeStyle;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
@@ -63,6 +59,7 @@ public class PaperPluginMeta implements PluginMeta {
@FlattenedResolver
private PermissionConfiguration permissionConfiguration = new PermissionConfiguration(PermissionDefault.OP, List.of());
@Required
@ApiVersion.Minimum("1.19")
private ApiVersion apiVersion;
private Map<PluginDependencyLifeCycle, Map<String, DependencyConfiguration>> dependencies = new EnumMap<>(PluginDependencyLifeCycle.class);
@@ -70,7 +67,6 @@ public class PaperPluginMeta implements PluginMeta {
public PaperPluginMeta() {
}
static final ApiVersion MINIMUM = ApiVersion.getOrCreateVersion("1.19");
public static PaperPluginMeta create(BufferedReader reader) throws ConfigurateException {
YamlConfigurationLoader loader = YamlConfigurationLoader.builder()
.indent(2)
@@ -78,28 +74,9 @@ public class PaperPluginMeta implements PluginMeta {
.headerMode(HeaderMode.NONE)
.source(() -> reader)
.defaultOptions((options) -> {
return options.serializers((serializers) -> {
serializers
.register(new ScalarSerializer<>(ApiVersion.class) {
@Override
public ApiVersion deserialize(final Type type, final Object obj) throws SerializationException {
try {
final ApiVersion version = ApiVersion.getOrCreateVersion(obj.toString());
if (version.isOlderThan(MINIMUM)) {
throw new SerializationException(version + " is too old for a paper plugin!");
}
return version;
} catch (final IllegalArgumentException e) {
throw new SerializationException(e);
}
}
@Override
protected Object serialize(final ApiVersion item, final Predicate<Class<?>> typeSupported) {
return item.getVersionString();
}
})
.register(ApiVersion.SERIALIZER)
.register(new EnumValueSerializer())
.register(PermissionConfiguration.class, PermissionConfigurationSerializer.SERIALIZER)
.register(new ComponentSerializer())
@@ -4,12 +4,18 @@ import java.util.function.Predicate;
import net.minecraft.world.item.ItemStack;
import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.craftbukkit.inventory.CraftRecipe;
import org.jspecify.annotations.NullMarked;
import org.bukkit.inventory.RecipeChoice;
@NullMarked
public record PaperPotionMix(ItemStack result, Predicate<ItemStack> input, Predicate<ItemStack> ingredient) {
public PaperPotionMix(final PotionMix potionMix) {
this(CraftItemStack.asNMSCopy(potionMix.getResult()), CraftRecipe.toIngredient(potionMix.getInput(), true), CraftRecipe.toIngredient(potionMix.getIngredient(), true));
public PaperPotionMix(PotionMix potionMix) {
this(CraftItemStack.asNMSCopy(potionMix.getResult()), convert(potionMix.getInput()), convert(potionMix.getIngredient()));
}
static Predicate<ItemStack> convert(final RecipeChoice choice) {
if (choice instanceof PredicateRecipeChoice predicateRecipeChoice) {
return stack -> predicateRecipeChoice.test(CraftItemStack.asBukkitCopy(stack));
}
return CraftRecipe.toIngredient(choice, true);
}
}
@@ -1,5 +1,6 @@
package io.papermc.paper.registry.entry;
import io.papermc.paper.plugin.ApiVersion;
import io.papermc.paper.registry.PaperRegistryBuilder;
import io.papermc.paper.registry.RegistryKey;
import java.util.function.BiFunction;
@@ -10,7 +11,6 @@ import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceKey;
import org.bukkit.Keyed;
import org.bukkit.NamespacedKey;
import org.bukkit.craftbukkit.util.ApiVersion;
import static io.papermc.paper.registry.entry.RegistryEntryMeta.RegistryModificationApiSupport.ADDABLE;
import static io.papermc.paper.registry.entry.RegistryEntryMeta.RegistryModificationApiSupport.MODIFIABLE;
@@ -1,6 +1,7 @@
package io.papermc.paper.registry.entry;
import com.google.common.base.Preconditions;
import io.papermc.paper.plugin.ApiVersion;
import io.papermc.paper.registry.PaperRegistryBuilder;
import io.papermc.paper.registry.RegistryKey;
import io.papermc.paper.registry.TypedKey;
@@ -16,7 +17,6 @@ import net.minecraft.resources.ResourceKey;
import org.bukkit.Keyed;
import org.bukkit.NamespacedKey;
import org.bukkit.craftbukkit.CraftRegistry;
import org.bukkit.craftbukkit.util.ApiVersion;
public sealed interface RegistryEntryMeta<M, A extends Keyed> permits RegistryEntryMeta.ApiOnly, RegistryEntryMeta.ServerSide { // TODO remove Keyed
@@ -1,6 +1,7 @@
package org.bukkit.craftbukkit;
import com.google.common.base.Preconditions;
import io.papermc.paper.plugin.ApiVersion;
import io.papermc.paper.registry.PaperRegistries;
import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey;
@@ -8,7 +9,6 @@ import io.papermc.paper.registry.entry.RegistryEntryMeta;
import io.papermc.paper.registry.set.NamedRegistryKeySetImpl;
import io.papermc.paper.registry.tag.Tag;
import io.papermc.paper.util.Holderable;
import io.papermc.paper.util.MCUtil;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
@@ -24,7 +24,6 @@ import org.bukkit.NamespacedKey;
import org.bukkit.Particle;
import org.bukkit.Registry;
import org.bukkit.craftbukkit.legacy.FieldRename;
import org.bukkit.craftbukkit.util.ApiVersion;
import org.bukkit.craftbukkit.util.CraftNamespacedKey;
import org.bukkit.craftbukkit.util.Handleable;
import org.bukkit.entity.EntityType;
@@ -13,6 +13,7 @@ import com.mojang.serialization.JsonOps;
import io.papermc.paper.configuration.GlobalConfiguration;
import io.papermc.paper.configuration.PaperServerConfiguration;
import io.papermc.paper.configuration.ServerConfiguration;
import io.papermc.paper.plugin.ApiVersion;
import io.papermc.paper.util.MCUtil;
import io.papermc.paper.world.PaperWorldLoader;
import io.papermc.paper.world.migration.WorldFolderMigration;
@@ -153,7 +154,6 @@ import org.bukkit.craftbukkit.ban.CraftIpBanList;
import org.bukkit.craftbukkit.ban.CraftProfileBanList;
import org.bukkit.craftbukkit.block.data.CraftBlockData;
import org.bukkit.craftbukkit.boss.CraftBossBar;
import org.bukkit.craftbukkit.boss.CraftKeyedBossbar;
import org.bukkit.craftbukkit.command.CraftCommandMap;
import org.bukkit.craftbukkit.command.VanillaCommandWrapper;
import org.bukkit.craftbukkit.entity.CraftEntityFactory;
@@ -197,7 +197,6 @@ import org.bukkit.craftbukkit.tag.CraftEntityTag;
import org.bukkit.craftbukkit.tag.CraftFluidTag;
import org.bukkit.craftbukkit.tag.CraftGameEventTag;
import org.bukkit.craftbukkit.tag.CraftItemTag;
import org.bukkit.craftbukkit.util.ApiVersion;
import org.bukkit.craftbukkit.util.CraftChatMessage;
import org.bukkit.craftbukkit.util.CraftIconCache;
import org.bukkit.craftbukkit.util.CraftLocation;
@@ -1,6 +1,7 @@
package org.bukkit.craftbukkit.attribute;
import com.google.common.base.Preconditions;
import io.papermc.paper.plugin.ApiVersion;
import io.papermc.paper.registry.RegistryKey;
import io.papermc.paper.util.OldEnumHolderable;
import java.util.Locale;
@@ -10,7 +11,6 @@ import org.bukkit.NamespacedKey;
import org.bukkit.attribute.Attribute;
import org.bukkit.craftbukkit.CraftRegistry;
import org.bukkit.craftbukkit.legacy.FieldRename;
import org.bukkit.craftbukkit.util.ApiVersion;
public class CraftAttribute extends OldEnumHolderable<Attribute, net.minecraft.world.entity.ai.attributes.Attribute> implements Attribute {
@@ -1,6 +1,7 @@
package org.bukkit.craftbukkit.enchantments;
import com.google.common.base.Preconditions;
import io.papermc.paper.plugin.ApiVersion;
import io.papermc.paper.registry.RegistryKey;
import io.papermc.paper.util.Holderable;
import java.util.Locale;
@@ -12,7 +13,6 @@ import org.bukkit.NamespacedKey;
import org.bukkit.craftbukkit.CraftRegistry;
import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.craftbukkit.legacy.FieldRename;
import org.bukkit.craftbukkit.util.ApiVersion;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.enchantments.EnchantmentTarget;
import org.bukkit.enchantments.EnchantmentWrapper;
@@ -1,6 +1,7 @@
package org.bukkit.craftbukkit.entity;
import com.google.common.base.Preconditions;
import io.papermc.paper.plugin.ApiVersion;
import io.papermc.paper.registry.RegistryKey;
import java.util.Locale;
import net.minecraft.core.Holder;
@@ -9,7 +10,6 @@ import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.craftbukkit.CraftRegistry;
import org.bukkit.craftbukkit.legacy.FieldRename;
import org.bukkit.craftbukkit.util.ApiVersion;
import org.bukkit.craftbukkit.util.CraftNamespacedKey;
import org.bukkit.entity.EntityType;
@@ -1,8 +1,8 @@
package org.bukkit.craftbukkit.inventory;
import com.google.common.base.Preconditions;
import io.papermc.paper.plugin.ApiVersion;
import org.bukkit.craftbukkit.legacy.FieldRename;
import org.bukkit.craftbukkit.util.ApiVersion;
import org.bukkit.inventory.ItemFlag;
public class CraftItemFlag {
@@ -6,10 +6,8 @@ import io.papermc.paper.registry.data.util.Conversions;
import io.papermc.paper.registry.set.PaperRegistrySets;
import io.papermc.paper.registry.set.RegistryKeySet;
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.world.item.crafting.Ingredient;
import org.bukkit.inventory.ItemType;
@@ -34,9 +32,6 @@ public interface CraftRecipe extends Recipe {
stack = Ingredient.of();
} else if (bukkit instanceof final RecipeChoice.ItemTypeChoice itemTypeChoice) {
stack = Ingredient.of(PaperRegistrySets.convertToNms(Registries.ITEM, Conversions.global().lookup(), itemTypeChoice.itemTypes()));
} else if (bukkit instanceof final 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(CraftItemType::bukkitToMinecraft));
} else if (bukkit instanceof RecipeChoice.ExactChoice) {
@@ -68,19 +63,13 @@ public interface CraftRecipe extends Recipe {
return RecipeChoice.empty(); // Paper - null breaks API contracts
}
if (ingredient.stackPredicate != null) {
net.minecraft.world.item.ItemStack stack = ingredient.itemStacks().iterator().next();
Predicate<org.bukkit.inventory.ItemStack> predicate = bukkitStack -> ingredient.stackPredicate.test(CraftItemStack.asNMSCopy(bukkitStack));
return RecipeChoice.predicateChoice(predicate, CraftItemStack.asBukkitCopy(stack));
}
if (ingredient.isExact()) {
List<org.bukkit.inventory.ItemStack> choices = new ArrayList<>(ingredient.itemStacks().size());
for (net.minecraft.world.item.ItemStack i : ingredient.itemStacks()) {
choices.add(CraftItemStack.asBukkitCopy(i));
}
return RecipeChoice.exactChoice(choices);
return new RecipeChoice.ExactChoice(choices);
} else {
final RegistryKeySet<ItemType> itemTypes = PaperRegistrySets.convertToApi(RegistryKey.ITEM, ingredient.values);
return RecipeChoice.itemType(itemTypes);
@@ -1,5 +1,6 @@
package org.bukkit.craftbukkit.legacy;
import io.papermc.paper.plugin.ApiVersion;
import java.util.function.BiFunction;
import org.bukkit.NamespacedKey;
import org.bukkit.Particle;
@@ -12,7 +13,6 @@ import org.bukkit.craftbukkit.legacy.reroute.DoNotReroute;
import org.bukkit.craftbukkit.legacy.reroute.InjectPluginVersion;
import org.bukkit.craftbukkit.legacy.reroute.RerouteMethodName;
import org.bukkit.craftbukkit.legacy.reroute.RerouteStatic;
import org.bukkit.craftbukkit.util.ApiVersion;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.EntityType;
import org.bukkit.inventory.ItemFlag;
@@ -1,5 +1,6 @@
package org.bukkit.craftbukkit.legacy;
import io.papermc.paper.plugin.ApiVersion;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
@@ -29,7 +30,6 @@ import org.bukkit.craftbukkit.legacy.reroute.InjectPluginVersion;
import org.bukkit.craftbukkit.legacy.reroute.RerouteStatic;
import org.bukkit.craftbukkit.tag.CraftBlockTag;
import org.bukkit.craftbukkit.tag.CraftItemTag;
import org.bukkit.craftbukkit.util.ApiVersion;
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
import org.bukkit.enchantments.EnchantmentTarget;
import org.bukkit.entity.Animals;
@@ -1,7 +0,0 @@
package org.bukkit.craftbukkit.legacy.enums;
/**
* A crash dummy to use, instead of the old enums which matured to Abstracthood or Interfacehood and the baby enums which are still growing.
*/
public enum DummyEnum {
}
@@ -1,288 +0,0 @@
package org.bukkit.craftbukkit.legacy.enums;
import com.google.common.base.Converter;
import com.google.common.base.Enums;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import org.bukkit.Art;
import org.bukkit.Fluid;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.Sound;
import org.bukkit.attribute.Attribute;
import org.bukkit.block.Biome;
import org.bukkit.block.banner.PatternType;
import org.bukkit.craftbukkit.legacy.FieldRename;
import org.bukkit.craftbukkit.legacy.reroute.DoNotReroute;
import org.bukkit.craftbukkit.legacy.reroute.InjectPluginVersion;
import org.bukkit.craftbukkit.legacy.reroute.NotInBukkit;
import org.bukkit.craftbukkit.legacy.reroute.RequireCompatibility;
import org.bukkit.craftbukkit.legacy.reroute.RequirePluginVersion;
import org.bukkit.craftbukkit.legacy.reroute.RerouteArgumentType;
import org.bukkit.craftbukkit.legacy.reroute.RerouteReturnType;
import org.bukkit.craftbukkit.legacy.reroute.RerouteStatic;
import org.bukkit.craftbukkit.util.ApiVersion;
import org.bukkit.craftbukkit.util.ClassTraverser;
import org.bukkit.entity.Cat;
import org.bukkit.entity.Frog;
import org.bukkit.entity.Villager;
import org.bukkit.map.MapCursor;
import org.bukkit.util.OldEnum;
@Deprecated
@NotInBukkit
@RequireCompatibility("enum-compatibility-mode")
@RequirePluginVersion(maxInclusive = "1.20.6")
public class EnumEvil {
private static final Map<Class<?>, LegacyRegistryData> REGISTRIES = new HashMap<>();
static {
// Add Classes which got changed here
REGISTRIES.put(Art.class, new LegacyRegistryData(Registry.ART, Art::valueOf));
REGISTRIES.put(Attribute.class, new LegacyRegistryData(Registry.ATTRIBUTE, Attribute::valueOf));
REGISTRIES.put(Biome.class, new LegacyRegistryData(Registry.BIOME, Biome::valueOf));
REGISTRIES.put(Fluid.class, new LegacyRegistryData(Registry.FLUID, Fluid::valueOf));
REGISTRIES.put(Villager.Type.class, new LegacyRegistryData(Registry.VILLAGER_TYPE, Villager.Type::valueOf));
REGISTRIES.put(Villager.Profession.class, new LegacyRegistryData(Registry.VILLAGER_PROFESSION, Villager.Profession::valueOf));
REGISTRIES.put(Sound.class, new LegacyRegistryData(Registry.SOUNDS, Sound::valueOf));
REGISTRIES.put(Frog.Variant.class, new LegacyRegistryData(Registry.FROG_VARIANT, Frog.Variant::valueOf));
REGISTRIES.put(Cat.Type.class, new LegacyRegistryData(Registry.CAT_VARIANT, Cat.Type::valueOf));
REGISTRIES.put(MapCursor.Type.class, new LegacyRegistryData(Registry.MAP_DECORATION_TYPE, MapCursor.Type::valueOf));
REGISTRIES.put(PatternType.class, new LegacyRegistryData(Registry.BANNER_PATTERN, PatternType::valueOf));
}
public static LegacyRegistryData getRegistryData(Class<?> clazz) {
ClassTraverser it = new ClassTraverser(clazz);
LegacyRegistryData registryData;
while (it.hasNext()) {
registryData = EnumEvil.REGISTRIES.get(it.next());
if (registryData != null) {
return registryData;
}
}
return null;
}
@DoNotReroute
public static Registry<?> getRegistry(Class<?> clazz) {
LegacyRegistryData registryData = EnumEvil.getRegistryData(clazz);
if (registryData != null) {
return registryData.registry();
}
return null;
}
@RerouteStatic("com/google/common/collect/Maps")
@RerouteReturnType("java/util/EnumSet")
public static ImposterEnumMap newEnumMap(Class<?> objectClass) {
return new ImposterEnumMap(objectClass);
}
@RerouteStatic("com/google/common/collect/Maps")
@RerouteReturnType("java/util/EnumSet")
public static ImposterEnumMap newEnumMap(Map map) {
return new ImposterEnumMap(map);
}
@RerouteStatic("com/google/common/collect/Sets")
public static Collector<?, ?, ?> toImmutableEnumSet() {
return Collectors.toUnmodifiableSet();
}
@RerouteStatic("com/google/common/collect/Sets")
@RerouteReturnType("java/util/EnumSet")
public static ImposterEnumSet newEnumSet(Iterable<?> iterable, Class<?> clazz) {
ImposterEnumSet set = ImposterEnumSet.noneOf(clazz);
for (Object some : iterable) {
set.add(some);
}
return set;
}
@RerouteStatic("com/google/common/collect/Sets")
public static ImmutableSet<?> immutableEnumSet(Iterable<?> iterable) {
return ImmutableSet.of(iterable);
}
@RerouteStatic("com/google/common/collect/Sets")
public static ImmutableSet<?> immutableEnumSet(@RerouteArgumentType("java/lang/Enum") Object first, @RerouteArgumentType("[java/lang/Enum") Object... rest) {
return ImmutableSet.of(first, rest);
}
@RerouteStatic("com/google/common/base/Enums")
public static Field getField(@RerouteArgumentType("java/lang/Enum") Object value) {
if (value instanceof Enum eValue) {
return Enums.getField(eValue);
}
try {
return value.getClass().getField(((OldEnum) value).name());
} catch (NoSuchFieldException impossible) {
throw new AssertionError(impossible);
}
}
@RerouteStatic("com/google/common/base/Enums")
public static com.google.common.base.Optional getIfPresent(Class clazz, String name, @InjectPluginVersion ApiVersion apiVersion) {
if (clazz.isEnum()) {
return Enums.getIfPresent(clazz, name);
}
Registry registry = EnumEvil.getRegistry(clazz);
if (registry == null) {
return com.google.common.base.Optional.absent();
}
name = FieldRename.rename(apiVersion, clazz.getName().replace('.', '/'), name);
return com.google.common.base.Optional.fromNullable(registry.get(NamespacedKey.fromString(name.toLowerCase(Locale.ROOT))));
}
@RerouteStatic("com/google/common/base/Enums")
public static Converter stringConverter(Class clazz, @InjectPluginVersion ApiVersion apiVersion) {
if (clazz.isEnum()) {
return Enums.stringConverter(clazz);
}
return new StringConverter(apiVersion, clazz);
}
public static Object[] getEnumConstants(Class<?> clazz) {
if (clazz.isEnum()) {
return clazz.getEnumConstants();
}
Registry<?> registry = EnumEvil.getRegistry(clazz);
if (registry == null) {
return clazz.getEnumConstants();
}
// Need to do this in such away to avoid ClassCastException
List<?> values = Lists.newArrayList(registry);
Object array = Array.newInstance(clazz, values.size());
for (int i = 0; i < values.size(); i++) {
Array.set(array, i, values.get(i));
}
return (Object[]) array;
}
public static String name(@RerouteArgumentType("java/lang/Enum") Object object) {
if (object instanceof OldEnum<?>) {
return ((OldEnum<?>) object).name();
}
return ((Enum<?>) object).name();
}
public static int compareTo(@RerouteArgumentType("java/lang/Enum") Object object, @RerouteArgumentType("java/lang/Enum") Object other) {
if (object instanceof OldEnum<?>) {
return ((OldEnum) object).compareTo((OldEnum) other);
}
return ((Enum) object).compareTo((Enum) other);
}
public static Class<?> getDeclaringClass(@RerouteArgumentType("java/lang/Enum") Object object) {
Class<?> clazz = object.getClass();
Class<?> zuper = clazz.getSuperclass();
return (zuper == Enum.class) ? clazz : zuper;
}
public static Optional<Enum.EnumDesc> describeConstable(@RerouteArgumentType("java/lang/Enum") Object object) {
return EnumEvil.getDeclaringClass(object)
.describeConstable()
.map(c -> Enum.EnumDesc.of(c, EnumEvil.name(object)));
}
@RerouteStatic("java/lang/Enum")
@RerouteReturnType("java/lang/Enum")
public static Object valueOf(Class enumClass, String name, @InjectPluginVersion ApiVersion apiVersion) {
name = FieldRename.rename(apiVersion, enumClass.getName().replace('.', '/'), name);
LegacyRegistryData registryData = EnumEvil.getRegistryData(enumClass);
if (registryData != null) {
return registryData.function().apply(name);
}
return Enum.valueOf(enumClass, name);
}
public static String toString(@RerouteArgumentType("java/lang/Enum") Object object) {
return object.toString();
}
public static int ordinal(@RerouteArgumentType("java/lang/Enum") Object object) {
if (object instanceof OldEnum<?>) {
return ((OldEnum<?>) object).ordinal();
}
return ((Enum<?>) object).ordinal();
}
public record LegacyRegistryData(Registry<?> registry, Function<String, ?> function) {
}
private static final class StringConverter<T extends OldEnum<T>> extends Converter<String, T> implements Serializable {
private final ApiVersion apiVersion;
private final Class<T> clazz;
private transient LegacyRegistryData registryData;
StringConverter(ApiVersion apiVersion, Class<T> clazz) {
this.apiVersion = apiVersion;
this.clazz = clazz;
}
@Override
protected T doForward(String value) {
if (this.registryData == null) {
this.registryData = EnumEvil.getRegistryData(this.clazz);
}
value = FieldRename.rename(this.apiVersion, this.clazz.getName().replace('.', '/'), value);
return (T) this.registryData.function().apply(value);
}
@Override
protected String doBackward(T enumValue) {
return enumValue.name();
}
@Override
public boolean equals(Object object) {
if (object instanceof StringConverter<?> that) {
return this.clazz.equals(that.clazz);
}
return false;
}
@Override
public int hashCode() {
return this.clazz.hashCode();
}
@Override
public String toString() {
return "Enums.stringConverter(" + this.clazz.getName() + ".class)";
}
private static final long serialVersionUID = 0L;
}
}
@@ -1,137 +0,0 @@
package org.bukkit.craftbukkit.legacy.enums;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* The "I can't believe it works" map.
* It replaces every EnumMap with the ImposterEnumMap and uses a HashMap instead of an object array.
* Used so that plugins which use an EnumMap still work.
*/
public class ImposterEnumMap extends AbstractMap<Object, Object> {
private final Class<?> objectClass;
private final Map map;
public ImposterEnumMap(Class<?> objectClass) {
this.objectClass = objectClass;
this.map = ImposterEnumMap.getMap(objectClass);
}
public ImposterEnumMap(EnumMap enumMap) {
this.objectClass = DummyEnum.class;
this.map = enumMap.clone();
}
public ImposterEnumMap(Map map) {
if (map instanceof ImposterEnumMap) {
this.objectClass = ((ImposterEnumMap) map).objectClass;
this.map = ImposterEnumMap.getMap(this.objectClass);
} else {
this.objectClass = DummyEnum.class;
this.map = new TreeMap();
}
this.map.putAll(map);
}
private static Map getMap(Class<?> objectClass) {
// Since we replace every enum map we might also replace some maps which are for real enums.
// If this is the case use a EnumMap instead of a HashMap
if (objectClass.isEnum()) {
return new EnumMap(objectClass);
} else {
return new HashMap();
}
}
@Override
public int size() {
return this.map.size();
}
@Override
public boolean containsValue(Object value) {
return this.map.containsValue(value);
}
@Override
public boolean containsKey(Object key) {
return this.map.containsKey(key);
}
@Override
public Object get(Object key) {
return this.map.get(key);
}
@Override
public Object put(Object key, Object value) {
this.typeCheck(key);
return this.map.put(key, value);
}
@Override
public Object remove(Object key) {
return this.map.remove(key);
}
@Override
public void putAll(Map<? extends Object, ?> m) {
if (this.map instanceof EnumMap<?, ?>) {
this.map.putAll(m);
}
super.putAll(m);
}
@Override
public void clear() {
this.map.clear();
}
@Override
public Set<Object> keySet() {
return this.map.keySet();
}
@Override
public Collection<Object> values() {
return this.map.values();
}
@Override
public Set<Entry<Object, Object>> entrySet() {
return this.map.entrySet();
}
@Override
public boolean equals(Object o) {
return this.map.equals(o);
}
@Override
public int hashCode() {
return this.map.hashCode();
}
@Override
public ImposterEnumMap clone() {
ImposterEnumMap enumMap = new ImposterEnumMap(this.objectClass);
enumMap.putAll(this.map);
return enumMap;
}
private void typeCheck(Object object) {
if (this.objectClass != DummyEnum.class) {
if (!this.objectClass.isAssignableFrom(object.getClass())) {
throw new ClassCastException(object.getClass() + " != " + this.objectClass);
}
}
}
}
@@ -1,346 +0,0 @@
package org.bukkit.craftbukkit.legacy.enums;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
import org.bukkit.Registry;
import org.bukkit.util.OldEnum;
import org.jetbrains.annotations.NotNull;
public class ImposterEnumSet extends AbstractSet<Object> {
private final Class<?> objectClass;
private final Set set;
private static Set createSet(Class<?> clazz) {
if (clazz.isEnum()) {
return EnumSet.noneOf((Class<Enum>) clazz);
} else {
return new TreeSet();
}
}
public static ImposterEnumSet noneOf(Class<?> clazz) {
Set set = ImposterEnumSet.createSet(clazz);
return new ImposterEnumSet(set, clazz);
}
public static ImposterEnumSet allOf(Class<?> clazz) {
Set set;
if (clazz.isEnum()) {
set = EnumSet.allOf((Class<Enum>) clazz);
} else {
set = new HashSet();
Registry registry = EnumEvil.getRegistry(clazz);
if (registry == null) {
throw new IllegalArgumentException("Class " + clazz + " is not an Enum nor an OldEnum");
}
for (Object object : registry) {
set.add(object);
}
}
return new ImposterEnumSet(set, clazz);
}
public static ImposterEnumSet copyOf(Set set) {
Class<?> clazz;
if (set instanceof ImposterEnumSet imposter) {
set = imposter.set;
clazz = imposter.objectClass;
} else {
if (!set.isEmpty()) {
clazz = (Class<?>) set.stream()
.filter(val -> val != null)
.map(val -> val.getClass())
.findAny()
.orElse(Object.class);
} else {
clazz = Object.class;
}
}
Set newSet = ImposterEnumSet.createSet(clazz);
newSet.addAll(set);
return new ImposterEnumSet(newSet, clazz);
}
public static ImposterEnumSet copyOf(Collection collection) {
Class<?> clazz;
if (collection instanceof ImposterEnumSet imposter) {
collection = imposter.set;
clazz = imposter.objectClass;
} else {
if (!collection.isEmpty()) {
clazz = (Class<?>) collection.stream()
.filter(val -> val != null)
.map(val -> val.getClass())
.findAny()
.orElse(Object.class);
} else {
clazz = Object.class;
}
}
Set newSet = ImposterEnumSet.createSet(clazz);
newSet.addAll(collection);
return new ImposterEnumSet(newSet, clazz);
}
public static ImposterEnumSet complementOf(Set set) {
Class<?> clazz = null;
if (set instanceof ImposterEnumSet imposter) {
set = imposter.set;
clazz = imposter.objectClass;
}
if (set instanceof EnumSet<?> enumSet) {
enumSet = EnumSet.complementOf(enumSet);
if (clazz != null) {
return new ImposterEnumSet(enumSet, clazz);
}
if (!set.isEmpty()) {
clazz = (Class<?>) set.stream()
.filter(val -> val != null)
.map(val -> val.getClass())
.findAny()
.orElse(Object.class);
} else {
clazz = (Class<?>) enumSet.stream()
.filter(val -> val != null)
.map(val -> val.getClass())
.map(val -> (Class) val)
.findAny()
.orElse(Object.class);
}
return new ImposterEnumSet(enumSet, clazz);
}
if (set.isEmpty() && clazz == null) {
throw new IllegalStateException("Class is null and set is empty, cannot get class!");
}
if (clazz == null) {
clazz = (Class<?>) set.stream()
.filter(val -> val != null)
.map(val -> val.getClass())
.findAny()
.orElse(Object.class);
}
Registry registry = EnumEvil.getRegistry(clazz);
Set newSet = new HashSet();
for (Object value : registry) {
if (set.contains(value)) {
continue;
}
newSet.add(value);
}
return new ImposterEnumSet(newSet, clazz);
}
public static ImposterEnumSet of(Object e) {
Set set = ImposterEnumSet.createSet(e.getClass());
set.add(e);
return new ImposterEnumSet(set, e.getClass());
}
public static ImposterEnumSet of(Object e1, Object e2) {
Set set = ImposterEnumSet.createSet(e1.getClass());
set.add(e1);
set.add(e2);
return new ImposterEnumSet(set, e1.getClass());
}
public static ImposterEnumSet of(Object e1, Object e2, Object e3) {
Set set = ImposterEnumSet.createSet(e1.getClass());
set.add(e1);
set.add(e2);
set.add(e3);
return new ImposterEnumSet(set, e1.getClass());
}
public static ImposterEnumSet of(Object e1, Object e2, Object e3, Object e4) {
Set set = ImposterEnumSet.createSet(e1.getClass());
set.add(e1);
set.add(e2);
set.add(e3);
set.add(e4);
return new ImposterEnumSet(set, e1.getClass());
}
public static ImposterEnumSet of(Object e1, Object e2, Object e3, Object e4, Object e5) {
Set set = ImposterEnumSet.createSet(e1.getClass());
set.add(e1);
set.add(e2);
set.add(e3);
set.add(e4);
set.add(e5);
return new ImposterEnumSet(set, e1.getClass());
}
public static ImposterEnumSet of(Object e, Object... rest) {
Set set = ImposterEnumSet.createSet(e.getClass());
set.add(e);
Collections.addAll(set, rest);
return new ImposterEnumSet(set, e.getClass());
}
public static ImposterEnumSet range(Object from, Object to) {
Set set;
if (from.getClass().isEnum()) {
set = EnumSet.range((Enum) from, (Enum) to);
} else {
set = new HashSet();
Registry registry = EnumEvil.getRegistry(from.getClass());
for (Object o : registry) {
if (((OldEnum) o).ordinal() < ((OldEnum) from).ordinal()) {
continue;
}
if (((OldEnum) o).ordinal() > ((OldEnum) to).ordinal()) {
continue;
}
set.add(o);
}
}
return new ImposterEnumSet(set, from.getClass());
}
private ImposterEnumSet(Set set, Class<?> objectClass) {
this.set = set;
this.objectClass = objectClass;
}
@Override
public Iterator<Object> iterator() {
return this.set.iterator();
}
@Override
public int size() {
return this.set.size();
}
@Override
public boolean equals(Object o) {
return this.set.equals(o);
}
@Override
public int hashCode() {
return this.set.hashCode();
}
@Override
public boolean removeAll(Collection<?> c) {
return this.set.removeAll(c);
}
@Override
public boolean isEmpty() {
return this.set.isEmpty();
}
@Override
public boolean contains(Object o) {
return this.set.contains(o);
}
@NotNull
@Override
public Object[] toArray() {
return this.set.toArray();
}
@NotNull
@Override
public <T> T[] toArray(@NotNull T[] a) {
return (T[]) this.set.toArray(a);
}
@Override
public boolean add(Object o) {
this.typeCheck(o);
return this.set.add(o);
}
@Override
public boolean remove(Object o) {
return this.set.remove(o);
}
@Override
public boolean containsAll(@NotNull Collection<?> c) {
return this.set.containsAll(c);
}
@Override
public boolean addAll(@NotNull Collection<?> c) {
if (this.set instanceof EnumSet<?>) {
this.set.addAll(c);
}
return super.addAll(c);
}
@Override
public boolean retainAll(@NotNull Collection<?> c) {
return this.set.retainAll(c);
}
@Override
public void clear() {
this.set.clear();
}
@Override
public String toString() {
return this.set.toString();
}
public ImposterEnumSet clone() {
Set newSet;
if (this.set instanceof EnumSet<?> enumSet) {
newSet = enumSet.clone();
} else {
newSet = new HashSet();
newSet.addAll(this.set);
}
return new ImposterEnumSet(newSet, this.objectClass);
}
private void typeCheck(Object object) {
if (this.objectClass != DummyEnum.class) {
if (!this.objectClass.isAssignableFrom(object.getClass())) {
throw new ClassCastException(object.getClass() + " != " + this.objectClass);
}
}
}
}
@@ -1,12 +1,12 @@
package org.bukkit.craftbukkit.legacy.fieldrename;
import io.papermc.paper.plugin.ApiVersion;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
import org.bukkit.NamespacedKey;
import org.bukkit.craftbukkit.util.ApiVersion;
public record FieldRenameData(RenameData<String> renameData, RenameData<NamespacedKey> keyRenameData) {
@@ -1,6 +1,7 @@
package org.bukkit.craftbukkit.legacy.reroute;
import org.bukkit.craftbukkit.util.ApiVersion;
import io.papermc.paper.plugin.ApiVersion;
public record RequirePluginVersionData(ApiVersion minInclusive, ApiVersion maxInclusive) {
@@ -1,9 +1,9 @@
package org.bukkit.craftbukkit.legacy.reroute;
import io.papermc.paper.plugin.ApiVersion;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import org.bukkit.craftbukkit.util.ApiVersion;
import org.bukkit.craftbukkit.util.ClassTraverser;
import org.jetbrains.annotations.VisibleForTesting;
import org.objectweb.asm.Type;
@@ -1,6 +1,7 @@
package org.bukkit.craftbukkit.legacy.reroute;
import com.google.common.base.Preconditions;
import io.papermc.paper.plugin.ApiVersion;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
@@ -10,7 +11,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import org.bukkit.craftbukkit.util.ApiVersion;
import org.objectweb.asm.Type;
public class RerouteBuilder {
@@ -2,6 +2,7 @@ package org.bukkit.craftbukkit.potion;
import com.google.common.base.Preconditions;
import com.google.common.base.Suppliers;
import io.papermc.paper.plugin.ApiVersion;
import io.papermc.paper.registry.RegistryKey;
import java.util.List;
import java.util.Locale;
@@ -13,7 +14,6 @@ import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.craftbukkit.CraftRegistry;
import org.bukkit.craftbukkit.legacy.FieldRename;
import org.bukkit.craftbukkit.util.ApiVersion;
import org.bukkit.craftbukkit.util.CraftNamespacedKey;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
@@ -1,132 +0,0 @@
package org.bukkit.craftbukkit.util;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
public final class ApiVersion implements Comparable<ApiVersion>, Serializable {
public static final ApiVersion CURRENT;
public static final ApiVersion FLATTENING;
public static final ApiVersion FIELD_NAME_PARITY;
public static final ApiVersion ABSTRACT_COW;
public static final ApiVersion ABSTRACT_CUBE_MOB;
public static final ApiVersion NONE;
private static final Map<String, ApiVersion> versions;
static {
versions = new HashMap<>();
CURRENT = getOrCreateVersion(Versioning.getCurrentApiVersion());
FLATTENING = getOrCreateVersion("1.13");
FIELD_NAME_PARITY = getOrCreateVersion("1.20.5");
ABSTRACT_COW = getOrCreateVersion("1.21.5");
ABSTRACT_CUBE_MOB = getOrCreateVersion("26.2");
NONE = getOrCreateVersion("none");
}
private final boolean none;
private final int major;
private final int minor;
private final int patch;
private ApiVersion() {
this.none = true;
this.major = Integer.MIN_VALUE;
this.minor = Integer.MIN_VALUE;
this.patch = Integer.MIN_VALUE;
}
private ApiVersion(int major, int minor, int patch) {
this.none = false;
this.major = major;
this.minor = minor;
this.patch = patch;
}
public static ApiVersion getOrCreateVersion(String versionString) {
if (versionString == null || versionString.trim().isEmpty() || versionString.equalsIgnoreCase("none")) {
return ApiVersion.versions.computeIfAbsent("none", s -> new ApiVersion());
}
ApiVersion version = ApiVersion.versions.get(versionString);
if (version != null) {
return version;
}
String[] versionParts = versionString.split("\\.");
if (versionParts.length != 2 && versionParts.length != 3) {
throw new IllegalArgumentException(String.format("API version string should be of format \"major.minor.patch\" or \"major.minor\", where \"major\", \"minor\" and \"patch\" are numbers. For example \"1.18.2\" or \"1.13\", but got '%s' instead.", versionString));
}
int major = ApiVersion.parseNumber(versionParts[0]);
int minor = ApiVersion.parseNumber(versionParts[1]);
int patch;
if (versionParts.length == 3) {
patch = ApiVersion.parseNumber(versionParts[2]);
} else {
patch = 0;
}
versionString = ApiVersion.toVersionString(major, minor, patch);
return ApiVersion.versions.computeIfAbsent(versionString, s -> new ApiVersion(major, minor, patch));
}
private static int parseNumber(String number) {
return Integer.parseInt(number);
}
private static String toVersionString(int major, int minor, int patch) {
return major + "." + minor + "." + patch;
}
@Override
public int compareTo(@NotNull ApiVersion other) {
int result = Integer.compare(this.major, other.major);
if (result == 0) {
result = Integer.compare(this.minor, other.minor);
}
if (result == 0) {
result = Integer.compare(this.patch, other.patch);
}
return result;
}
public String getVersionString() {
if (this.none) {
return "none";
}
return ApiVersion.toVersionString(this.major, this.minor, this.patch);
}
public boolean isNewerThan(ApiVersion apiVersion) {
return this.compareTo(apiVersion) > 0;
}
public boolean isOlderThan(ApiVersion apiVersion) {
return this.compareTo(apiVersion) < 0;
}
public boolean isNewerThanOrSameAs(ApiVersion apiVersion) {
return this.compareTo(apiVersion) >= 0;
}
public boolean isOlderThanOrSameAs(ApiVersion apiVersion) {
return this.compareTo(apiVersion) <= 0;
}
@Override
public String toString() {
return this.getVersionString();
}
private static final long serialVersionUID = 0L;
}
@@ -1,15 +1,13 @@
package org.bukkit.craftbukkit.util;
import com.google.common.base.Predicates;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.io.ByteStreams;
import io.papermc.paper.plugin.ApiVersion;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
@@ -31,7 +29,6 @@ import org.bukkit.Material;
import org.bukkit.craftbukkit.legacy.FieldRename;
import org.bukkit.craftbukkit.legacy.MaterialRerouting;
import org.bukkit.craftbukkit.legacy.MethodRerouting;
import org.bukkit.craftbukkit.legacy.enums.EnumEvil;
import org.bukkit.craftbukkit.legacy.reroute.Reroute;
import org.bukkit.craftbukkit.legacy.reroute.RerouteArgument;
import org.bukkit.craftbukkit.legacy.reroute.RerouteBuilder;
@@ -70,12 +67,6 @@ public class Commodore {
"org/bukkit/inventory/ItemStack (S)V setDurability"
));
private static final Map<String, String> ENUM_RENAMES = Map.of(
"java/lang/Enum", "java/lang/Object",
"java/util/EnumSet", "org/bukkit/craftbukkit/legacy/enums/ImposterEnumSet",
"java/util/EnumMap", "org/bukkit/craftbukkit/legacy/enums/ImposterEnumMap"
);
private static final Map<String, String> RENAMES = Map.of(
"org/bukkit/entity/TextDisplay$TextAligment", "org/bukkit/entity/TextDisplay$TextAlignment", // SPIGOT-7335
"org/spigotmc/event/entity/EntityMountEvent", "org/bukkit/event/entity/EntityMountEvent",
@@ -123,7 +114,6 @@ public class Commodore {
.create(compatibilityPresent)
.forClass(FieldRename.class)
.forClass(MethodRerouting.class)
.forClass(EnumEvil.class)
.build();
this.reroutes.clear();
@@ -19,6 +19,8 @@ import io.papermc.paper.adventure.PaperAdventure;
import io.papermc.paper.datapack.DynamicBuiltinPack;
import io.papermc.paper.datapack.DynamicBuiltinPacks;
import io.papermc.paper.entity.EntitySerializationFlag;
import io.papermc.paper.plugin.ApiVersion;
import io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier;
import io.papermc.paper.registry.RegistryKey;
import io.papermc.paper.util.MCUtil;
import java.io.IOException;
@@ -421,6 +423,7 @@ public final class CraftMagicNumbers implements UnsafeValues {
// Paper end
try {
clazz = this.commodore.convert(clazz, pdf.getName(), ApiVersion.getOrCreateVersion(pdf.getAPIVersion()), ((CraftServer) Bukkit.getServer()).activeCompatibilities);
clazz = ClassloaderBytecodeModifier.bytecodeModifier().modify(pdf, clazz);
} catch (Exception ex) {
Bukkit.getLogger().log(Level.SEVERE, "Fatal error trying to convert " + pdf.getFullName() + ":" + path, ex);
}
@@ -1 +1 @@
io.papermc.paper.plugin.entrypoint.classloader.PaperClassloaderBytecodeModifier
io.papermc.paper.plugin.entrypoint.classloader.bytecode.PaperClassloaderBytecodeModifier
@@ -1,221 +0,0 @@
package io.papermc.paper.inventory.recipe;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import net.minecraft.world.entity.player.StackedContents;
import net.minecraft.world.entity.player.StackedItemContents;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.crafting.PlacementInfo;
import net.minecraft.world.item.crafting.Recipe;
import org.bukkit.support.environment.VanillaFeature;
import org.junit.jupiter.api.Test;
@VanillaFeature
class PredicateChoiceMatchingTest {
private static Ingredient regular(final net.minecraft.world.item.Item item) {
return Ingredient.of(item);
}
private static Ingredient exact(final ItemStack... stacks) {
return Ingredient.ofStacks(List.of(stacks));
}
private static Ingredient predicate(final Predicate<ItemStack> pred, final ItemStack example) {
final Ingredient ingredient = Ingredient.ofStacks(List.of(example));
ingredient.stackPredicate = pred;
return ingredient;
}
private static ItemStack damaged(final net.minecraft.world.item.Item item, final int damage) {
final ItemStack stack = new ItemStack(item);
stack.setDamageValue(damage);
return stack;
}
private record Outcome(boolean canCraftOne, int biggestStack, int effectiveBatch, List<ItemOrExact> itemsUsed) {}
private static Outcome run(final List<Ingredient> ingredients, final List<ItemStack> inventory) {
final StackedItemContents contents = new StackedItemContents();
final PlacementInfo placementInfo = PlacementInfo.create(ingredients);
final Recipe<?> recipe = mock(Recipe.class);
when(recipe.placementInfo()).thenReturn(placementInfo);
contents.initializeExtras(recipe, null);
for (final ItemStack stack : inventory) {
contents.accountSimpleStack(stack);
}
final boolean canCraftOne = contents.canCraft(recipe, null);
final int biggest = contents.getBiggestCraftableStack(recipe, null);
final List<ItemOrExact> itemsUsed = new ArrayList<>();
int effectiveBatch = 0;
if (biggest > 0) {
contents.canCraft(recipe, biggest, itemsUsed::add);
effectiveBatch = biggest;
for (final ItemOrExact used : itemsUsed) {
effectiveBatch = Math.min(effectiveBatch, used.getMaxStackSize());
}
}
return new Outcome(canCraftOne, biggest, effectiveBatch, itemsUsed);
}
@Test
void regularIngredient_countsNormally() {
final Outcome o = run(List.of(regular(Items.STONE)), List.of(new ItemStack(Items.STONE, 64)));
assertTrue(o.canCraftOne(), "one stone should satisfy a stone ingredient");
assertEquals(64, o.biggestStack(), "64 stone -> 64 craftable");
}
@Test
void exactIngredient_matchesByComponents() {
final ItemStack named = new ItemStack(Items.STONE);
named.setDamageValue(0);
final Outcome o = run(List.of(exact(new ItemStack(Items.DIAMOND, 1))), List.of(new ItemStack(Items.DIAMOND, 5)));
assertTrue(o.canCraftOne());
assertEquals(5, o.biggestStack());
}
@Test
void predicate_singleIngredient_stackableItems() {
final Outcome o = run(
List.of(predicate(s -> s.is(Items.STONE), new ItemStack(Items.STONE))),
List.of(new ItemStack(Items.STONE, 64))
);
assertTrue(o.canCraftOne(), "stone matches the predicate");
assertEquals(64, o.biggestStack(), "64 stackable matching items -> 64 craftable");
assertEquals(64, o.effectiveBatch());
}
@Test
void predicate_distinctNonStackingItems() {
// Two DIFFERENT damaged pickaxes (different components -> distinct Exact keys, maxStack 1).
final Outcome o = run(
List.of(predicate(s -> s.is(Items.DIAMOND_PICKAXE), new ItemStack(Items.DIAMOND_PICKAXE))),
List.of(damaged(Items.DIAMOND_PICKAXE, 10), damaged(Items.DIAMOND_PICKAXE, 20))
);
assertTrue(o.canCraftOne(), "either pickaxe can satisfy the single ingredient");
// Two separate single crafts are possible, but never as one stacked batch (maxStack 1).
assertEquals(1, o.effectiveBatch(), "distinct non-stacking items cannot be crafted as a batch");
}
@Test
void predicate_identicalNonStackingItems_summing() {
// Three IDENTICAL damaged pickaxes: same components -> ONE Exact key with count 3,
// even though they are physically non-stackable (maxStack 1). This is the "it thinks it
// can stack all the matching items together" case.
final Outcome o = run(
List.of(predicate(s -> s.is(Items.DIAMOND_PICKAXE), new ItemStack(Items.DIAMOND_PICKAXE))),
List.of(damaged(Items.DIAMOND_PICKAXE, 100), damaged(Items.DIAMOND_PICKAXE, 100), damaged(Items.DIAMOND_PICKAXE, 100))
);
assertTrue(o.canCraftOne());
// getBiggestCraftableStack ignores per-item max stack size, so it reports 3 ...
assertEquals(3, o.biggestStack(), "raw picker sums the identical items");
// ... but the effective (placeable) batch must clamp to the real max stack size of 1.
assertEquals(1, o.effectiveBatch(), "cannot actually place a batch bigger than maxStackSize");
}
@Test
void predicateAndRegular_shareItem_shouldStillCraft() {
// Shapeless recipe: [ any-stone predicate , stone ]. Two stone in inventory is enough:
// one satisfies the predicate slot, one satisfies the regular slot.
final Outcome o = run(
List.of(predicate(s -> s.is(Items.STONE), new ItemStack(Items.STONE)), regular(Items.STONE)),
List.of(new ItemStack(Items.STONE, 2))
);
assertTrue(o.canCraftOne(), "2 stone should satisfy a predicate-stone + regular-stone recipe");
}
@Test
void predicateAndRegular_singleItem_correctlyUncraftable() {
final Outcome o = run(
List.of(predicate(s -> s.is(Items.STONE), new ItemStack(Items.STONE)), regular(Items.STONE)),
List.of(new ItemStack(Items.STONE, 1))
);
assertFalse(o.canCraftOne(), "one stone cannot fill both a predicate and a regular slot");
}
@Test
void twoPredicates_sharedPool_halveCorrectly() {
final Predicate<ItemStack> isStone = s -> s.is(Items.STONE);
final Outcome o = run(
List.of(predicate(isStone, new ItemStack(Items.STONE)), predicate(isStone, new ItemStack(Items.STONE))),
List.of(new ItemStack(Items.STONE, 64))
);
assertTrue(o.canCraftOne());
assertEquals(32, o.biggestStack(), "64 stone across two ingredient slots -> 32 crafts");
}
@Test
void initialize_clearsStalePredicateIngredients() {
// initialize() rebuilds the extras from a recipe. It clears exactIngredients but must ALSO
// clear predicateIngredients, otherwise a reused extras map (e.g. a cached
// CraftingInput#stackedContents whose resetExtras was skipped because amounts was empty)
// carries a previous recipe's predicates into the next recipe's matching.
final StackedContents<ItemOrExact> raw = new StackedContents<>();
final StackedContentsExtrasMap extras = new StackedContentsExtrasMap(raw);
final Recipe<?> withPredicate = mock(Recipe.class);
when(withPredicate.placementInfo()).thenReturn(PlacementInfo.create(List.of(
predicate(s -> s.is(Items.STONE), new ItemStack(Items.STONE))
)));
extras.initialize(withPredicate);
assertEquals(1, extras.predicateIngredients.size(), "the predicate was registered");
// Re-initialize for a predicate-free recipe, mirroring a reuse where resetExtras did not run.
final Recipe<?> noPredicate = mock(Recipe.class);
when(noPredicate.placementInfo()).thenReturn(PlacementInfo.create(List.of(regular(Items.DIRT))));
extras.initialize(noPredicate);
assertEquals(0, extras.predicateIngredients.size(), "initialize() must drop the stale predicate");
}
@Test
void twoPredicates_notEnough() {
final Predicate<ItemStack> isStone = s -> s.is(Items.STONE);
final Outcome o = run(
List.of(predicate(isStone, new ItemStack(Items.STONE)), predicate(isStone, new ItemStack(Items.STONE))),
List.of(new ItemStack(Items.STONE, 1))
);
assertFalse(o.canCraftOne(), "one stone cannot fill two predicate slots");
}
// ---- lifecycle / leak ----------------------------------------------------------------------
@Test
void predicateIngredients_doNotLeakAcrossReuse() {
// Reuse ONE StackedItemContents across two recipe checks, mirroring how a cached
// CraftingInput#stackedContents() is reused. initialize() clears exactIngredients but not
// predicateIngredients, and resetExtras() is skipped when amounts is empty.
final StackedItemContents contents = new StackedItemContents();
// Recipe 1: predicate matching DIAMOND. No matching items accounted -> amounts stays empty.
final Recipe<?> recipe1 = mock(Recipe.class);
when(recipe1.placementInfo()).thenReturn(PlacementInfo.create(List.of(
predicate(s -> s.is(Items.DIAMOND), new ItemStack(Items.DIAMOND))
)));
contents.initializeExtras(recipe1, null);
contents.resetExtras(); // amounts empty -> this is a no-op, predicate list NOT cleared
contents.clear();
// Recipe 2: a plain regular DIRT recipe, no predicates at all.
final Recipe<?> recipe2 = mock(Recipe.class);
when(recipe2.placementInfo()).thenReturn(PlacementInfo.create(List.of(regular(Items.DIRT))));
contents.initializeExtras(recipe2, null);
// Account a DIAMOND. For recipe2 it should be irrelevant regular content. If the stale
// DIAMOND predicate leaked, the diamond gets mis-routed into an Exact key.
contents.accountSimpleStack(new ItemStack(Items.DIAMOND, 1));
// Recipe2 needs DIRT which we do not have -> must be uncraftable regardless.
assertFalse(contents.canCraft(recipe2, null), "a diamond must not craft a dirt recipe");
}
}
@@ -1,804 +0,0 @@
package io.papermc.paper.inventory.recipe;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import net.minecraft.core.component.DataComponents;
import net.minecraft.world.SimpleContainer;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.PlayerEquipment;
import net.minecraft.world.entity.player.StackedItemContents;
import net.minecraft.world.inventory.RecipeBookMenu;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.crafting.AbstractCookingRecipe;
import net.minecraft.world.item.crafting.CraftingRecipe;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.crafting.PlacementInfo;
import net.minecraft.world.item.crafting.RecipeHolder;
import net.minecraft.world.item.crafting.ShapedRecipe;
import net.minecraft.recipebook.ServerPlaceRecipe;
import org.bukkit.support.environment.VanillaFeature;
import org.junit.jupiter.api.Test;
@VanillaFeature
class RecipeAutofillPlacementTest {
private static final boolean CRAFT_ONE = false;
private static final boolean CRAFT_MAX = true;
// ---- ingredient builders -------------------------------------------------------------------
private static Ingredient regular(final Item item) {
return Ingredient.of(item);
}
private static Ingredient exact(final ItemStack... stacks) {
return Ingredient.ofStacks(List.of(stacks));
}
private static Ingredient predicate(final Predicate<ItemStack> pred, final ItemStack example) {
final Ingredient ingredient = Ingredient.ofStacks(List.of(example));
ingredient.stackPredicate = pred;
return ingredient;
}
private static ItemStack stack(final Item item, final int count) {
return new ItemStack(item, count);
}
private static ItemStack damaged(final Item item, final int damage) {
final ItemStack s = new ItemStack(item);
s.setDamageValue(damage);
return s;
}
private static ItemStack renamed(final Item item, final int count, final String name) {
final ItemStack s = new ItemStack(item, count);
s.set(DataComponents.CUSTOM_NAME, net.minecraft.network.chat.Component.literal(name));
return s;
}
private record PlaceResult(RecipeBookMenu.PostPlaceAction action, List<ItemStack> grid, Inventory inventory) {
boolean placed() {
return this.action == RecipeBookMenu.PostPlaceAction.NOTHING;
}
boolean ghost() {
return this.action == RecipeBookMenu.PostPlaceAction.PLACE_GHOST_RECIPE;
}
int gridCount(final Item item) {
int total = 0;
for (final ItemStack s : this.grid) {
if (s.is(item)) {
total += s.getCount();
}
}
return total;
}
int inventoryCount(final Item item) {
int total = 0;
for (final ItemStack s : this.inventory.getNonEquipmentItems()) {
if (s.is(item)) {
total += s.getCount();
}
}
return total;
}
long filledSlots() {
return this.grid.stream().filter(s -> !s.isEmpty()).count();
}
}
private static PlaceResult autofill(
final List<Ingredient> ingredients, final int gridW, final int gridH,
final List<ItemStack> inventory, final boolean useMaxItems
) {
final CraftingRecipe recipe = mock(CraftingRecipe.class);
when(recipe.placementInfo()).thenReturn(PlacementInfo.create(ingredients));
return place(recipe, gridW, gridH, inventory, useMaxItems, List.of(), false);
}
private static PlaceResult autofillShaped(
final List<Ingredient> ingredients, final int recipeW, final int recipeH,
final int gridW, final int gridH, final List<ItemStack> inventory, final boolean useMaxItems
) {
final ShapedRecipe recipe = mock(ShapedRecipe.class);
when(recipe.getWidth()).thenReturn(recipeW);
when(recipe.getHeight()).thenReturn(recipeH);
when(recipe.placementInfo()).thenReturn(PlacementInfo.create(ingredients));
return place(recipe, gridW, gridH, inventory, useMaxItems, List.of(), false);
}
private static PlaceResult autofillPattern(
final List<Optional<Ingredient>> pattern, final int recipeW, final int recipeH,
final int gridW, final int gridH, final List<ItemStack> inventory, final boolean useMaxItems
) {
final ShapedRecipe recipe = mock(ShapedRecipe.class);
when(recipe.getWidth()).thenReturn(recipeW);
when(recipe.getHeight()).thenReturn(recipeH);
when(recipe.placementInfo()).thenReturn(PlacementInfo.createFromOptionals(pattern));
return place(recipe, gridW, gridH, inventory, useMaxItems, List.of(), false);
}
private static PlaceResult autofillFurnace(
final Ingredient ingredient, final List<ItemStack> inventory, final boolean useMaxItems
) {
final Inventory inv = new Inventory(null, new PlayerEquipment(null));
for (int i = 0; i < inventory.size(); i++) {
inv.setItem(i, inventory.get(i).copy());
}
final SimpleContainer furnace = new SimpleContainer(3); // input=0, fuel=1, result=2
final Slot inputSlot = new Slot(furnace, 0, 0, 0);
final Slot resultSlot = new Slot(furnace, 2, 0, 0);
final List<Slot> clear = List.of(inputSlot, resultSlot);
final AbstractCookingRecipe recipe = mock(AbstractCookingRecipe.class);
when(recipe.placementInfo()).thenReturn(PlacementInfo.create(List.of(ingredient)));
final RecipeHolder<AbstractCookingRecipe> holder = new RecipeHolder<>(null, recipe);
final ServerPlaceRecipe.CraftingMenuAccess<AbstractCookingRecipe> menu = new ServerPlaceRecipe.CraftingMenuAccess<>() {
@Override
public void fillCraftSlotsStackedContents(final StackedItemContents stackedContents) {
if (!inputSlot.getItem().isEmpty()) {
stackedContents.accountSimpleStack(inputSlot.getItem());
}
}
@Override
public void clearCraftingContent() {
clear.forEach(s -> s.set(ItemStack.EMPTY));
}
@Override
public boolean recipeMatches(final RecipeHolder<AbstractCookingRecipe> recipe) {
return false;
}
};
final RecipeBookMenu.PostPlaceAction action = ServerPlaceRecipe.placeRecipe(
menu, 1, 1, List.of(inputSlot), clear, inv, holder, useMaxItems, true
);
return new PlaceResult(action, List.of(inputSlot.getItem()), inv);
}
private static PlaceResult place(
final CraftingRecipe recipe, final int gridW, final int gridH,
final List<ItemStack> inventory, final boolean useMaxItems,
final List<ItemStack> presetGrid, final boolean recipeMatches
) {
final Inventory inv = new Inventory(null, new PlayerEquipment(null));
for (int i = 0; i < inventory.size(); i++) {
inv.setItem(i, inventory.get(i).copy());
}
final SimpleContainer gridContainer = new SimpleContainer(gridW * gridH);
for (int i = 0; i < presetGrid.size(); i++) {
gridContainer.setItem(i, presetGrid.get(i).copy());
}
final List<Slot> slots = new ArrayList<>();
for (int i = 0; i < gridW * gridH; i++) {
slots.add(new Slot(gridContainer, i, 0, 0));
}
final RecipeHolder<CraftingRecipe> holder = new RecipeHolder<>(null, recipe);
final ServerPlaceRecipe.CraftingMenuAccess<CraftingRecipe> menu = new ServerPlaceRecipe.CraftingMenuAccess<>() {
@Override
public void fillCraftSlotsStackedContents(final StackedItemContents stackedContents) {
for (final Slot slot : slots) {
if (!slot.getItem().isEmpty()) {
stackedContents.accountSimpleStack(slot.getItem());
}
}
}
@Override
public void clearCraftingContent() {
gridContainer.clearContent();
}
@Override
public boolean recipeMatches(final RecipeHolder<CraftingRecipe> recipe) {
return recipeMatches;
}
};
final RecipeBookMenu.PostPlaceAction action = ServerPlaceRecipe.placeRecipe(
menu, gridW, gridH, slots, slots, inv, holder, useMaxItems, true
);
final List<ItemStack> gridOut = new ArrayList<>();
for (final Slot slot : slots) {
gridOut.add(slot.getItem());
}
return new PlaceResult(action, gridOut, inv);
}
// ============================================================================================
// Category A — regular ingredients (baseline; must always pass)
// ============================================================================================
@Test
void regular_single_craftOne() {
final PlaceResult r = autofill(List.of(regular(Items.STONE)), 1, 1, List.of(stack(Items.STONE, 64)), CRAFT_ONE);
assertTrue(r.placed());
assertEquals(1, r.gridCount(Items.STONE), "one placed in the grid");
assertEquals(63, r.inventoryCount(Items.STONE), "one consumed from inventory");
}
@Test
void regular_single_craftMax_fillsStack() {
final PlaceResult r = autofill(List.of(regular(Items.STONE)), 1, 1, List.of(stack(Items.STONE, 64)), CRAFT_MAX);
assertTrue(r.placed());
assertEquals(64, r.gridCount(Items.STONE), "whole stack moved into the single grid slot");
assertEquals(0, r.inventoryCount(Items.STONE));
}
@Test
void regular_twoDistinctIngredients_craftMax() {
final PlaceResult r = autofill(
List.of(regular(Items.STONE), regular(Items.DIRT)), 2, 1,
List.of(stack(Items.STONE, 64), stack(Items.DIRT, 64)), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(64, r.gridCount(Items.STONE));
assertEquals(64, r.gridCount(Items.DIRT));
}
@Test
void regular_sharedPool_twoSlotsSameItem_craftMax_halves() {
// 64 stone across two ingredient slots -> 32 per slot (32 crafts).
final PlaceResult r = autofill(
List.of(regular(Items.STONE), regular(Items.STONE)), 2, 1,
List.of(stack(Items.STONE, 64)), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(2, r.filledSlots());
assertEquals(32, r.grid.get(0).getCount());
assertEquals(32, r.grid.get(1).getCount());
}
@Test
void regular_notEnough_ghost() {
final PlaceResult r = autofill(
List.of(regular(Items.STONE), regular(Items.DIRT)), 2, 1,
List.of(stack(Items.STONE, 1)), CRAFT_ONE
);
assertTrue(r.ghost(), "missing dirt -> ghost recipe, nothing placed");
assertEquals(0, r.filledSlots());
assertEquals(1, r.inventoryCount(Items.STONE), "inventory untouched");
}
@Test
void regular_nonStackableItem_craftMax_clampsToOne() {
// Undamaged pickaxe is usable for crafting; maxStack 1 -> batch clamps to 1.
final PlaceResult r = autofill(
List.of(regular(Items.DIAMOND_PICKAXE)), 1, 1,
List.of(stack(Items.DIAMOND_PICKAXE, 1), stack(Items.DIAMOND_PICKAXE, 1)), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(1, r.gridCount(Items.DIAMOND_PICKAXE));
assertEquals(1, r.inventoryCount(Items.DIAMOND_PICKAXE), "only one pickaxe consumed");
}
@Test
void regular_multiStackInventory_craftMax() {
// Stone spread across three inventory slots (64+64+32) -> up to 64 into one grid slot.
final PlaceResult r = autofill(
List.of(regular(Items.STONE)), 1, 1,
List.of(stack(Items.STONE, 64), stack(Items.STONE, 64), stack(Items.STONE, 32)), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(64, r.gridCount(Items.STONE), "one full stack into the slot");
assertEquals(96, r.inventoryCount(Items.STONE), "160 - 64 remain");
}
@Test
void regular_fullThreeByThree() {
final List<Ingredient> ing = new ArrayList<>();
for (int i = 0; i < 9; i++) {
ing.add(regular(Items.STONE));
}
final PlaceResult r = autofill(ing, 3, 3, List.of(stack(Items.STONE, 9)), CRAFT_ONE);
assertTrue(r.placed());
assertEquals(9, r.filledSlots());
assertEquals(9, r.gridCount(Items.STONE));
}
@Test
void regular_damagedItem_notUsable_ghost() {
// Damaged items are excluded from regular recipe-book crafting.
final PlaceResult r = autofill(
List.of(regular(Items.DIAMOND_PICKAXE)), 1, 1,
List.of(damaged(Items.DIAMOND_PICKAXE, 50)), CRAFT_ONE
);
assertTrue(r.ghost(), "a damaged pickaxe is not usable for a regular ingredient");
}
// ============================================================================================
// Category B — exact ingredients (baseline; user says these already work)
// ============================================================================================
@Test
void exact_matchesOnlySameComponents() {
final ItemStack named = renamed(Items.DIAMOND, 1, "Special");
final PlaceResult r = autofill(
List.of(exact(named.copy())), 1, 1,
List.of(stack(Items.DIAMOND, 5), named.copy()), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(1, r.gridCount(Items.DIAMOND), "only the named diamond matches; maxStack irrelevant here");
// The plain diamonds must be left untouched.
assertTrue(r.grid.get(0).has(DataComponents.CUSTOM_NAME), "the exact (named) item was placed");
}
@Test
void exact_stackable_craftMax() {
final PlaceResult r = autofill(
List.of(exact(stack(Items.DIAMOND, 1))), 1, 1,
List.of(stack(Items.DIAMOND, 40)), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(40, r.gridCount(Items.DIAMOND));
}
@Test
void exact_multipleAllowedStacks() {
final PlaceResult r = autofill(
List.of(exact(stack(Items.DIAMOND, 1), stack(Items.EMERALD, 1))), 1, 1,
List.of(stack(Items.EMERALD, 3)), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(3, r.gridCount(Items.EMERALD));
}
@Test
void exact_nonStackable_craftMax_clampsToOne() {
final ItemStack tool = damaged(Items.DIAMOND_PICKAXE, 7);
final PlaceResult r = autofill(
List.of(exact(tool.copy())), 1, 1,
List.of(tool.copy(), tool.copy()), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(1, r.gridCount(Items.DIAMOND_PICKAXE), "non-stackable exact -> one per slot");
assertEquals(1, r.inventoryCount(Items.DIAMOND_PICKAXE));
}
@Test
void exact_twoSlotsSharedPool_craftMax_halves() {
final PlaceResult r = autofill(
List.of(exact(stack(Items.DIAMOND, 1)), exact(stack(Items.DIAMOND, 1))), 2, 1,
List.of(stack(Items.DIAMOND, 10)), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(5, r.grid.get(0).getCount());
assertEquals(5, r.grid.get(1).getCount());
}
@Test
void exact_notPresent_ghost() {
final PlaceResult r = autofill(
List.of(exact(renamed(Items.DIAMOND, 1, "Special"))), 1, 1,
List.of(stack(Items.DIAMOND, 5)), CRAFT_ONE
);
assertTrue(r.ghost(), "plain diamonds do not satisfy an exact named-diamond ingredient");
}
// ============================================================================================
// Category C — mixed regular + exact (baseline)
// ============================================================================================
@Test
void mixed_regularAndExact_distinctItems() {
final PlaceResult r = autofill(
List.of(regular(Items.STICK), exact(stack(Items.DIAMOND, 1))), 2, 1,
List.of(stack(Items.STICK, 8), stack(Items.DIAMOND, 8)), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(8, r.gridCount(Items.STICK));
assertEquals(8, r.gridCount(Items.DIAMOND));
}
// ============================================================================================
// Category D — predicate ingredients (bug-hunting; document current behaviour)
// ============================================================================================
@Test
void predicate_single_stackable_craftMax() {
final PlaceResult r = autofill(
List.of(predicate(s -> s.is(Items.STONE), stack(Items.STONE, 1))), 1, 1,
List.of(stack(Items.STONE, 64)), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(64, r.gridCount(Items.STONE));
}
@Test
void predicate_single_craftOne_pullsMatchingItem() {
final PlaceResult r = autofill(
List.of(predicate(s -> s.is(Items.DIAMOND_PICKAXE), stack(Items.DIAMOND_PICKAXE, 1))), 1, 1,
List.of(damaged(Items.DIAMOND_PICKAXE, 10)), CRAFT_ONE
);
assertTrue(r.placed(), "the damaged pickaxe satisfies the predicate even though it is not usable for regular crafting");
assertEquals(1, r.gridCount(Items.DIAMOND_PICKAXE));
assertEquals(0, r.inventoryCount(Items.DIAMOND_PICKAXE));
}
@Test
void predicate_distinctNonStacking_craftMax_placesOne() {
final PlaceResult r = autofill(
List.of(predicate(s -> s.is(Items.DIAMOND_PICKAXE), stack(Items.DIAMOND_PICKAXE, 1))), 1, 1,
List.of(damaged(Items.DIAMOND_PICKAXE, 10), damaged(Items.DIAMOND_PICKAXE, 20)), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(1, r.gridCount(Items.DIAMOND_PICKAXE), "distinct non-stacking items cannot be batched");
assertEquals(1, r.inventoryCount(Items.DIAMOND_PICKAXE), "only one consumed");
}
@Test
void predicate_identicalNonStacking_craftMax_clampsToOne() {
// Same components (same damage) -> one Exact key summing to 3, but maxStack 1 clamps the batch.
final PlaceResult r = autofill(
List.of(predicate(s -> s.is(Items.DIAMOND_PICKAXE), stack(Items.DIAMOND_PICKAXE, 1))), 1, 1,
List.of(damaged(Items.DIAMOND_PICKAXE, 100), damaged(Items.DIAMOND_PICKAXE, 100), damaged(Items.DIAMOND_PICKAXE, 100)),
CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(1, r.gridCount(Items.DIAMOND_PICKAXE), "clamped to a single non-stackable item");
assertEquals(2, r.inventoryCount(Items.DIAMOND_PICKAXE));
}
@Test
void predicate_matchesMultipleItems_placesSomethingValid() {
// Predicate matches any of several stackable items; whichever is chosen must be placed.
final PlaceResult r = autofill(
List.of(predicate(s -> s.is(Items.OAK_PLANKS) || s.is(Items.BIRCH_PLANKS), stack(Items.OAK_PLANKS, 1))), 1, 1,
List.of(stack(Items.BIRCH_PLANKS, 5)), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(5, r.gridCount(Items.BIRCH_PLANKS), "the matching birch planks were placed");
}
@Test
void predicate_twoSlots_sharedPool_craftMax_halves() {
final Predicate<ItemStack> isStone = s -> s.is(Items.STONE);
final PlaceResult r = autofill(
List.of(predicate(isStone, stack(Items.STONE, 1)), predicate(isStone, stack(Items.STONE, 1))), 2, 1,
List.of(stack(Items.STONE, 64)), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(32, r.grid.get(0).getCount());
assertEquals(32, r.grid.get(1).getCount());
}
@Test
void predicate_notEnough_ghost() {
final Predicate<ItemStack> isStone = s -> s.is(Items.STONE);
final PlaceResult r = autofill(
List.of(predicate(isStone, stack(Items.STONE, 1)), predicate(isStone, stack(Items.STONE, 1))), 2, 1,
List.of(stack(Items.STONE, 1)), CRAFT_ONE
);
assertTrue(r.ghost(), "one stone cannot fill two predicate slots");
}
// ---- known bugs (kept disabled so the suite stays green; flip when the algorithm is fixed) --
@Test
void predicateAndRegular_shareItem_shouldPlace() {
final PlaceResult r = autofill(
List.of(predicate(s -> s.is(Items.STONE), stack(Items.STONE, 1)), regular(Items.STONE)), 2, 1,
List.of(stack(Items.STONE, 2)), CRAFT_ONE
);
assertTrue(r.placed(), "2 stone should satisfy predicate-stone + regular-stone");
assertEquals(2, r.gridCount(Items.STONE));
}
// ============================================================================================
// Category E — more mixed combinations
// ============================================================================================
@Test
void predicateAndExact_distinctItems() {
final PlaceResult r = autofill(
List.of(predicate(s -> s.is(Items.STONE), stack(Items.STONE, 1)), exact(stack(Items.DIAMOND, 1))), 2, 1,
List.of(stack(Items.STONE, 8), stack(Items.DIAMOND, 8)), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(8, r.gridCount(Items.STONE));
assertEquals(8, r.gridCount(Items.DIAMOND));
}
@Test
void predicate_ignoresNonMatchingInventory() {
// Predicate matches only stone; unrelated dirt in the inventory must be left alone.
final PlaceResult r = autofill(
List.of(predicate(s -> s.is(Items.STONE), stack(Items.STONE, 1))), 1, 1,
List.of(stack(Items.DIRT, 64), stack(Items.STONE, 3)), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(3, r.gridCount(Items.STONE));
assertEquals(64, r.inventoryCount(Items.DIRT), "non-matching dirt untouched");
}
@Test
void exact_leavesNonMatchingComponentsUntouched() {
final ItemStack special = renamed(Items.DIAMOND, 1, "Special");
final PlaceResult r = autofill(
List.of(exact(special.copy())), 1, 1,
List.of(stack(Items.DIAMOND, 10), special.copy()), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(1, r.gridCount(Items.DIAMOND), "only the named diamond was placed");
assertEquals(10, r.inventoryCount(Items.DIAMOND), "the 10 plain diamonds are untouched");
}
// ============================================================================================
// Category F — edge cases
// ============================================================================================
@Test
void emptyInventory_ghost() {
final PlaceResult r = autofill(List.of(regular(Items.STONE)), 1, 1, List.of(), CRAFT_ONE);
assertTrue(r.ghost());
assertEquals(0, r.filledSlots());
}
@Test
void fewerIngredientsThanGrid_leavesRemainderEmpty() {
final PlaceResult r = autofill(
List.of(regular(Items.STONE), regular(Items.DIRT)), 3, 3,
List.of(stack(Items.STONE, 1), stack(Items.DIRT, 1)), CRAFT_ONE
);
assertTrue(r.placed());
assertEquals(2, r.filledSlots(), "only two slots filled in a 3x3 grid");
}
@Test
void customMaxStackSizeComponent_regular_clampsBatch() {
// A stone whose stack was given a MAX_STACK_SIZE=16 component. As a regular (Item-key)
// ingredient the picker uses the item's DEFAULT max stack; this test locks in current
// behaviour so a change to that assumption is caught.
final ItemStack capped = stack(Items.STONE, 16);
capped.set(DataComponents.MAX_STACK_SIZE, 16);
final PlaceResult r = autofill(List.of(regular(Items.STONE)), 1, 1, List.of(capped), CRAFT_MAX);
assertTrue(r.placed());
assertEquals(16, r.gridCount(Items.STONE), "all 16 available were placed");
}
// ============================================================================================
// Category G — shaped-recipe centering (exercises the ShapedRecipe branch of PlaceRecipeHelper)
// ============================================================================================
@Test
void shaped_twoWide_centeredInThreeByThree() {
// A 2x1 recipe placed into a 3x3 grid: PlaceRecipeHelper centers it vertically (row 1).
final PlaceResult r = autofillShaped(
List.of(regular(Items.STONE), regular(Items.DIRT)), 2, 1, 3, 3,
List.of(stack(Items.STONE, 1), stack(Items.DIRT, 1)), CRAFT_ONE
);
assertTrue(r.placed());
assertEquals(2, r.filledSlots());
assertEquals(1, r.gridCount(Items.STONE));
assertEquals(1, r.gridCount(Items.DIRT));
// Centered vertically onto the middle row (slots 3 & 4), not the top-left.
assertTrue(r.grid.get(0).isEmpty(), "top-left slot should be empty after centering");
assertSame(Items.STONE, r.grid.get(3).getItem());
assertSame(Items.DIRT, r.grid.get(4).getItem());
}
@Test
void shaped_fullThreeByThree_noCentering() {
final List<Ingredient> ing = new ArrayList<>();
for (int i = 0; i < 9; i++) {
ing.add(regular(Items.STONE));
}
final PlaceResult r = autofillShaped(ing, 3, 3, 3, 3, List.of(stack(Items.STONE, 9)), CRAFT_ONE);
assertTrue(r.placed());
assertEquals(9, r.filledSlots());
}
// ============================================================================================
// Category H — top-up path (recipe already placed in the grid, recipeMatches == true)
// ============================================================================================
@Test
void topUp_existingGrid_addsOneMore() {
// Grid already holds 1 stone in each of two slots; a matching craft-one should top each to 2.
final CraftingRecipe recipe = mock(CraftingRecipe.class);
when(recipe.placementInfo()).thenReturn(PlacementInfo.create(List.of(regular(Items.STONE), regular(Items.STONE))));
final PlaceResult r = place(
recipe, 2, 1, List.of(stack(Items.STONE, 64)), CRAFT_ONE,
List.of(stack(Items.STONE, 1), stack(Items.STONE, 1)), true
);
assertTrue(r.placed());
assertEquals(2, r.grid.get(0).getCount(), "topped up to 2");
assertEquals(2, r.grid.get(1).getCount());
}
// ============================================================================================
// Category I — shaped recipes WITH gaps (L-shapes, tool shapes, ...)
// ============================================================================================
@Test
void shapedWithGap_placesIntoCorrectCells_leavesGapEmpty() {
// 2x2 recipe box, bottom-right cell is a gap: [A B / C .]
final List<Optional<Ingredient>> pattern = List.of(
Optional.of(regular(Items.STONE)), // (0,0)
Optional.of(regular(Items.DIRT)), // (1,0)
Optional.of(regular(Items.DIAMOND)), // (0,1)
Optional.empty() // (1,1) gap
);
final PlaceResult r = autofillPattern(
pattern, 2, 2, 3, 3,
List.of(stack(Items.STONE, 1), stack(Items.DIRT, 1), stack(Items.DIAMOND, 1)), CRAFT_ONE
);
assertTrue(r.placed());
assertEquals(3, r.filledSlots(), "only the three non-gap cells are filled");
// 2x2 box is not centered in 3x3, so it maps to grid slots 0,1 (top row) and 3,4 (next row).
assertSame(Items.STONE, r.grid.get(0).getItem());
assertSame(Items.DIRT, r.grid.get(1).getItem());
assertSame(Items.DIAMOND, r.grid.get(3).getItem());
assertTrue(r.grid.get(4).isEmpty(), "the gap cell (slot 4) stays empty");
}
@Test
void shapedWithGap_notEnoughForOneIngredient_ghost() {
final List<Optional<Ingredient>> pattern = List.of(
Optional.of(regular(Items.STONE)),
Optional.empty(),
Optional.of(regular(Items.DIRT))
);
// 3x1 box [stone, gap, dirt] but no dirt in inventory.
final PlaceResult r = autofillPattern(pattern, 3, 1, 3, 3, List.of(stack(Items.STONE, 5)), CRAFT_ONE);
assertTrue(r.ghost());
}
// ============================================================================================
// Category J — furnace / smoker / blast-furnace recipe book (AbstractFurnaceMenu)
// ============================================================================================
@Test
void furnace_regular_craftOne() {
final PlaceResult r = autofillFurnace(regular(Items.RAW_IRON), List.of(stack(Items.RAW_IRON, 10)), CRAFT_ONE);
assertTrue(r.placed());
assertEquals(1, r.gridCount(Items.RAW_IRON), "one raw iron placed into the input slot");
assertEquals(9, r.inventoryCount(Items.RAW_IRON));
}
@Test
void furnace_regular_craftMax_fillsInputSlot() {
final PlaceResult r = autofillFurnace(regular(Items.RAW_IRON), List.of(stack(Items.RAW_IRON, 64)), CRAFT_MAX);
assertTrue(r.placed());
assertEquals(64, r.gridCount(Items.RAW_IRON), "whole stack moved into the furnace input");
}
@Test
void furnace_exactIngredient() {
final ItemStack special = renamed(Items.RAW_IRON, 1, "Ore");
final PlaceResult r = autofillFurnace(exact(special.copy()), List.of(stack(Items.RAW_IRON, 5), special.copy()), CRAFT_MAX);
assertTrue(r.placed());
assertEquals(1, r.gridCount(Items.RAW_IRON), "only the exact named ore matches");
assertTrue(r.grid.get(0).has(DataComponents.CUSTOM_NAME));
}
@Test
void furnace_predicateIngredient() {
final PlaceResult r = autofillFurnace(
predicate(s -> s.is(Items.RAW_IRON), stack(Items.RAW_IRON, 1)),
List.of(stack(Items.RAW_IRON, 3)), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(3, r.gridCount(Items.RAW_IRON));
}
@Test
void furnace_notEnough_ghost() {
final PlaceResult r = autofillFurnace(regular(Items.RAW_IRON), List.of(), CRAFT_ONE);
assertTrue(r.ghost());
assertEquals(0, r.filledSlots());
}
// ============================================================================================
// Category K — predicate/exact combined with every other choice type (regression for the fix)
// ============================================================================================
@Test
void exactAndRegular_shareItem_shouldPlace() {
// The exact-choice analog of the predicate bug: a plain item routed to the Exact pool
// (because it matches an exact ingredient) must still satisfy a regular ingredient too.
final PlaceResult r = autofill(
List.of(exact(stack(Items.STONE, 1)), regular(Items.STONE)), 2, 1,
List.of(stack(Items.STONE, 2)), CRAFT_ONE
);
assertTrue(r.placed(), "2 stone should satisfy exact-stone + regular-stone");
assertEquals(2, r.gridCount(Items.STONE));
}
@Test
void predicateAndRegular_shareItem_craftMax() {
final PlaceResult r = autofill(
List.of(predicate(s -> s.is(Items.STONE), stack(Items.STONE, 1)), regular(Items.STONE)), 2, 1,
List.of(stack(Items.STONE, 64)), CRAFT_MAX
);
assertTrue(r.placed());
assertEquals(32, r.grid.get(0).getCount(), "64 stone / 2 slots -> 32 per slot");
assertEquals(32, r.grid.get(1).getCount());
}
@Test
void predicateAndExact_overlappingItem() {
// A plain diamond matches both the predicate and the exact ingredient; it is pooled once
// (Exact) and shared by both slots. Two diamonds -> one craft.
final PlaceResult r = autofill(
List.of(predicate(s -> s.is(Items.DIAMOND), stack(Items.DIAMOND, 1)), exact(stack(Items.DIAMOND, 1))), 2, 1,
List.of(stack(Items.DIAMOND, 2)), CRAFT_ONE
);
assertTrue(r.placed());
assertEquals(2, r.gridCount(Items.DIAMOND));
}
@Test
void predicateAndExact_overlappingItem_onlyOne_ghost() {
final PlaceResult r = autofill(
List.of(predicate(s -> s.is(Items.DIAMOND), stack(Items.DIAMOND, 1)), exact(stack(Items.DIAMOND, 1))), 2, 1,
List.of(stack(Items.DIAMOND, 1)), CRAFT_ONE
);
assertTrue(r.ghost(), "one diamond cannot fill both overlapping slots");
}
@Test
void predicateAndRegular_renamedItem_regularCannotUseIt() {
// A renamed stone matches the predicate but is NOT usable for the regular ingredient. With a
// plain stone available for the regular slot, the recipe still crafts (predicate<-renamed,
// regular<-plain).
final PlaceResult r = autofill(
List.of(predicate(s -> s.is(Items.STONE), stack(Items.STONE, 1)), regular(Items.STONE)), 2, 1,
List.of(renamed(Items.STONE, 1, "Fancy"), stack(Items.STONE, 1)), CRAFT_ONE
);
assertTrue(r.placed());
assertEquals(2, r.filledSlots());
assertEquals(2, r.gridCount(Items.STONE));
}
@Test
void predicateAndRegular_onlyRenamedItem_ghost() {
// Only a renamed stone (matches predicate but not usable for the regular slot) -> cannot fill
// both slots.
final PlaceResult r = autofill(
List.of(predicate(s -> s.is(Items.STONE), stack(Items.STONE, 1)), regular(Items.STONE)), 2, 1,
List.of(renamed(Items.STONE, 1, "Fancy")), CRAFT_ONE
);
assertTrue(r.ghost(), "a renamed stone cannot satisfy the regular slot");
}
@Test
void threeWayMix_predicateExactRegular() {
final ItemStack named = renamed(Items.DIAMOND, 1, "Gem");
final PlaceResult r = autofill(
List.of(
predicate(s -> s.is(Items.STONE), stack(Items.STONE, 1)),
exact(named.copy()),
regular(Items.STICK)
), 3, 1,
List.of(stack(Items.STONE, 1), named.copy(), stack(Items.STICK, 1)), CRAFT_ONE
);
assertTrue(r.placed());
assertEquals(1, r.gridCount(Items.STONE));
assertEquals(1, r.gridCount(Items.DIAMOND));
assertEquals(1, r.gridCount(Items.STICK));
}
}
@@ -1,194 +0,0 @@
package io.papermc.paper.inventory.recipe;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.function.Predicate;
import net.minecraft.core.component.DataComponents;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.ItemStackTemplate;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.crafting.CraftingBookCategory;
import net.minecraft.world.item.crafting.CraftingInput;
import net.minecraft.world.item.crafting.CraftingRecipe;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.crafting.Recipe;
import net.minecraft.world.item.crafting.ShapelessRecipe;
import org.bukkit.support.environment.VanillaFeature;
import org.junit.jupiter.api.Test;
/**
* Tests the <b>actual shapeless crafting</b> path: {@link ShapelessRecipe#matches} over a real
* {@link ShapelessRecipe}. This exercises a different accounting route than the recipe-book placer
* ({@code CraftingInput} ctor + {@code StackedContentsExtrasMap#accountInput}/{@code regularRemoved}),
* plus the single-ingredient {@code Ingredient#test} shortcut. The same {@link ShapelessRecipe#matches}
* is what the Crafter block and normal crafting menus use to decide whether a grid produces a result.
*
* <p>Regular/exact ingredients are the must-pass baseline; predicate + combination cases guard the
* predicate integration fix.
*/
@VanillaFeature
class ShapelessRecipeMatchTest {
private static Ingredient regular(final Item item) {
return Ingredient.of(item);
}
private static Ingredient exact(final ItemStack... stacks) {
return Ingredient.ofStacks(List.of(stacks));
}
private static Ingredient predicate(final Predicate<ItemStack> pred, final ItemStack example) {
final Ingredient ingredient = Ingredient.ofStacks(List.of(example));
ingredient.stackPredicate = pred;
return ingredient;
}
private static ItemStack renamed(final Item item, final String name) {
final ItemStack s = new ItemStack(item);
s.set(DataComponents.CUSTOM_NAME, net.minecraft.network.chat.Component.literal(name));
return s;
}
private static ShapelessRecipe shapeless(final Ingredient... ingredients) {
return new ShapelessRecipe(
new Recipe.CommonInfo(true),
new CraftingRecipe.CraftingBookInfo(CraftingBookCategory.MISC, ""),
new ItemStackTemplate(Items.DIAMOND), // result is irrelevant to matches()
List.of(ingredients)
);
}
/** Runs the real matches() over a grid sized to hold exactly the given non-empty items. */
private static boolean matches(final ShapelessRecipe recipe, final ItemStack... grid) {
final CraftingInput input = CraftingInput.of(grid.length, 1, List.of(grid));
return recipe.matches(input, null); // shapeless matching ignores the level
}
// ---- regular ingredients (baseline) --------------------------------------------------------
@Test
void regular_exactMatch() {
assertTrue(matches(shapeless(regular(Items.STONE), regular(Items.DIRT)),
new ItemStack(Items.STONE), new ItemStack(Items.DIRT)));
}
@Test
void regular_orderIndependent() {
// Shapeless: item order in the grid must not matter.
assertTrue(matches(shapeless(regular(Items.STONE), regular(Items.DIRT)),
new ItemStack(Items.DIRT), new ItemStack(Items.STONE)));
}
@Test
void regular_wrongItem_noMatch() {
assertFalse(matches(shapeless(regular(Items.STONE), regular(Items.DIRT)),
new ItemStack(Items.STONE), new ItemStack(Items.COBBLESTONE)));
}
@Test
void regular_missingItem_noMatch() {
assertFalse(matches(shapeless(regular(Items.STONE), regular(Items.DIRT)),
new ItemStack(Items.STONE), new ItemStack(Items.STONE)));
}
@Test
void regular_extraItem_noMatch() {
// Three grid items for a two-ingredient recipe -> ingredientCount mismatch.
assertFalse(matches(shapeless(regular(Items.STONE), regular(Items.DIRT)),
new ItemStack(Items.STONE), new ItemStack(Items.DIRT), new ItemStack(Items.DIAMOND)));
}
@Test
void regular_duplicateIngredient() {
assertTrue(matches(shapeless(regular(Items.STONE), regular(Items.STONE)),
new ItemStack(Items.STONE), new ItemStack(Items.STONE)));
assertFalse(matches(shapeless(regular(Items.STONE), regular(Items.STONE)),
new ItemStack(Items.STONE), new ItemStack(Items.DIRT)));
}
@Test
void regular_singleIngredientShortcut() {
assertTrue(matches(shapeless(regular(Items.STONE)), new ItemStack(Items.STONE)));
assertFalse(matches(shapeless(regular(Items.STONE)), new ItemStack(Items.DIRT)));
}
// ---- exact ingredients ---------------------------------------------------------------------
@Test
void exact_matchesOnlySameComponents() {
assertTrue(matches(shapeless(exact(renamed(Items.DIAMOND, "Special"))), renamed(Items.DIAMOND, "Special")));
assertFalse(matches(shapeless(exact(renamed(Items.DIAMOND, "Special"))), new ItemStack(Items.DIAMOND)));
}
@Test
void exact_plusRegular() {
assertTrue(matches(shapeless(exact(renamed(Items.DIAMOND, "Special")), regular(Items.STONE)),
renamed(Items.DIAMOND, "Special"), new ItemStack(Items.STONE)));
}
// ---- predicate ingredients -----------------------------------------------------------------
@Test
void predicate_singleShortcut() {
assertTrue(matches(shapeless(predicate(s -> s.is(Items.STONE), new ItemStack(Items.STONE))), new ItemStack(Items.STONE)));
assertFalse(matches(shapeless(predicate(s -> s.is(Items.STONE), new ItemStack(Items.STONE))), new ItemStack(Items.DIRT)));
}
@Test
void predicate_twoPredicates() {
final Predicate<ItemStack> isStone = s -> s.is(Items.STONE);
assertTrue(matches(shapeless(predicate(isStone, new ItemStack(Items.STONE)), predicate(isStone, new ItemStack(Items.STONE))),
new ItemStack(Items.STONE), new ItemStack(Items.STONE)));
}
@Test
void predicate_plusRegular_shareItem() {
// The fix: grid accounting routes both stone to the Exact pool; the regular stone ingredient
// must still be satisfiable. (This is the actual-crafting analog of the recipe-book bug.)
assertTrue(matches(shapeless(predicate(s -> s.is(Items.STONE), new ItemStack(Items.STONE)), regular(Items.STONE)),
new ItemStack(Items.STONE), new ItemStack(Items.STONE)));
}
@Test
void predicate_plusRegular_distinctItems_orderIndependent() {
final ShapelessRecipe recipe = shapeless(predicate(s -> s.is(Items.DIAMOND), new ItemStack(Items.DIAMOND)), regular(Items.STONE));
assertTrue(matches(recipe, new ItemStack(Items.DIAMOND), new ItemStack(Items.STONE)));
assertTrue(matches(recipe, new ItemStack(Items.STONE), new ItemStack(Items.DIAMOND)));
}
@Test
void predicate_plusRegular_noMatchingRegularItem() {
// grid stone + dirt: dirt matches neither ingredient -> no match.
assertFalse(matches(shapeless(predicate(s -> s.is(Items.STONE), new ItemStack(Items.STONE)), regular(Items.STONE)),
new ItemStack(Items.STONE), new ItemStack(Items.DIRT)));
}
@Test
void predicate_plusExact() {
assertTrue(matches(shapeless(predicate(s -> s.is(Items.DIAMOND), new ItemStack(Items.DIAMOND)), exact(renamed(Items.EMERALD, "Gem"))),
new ItemStack(Items.DIAMOND), renamed(Items.EMERALD, "Gem")));
}
@Test
void predicate_plusRegular_renamedSatisfiesPredicateOnly() {
// renamed stone -> predicate slot; plain stone -> regular slot. Order-independent.
final ShapelessRecipe recipe = shapeless(predicate(s -> s.is(Items.STONE), new ItemStack(Items.STONE)), regular(Items.STONE));
assertTrue(matches(recipe, renamed(Items.STONE, "Fancy"), new ItemStack(Items.STONE)));
// Two renamed stones: the regular slot cannot use a renamed (non-usable) stone -> no match.
assertFalse(matches(recipe, renamed(Items.STONE, "Fancy"), renamed(Items.STONE, "Fancy2")));
}
@Test
void threeWayMix_predicateExactRegular() {
final ShapelessRecipe recipe = shapeless(
predicate(s -> s.is(Items.STONE), new ItemStack(Items.STONE)),
exact(renamed(Items.DIAMOND, "Gem")),
regular(Items.STICK)
);
assertTrue(matches(recipe, new ItemStack(Items.STICK), renamed(Items.DIAMOND, "Gem"), new ItemStack(Items.STONE)));
assertFalse(matches(recipe, new ItemStack(Items.STICK), new ItemStack(Items.DIAMOND), new ItemStack(Items.STONE)));
}
}
@@ -3,6 +3,7 @@ package org.bukkit.craftbukkit.legacy;
import static org.junit.jupiter.api.Assertions.*;
import com.google.common.base.Joiner;
import com.google.common.base.Predicates;
import io.papermc.paper.plugin.ApiVersion;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -15,7 +16,6 @@ import java.util.stream.Stream;
import org.bukkit.Bukkit;
import org.bukkit.craftbukkit.legacy.reroute.Reroute;
import org.bukkit.craftbukkit.legacy.reroute.RerouteBuilder;
import org.bukkit.craftbukkit.util.ApiVersion;
import org.bukkit.craftbukkit.util.Commodore;
import org.bukkit.support.environment.Normal;
import org.junit.jupiter.api.AfterAll;
@@ -2,9 +2,9 @@ package org.bukkit.craftbukkit.legacy.reroute;
import static org.junit.jupiter.api.Assertions.*;
import com.google.common.base.Predicates;
import io.papermc.paper.plugin.ApiVersion;
import java.util.List;
import java.util.Map;
import org.bukkit.craftbukkit.util.ApiVersion;
import org.bukkit.support.environment.Normal;
import org.junit.jupiter.api.Test;
@@ -20,12 +20,12 @@ public class InjectPluginVersionTest extends AbstractRerouteTest {
"java/lang/Object",
"getList",
false,
"(Ljava/lang/Object;Lorg/bukkit/craftbukkit/util/ApiVersion;)Ljava/util/List;",
"(Ljava/lang/Object;Lio/papermc/paper/plugin/ApiVersion;)Ljava/util/List;",
"org/bukkit/craftbukkit/legacy/reroute/InjectPluginVersionTest$InjectPluginVersionTestData",
"getList",
this.create(
this.create("Ljava/lang/Object;", "Ljava/lang/Object;", false, false, null),
this.create("Lorg/bukkit/craftbukkit/util/ApiVersion;", "Lorg/bukkit/craftbukkit/util/ApiVersion;", false, true, null)
this.create("Lio/papermc/paper/plugin/ApiVersion;", "Lio/papermc/paper/plugin/ApiVersion;", false, true, null)
),
"Ljava/util/List;",
true,
@@ -1,8 +1,9 @@
package org.bukkit.craftbukkit.legacy.reroute;
import static org.junit.jupiter.api.Assertions.*;
import io.papermc.paper.plugin.ApiVersion;
import java.lang.annotation.Annotation;
import org.bukkit.craftbukkit.util.ApiVersion;
import org.bukkit.support.environment.Normal;
import org.junit.jupiter.api.Test;
@@ -19,56 +20,56 @@ public class RequirePluginVersionDataTest {
@Test
public void testDifferent() {
RequirePluginVersionData data = RequirePluginVersionData.create(new RequirePluginVersionImpl("", "1.21", "1.23"));
RequirePluginVersionData data = RequirePluginVersionData.create(new RequirePluginVersionImpl("", "1.20", "1.21"));
assertEquals(ApiVersion.getOrCreateVersion("1.21"), data.minInclusive());
assertEquals(ApiVersion.getOrCreateVersion("1.23"), data.maxInclusive());
assertEquals(ApiVersion.getOrCreateVersion("1.20"), data.minInclusive());
assertEquals(ApiVersion.getOrCreateVersion("1.21"), data.maxInclusive());
}
@Test
public void testValue() {
RequirePluginVersionData data = RequirePluginVersionData.create(new RequirePluginVersionImpl("1.42", "", ""));
RequirePluginVersionData data = RequirePluginVersionData.create(new RequirePluginVersionImpl("1.21", "", ""));
assertEquals(ApiVersion.getOrCreateVersion("1.42"), data.minInclusive());
assertEquals(ApiVersion.getOrCreateVersion("1.42"), data.maxInclusive());
assertEquals(ApiVersion.getOrCreateVersion("1.21"), data.minInclusive());
assertEquals(ApiVersion.getOrCreateVersion("1.21"), data.maxInclusive());
}
@Test
public void testValueAndMin() {
assertThrows(IllegalArgumentException.class, () -> RequirePluginVersionData.create(new RequirePluginVersionImpl("1.42", "1.52", "")));
assertThrows(IllegalArgumentException.class, () -> RequirePluginVersionData.create(new RequirePluginVersionImpl("1.20", "1.21", "")));
}
@Test
public void testValueAndMax() {
assertThrows(IllegalArgumentException.class, () -> RequirePluginVersionData.create(new RequirePluginVersionImpl("1.42", "", "1.53")));
assertThrows(IllegalArgumentException.class, () -> RequirePluginVersionData.create(new RequirePluginVersionImpl("1.20", "", "1.21")));
}
@Test
public void testValueAndMinMax() {
assertThrows(IllegalArgumentException.class, () -> RequirePluginVersionData.create(new RequirePluginVersionImpl("1.42", "1.51", "1.53")));
assertThrows(IllegalArgumentException.class, () -> RequirePluginVersionData.create(new RequirePluginVersionImpl("1.19", "1.20", "1.21")));
}
@Test
public void testMinNewerThanMax() {
assertThrows(IllegalArgumentException.class, () -> RequirePluginVersionData.create(new RequirePluginVersionImpl("", "1.59", "1.57")));
assertThrows(IllegalArgumentException.class, () -> RequirePluginVersionData.create(new RequirePluginVersionImpl("", "1.21", "1.20")));
}
@Test
public void testOnlyMin() {
RequirePluginVersionData data = RequirePluginVersionData.create(new RequirePluginVersionImpl("", "1.44", ""));
RequirePluginVersionData data = RequirePluginVersionData.create(new RequirePluginVersionImpl("", "1.21", ""));
assertEquals(ApiVersion.getOrCreateVersion("1.44"), data.minInclusive());
assertEquals(ApiVersion.getOrCreateVersion("1.21"), data.minInclusive());
assertNull(data.maxInclusive());
}
@Test
public void testOnlyMax() {
RequirePluginVersionData data = RequirePluginVersionData.create(new RequirePluginVersionImpl("", "", "1.96"));
RequirePluginVersionData data = RequirePluginVersionData.create(new RequirePluginVersionImpl("", "", "1.21"));
assertNull(data.minInclusive());
assertEquals(ApiVersion.getOrCreateVersion("1.96"), data.maxInclusive());
assertEquals(ApiVersion.getOrCreateVersion("1.21"), data.maxInclusive());
}
@Test
@@ -1,8 +1,8 @@
package org.bukkit.craftbukkit.legacy.reroute;
import io.papermc.paper.plugin.ApiVersion;
import java.util.List;
import java.util.Map;
import org.bukkit.craftbukkit.util.ApiVersion;
import org.bukkit.support.environment.Normal;
import org.junit.jupiter.api.Test;
@@ -26,7 +26,7 @@ public class RequirePluginVersionTest extends AbstractRerouteTest {
),
"Ljava/util/List;",
true,
new RequirePluginVersionData(ApiVersion.getOrCreateVersion("1.42"), ApiVersion.getOrCreateVersion("1.42"))
new RequirePluginVersionData(ApiVersion.getOrCreateVersion("1.21"), ApiVersion.getOrCreateVersion("1.21"))
)
)
);
@@ -34,7 +34,7 @@ public class RequirePluginVersionTest extends AbstractRerouteTest {
public static class RequirePluginVersionTestData {
@RequirePluginVersion("1.42")
@RequirePluginVersion("1.21")
public static List<String> getList(Object o) {
return null;
}
@@ -1,6 +1,8 @@
package org.bukkit.craftbukkit.util;
import static org.junit.jupiter.api.Assertions.*;
import io.papermc.paper.plugin.ApiVersion;
import java.util.stream.Stream;
import net.minecraft.SharedConstants;
import org.bukkit.support.environment.VanillaFeature;
@@ -19,7 +21,9 @@ public class ApiVersionTest {
Arguments.of("none", "none"),
Arguments.of("1.12", "1.12.0"),
Arguments.of("1.13.3", "1.13.3"),
Arguments.of("1.+20.3", "1.20.3")
Arguments.of("1.+20.3", "1.20.3"),
Arguments.of("26.1", "26.1.0"),
Arguments.of("28.2.3", "28.2.3")
);
}
@@ -27,7 +31,11 @@ public class ApiVersionTest {
return Stream.of(
Arguments.of("none", "none", CompareResult.SAME),
Arguments.of("none", "1.20", CompareResult.SMALLER),
Arguments.of("2.20.3", "1.30.4", CompareResult.BIGGER),
Arguments.of("1.20.3", "1.19.4", CompareResult.BIGGER),
Arguments.of("26.1", "1.21.4", CompareResult.BIGGER),
Arguments.of("26.3.2", "1.21.11", CompareResult.BIGGER),
Arguments.of("none", "26.1.2", CompareResult.SMALLER),
Arguments.of("1.8.9", "26.1.2", CompareResult.SMALLER),
Arguments.of("1.13", "1.12", CompareResult.BIGGER),
Arguments.of("1.13.2", "1.13.3", CompareResult.SMALLER)
);
@@ -90,8 +98,8 @@ public class ApiVersionTest {
@Test
public void testSameInstance() {
ApiVersion one = ApiVersion.getOrCreateVersion("1.23.3");
ApiVersion second = ApiVersion.getOrCreateVersion("1.+23.3");
ApiVersion one = ApiVersion.getOrCreateVersion("1.20.3");
ApiVersion second = ApiVersion.getOrCreateVersion("1.+20.3");
assertSame(one, second);
}