diff --git a/src/main/java/net/minestom/server/inventory/ContainerInventory.java b/src/main/java/net/minestom/server/inventory/ContainerInventory.java index a37caeb24..2bb6d6041 100644 --- a/src/main/java/net/minestom/server/inventory/ContainerInventory.java +++ b/src/main/java/net/minestom/server/inventory/ContainerInventory.java @@ -7,6 +7,7 @@ import net.minestom.server.event.inventory.InventoryClickEvent; import net.minestom.server.event.inventory.InventoryPostClickEvent; import net.minestom.server.event.inventory.InventoryPreClickEvent; import net.minestom.server.inventory.click.Click; +import net.minestom.server.inventory.click.ClickProcessors; import net.minestom.server.item.ItemStack; import net.minestom.server.network.packet.server.play.OpenWindowPacket; import net.minestom.server.network.packet.server.play.WindowPropertyPacket; @@ -14,7 +15,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.BiFunction; /** * Represents an inventory which can be viewed by a collection of {@link Player}. @@ -26,12 +26,14 @@ public non-sealed class ContainerInventory extends InventoryImpl { /** * Processes a click, returning a result. This will call events for the click. + * * @param inventory the clicked inventory (could be a player inventory) - * @param player the player who clicked - * @param info the click info describing the click + * @param player the player who clicked + * @param info the click info describing the click * @return the click result, or null if the click did not occur */ - public static @Nullable Click.Result handleClick(@NotNull Inventory inventory, @NotNull Player player, @NotNull Click.Info info, @NotNull BiFunction processor) { + public static @Nullable Click.Result handleClick(@NotNull Inventory inventory, @NotNull Player player, @NotNull Click.Info info, + @NotNull ClickProcessors.InventoryProcessor processor) { PlayerInventory playerInventory = player.getInventory(); InventoryPreClickEvent preClickEvent = new InventoryPreClickEvent(playerInventory, inventory, player, info); @@ -141,6 +143,12 @@ public non-sealed class ContainerInventory extends InventoryImpl { update(); } + @Override + public @Nullable Click.Result handleClick(@NotNull Player player, Click.@NotNull Info info) { + return ContainerInventory.handleClick(this, player, info, + ClickProcessors.PROCESSORS_MAP.getOrDefault(inventoryType, ClickProcessors.GENERIC_PROCESSOR)); + } + @Override public byte getWindowId() { return id; diff --git a/src/main/java/net/minestom/server/inventory/InventoryImpl.java b/src/main/java/net/minestom/server/inventory/InventoryImpl.java index 9a3269369..675f0f925 100644 --- a/src/main/java/net/minestom/server/inventory/InventoryImpl.java +++ b/src/main/java/net/minestom/server/inventory/InventoryImpl.java @@ -3,8 +3,6 @@ package net.minestom.server.inventory; import net.minestom.server.entity.Player; import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.inventory.InventoryItemChangeEvent; -import net.minestom.server.inventory.click.Click; -import net.minestom.server.inventory.click.ClickProcessors; import net.minestom.server.item.ItemStack; import net.minestom.server.network.packet.server.play.CloseWindowPacket; import net.minestom.server.network.packet.server.play.SetSlotPacket; @@ -13,7 +11,6 @@ import net.minestom.server.tag.TagHandler; import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; @@ -110,19 +107,6 @@ sealed abstract class InventoryImpl implements Inventory permits ContainerInvent player.sendPacket(new WindowItemsPacket(getWindowId(), 0, List.of(itemStacks), player.getInventory().getCursorItem())); } - @Override - public @Nullable Click.Result handleClick(@NotNull Player player, @NotNull Click.Info info) { - var processor = ClickProcessors.standard( - (builder, item, slot) -> slot >= getSize() ? - IntStream.range(0, getSize()) : - PlayerInventory.getInnerShiftClickSlots(getSize()), - (builder, item, slot) -> IntStream.concat( - IntStream.range(0, getSize()), - PlayerInventory.getInnerDoubleClickSlots(getSize()) - )); - return ContainerInventory.handleClick(this, player, info, processor); - } - @Override public @NotNull ItemStack getItemStack(int slot) { return (ItemStack) ITEM_UPDATER.getVolatile(itemStacks, slot); @@ -168,7 +152,6 @@ sealed abstract class InventoryImpl implements Inventory permits ContainerInvent */ protected final void safeItemInsert(int slot, @NotNull ItemStack itemStack, boolean sendPacket) { lock.lock(); - try { ItemStack previous = itemStacks[slot]; if (itemStack.equals(previous)) return; // Avoid sending updates if the item has not changed @@ -189,7 +172,6 @@ sealed abstract class InventoryImpl implements Inventory permits ContainerInvent @Override public void replaceItemStack(int slot, @NotNull UnaryOperator<@NotNull ItemStack> operator) { lock.lock(); - try { var currentItem = getItemStack(slot); setItemStack(slot, operator.apply(currentItem)); @@ -201,12 +183,10 @@ sealed abstract class InventoryImpl implements Inventory permits ContainerInvent @Override public void clear() { lock.lock(); - try { for (Player viewer : getViewers()) { viewer.getInventory().setCursorItem(ItemStack.AIR, false); } - // Clear the item array for (int i = 0; i < size; i++) { safeItemInsert(i, ItemStack.AIR, false); @@ -294,5 +274,4 @@ sealed abstract class InventoryImpl implements Inventory permits ContainerInvent } return result; } - } diff --git a/src/main/java/net/minestom/server/inventory/PlayerInventory.java b/src/main/java/net/minestom/server/inventory/PlayerInventory.java index 1d75c6977..acf89cb85 100644 --- a/src/main/java/net/minestom/server/inventory/PlayerInventory.java +++ b/src/main/java/net/minestom/server/inventory/PlayerInventory.java @@ -7,15 +7,12 @@ import net.minestom.server.event.item.EntityEquipEvent; import net.minestom.server.inventory.click.Click; import net.minestom.server.inventory.click.ClickProcessors; import net.minestom.server.item.ItemStack; -import net.minestom.server.item.Material; import net.minestom.server.network.packet.server.play.SetSlotPacket; import net.minestom.server.network.packet.server.play.WindowItemsPacket; import net.minestom.server.utils.inventory.PlayerInventoryUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -171,42 +168,7 @@ public non-sealed class PlayerInventory extends InventoryImpl { @Override public @Nullable Click.Result handleClick(@NotNull Player player, @NotNull Click.Info info) { - var processor = ClickProcessors.standard( - (getter, item, slot) -> { - List slots = new ArrayList<>(); - - final EquipmentSlot equipmentSlot = item.material().registry().equipmentSlot(); - if (equipmentSlot != null && slot != equipmentSlot.armorSlot()) { - slots.add(equipmentSlot.armorSlot()); - } - - if (item.material() == Material.SHIELD && slot != OFF_HAND_SLOT) { - slots.add(OFF_HAND_SLOT); - } - - if (slot < 9 || slot > 35) { - IntStream.range(9, 36).forEach(slots::add); - } - - if (slot < 0 || slot > 8) { - IntStream.range(0, 9).forEach(slots::add); - } - - if (slot == CRAFT_RESULT) { - Collections.reverse(slots); - } - - return slots.stream().mapToInt(i -> i); - }, - (getter, item, slot) -> Stream.of( - IntStream.range(CRAFT_SLOT_1, CRAFT_SLOT_4 + 1), // 1-4 - IntStream.range(HELMET_SLOT, BOOTS_SLOT + 1), // 5-8 - IntStream.range(9, 36), // 9-35 - IntStream.range(0, 9), // 36-44 - IntStream.of(OFF_HAND_SLOT) // 45 - ).flatMapToInt(i -> i) - ); - return ContainerInventory.handleClick(this, player, info, processor); + return ContainerInventory.handleClick(this, player, info, ClickProcessors.PLAYER_PROCESSOR); } public @NotNull ItemStack getEquipment(@NotNull EquipmentSlot slot, int heldSlot) { diff --git a/src/main/java/net/minestom/server/inventory/click/ClickProcessors.java b/src/main/java/net/minestom/server/inventory/click/ClickProcessors.java index 3a5ae9d65..bf1da5c67 100644 --- a/src/main/java/net/minestom/server/inventory/click/ClickProcessors.java +++ b/src/main/java/net/minestom/server/inventory/click/ClickProcessors.java @@ -1,18 +1,27 @@ package net.minestom.server.inventory.click; import it.unimi.dsi.fastutil.Pair; +import net.minestom.server.entity.EquipmentSlot; +import net.minestom.server.inventory.InventoryType; +import net.minestom.server.inventory.PlayerInventory; import net.minestom.server.inventory.TransactionOperator; import net.minestom.server.inventory.TransactionType; import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; import net.minestom.server.item.StackingRule; import net.minestom.server.utils.inventory.PlayerInventoryUtils; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.BiFunction; import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static java.util.Map.entry; +import static net.minestom.server.utils.inventory.PlayerInventoryUtils.*; /** * Provides standard implementations of most click functions. @@ -151,14 +160,17 @@ public final class ClickProcessors { * @param shiftClickSlots the shift click slot supplier * @param doubleClickSlots the double click slot supplier */ - public static @NotNull BiFunction standard(@NotNull SlotSuggestor shiftClickSlots, @NotNull SlotSuggestor doubleClickSlots) { + public static ClickProcessors.@NotNull InventoryProcessor standard(@NotNull SlotSuggestor shiftClickSlots, @NotNull SlotSuggestor doubleClickSlots) { return (info, getter) -> switch (info) { case Click.Info.Left(int slot) -> leftClick(slot, getter); case Click.Info.Right(int slot) -> rightClick(slot, getter); case Click.Info.Middle(int slot) -> middleClick(slot, getter); - case Click.Info.LeftShift(int slot) -> shiftClick(slot, shiftClickSlots.getList(getter, getter.get(slot), slot), getter); - case Click.Info.RightShift(int slot) -> shiftClick(slot, shiftClickSlots.getList(getter, getter.get(slot), slot), getter); - case Click.Info.Double(int slot) -> doubleClick(doubleClickSlots.getList(getter, getter.get(slot), slot), getter); + case Click.Info.LeftShift(int slot) -> + shiftClick(slot, shiftClickSlots.getList(getter, getter.get(slot), slot), getter); + case Click.Info.RightShift(int slot) -> + shiftClick(slot, shiftClickSlots.getList(getter, getter.get(slot), slot), getter); + case Click.Info.Double(int slot) -> + doubleClick(doubleClickSlots.getList(getter, getter.get(slot), slot), getter); case Click.Info.LeftDrag(List slots) -> { int cursorAmount = RULE.getAmount(getter.cursor()); int amount = (int) Math.floor(cursorAmount / (double) slots.size()); @@ -166,7 +178,8 @@ public final class ClickProcessors { } case Click.Info.RightDrag(List slots) -> dragClick(1, slots, getter); case Click.Info.MiddleDrag(List slots) -> middleDragClick(slots, getter); - case Click.Info.DropSlot(int slot, boolean all) -> dropFromSlot(slot, all ? RULE.getAmount(getter.get(slot)) : 1, getter); + case Click.Info.DropSlot(int slot, boolean all) -> + dropFromSlot(slot, all ? RULE.getAmount(getter.get(slot)) : 1, getter); case Click.Info.LeftDropCursor() -> dropFromCursor(getter.cursor().amount(), getter); case Click.Info.RightDropCursor() -> dropFromCursor(1, getter); case Click.Info.MiddleDropCursor() -> Click.Result.NOTHING; @@ -185,10 +198,14 @@ public final class ClickProcessors { yield getter.setter().setPlayer(PlayerInventoryUtils.OFF_HAND_SLOT, selectedItem).set(slot, offhandItem).build(); } case Click.Info.CreativeSetItem(int slot, ItemStack item) -> getter.setter().set(slot, item).build(); - case Click.Info.CreativeDropItem(ItemStack item) -> getter.setter().sideEffects(new Click.SideEffect.DropFromPlayer(item)).build(); + case Click.Info.CreativeDropItem(ItemStack item) -> + getter.setter().sideEffects(new Click.SideEffect.DropFromPlayer(item)).build(); }; } + public interface InventoryProcessor extends BiFunction { + } + /** * A generic interface for providing options for clicks like shift clicks and double clicks.
* This addresses the issue of certain click operations only being able to interact with certain slots: for example, @@ -213,4 +230,89 @@ public final class ClickProcessors { return get(builder, item, slot).boxed().toList(); } } + + /** + * Handle player inventory (without any container open). + */ + public static final InventoryProcessor PLAYER_PROCESSOR = ClickProcessors.standard( + (getter, item, slot) -> { + List slots = new ArrayList<>(); + + final EquipmentSlot equipmentSlot = item.material().registry().equipmentSlot(); + if (equipmentSlot != null && slot != equipmentSlot.armorSlot()) { + slots.add(equipmentSlot.armorSlot()); + } + + if (item.material() == Material.SHIELD && slot != OFF_HAND_SLOT) { + slots.add(OFF_HAND_SLOT); + } + + if (slot < 9 || slot > 35) IntStream.range(9, 36).forEach(slots::add); + if (slot < 0 || slot > 8) IntStream.range(0, 9).forEach(slots::add); + + if (slot == CRAFT_RESULT) { + Collections.reverse(slots); + } + + return slots.stream().mapToInt(i -> i); + }, + (getter, item, slot) -> Stream.of( + IntStream.range(CRAFT_SLOT_1, CRAFT_SLOT_4 + 1), // 1-4 + IntStream.range(HELMET_SLOT, BOOTS_SLOT + 1), // 5-8 + IntStream.range(9, 36), // 9-35 + IntStream.range(0, 9), // 36-44 + IntStream.of(OFF_HAND_SLOT) // 45 + ).flatMapToInt(i -> i) + ); + + /** + * Assumes all the container's slots to be accessible. + */ + public static final InventoryProcessor GENERIC_PROCESSOR = ClickProcessors.standard( + (builder, item, slot) -> { + final int size = builder.mainSize(); + return slot >= size ? + IntStream.range(0, size) : + PlayerInventory.getInnerShiftClickSlots(size); + }, + (builder, item, slot) -> { + final int size = builder.mainSize(); + return IntStream.concat( + IntStream.range(0, size), + PlayerInventory.getInnerDoubleClickSlots(size) + ); + }); + + + // SPECIALIZED PROCESSORS DEFINITIONS + + /** + * Client prediction appears to disallow shift clicking into furnace inventories.
+ * Instead: + * - Shift clicks in the inventory go to the player inventory like normal + * - Shift clicks in the hotbar go to the storage + * - Shift clicks in the storage go to the hotbar + */ + public static final InventoryProcessor FURNACE_PROCESSOR = ClickProcessors.standard( + (builder, item, slot) -> { + final int size = builder.mainSize(); + if (slot < size) { + return PlayerInventory.getInnerShiftClickSlots(size); + } else if (slot < size + 27) { + return IntStream.range(27, 36).map(i -> i + size); + } else { + return IntStream.range(0, 27).map(i -> i + size); + } + }, + (builder, item, slot) -> { + final int size = builder.mainSize(); + return IntStream.concat( + IntStream.range(0, size), + PlayerInventory.getInnerDoubleClickSlots(size) + ); + }); + + public static final Map PROCESSORS_MAP = Map.ofEntries( + entry(InventoryType.FURNACE, FURNACE_PROCESSOR) + ); } diff --git a/src/main/java/net/minestom/server/inventory/type/FurnaceInventory.java b/src/main/java/net/minestom/server/inventory/type/FurnaceInventory.java index 92fc59a03..269da3aa9 100644 --- a/src/main/java/net/minestom/server/inventory/type/FurnaceInventory.java +++ b/src/main/java/net/minestom/server/inventory/type/FurnaceInventory.java @@ -1,16 +1,10 @@ package net.minestom.server.inventory.type; import net.kyori.adventure.text.Component; -import net.minestom.server.entity.Player; import net.minestom.server.inventory.ContainerInventory; import net.minestom.server.inventory.InventoryProperty; import net.minestom.server.inventory.InventoryType; -import net.minestom.server.inventory.PlayerInventory; -import net.minestom.server.inventory.click.*; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.stream.IntStream; public class FurnaceInventory extends ContainerInventory { @@ -27,32 +21,6 @@ public class FurnaceInventory extends ContainerInventory { super(InventoryType.FURNACE, title); } - /** - * Client prediction appears to disallow shift clicking into furnace inventories.
- * Instead: - * - Shift clicks in the inventory go to the player inventory like normal - * - Shift clicks in the hotbar go to the storage - * - Shift clicks in the storage go to the hotbar - */ - @Override - public @Nullable Click.Result handleClick(@NotNull Player player, @NotNull Click.Info info) { - var processor = ClickProcessors.standard( - (builder, item, slot) -> { - if (slot < getSize()) { - return PlayerInventory.getInnerShiftClickSlots(getSize()); - } else if (slot < getSize() + 27) { - return IntStream.range(27, 36).map(i -> i + getSize()); - } else { - return IntStream.range(0, 27).map(i -> i + getSize()); - } - }, - (builder, item, slot) -> IntStream.concat( - IntStream.range(0, getSize()), - PlayerInventory.getInnerDoubleClickSlots(getSize()) - )); - return ContainerInventory.handleClick(this, player, info, processor); - } - /** * Represents the amount of tick until the fire icon come empty. *