Compare commits

...
Author SHA1 Message Date
Shane Freeder 5bf65680a1 Restore detailed entity information in watchdog dumps
Watchdog dumps lost their per-entity detail in 1.21.4. The instrumentation
survived - ServerLevel still tracks the currently ticking entity, and Entity
still records the move() start position and move vector under posLock - but
the code that printed it was dropped while the "Detail more information in
watchdog dumps" feature patch was inlined into the per-file patches, along
with the four Entity accessors that exposed the move state.

This restores the readout and those accessors. It lives in WatchdogThread
rather than FeatureHooks since none of it is chunk-system dependent. The dump
is wrapped in a catch-all: it reads concurrently mutating entity state off the
server thread, and an exception there would otherwise cost us the thread dump
and the halt that follows it.
2026-09-09 21:42:41 +00:00
Charles Richards 50018799f2 Assign BlockProjectileSource before dispenser projectiles spawn (#14201) 2026-09-09 19:31:52 +02:00
Amine Kacimi a3b2cb162a Fix zombie fly when trampling after cancelling EntityInteractEvent (#14225) 2026-09-09 19:12:36 +02:00
Creeam a2a42c5b12 Correctly consume chunkStatus in loadChunksAsync (#14220)
Reintroduce a missing patch hunk from moonrise which consumes the added chunk status parameter in ServerLevel#loadChunksAsync.
2026-08-29 12:31:38 +01:00
CPieterandJake Potrebic 1797fbccf1 Add PredicateChoice (#12017)
Co-authored-by: Jake Potrebic <jake.m.potrebic@gmail.com>
2026-08-28 20:55:17 +01:00
Pedro bb09b431d2 Bump spark integration to version 1.10.180 (#14214) 2026-08-25 14:57:53 +01:00
Spottedleaf 5d4f9bd0e4 Sync DataConverter
- https://github.com/PaperMC/DataConverter/commit/72494376717b1a7912fad592a6cf9a203ff66109
- https://github.com/PaperMC/DataConverter/commit/01cfab62813b3200c9f9e94a5d8feccbc6875d43
2026-08-25 06:05:47 -07:00
Pedro bd0c804a19 [ci-skip] Fix badge version filter syntax in README.md [ci skip] (#14140) 2026-08-24 17:49:23 +01:00
Shane Freeder 27af5dde55 Bump velocity-natives to 4.1.0 release 2026-08-24 17:42:26 +01:00
Spottedleaf 37dc5450d9 Update Leafpile to 1.1.0
This is mainly to include a fix for MultiThreadedQueue iterator
removal and SWMR hash table equality comparison.
2026-08-23 01:56:34 -07:00
Warrior de518f79b5 Bump spark-paper to the latest version (#14205) 2026-08-22 18:15:46 +01:00
Strokkur24 0bdc80db32 Remove name field in Event class (#14198) 2026-08-20 21:05:12 +02:00
Spottedleaf c9e894d3cc Do not re-order ClientboundLoginCompressionPacket
Using Connection#scheduleNettyTask will schedule a task on the netty thread
in the same manner that Connection#send would. However,
Connection#send will append to the pending actions if it is not
empty - causing a re-order if used during the task scheduled with
Connection#scheduleNettyTask.
2026-08-10 22:12:14 -07:00
29 changed files with 4785 additions and 3942 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,6 +5,7 @@ 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;
@@ -41,10 +42,12 @@ 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 new PredicateRecipeChoice(stackPredicate);
return RecipeChoice.predicateChoice(stackPredicate, ItemType.STONE.createItemStack());
}
@Override
@@ -15,7 +15,6 @@ import org.jetbrains.annotations.NotNull;
*/
public abstract class Event {
private String name;
private final boolean isAsync;
/**
@@ -60,10 +59,7 @@ public abstract class Event {
*/
@NotNull
public String getEventName() {
if (this.name == null) {
this.name = this.getClass().getSimpleName();
}
return this.name;
return this.getClass().getSimpleName();
}
@NotNull
@@ -0,0 +1,38 @@
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,6 +70,53 @@ 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.
*
@@ -228,14 +275,26 @@ 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");
@@ -341,4 +400,13 @@ 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, new RecipeChoice.ExactChoice(item.clone())); // Paper
return setIngredient(key, 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(new RecipeChoice.ExactChoice(item));
this.ingredients.add(RecipeChoice.exactChoice(item));
}
return this;
}
+3 -3
View File
@@ -110,7 +110,7 @@ abstract class MockitoAgentProvider : CommandLineArgumentProvider {
dependencies {
implementation(project(":paper-api"))
implementation("ca.spottedleaf:leafpile:1.0.0")
implementation("ca.spottedleaf:leafpile:1.2.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:3.4.0-SNAPSHOT") {
implementation("com.velocitypowered:velocity-native:4.1.0") {
isTransitive = false
}
implementation("io.netty:netty-codec-haproxy:4.2.15.Final") // Add support for proxy protocol
@@ -156,7 +156,7 @@ dependencies {
// Spark
implementation("me.lucko:spark-api:0.1-20240720.200737-2")
implementation("me.lucko:spark-paper:1.10.172")
implementation("me.lucko:spark-paper:1.10.180")
}
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..15568f598b8c61cf92563b6a7f8cae526b84484e
index 0000000000000000000000000000000000000000..54b2492c3ab6bf9ff52cec53dc4635f725f9eb4f
--- /dev/null
+++ b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
@@ -0,0 +1,2199 @@
@@ -0,0 +1,2200 @@
+package ca.spottedleaf.moonrise.patches.collisions;
+
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
@@ -18048,6 +18048,7 @@ index 0000000000000000000000000000000000000000..15568f598b8c61cf92563b6a7f8cae52
+ 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)) {
@@ -27402,7 +27403,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..e5328f08d6a323aa1307ed791a88916b4588939c 100644
index 473feb1aa6ccf0f03ce0bc895de367b3e554e1bc..4fce291cb7cd6eb2f4e47237fe247a5d666da0ad 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;
@@ -27625,7 +27626,13 @@ index 473feb1aa6ccf0f03ce0bc895de367b3e554e1bc..e5328f08d6a323aa1307ed791a88916b
}
}
}
@@ -340,16 +476,135 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
@@ -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
);
}
}
}
@@ -29291,7 +29298,7 @@ index 09b6da207530987b483444cebeadc2f1c9a15247..3666c3efd188508153b6482db425648e
+ // Paper end - block counting
}
diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java
index ec8b47eebc30a73adb3195aa91fd5b82518cac2d..4b74c91b47318d048abc53952ebeae7615f50524 100644
index d6772f96af5a103c41fcc776f478f79ca72da447..23001241b9d9927b7207f40ad6d61811ff30f9eb 100644
--- a/net/minecraft/world/entity/Entity.java
+++ b/net/minecraft/world/entity/Entity.java
@@ -164,7 +164,7 @@ public abstract class Entity
@@ -29542,7 +29549,7 @@ index ec8b47eebc30a73adb3195aa91fd5b82518cac2d..4b74c91b47318d048abc53952ebeae76
public Entity(final EntityType<?> type, final Level level) {
this.type = type;
@@ -1484,34 +1590,76 @@ public abstract class Entity
@@ -1501,34 +1607,76 @@ public abstract class Entity
}
private Vec3 collide(final Vec3 movement) {
@@ -29642,7 +29649,7 @@ index ec8b47eebc30a73adb3195aa91fd5b82518cac2d..4b74c91b47318d048abc53952ebeae76
}
private static float[] collectCandidateStepUpHeights(
@@ -2832,17 +2980,106 @@ public abstract class Entity
@@ -2849,17 +2997,106 @@ public abstract class Entity
return false;
}
@@ -29759,7 +29766,7 @@ index ec8b47eebc30a73adb3195aa91fd5b82518cac2d..4b74c91b47318d048abc53952ebeae76
}
public InteractionResult interact(final Player player, final InteractionHand hand, final Vec3 location) {
@@ -4449,15 +4686,17 @@ public abstract class Entity
@@ -4466,15 +4703,17 @@ public abstract class Entity
}
public Iterable<Entity> getIndirectPassengers() {
@@ -29785,7 +29792,7 @@ index ec8b47eebc30a73adb3195aa91fd5b82518cac2d..4b74c91b47318d048abc53952ebeae76
}
public int countPlayerPassengers() {
@@ -4768,6 +5007,15 @@ public abstract class Entity
@@ -4785,6 +5024,15 @@ public abstract class Entity
}
public final void setPosRaw(double x, double y, double z, boolean forceBoundingBoxUpdate) {
@@ -29801,7 +29808,7 @@ index ec8b47eebc30a73adb3195aa91fd5b82518cac2d..4b74c91b47318d048abc53952ebeae76
if (!checkPosition(this, x, y, z)) {
return;
}
@@ -4917,6 +5165,12 @@ public abstract class Entity
@@ -4934,6 +5182,12 @@ public abstract class Entity
@Override
public final void setRemoved(final Entity.RemovalReason reason, org.bukkit.event.entity.EntityRemoveEvent.@Nullable Cause cause) { // CraftBukkit - add Bukkit remove cause
@@ -29814,7 +29821,7 @@ index ec8b47eebc30a73adb3195aa91fd5b82518cac2d..4b74c91b47318d048abc53952ebeae76
org.bukkit.craftbukkit.event.CraftEventFactory.callEntityRemoveEvent(this, cause); // CraftBukkit
final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers
if (this.removalReason == null) {
@@ -4928,7 +5182,7 @@ public abstract class Entity
@@ -4945,7 +5199,7 @@ public abstract class Entity
this.stopRiding();
}
@@ -29823,7 +29830,7 @@ index ec8b47eebc30a73adb3195aa91fd5b82518cac2d..4b74c91b47318d048abc53952ebeae76
this.levelCallback.onRemove(reason);
this.onRemoval(reason);
// Paper start - Folia schedulers
@@ -4966,7 +5220,7 @@ public abstract class Entity
@@ -4983,7 +5237,7 @@ public abstract class Entity
public boolean shouldBeSaved() {
return (this.removalReason == null || this.removalReason.shouldSave())
&& !this.isPassenger()
File diff suppressed because it is too large. Load diff
@@ -355,7 +355,7 @@ index 0000000000000000000000000000000000000000..829f7685904fd741e00403647ae76d32
+ }
+}
diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
index e5328f08d6a323aa1307ed791a88916b4588939c..1afbe7a877f8055702809bc33b110bdf2933f999 100644
index 4fce291cb7cd6eb2f4e47237fe247a5d666da0ad..6cf92a7ae011fe4fa55cd744dce8d5f8fb49b1ec 100644
--- a/net/minecraft/server/level/ServerLevel.java
+++ b/net/minecraft/server/level/ServerLevel.java
@@ -880,6 +880,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
@@ -463,7 +463,7 @@ index 271ed1128959287f6c1b6ca59ded377b88f5afff..f03fa06c0ba56f7f5e1e45bc1568a490
public void tick() {
super.tick();
diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java
index a5792bc27fdc85ecf388ab44c0b1b3055fc41d21..4a18f74c7513f0ac6a3415163165d9239ee5b567 100644
index 23001241b9d9927b7207f40ad6d61811ff30f9eb..1484558ea50760fbee7a31df413637ce685ea001 100644
--- a/net/minecraft/world/entity/Entity.java
+++ b/net/minecraft/world/entity/Entity.java
@@ -383,6 +383,15 @@ public abstract class Entity
@@ -496,7 +496,7 @@ index a5792bc27fdc85ecf388ab44c0b1b3055fc41d21..4a18f74c7513f0ac6a3415163165d923
SynchedEntityData.Builder entityDataBuilder = new SynchedEntityData.Builder(this);
entityDataBuilder.define(DATA_SHARED_FLAGS_ID, (byte)0);
entityDataBuilder.define(DATA_AIR_SUPPLY_ID, this.getMaxAirSupply());
@@ -1141,6 +1157,10 @@ public abstract class Entity
@@ -1158,6 +1174,10 @@ public abstract class Entity
} else {
if (moverType == MoverType.PISTON) {
delta = this.limitPistonMovement(delta);
@@ -507,7 +507,7 @@ index a5792bc27fdc85ecf388ab44c0b1b3055fc41d21..4a18f74c7513f0ac6a3415163165d923
if (delta.equals(Vec3.ZERO)) {
return;
}
@@ -1156,6 +1176,13 @@ public abstract class Entity
@@ -1173,6 +1193,13 @@ public abstract class Entity
this.stuckSpeedMultiplier = Vec3.ZERO;
this.setDeltaMovement(Vec3.ZERO);
}
@@ -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 709f2c6b47f78920cd9432da4c75e0d1ab2bffbc..3f10ea62d2ef70e7e4f01c83942cd9051249954c 100644
index f46c55fd212ea68386342bc9ce71c04108758a8e..0b65c06e43fd22b2c30d9b8c5ef5b5e99301cec1 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 709f2c6b47f78920cd9432da4c75e0d1ab2bffbc..3f10ea62d2ef70e7e4f01c83942cd905
this.flushQueue();
}
}
@@ -444,21 +475,42 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
@@ -461,21 +492,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 709f2c6b47f78920cd9432da4c75e0d1ab2bffbc..3f10ea62d2ef70e7e4f01c83942cd905
}
}
@@ -470,16 +522,57 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
@@ -487,16 +539,57 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
}
}
@@ -221,7 +221,7 @@ index 709f2c6b47f78920cd9432da4c75e0d1ab2bffbc..3f10ea62d2ef70e7e4f01c83942cd905
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
@@ -549,6 +642,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
@@ -566,6 +659,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
public void disconnect(final DisconnectionDetails details) {
this.preparing = false; // Spigot
@@ -229,7 +229,7 @@ index 709f2c6b47f78920cd9432da4c75e0d1ab2bffbc..3f10ea62d2ef70e7e4f01c83942cd905
if (this.channel == null) {
this.delayedDisconnect = details;
}
@@ -738,7 +832,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
@@ -755,7 +849,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 709f2c6b47f78920cd9432da4c75e0d1ab2bffbc..3f10ea62d2ef70e7e4f01c83942cd905
} else {
this.disconnectionHandled = true;
PacketListener packetListener = this.getPacketListener();
@@ -749,7 +843,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
@@ -766,7 +860,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
);
disconnectListener.onDisconnect(details);
}
@@ -247,7 +247,7 @@ index 709f2c6b47f78920cd9432da4c75e0d1ab2bffbc..3f10ea62d2ef70e7e4f01c83942cd905
// Paper start - Add PlayerConnectionCloseEvent
if (packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) {
/* Player was logged in, either game listener or configuration listener */
@@ -792,4 +886,96 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
@@ -809,4 +903,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 3f10ea62d2ef70e7e4f01c83942cd9051249954c..9a04309584a5b9b55d0d383619113835190ad8f9 100644
index 0b65c06e43fd22b2c30d9b8c5ef5b5e99301cec1..9fbc7e93712672dbab67f931e2aee447d803607b 100644
--- a/net/minecraft/network/Connection.java
+++ b/net/minecraft/network/Connection.java
@@ -768,10 +768,24 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
@@ -785,10 +785,24 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
return connection;
}
@@ -298,7 +298,7 @@ index 3f10ea62d2ef70e7e4f01c83942cd9051249954c..9a04309584a5b9b55d0d383619113835
public boolean isConnected() {
return this.channel != null && this.channel.isOpen();
@@ -805,16 +819,17 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
@@ -822,16 +836,17 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
public void setupCompression(final int threshold, final boolean validateDecompressed) {
if (threshold >= 0) {
@@ -320,7 +320,7 @@ index 3f10ea62d2ef70e7e4f01c83942cd9051249954c..9a04309584a5b9b55d0d383619113835
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 1a60071f32a0c92c69f60c75035e5324aa144fb4..e84e243f35b09691d03e9a9cf3de0c093b1d5cfb 100644
index fa2467ff885e039f861bb9b1ae076a5a64b891ed..8a2f1ebcf06bb0c1e8b352652de69b6c1d74daf3 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 1a60071f32a0c92c69f60c75035e5324aa144fb4..e84e243f35b09691d03e9a9cf3de0c09
.add(
new ServerBootstrap()
diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
index d6b6c6e7dce618c3f4aecc0969b7423771eab3ba..b94e01aa54b932b29370d1742b00ed73254fe4dc 100644
index 24bcd1067db20fc09b87b6e300615e3fa590049a..1d892a8c905496af9d56cec86941e82ec48505db 100644
--- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
@@ -257,9 +257,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
@@ -259,9 +259,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;
@@ -66,10 +66,10 @@ index 6de2929fda582cc4ceecd7bfe0b440ac6608669d..2381f8571f6e614dba1cfb62a1d396be
io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.ADVENTURE_CLICK_MANAGER.handleQueue(this.tickCount); // Paper
io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.DIALOG_CLICK_MANAGER.handleQueue(this.tickCount); // Paper
diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java
index f8f479bd0ce2498c0cf4beab07d961247735e23c..d958c4717739e588c5f8985dec5d215c745f49fc 100644
index 1484558ea50760fbee7a31df413637ce685ea001..db67d4c41ad6174e9e543c5476b6d40aae1e7f04 100644
--- a/net/minecraft/world/entity/Entity.java
+++ b/net/minecraft/world/entity/Entity.java
@@ -5237,6 +5237,11 @@ public abstract class Entity
@@ -5254,6 +5254,11 @@ public abstract class Entity
this.getBukkitEntity().taskScheduler.retire();
}
// Paper end - Folia schedulers
@@ -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..f47c12e9dd6cfa857ca07a764edc22de372e25b6
index 0000000000000000000000000000000000000000..a9dcccefb19e6610076e1153547d2f9156042313
--- /dev/null
+++ b/io/papermc/paper/inventory/recipe/StackedContentsExtrasMap.java
@@ -0,0 +1,68 @@
@@ -0,0 +1,81 @@
+package io.papermc.paper.inventory.recipe;
+
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
@@ -94,12 +94,16 @@ index 0000000000000000000000000000000000000000..f47c12e9dd6cfa857ca07a764edc22de
+import net.minecraft.world.item.crafting.CraftingInput;
+import net.minecraft.world.item.crafting.Ingredient;
+import net.minecraft.world.item.crafting.Recipe;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+
+public final class StackedContentsExtrasMap {
+
+ private final StackedContents<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;
@@ -107,8 +111,11 @@ index 0000000000000000000000000000000000000000..f47c12e9dd6cfa857ca07a764edc22de
+
+ public void initialize(final Recipe<?> recipe) {
+ this.exactIngredients.clear();
+ this.predicateIngredients.clear();
+ for (final Ingredient ingredient : recipe.placementInfo().ingredients()) {
+ if (ingredient.isExact()) {
+ if (ingredient.stackPredicate != null) {
+ this.predicateIngredients.add(ingredient.stackPredicate);
+ } else if (ingredient.isExact()) {
+ this.exactIngredients.addAll(ingredient.itemStacks());
+ }
+ }
@@ -138,11 +145,17 @@ index 0000000000000000000000000000000000000000..f47c12e9dd6cfa857ca07a764edc22de
+ 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;
@@ -344,7 +357,7 @@ index ffaa8e450a11a01060b6847d1b66a76323f9d8ba..1ace17d8fc7e7b8b4722a1889d49233c
}
diff --git a/net/minecraft/world/item/crafting/Ingredient.java b/net/minecraft/world/item/crafting/Ingredient.java
index 1a6870fd5dd02022d6b0df88e8f55f8ba9cf07c9..4f3697573fbfec3865751037de00a562ad81cddb 100644
index 218d56fa066513fd8d9761af36bcc2873a34406b..778b38911b0d244738fd3db3b275b0c78a17339f 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;
@@ -356,9 +369,9 @@ index 1a6870fd5dd02022d6b0df88e8f55f8ba9cf07c9..4f3697573fbfec3865751037de00a562
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)
@@ -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);
@@ -35,19 +35,23 @@ public final class Ingredient implements Predicate<ItemStack>, StackedContents.I
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
@@ -383,8 +396,8 @@ index 1a6870fd5dd02022d6b0df88e8f55f8ba9cf07c9..4f3697573fbfec3865751037de00a562
return ingredient;
}
// CraftBukkit end
@@ -81,21 +85,22 @@ public final class Ingredient implements Predicate<ItemStack>, StackedContents.I
public boolean test(final ItemStack input) {
@@ -87,21 +91,32 @@ public final class Ingredient implements Predicate<ItemStack>, StackedContents.I
// Paper end - add PredicateChoice
// CraftBukkit start
if (this.isExact()) {
- for (ItemStack item : this.itemStacks()) {
@@ -408,14 +421,24 @@ index 1a6870fd5dd02022d6b0df88e8f55f8ba9cf07c9..4f3697573fbfec3865751037de00a562
+ 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) ->
+ this.isExact() && this.itemStacks.contains(exact);
+ case io.papermc.paper.inventory.recipe.ItemOrExact.Exact(final ItemStack exact) -> {
+ if (this.stackPredicate != null) {
+ yield this.stackPredicate.test(exact);
+ }
+ 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);
+ }
+ };
+ // Paper end - Improve exact choice recipe ingredients
}
@Override
@@ -125,6 +130,11 @@ public final class Ingredient implements Predicate<ItemStack>, StackedContents.I
@@ -131,6 +146,11 @@ public final class Ingredient implements Predicate<ItemStack>, StackedContents.I
}
public SlotDisplay display() {
@@ -4,43 +4,6 @@ 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 a74821562154c99ee3a8571ff8dbf8fe5eacf416..8d47addc3cf6d0c551809463992da8551c2835f6 100644
index 6aef3311ca59220e8d7c97a4a5c1f14e96d08117..e70408061c8a495af319c810ee93d51a2644e465 100644
--- a/ca/spottedleaf/dataconverter/minecraft/converters/tileentity/ConverterAbstractTileEntityRemove.java
+++ b/ca/spottedleaf/dataconverter/minecraft/converters/tileentity/ConverterAbstractTileEntityRemove.java
@@ -61,8 +61,43 @@ public final class ConverterAbstractTileEntityRemove {
@@ -63,8 +63,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 ca.spottedleaf.dataconverter.types.MapType context = data.getMap(net.minecraft.util.datafix.fixes.ChunkHeightAndBiomeFix.DATAFIXER_CONTEXT_TAG);
+ final 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 +_,36 @@
@@ -27,17 +_,38 @@
ServerLevel level = source.level();
Direction direction = source.state().getValue(DispenserBlock.FACING);
Position position = this.dispenseConfig.positionFunction().getDispensePosition(source, direction);
@@ -40,8 +40,10 @@
+
+ // SPIGOT-7923: Avoid create projectiles with empty item
+ if (!singleItemStack.isEmpty()) {
+ 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 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.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 +_,30 @@
@@ -297,6 +_,47 @@
}
}
@@ -195,7 +195,7 @@
+ *
+ * @see <a href="https://mojira.dev/MC-308621">MC-308621</a>
+ */
+ public void scheduleNettyTask(final Runnable task) {
+ private void scheduleNettyTask(final Runnable task) {
+ final boolean connected = this.isConnected();
+ if (!connected && !this.preparing) {
+ return;
@@ -208,6 +208,23 @@
+ 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 +_,23 @@
@@ -139,21 +_,25 @@
private void verifyLoginAndFinishConnectionSetup(final GameProfile profile) {
PlayerList playerList = this.server.getPlayerList();
@@ -164,10 +164,12 @@
- );
+ // Paper start - Keep compression packet write and setup in the same Netty task
+ final int compressionThreshold = this.server.getCompressionThreshold();
+ this.connection.scheduleNettyTask(() -> {
+ this.connection.send(new ClientboundLoginCompressionPacket(compressionThreshold));
+ this.connection.setupCompression(compressionThreshold, true);
+ });
+ this.connection.sendPacketAndScheduleNettyTask(
+ new ClientboundLoginCompressionPacket(compressionThreshold), true,
+ () -> {
+ this.connection.setupCompression(compressionThreshold, true);
+ }
+ );
+ // Paper end - Keep compression packet write and setup in the same Netty task
}
@@ -492,7 +492,7 @@
}
public boolean isFree(final double xa, final double ya, final double za) {
@@ -709,7 +_,28 @@
@@ -709,7 +_,45 @@
return this.onGround;
}
@@ -504,6 +504,23 @@
+ private double moveStartX;
+ private double moveStartY;
+ private double moveStartZ;
+
+ @Nullable
+ public final Vec3 getMoveVector() {
+ return this.moveVector;
+ }
+
+ public final double getMoveStartX() {
+ return this.moveStartX;
+ }
+
+ public final double getMoveStartY() {
+ return this.moveStartY;
+ }
+
+ public final double getMoveStartZ() {
+ return this.moveStartZ;
+ }
+ // Paper end - detailed watchdog information
+
public void move(final MoverType moverType, Vec3 delta) {
@@ -1,11 +1,12 @@
--- a/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java
+++ b/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java
@@ -101,6 +_,11 @@
@@ -101,6 +_,12 @@
}
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,9 +1,10 @@
--- a/net/minecraft/world/item/crafting/Ingredient.java
+++ b/net/minecraft/world/item/crafting/Ingredient.java
@@ -33,6 +_,23 @@
@@ -33,6 +_,24 @@
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;
+
@@ -24,10 +25,15 @@
private Ingredient(final HolderSet<Item> values) {
values.unwrap().ifRight(directValues -> {
@@ -62,6 +_,17 @@
@@ -62,6 +_,22 @@
@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()) {
@@ -4,18 +4,12 @@ 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.bukkit.inventory.RecipeChoice;
import org.jspecify.annotations.NullMarked;
@NullMarked
public record PaperPotionMix(ItemStack result, Predicate<ItemStack> input, Predicate<ItemStack> ingredient) {
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);
public PaperPotionMix(final PotionMix potionMix) {
this(CraftItemStack.asNMSCopy(potionMix.getResult()), CraftRecipe.toIngredient(potionMix.getInput(), true), CraftRecipe.toIngredient(potionMix.getIngredient(), true));
}
}
@@ -6,8 +6,10 @@ 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;
@@ -32,6 +34,9 @@ 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) {
@@ -63,13 +68,19 @@ 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 new RecipeChoice.ExactChoice(choices);
return RecipeChoice.exactChoice(choices);
} else {
final RegistryKeySet<ItemType> itemTypes = PaperRegistrySets.convertToApi(RegistryKey.ITEM, ingredient.values);
return RecipeChoice.itemType(itemTypes);
@@ -109,6 +109,7 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre
logger.log(Level.SEVERE, "------------------------------");
logger.log(Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):"); // Paper
FeatureHooks.dumpAllChunkLoadInfo(MinecraftServer.getServer(), isLongTimeout); // Paper - log detailed tick information
WatchdogThread.dumpTickingInfo(logger);
WatchdogThread.dumpThread(ManagementFactory.getThreadMXBean().getThreadInfo(MinecraftServer.getServer().getRunningThread().threadId(), Integer.MAX_VALUE), logger);
logger.log(Level.SEVERE, "------------------------------");
@@ -156,6 +157,79 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre
}
}
private static void dumpTickingInfo(Logger logger) {
// The state read below is mutated by the (hung) server thread while we read it, so it may be torn or null
// in ways the normal invariants disallow. Never let that take down the watchdog itself - a failed dump must
// not stop us from printing the thread dump and, on a long timeout, halting the server.
try {
// ticking entities
for (net.minecraft.world.entity.Entity entity : net.minecraft.server.level.ServerLevel.getCurrentlyTickingEntities()) {
WatchdogThread.dumpEntity(entity, logger);
net.minecraft.world.entity.Entity vehicle = entity.getVehicle();
if (vehicle != null) {
logger.log(Level.SEVERE, "Detailing vehicle for above entity:");
WatchdogThread.dumpEntity(vehicle, logger);
}
}
// packet processors
for (net.minecraft.network.PacketListener packetListener : net.minecraft.network.PacketProcessor.getCurrentPacketProcessors()) {
if (packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl gamePacketListener) {
net.minecraft.server.level.ServerPlayer player = gamePacketListener.player;
long totalPackets = net.minecraft.network.PacketProcessor.getTotalProcessedPackets();
if (player == null) {
logger.log(Level.SEVERE, "Handling packet for player connection or ticking player connection (null player): " + packetListener);
} else {
WatchdogThread.dumpEntity(player, logger);
net.minecraft.world.entity.Entity vehicle = player.getVehicle();
if (vehicle != null) {
logger.log(Level.SEVERE, "Detailing vehicle for above entity:");
WatchdogThread.dumpEntity(vehicle, logger);
}
}
logger.log(Level.SEVERE, "Total packets processed on the main thread for all players: " + totalPackets);
} else {
logger.log(Level.SEVERE, "Handling packet for connection: " + packetListener);
}
}
} catch (Throwable thr) {
logger.log(Level.SEVERE, "Failed to dump ticking information", thr);
}
}
private static void dumpEntity(final net.minecraft.world.entity.Entity entity, Logger logger) {
double posX, posY, posZ;
net.minecraft.world.phys.Vec3 mot;
double moveStartX, moveStartY, moveStartZ;
net.minecraft.world.phys.Vec3 moveVec;
synchronized (entity.posLock) {
posX = entity.getX();
posY = entity.getY();
posZ = entity.getZ();
mot = entity.getDeltaMovement();
moveStartX = entity.getMoveStartX();
moveStartY = entity.getMoveStartY();
moveStartZ = entity.getMoveStartZ();
moveVec = entity.getMoveVector();
}
String entityType = net.minecraft.world.entity.EntityType.getKey(entity.getType()).toString();
java.util.UUID entityUUID = entity.getUUID();
net.minecraft.world.level.Level world = entity.level();
logger.log(Level.SEVERE, "Ticking entity: " + entityType + ", entity class: " + entity.getClass().getName());
logger.log(Level.SEVERE, "Entity status: removed: " + entity.isRemoved() + ", valid: " + entity.valid + ", alive: " + entity.isAlive() + ", is passenger: " + entity.isPassenger());
logger.log(Level.SEVERE, "Entity UUID: " + entityUUID);
logger.log(Level.SEVERE, "Position: world: '" + (world == null ? "unknown world?" : world.getWorld().getName()) + "' at location (" + posX + ", " + posY + ", " + posZ + ")");
logger.log(Level.SEVERE, "Velocity: " + (mot == null ? "unknown velocity" : mot.toString()) + " (in blocks per tick)");
logger.log(Level.SEVERE, "Entity AABB: " + entity.getBoundingBox());
if (moveVec != null) {
logger.log(Level.SEVERE, "Move call information: ");
logger.log(Level.SEVERE, "Start position: (" + moveStartX + ", " + moveStartY + ", " + moveStartZ + ")");
logger.log(Level.SEVERE, "Move vector: " + moveVec.toString());
}
}
private static void dumpThread(ThreadInfo thread, Logger logger) {
logger.log(Level.SEVERE, "------------------------------");
@@ -0,0 +1,221 @@
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");
}
}
@@ -0,0 +1,804 @@
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));
}
}
@@ -0,0 +1,194 @@
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)));
}
}