diff --git a/paper-api/src/main/java/io/papermc/paper/event/inventory/PlayerBundleItemSelectEvent.java b/paper-api/src/main/java/io/papermc/paper/event/inventory/PlayerBundleItemSelectEvent.java new file mode 100644 index 000000000000..6a16e31208c4 --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/event/inventory/PlayerBundleItemSelectEvent.java @@ -0,0 +1,141 @@ +package io.papermc.paper.event.inventory; + +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; + +/** + * Called when a {@link Player} selects an item inside a bundle. + *
+ * NOTE: This event does not fire for bundle item selections in creative mode player inventories. + */ +@NullMarked +public final class PlayerBundleItemSelectEvent extends InventoryEvent { + + private static final HandlerList HANDLER_LIST = new HandlerList(); + + private final ItemStack bundle; + private final int rawSlot; + private final int slot; + + private final ItemStack previousItem; + private final ItemStack selectedItem; + + private final int previousIndex; + private final int selectedIndex; + private final int direction; + + @ApiStatus.Internal + public PlayerBundleItemSelectEvent(final InventoryView view, final ItemStack bundle, final int rawSlot, final ItemStack previousItem, final ItemStack selectedItem, final int previousIndex, final int selectedIndex, final int direction) { + super(view); + + this.bundle = bundle; + this.rawSlot = rawSlot; + this.slot = view.convertSlot(rawSlot); + + this.previousItem = previousItem; + this.selectedItem = selectedItem; + + this.previousIndex = previousIndex; + this.selectedIndex = selectedIndex; + this.direction = direction; + } + + /** + * Gets the player who triggered the event. + * + * @return the player + */ + public Player getPlayer() { + return (Player) this.getView().getPlayer(); + } + + /** + * Gets the bundle item. + * + * @return the bundle item + */ + public ItemStack getBundle() { + return this.bundle; + } + + /** + * Gets the slot number of the bundle, depending on which {@link Inventory} it is located in. + * + * @return the slot number + * @see InventoryClickEvent#getSlot() + */ + public int getSlot() { + return this.slot; + } + + /** + * Gets the raw slot number of the bundle inside the {@link InventoryView}. + * + * @return the raw slot number + * @see InventoryClickEvent#getRawSlot() + */ + public int getRawSlot() { + return this.rawSlot; + } + + /** + * Gets the previously selected item inside the bundle. If no item was previously selected, this will return an empty item. + + * @return the previously selected item + */ + public ItemStack getPreviousItem() { + return this.previousItem.clone(); + } + + /** + * Gets the selected item inside the bundle. + * + * @return the selected item + */ + public ItemStack getSelectedItem() { + return this.selectedItem.clone(); + } + + /** + * Gets the previously selected index. If no item was previously selected, this will be -1. + * + * @return the previously selected index + */ + public int getPreviousIndex() { + return this.previousIndex; + } + + /** + * Gets the selected index. + * + * @return the selected index + */ + public int getSelectedIndex() { + return this.selectedIndex; + } + + /** + * Gets the direction from the previous selection to the new one. + * + * @return +1 if forwards, -1 if backwards + */ + public int getDirection() { + return this.direction; + } + + @Override + public HandlerList getHandlers() { + return HANDLER_LIST; + } + + public static HandlerList getHandlerList() { + return HANDLER_LIST; + } +} diff --git a/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch b/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch index 8a9fec619627..97abc1f683bb 100644 --- a/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch @@ -382,7 +382,14 @@ } @Override -@@ -572,6 +_,7 @@ +@@ -566,12 +_,14 @@ + @Override + public void handleBundleItemSelectedPacket(final ServerboundSelectBundleItemPacket packet) { + PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level()); ++ CraftEventFactory.callPlayerBundleItemSelectEvent(this.player, packet.slotId(), packet.selectedItemIndex()); // Paper - PlayerBundleItemSelectEvent + this.player.containerMenu.setSelectedBundleItemIndex(packet.slotId(), packet.selectedItemIndex()); + } + @Override public void handleRecipeBookChangeSettingsPacket(final ServerboundRecipeBookChangeSettingsPacket packet) { PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level()); diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java index 7a42d1810f74..bf69b044a428 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java @@ -1606,6 +1606,56 @@ public static BlockIgniteEvent callBlockIgniteEvent(Level level, BlockPos pos, I return event; } + public static void callPlayerBundleItemSelectEvent(final net.minecraft.world.entity.player.Player player, final int slotId, final int selectedIndex) { + if (player.isCreative() && player.containerMenu instanceof net.minecraft.world.inventory.InventoryMenu) { + // The packet's slotId will always be 0 if this packet is sent for a player selecting an item in a bundle in their creative inventory, + // making it unfit for firing an event. + return; + } + + if (slotId < 0 || slotId >= player.containerMenu.slots.size() || selectedIndex <= net.minecraft.world.item.component.BundleContents.NO_SELECTED_ITEM_INDEX) { + return; + } + + final net.minecraft.world.item.ItemStack item = player.containerMenu.slots.get(slotId).getItem(); + if (!item.is(net.minecraft.tags.ItemTags.BUNDLES)) { + return; + } + + final net.minecraft.world.item.component.BundleContents contents = item.get(net.minecraft.core.component.DataComponents.BUNDLE_CONTENTS); + if (contents == null) { + return; + } + + final int previousIndex = contents.getSelectedItemIndex(); + if (selectedIndex >= contents.size() || selectedIndex == previousIndex) { + return; + } + + int direction; + if (previousIndex == -1) { + direction = selectedIndex == 0 ? 1 : -1; + } else { + int diff = selectedIndex - previousIndex; + direction = diff > 0 ? 1 : -1; + + if (Math.abs(diff) == contents.size() - 1) { + direction = -direction; + } + } + + new io.papermc.paper.event.inventory.PlayerBundleItemSelectEvent( + player.containerMenu.getBukkitView(), + item.asBukkitMirror(), + slotId, + contents.getSelectedItem() != null ? contents.getSelectedItem().create().asBukkitMirror() : org.bukkit.inventory.ItemStack.empty(), + contents.items().get(selectedIndex).create().asBukkitMirror(), + previousIndex, + selectedIndex, + direction + ).callEvent(); + } + public static void handleInventoryCloseEvent(net.minecraft.world.entity.player.Player human, org.bukkit.event.inventory.InventoryCloseEvent.Reason reason) { InventoryCloseEvent event = new InventoryCloseEvent(human.containerMenu.getBukkitView(), reason); // Paper human.level().getCraftServer().getPluginManager().callEvent(event);