diff --git a/patches/minecraft/net/minecraft/world/inventory/GrindstoneMenu.java.patch b/patches/minecraft/net/minecraft/world/inventory/GrindstoneMenu.java.patch
index 8f31fefb4a..954836aefd 100644
--- a/patches/minecraft/net/minecraft/world/inventory/GrindstoneMenu.java.patch
+++ b/patches/minecraft/net/minecraft/world/inventory/GrindstoneMenu.java.patch
@@ -43,8 +43,8 @@
ItemStack itemstack1 = this.f_39560_.m_8020_(1);
boolean flag = !itemstack.m_41619_() || !itemstack1.m_41619_();
boolean flag1 = !itemstack.m_41619_() && !itemstack1.m_41619_();
-+ this.xp = net.minecraftforge.common.ForgeHooks.onGrindstoneChange(itemstack, itemstack1, this.f_39559_, this.xp);
-+ if (this.xp != Integer.MIN_VALUE) return;
++ this.xp = net.minecraftforge.common.ForgeHooks.onGrindstoneChange(itemstack, itemstack1, this.f_39559_, -1);
++ if (this.xp == Integer.MIN_VALUE)
if (!flag) {
this.f_39559_.m_6836_(0, ItemStack.f_41583_);
} else {
diff --git a/src/main/java/net/minecraftforge/common/ForgeHooks.java b/src/main/java/net/minecraftforge/common/ForgeHooks.java
index ed57b0a316..c301c72a49 100644
--- a/src/main/java/net/minecraftforge/common/ForgeHooks.java
+++ b/src/main/java/net/minecraftforge/common/ForgeHooks.java
@@ -759,7 +759,11 @@ public class ForgeHooks
public static int onGrindstoneChange(@NotNull ItemStack top, @NotNull ItemStack bottom, Container outputSlot, int xp)
{
GrindstoneEvent.OnplaceItem e = new GrindstoneEvent.OnplaceItem(top, bottom, xp);
- if (MinecraftForge.EVENT_BUS.post(e)) return e.getXp();
+ if (MinecraftForge.EVENT_BUS.post(e))
+ {
+ outputSlot.setItem(0, ItemStack.EMPTY);
+ return -1;
+ }
if (e.getOutput().isEmpty()) return Integer.MIN_VALUE;
outputSlot.setItem(0, e.getOutput());
diff --git a/src/main/java/net/minecraftforge/event/GrindstoneEvent.java b/src/main/java/net/minecraftforge/event/GrindstoneEvent.java
index c363ee3fda..734a5478e0 100644
--- a/src/main/java/net/minecraftforge/event/GrindstoneEvent.java
+++ b/src/main/java/net/minecraftforge/event/GrindstoneEvent.java
@@ -41,10 +41,7 @@ public abstract class GrindstoneEvent extends Event
}
/**
- * This is the experience amount determined by the event, not the vanilla behavior.
- * If you are the first receiver of this event, it is guaranteed to be -1.
- * The value must be set to a positive value to override vanilla behavior.
- * if the value is equal to -1, the vanilla behavior for calculating experience will run.
+ * This is the experience amount determined by the event. It will be {@code -1} unless {@link #setXp(int)} is called.
* @return The experience amount given to the player.
*/
public int getXp()
@@ -64,11 +61,22 @@ public abstract class GrindstoneEvent extends Event
/**
* This event is {@link Cancelable}
* {@link OnplaceItem} is fired when the inputs to a grindstone are changed.
- * It is called from {@link GrindstoneMenu#createResult()}.
- * If the event is canceled, vanilla behavior will not run, and the output will be set to {@link ItemStack#EMPTY}.
- * If the event is not canceled, but the output is not empty, it will set the output and not run vanilla behavior.
- * if the output is empty, and the event is not canceled, vanilla behavior will execute.
- * if the amount of experience is larger than or equal 0, the vanilla behavior for calculating experience will not run.
+ *
+ * The following rules apply:
+ *
+ * - If the event is canceled, vanilla behavior will not run, and the output will be {@linkplain ItemStack#EMPTY empty}.
+ * - If the event is not canceled
+ *
+ * - and the output is empty, the output will be determined by vanilla.
+ * - and the output is not empty, the output will be set, without running vanilla behavior.
+ *
+ * - Vanilla XP calculation logic will be used unless all of the following criterias are met:
+ *
+ * - the amount of experience is greater than or equal to {@code 0};
+ * - the event is not {@linkplain #isCanceled() canceled};
+ * - the {@linkplain #getOutput() output} is not empty.
+ *
+ *
*/
@Cancelable
public static class OnplaceItem extends GrindstoneEvent
diff --git a/src/test/java/net/minecraftforge/debug/misc/GrindstoneEventTest.java b/src/test/java/net/minecraftforge/debug/misc/GrindstoneEventTest.java
new file mode 100644
index 0000000000..02b2d24bcb
--- /dev/null
+++ b/src/test/java/net/minecraftforge/debug/misc/GrindstoneEventTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) Forge Development LLC and contributors
+ * SPDX-License-Identifier: LGPL-2.1-only
+ */
+
+package net.minecraftforge.debug.misc;
+
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.event.GrindstoneEvent;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.fml.Bindings;
+import net.minecraftforge.fml.common.Mod;
+
+@Mod("grindstone_event_test")
+public class GrindstoneEventTest {
+ private static final boolean ENABLED = false;
+ public GrindstoneEventTest() {
+ if (ENABLED) {
+ MinecraftForge.EVENT_BUS.register(this);
+ }
+ }
+
+ @SubscribeEvent
+ public void onGrindestonePlace(GrindstoneEvent.OnplaceItem event)
+ {
+ // all of these "recipes" are slot sensitive, the top and bottom must match exactly for the behavior to change
+ // switching the order will cause the "recipe" to fail
+ ItemStack topItem = event.getTopItem();
+ ItemStack bottomItem = event.getBottomItem();
+ // craft lapis + netherite to get diamond and 5 XP
+ // this "recipe" is handled in OnTakeItem so the inputs only shrink by 1 each time
+ if (topItem.is(Items.LAPIS_LAZULI) && bottomItem.is(Items.NETHERITE_INGOT))
+ {
+ event.setOutput(new ItemStack(Items.DIAMOND, 1));
+ event.setXp(5);
+ }
+
+ // craft iron ore and flint to make raw iron ore, no XP is rewarded
+ // this "recipe" is *not* handled in OnTakeItem, so the inputs will always be set to empty regardless of stack size
+ if (topItem.is(Items.IRON_ORE) && bottomItem.is(Items.FLINT))
+ {
+ event.setOutput(new ItemStack(Items.RAW_IRON, 3));
+ event.setXp(0);
+ }
+
+ // when placing an iron axe in the top slot, simply copy it to the output without change (do not remove enchants)
+ // still grant XP equivalent to all enchantments on the axe
+ if (topItem.is(Items.IRON_AXE) && bottomItem.is(Items.AIR))
+ {
+ event.setOutput(topItem.copy());
+ event.setXp(-1);
+ }
+
+ // setting the output to empty will run default behavior, effectively ignoring all previous overrides to the event
+ // note this will ignore any XP value you set, effectively for this test mod this has no impact
+ if (topItem.is(Items.IRON_SHOVEL) && bottomItem.is(Items.AIR))
+ {
+ event.setOutput(ItemStack.EMPTY);
+ }
+
+ // canceling the event will prevent disenchanting an iron sword in the top slot
+ if (topItem.is(Items.IRON_SWORD) && bottomItem.is(Items.AIR))
+ {
+ event.setCanceled(true);
+ }
+ }
+
+ @SubscribeEvent
+ public void onGrindstoneTake(GrindstoneEvent.OnTakeItem event)
+ {
+ ItemStack topItem = event.getTopItem();
+ ItemStack bottomItem = event.getBottomItem();
+ // only shrink stacks by 1 for the lapis + netherite "recipe"
+ if (topItem.is(Items.LAPIS_LAZULI) && bottomItem.is(Items.NETHERITE_INGOT))
+ {
+ ItemStack top = topItem.copy();
+ ItemStack bottom = bottomItem.copy();
+ bottom.shrink(1);
+ top.shrink(1);
+ event.setNewBottomItem(bottom);
+ event.setNewTopItem(top);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/META-INF/mods.toml b/src/test/resources/META-INF/mods.toml
index a0fb47688f..5d5bfe507a 100644
--- a/src/test/resources/META-INF/mods.toml
+++ b/src/test/resources/META-INF/mods.toml
@@ -268,6 +268,8 @@ modId="custom_item_decorations_test"
modId="item_stacked_on_other_test"
[[mods]]
modId="ambient_occlusion_elements_test"
+[[mods]]
+modId="grindstone_event_test"
# ADD ABOVE THIS LINE