diff --git a/patches/net/minecraft/world/inventory/EnchantmentMenu.java.patch b/patches/net/minecraft/world/inventory/EnchantmentMenu.java.patch index be2dc61baa..817bb84ff8 100644 --- a/patches/net/minecraft/world/inventory/EnchantmentMenu.java.patch +++ b/patches/net/minecraft/world/inventory/EnchantmentMenu.java.patch @@ -45,13 +45,14 @@ - itemstack2 = itemstack.transmuteCopy(Items.ENCHANTED_BOOK); - this.enchantSlots.setItem(0, itemstack2); - } - +- - for (EnchantmentInstance enchantmentinstance : list) { - itemstack2.enchant(enchantmentinstance.enchantment, enchantmentinstance.level); - } + // Neo: Allow items to transform themselves when enchanted, instead of relying on hardcoded transformations for Items.BOOK + itemstack2 = itemstack.getItem().applyEnchantments(itemstack, list); + this.enchantSlots.setItem(0, itemstack2); ++ net.neoforged.neoforge.common.CommonHooks.onPlayerEnchantItem(p_39465_, itemstack2, list); itemstack1.consume(i, p_39465_); if (itemstack1.isEmpty()) { diff --git a/src/main/java/net/neoforged/neoforge/common/CommonHooks.java b/src/main/java/net/neoforged/neoforge/common/CommonHooks.java index f4c06f1758..25f4f3c588 100644 --- a/src/main/java/net/neoforged/neoforge/common/CommonHooks.java +++ b/src/main/java/net/neoforged/neoforge/common/CommonHooks.java @@ -102,6 +102,7 @@ import net.minecraft.world.inventory.AnvilMenu; import net.minecraft.world.inventory.ClickAction; import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.inventory.EnchantmentMenu; import net.minecraft.world.inventory.RecipeBookType; import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.AdventureModePredicate; @@ -120,6 +121,7 @@ import net.minecraft.world.item.component.ItemAttributeModifiers; import net.minecraft.world.item.context.UseOnContext; import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.item.enchantment.EnchantmentInstance; import net.minecraft.world.item.enchantment.ItemEnchantments; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.GameType; @@ -200,6 +202,7 @@ import net.neoforged.neoforge.event.entity.player.AnvilRepairEvent; import net.neoforged.neoforge.event.entity.player.AttackEntityEvent; import net.neoforged.neoforge.event.entity.player.CriticalHitEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEnchantItemEvent; import net.neoforged.neoforge.event.entity.player.PlayerEvent; import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent; import net.neoforged.neoforge.event.level.BlockDropsEvent; @@ -665,6 +668,18 @@ public static InteractionResult onPlaceItemIntoWorld(UseOnContext context) { return ret; } + /** + * Fires {@link PlayerEnchantItemEvent} in {@link EnchantmentMenu#clickMenuButton(Player, int)} after the enchants are + * applied to the item. + * + * @param player the player who clicked the menu button + * @param stack the item enchanted + * @param instances the specific enchantments that were applied to the item. + */ + public static void onPlayerEnchantItem(Player player, ItemStack stack, List instances) { + NeoForge.EVENT_BUS.post(new PlayerEnchantItemEvent(player, stack, instances)); + } + public static boolean onAnvilChange(AnvilMenu container, ItemStack left, ItemStack right, Container outputSlot, String name, long baseCost, Player player) { AnvilUpdateEvent e = new AnvilUpdateEvent(left, right, name, baseCost, player); if (NeoForge.EVENT_BUS.post(e).isCanceled()) diff --git a/src/main/java/net/neoforged/neoforge/event/entity/player/PlayerEnchantItemEvent.java b/src/main/java/net/neoforged/neoforge/event/entity/player/PlayerEnchantItemEvent.java new file mode 100644 index 0000000000..6ab2d9e60f --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/event/entity/player/PlayerEnchantItemEvent.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.event.entity.player; + +import java.util.List; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.enchantment.EnchantmentInstance; +import net.neoforged.neoforge.common.extensions.IItemExtension; + +/** + * This event fires when a player enchants an item, after {@link IItemExtension#applyEnchantments} has been called. + *

+ * This event is only fired on the logical server. + */ +public class PlayerEnchantItemEvent extends PlayerEvent { + private final ItemStack enchantedItem; + private final List enchantments; + + public PlayerEnchantItemEvent(Player player, ItemStack enchantedItem, List enchantments) { + super(player); + this.enchantedItem = enchantedItem; + this.enchantments = enchantments; + } + + /** + * @return the {@link ItemStack} after it was enchanted + */ + public ItemStack getEnchantedItem() { + return enchantedItem; + } + + /** + * @return the list of {@link EnchantmentInstance}s that were applied to the item for this event firing + */ + public List getEnchantments() { + return enchantments; + } +} diff --git a/tests/src/main/java/net/neoforged/neoforge/debug/enchantment/EnchantmentLevelTests.java b/tests/src/main/java/net/neoforged/neoforge/debug/enchantment/EnchantmentLevelTests.java index 848332d1a8..96d5e43e99 100644 --- a/tests/src/main/java/net/neoforged/neoforge/debug/enchantment/EnchantmentLevelTests.java +++ b/tests/src/main/java/net/neoforged/neoforge/debug/enchantment/EnchantmentLevelTests.java @@ -5,6 +5,8 @@ package net.neoforged.neoforge.debug.enchantment; +import java.util.Objects; +import net.minecraft.core.BlockPos; import net.minecraft.core.component.DataComponents; import net.minecraft.core.registries.Registries; import net.minecraft.gametest.framework.GameTest; @@ -14,7 +16,10 @@ import net.minecraft.world.item.component.CustomData; import net.minecraft.world.item.enchantment.Enchantments; import net.minecraft.world.item.enchantment.ItemEnchantments; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.EnchantingTableBlockEntity; import net.neoforged.neoforge.event.enchanting.GetEnchantmentLevelEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEnchantItemEvent; import net.neoforged.testframework.DynamicTest; import net.neoforged.testframework.annotation.ForEachTest; import net.neoforged.testframework.annotation.TestHolder; @@ -23,7 +28,7 @@ @ForEachTest(groups = EnchantmentLevelTests.GROUP) public class EnchantmentLevelTests { - public static final String GROUP = "enchantment.level"; + public static final String GROUP = "enchantment"; @GameTest @EmptyTemplate @@ -65,4 +70,35 @@ static void getEnchLevelEvent(final DynamicTest test, final RegistrationHelper r helper.succeed(); }); } + + @GameTest + @EmptyTemplate + @TestHolder(description = "Tests if the PlayerEnchantedItemEvent fired.") + static void playerEnchantItemTest(final DynamicTest test, final RegistrationHelper reg) { + test.eventListeners().forge().addListener((PlayerEnchantItemEvent event) -> { + event.getEnchantedItem().setDamageValue(1); //change a value we can reference in our test sequence + }); + final BlockPos pos = new BlockPos(1, 2, 1); + test.onGameTest(helper -> helper.startSequence(helper::makeMockPlayer) + //ensure the player has enough experience to perform an enchantment + .thenExecute(player -> player.experienceLevel = 30) + //place a table into the world to hold our container + .thenExecute(player -> helper.setBlock(pos, Blocks.ENCHANTING_TABLE)) + //open the menu container on the player + .thenExecute(player -> player.containerMenu = Objects.requireNonNull(Objects.requireNonNull( + Objects.requireNonNull(helper.getBlockEntity(pos, EnchantingTableBlockEntity.class)).getBlockState() + .getMenuProvider(player.level(), helper.absolutePos(pos))) + .createMenu(1, player.getInventory(), player))) + //simulate putting an iron sword in the first slot + .thenExecute(player -> player.containerMenu.setItem(0, player.containerMenu.getStateId(), new ItemStack(Items.IRON_SWORD))) + //simulate putting the lapis into the second slot + .thenExecute(player -> player.containerMenu.setItem(1, player.containerMenu.getStateId(), new ItemStack(Items.LAPIS_LAZULI))) + //ensure the lapis count is enough to pay for any enchanting costs + .thenExecute(player -> player.containerMenu.getSlot(1).getItem().setCount(64)) + //simulate clicking the button on the screen. + .thenExecute(player -> player.containerMenu.clickMenuButton(player, 1)) + //verify the event listener has set the damage value of our item to one + .thenWaitUntil(player -> helper.assertTrue(player.containerMenu.getSlot(0).getItem().getDamageValue() == 1, "Enchanted item damage not set by the event")) + .thenSucceed()); + } }