From 14f250c031b9f9c457033c7182b499eeb549211d Mon Sep 17 00:00:00 2001 From: GoldenStack Date: Tue, 8 Aug 2023 14:58:09 -0500 Subject: [PATCH] Inventory rework (88 squashed commits) --- .../java/net/minestom/demo/PlayerInit.java | 6 +- .../minestom/demo/commands/GiveCommand.java | 4 +- .../minestom/server/entity/EquipmentSlot.java | 11 +- .../net/minestom/server/entity/Player.java | 101 ++-- .../inventory/InventoryButtonClickEvent.java | 48 ++ .../event/inventory/InventoryClickEvent.java | 86 ++-- .../event/inventory/InventoryCloseEvent.java | 9 +- .../inventory/InventoryItemChangeEvent.java | 11 +- .../event/inventory/InventoryOpenEvent.java | 14 +- .../inventory/InventoryPostClickEvent.java | 60 +++ .../inventory/InventoryPreClickEvent.java | 87 +--- .../PlayerInventoryItemChangeEvent.java | 31 -- .../event/player/PlayerSwapItemEvent.java | 78 --- .../server/event/trait/InventoryEvent.java | 6 +- .../server/inventory/AbstractInventory.java | 258 ---------- .../server/inventory/ContainerInventory.java | 172 +++++++ .../server/inventory/EquipmentHandler.java | 11 +- .../minestom/server/inventory/Inventory.java | 426 ++++------------ .../inventory/InventoryClickHandler.java | 83 --- .../server/inventory/InventoryImpl.java | 298 +++++++++++ .../server/inventory/InventoryType.java | 2 +- .../server/inventory/PlayerInventory.java | 441 +++++++--------- .../server/inventory/TransactionOperator.java | 116 +++++ .../server/inventory/TransactionOption.java | 34 +- .../server/inventory/TransactionType.java | 160 +++--- .../server/inventory/click/Click.java | 280 +++++++++++ .../inventory/click/ClickProcessors.java | 219 ++++++++ .../server/inventory/click/ClickType.java | 26 - .../click/InventoryClickProcessor.java | 476 ------------------ .../inventory/click/InventoryClickResult.java | 43 -- .../condition/InventoryCondition.java | 25 - .../condition/InventoryConditionResult.java | 41 -- .../server/inventory/type/AnvilInventory.java | 4 +- .../inventory/type/BeaconInventory.java | 4 +- .../inventory/type/BrewingStandInventory.java | 4 +- .../type/EnchantmentTableInventory.java | 4 +- .../inventory/type/FurnaceInventory.java | 36 +- .../inventory/type/VillagerInventory.java | 12 +- .../net/minestom/server/item/ItemStack.java | 3 +- .../listener/BlockPlacementListener.java | 2 +- .../server/listener/BookListener.java | 6 +- .../CreativeInventoryActionListener.java | 33 +- .../listener/PlayerDiggingListener.java | 51 +- .../server/listener/UseItemListener.java | 9 +- .../server/listener/WindowListener.java | 105 +--- .../manager/PacketListenerManager.java | 1 + .../utils/inventory/PlayerInventoryUtils.java | 141 +++--- .../inventory/InventoryCloseStateTest.java | 2 +- .../inventory/InventoryIntegrationTest.java | 44 +- .../server/inventory/InventoryTest.java | 10 +- .../inventory/PlayerCreativeSlotTest.java | 4 +- .../PlayerInventoryIntegrationTest.java | 6 +- .../inventory/PlayerSlotConversionTest.java | 60 --- .../click/ClickPreprocessorTest.java | 96 ++++ .../server/inventory/click/ClickUtils.java | 84 ++++ .../integration/HeldClickIntegrationTest.java | 190 ------- .../integration/LeftClickIntegrationTest.java | 173 ------- .../RightClickIntegrationTest.java | 194 ------- .../type/InventoryCreativeDropItemTest.java | 33 ++ .../type/InventoryCreativeSetItemTest.java | 33 ++ .../click/type/InventoryDoubleClickTest.java | 91 ++++ .../click/type/InventoryDropCursorTest.java | 51 ++ .../click/type/InventoryDropSlotTest.java | 41 ++ .../click/type/InventoryHotbarSwapTest.java | 33 ++ .../click/type/InventoryLeftClickTest.java | 49 ++ .../click/type/InventoryLeftDragTest.java | 102 ++++ .../click/type/InventoryMiddleClickTest.java | 40 ++ .../click/type/InventoryMiddleDragTest.java | 50 ++ .../click/type/InventoryOffhandSwapTest.java | 32 ++ .../click/type/InventoryRightClickTest.java | 67 +++ .../click/type/InventoryRightDragTest.java | 77 +++ .../click/type/InventoryShiftClickTest.java | 149 ++++++ 72 files changed, 2916 insertions(+), 2873 deletions(-) create mode 100644 src/main/java/net/minestom/server/event/inventory/InventoryButtonClickEvent.java create mode 100644 src/main/java/net/minestom/server/event/inventory/InventoryPostClickEvent.java delete mode 100644 src/main/java/net/minestom/server/event/inventory/PlayerInventoryItemChangeEvent.java delete mode 100644 src/main/java/net/minestom/server/event/player/PlayerSwapItemEvent.java delete mode 100644 src/main/java/net/minestom/server/inventory/AbstractInventory.java create mode 100644 src/main/java/net/minestom/server/inventory/ContainerInventory.java delete mode 100644 src/main/java/net/minestom/server/inventory/InventoryClickHandler.java create mode 100644 src/main/java/net/minestom/server/inventory/InventoryImpl.java create mode 100644 src/main/java/net/minestom/server/inventory/TransactionOperator.java create mode 100644 src/main/java/net/minestom/server/inventory/click/Click.java create mode 100644 src/main/java/net/minestom/server/inventory/click/ClickProcessors.java delete mode 100644 src/main/java/net/minestom/server/inventory/click/ClickType.java delete mode 100644 src/main/java/net/minestom/server/inventory/click/InventoryClickProcessor.java delete mode 100644 src/main/java/net/minestom/server/inventory/click/InventoryClickResult.java delete mode 100644 src/main/java/net/minestom/server/inventory/condition/InventoryCondition.java delete mode 100644 src/main/java/net/minestom/server/inventory/condition/InventoryConditionResult.java delete mode 100644 src/test/java/net/minestom/server/inventory/PlayerSlotConversionTest.java create mode 100644 src/test/java/net/minestom/server/inventory/click/ClickPreprocessorTest.java create mode 100644 src/test/java/net/minestom/server/inventory/click/ClickUtils.java delete mode 100644 src/test/java/net/minestom/server/inventory/click/integration/HeldClickIntegrationTest.java delete mode 100644 src/test/java/net/minestom/server/inventory/click/integration/LeftClickIntegrationTest.java delete mode 100644 src/test/java/net/minestom/server/inventory/click/integration/RightClickIntegrationTest.java create mode 100644 src/test/java/net/minestom/server/inventory/click/type/InventoryCreativeDropItemTest.java create mode 100644 src/test/java/net/minestom/server/inventory/click/type/InventoryCreativeSetItemTest.java create mode 100644 src/test/java/net/minestom/server/inventory/click/type/InventoryDoubleClickTest.java create mode 100644 src/test/java/net/minestom/server/inventory/click/type/InventoryDropCursorTest.java create mode 100644 src/test/java/net/minestom/server/inventory/click/type/InventoryDropSlotTest.java create mode 100644 src/test/java/net/minestom/server/inventory/click/type/InventoryHotbarSwapTest.java create mode 100644 src/test/java/net/minestom/server/inventory/click/type/InventoryLeftClickTest.java create mode 100644 src/test/java/net/minestom/server/inventory/click/type/InventoryLeftDragTest.java create mode 100644 src/test/java/net/minestom/server/inventory/click/type/InventoryMiddleClickTest.java create mode 100644 src/test/java/net/minestom/server/inventory/click/type/InventoryMiddleDragTest.java create mode 100644 src/test/java/net/minestom/server/inventory/click/type/InventoryOffhandSwapTest.java create mode 100644 src/test/java/net/minestom/server/inventory/click/type/InventoryRightClickTest.java create mode 100644 src/test/java/net/minestom/server/inventory/click/type/InventoryRightDragTest.java create mode 100644 src/test/java/net/minestom/server/inventory/click/type/InventoryShiftClickTest.java diff --git a/demo/src/main/java/net/minestom/demo/PlayerInit.java b/demo/src/main/java/net/minestom/demo/PlayerInit.java index 81974b749..417fa6276 100644 --- a/demo/src/main/java/net/minestom/demo/PlayerInit.java +++ b/demo/src/main/java/net/minestom/demo/PlayerInit.java @@ -26,7 +26,7 @@ import net.minestom.server.instance.InstanceContainer; import net.minestom.server.instance.InstanceManager; import net.minestom.server.instance.LightingChunk; import net.minestom.server.instance.block.Block; -import net.minestom.server.inventory.Inventory; +import net.minestom.server.inventory.ContainerInventory; import net.minestom.server.inventory.InventoryType; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; @@ -45,7 +45,7 @@ import java.util.concurrent.atomic.AtomicReference; public class PlayerInit { - private static final Inventory inventory; + private static final ContainerInventory inventory; private static final EventNode DEMO_NODE = EventNode.all("demo") .addListener(EntityAttackEvent.class, event -> { @@ -187,7 +187,7 @@ public class PlayerInit { // System.out.println("light end"); // }); - inventory = new Inventory(InventoryType.CHEST_1_ROW, Component.text("Test inventory")); + inventory = new ContainerInventory(InventoryType.CHEST_1_ROW, Component.text("Test inventory")); inventory.setItemStack(3, ItemStack.of(Material.DIAMOND, 34)); } diff --git a/demo/src/main/java/net/minestom/demo/commands/GiveCommand.java b/demo/src/main/java/net/minestom/demo/commands/GiveCommand.java index 10a43bd07..97f5330eb 100644 --- a/demo/src/main/java/net/minestom/demo/commands/GiveCommand.java +++ b/demo/src/main/java/net/minestom/demo/commands/GiveCommand.java @@ -4,10 +4,10 @@ import net.kyori.adventure.text.Component; import net.minestom.server.command.builder.Command; import net.minestom.server.entity.Entity; import net.minestom.server.entity.Player; -import net.minestom.server.inventory.PlayerInventory; import net.minestom.server.inventory.TransactionOption; import net.minestom.server.item.ItemStack; import net.minestom.server.utils.entity.EntityFinder; +import net.minestom.server.utils.inventory.PlayerInventoryUtils; import java.util.ArrayList; import java.util.List; @@ -25,7 +25,7 @@ public class GiveCommand extends Command { addSyntax((sender, context) -> { final EntityFinder entityFinder = context.get("target"); int count = context.get("count"); - count = Math.min(count, PlayerInventory.INVENTORY_SIZE * 64); + count = Math.min(count, PlayerInventoryUtils.INVENTORY_SIZE * 64); ItemStack itemStack = context.get("item"); List itemStacks; diff --git a/src/main/java/net/minestom/server/entity/EquipmentSlot.java b/src/main/java/net/minestom/server/entity/EquipmentSlot.java index 2fa5bc189..1ec135bcb 100644 --- a/src/main/java/net/minestom/server/entity/EquipmentSlot.java +++ b/src/main/java/net/minestom/server/entity/EquipmentSlot.java @@ -1,19 +1,18 @@ package net.minestom.server.entity; import net.minestom.server.item.attribute.AttributeSlot; +import net.minestom.server.utils.inventory.PlayerInventoryUtils; import org.jetbrains.annotations.NotNull; import java.util.List; -import static net.minestom.server.utils.inventory.PlayerInventoryUtils.*; - public enum EquipmentSlot { MAIN_HAND(false, -1), OFF_HAND(false, -1), - BOOTS(true, BOOTS_SLOT), - LEGGINGS(true, LEGGINGS_SLOT), - CHESTPLATE(true, CHESTPLATE_SLOT), - HELMET(true, HELMET_SLOT); + BOOTS(true, PlayerInventoryUtils.BOOTS_SLOT), + LEGGINGS(true, PlayerInventoryUtils.LEGGINGS_SLOT), + CHESTPLATE(true, PlayerInventoryUtils.CHESTPLATE_SLOT), + HELMET(true, PlayerInventoryUtils.HELMET_SLOT); private static final List ARMORS = List.of(BOOTS, LEGGINGS, CHESTPLATE, HELMET); diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index b9df6b5c2..bd2113b50 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -45,9 +45,10 @@ import net.minestom.server.event.player.*; import net.minestom.server.instance.Chunk; import net.minestom.server.instance.EntityTracker; import net.minestom.server.instance.Instance; -import net.minestom.server.instance.block.Block; import net.minestom.server.inventory.Inventory; +import net.minestom.server.instance.block.Block; import net.minestom.server.inventory.PlayerInventory; +import net.minestom.server.inventory.click.Click; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.item.metadata.WrittenBookMeta; @@ -178,6 +179,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable, private int level; private int portalCooldown = 0; + protected Click.Preprocessor clickPreprocessor = new Click.Preprocessor(); protected PlayerInventory inventory; private Inventory openInventory; // Used internally to allow the closing of inventory within the inventory listener @@ -239,7 +241,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable, setRespawnPoint(Pos.ZERO); this.settings = new PlayerSettings(); - this.inventory = new PlayerInventory(this); + this.inventory = new PlayerInventory(); setCanPickupItem(true); // By default @@ -370,6 +372,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable, refreshHealth(); // Heal and send health packet refreshAbilities(); // Send abilities packet + inventory.addViewer(this); + return setInstance(spawnInstance); } @@ -1008,11 +1012,11 @@ public class Player extends LivingEntity implements CommandSender, Localizable, .pages(book.pages())) .build(); // Set book in offhand - sendPacket(new SetSlotPacket((byte) 0, 0, (short) PlayerInventoryUtils.OFFHAND_SLOT, writtenBook)); + sendPacket(new SetSlotPacket((byte) 0, 0, (short) PlayerInventoryUtils.OFF_HAND_SLOT, writtenBook)); // Open the book sendPacket(new OpenBookPacket(Hand.OFF)); // Restore the item in offhand - sendPacket(new SetSlotPacket((byte) 0, 0, (short) PlayerInventoryUtils.OFFHAND_SLOT, getItemInOffHand())); + sendPacket(new SetSlotPacket((byte) 0, 0, (short) PlayerInventoryUtils.OFF_HAND_SLOT, getItemInOffHand())); } @Override @@ -1717,6 +1721,10 @@ public class Player extends LivingEntity implements CommandSender, Localizable, this.belowNameTag = belowNameTag; } + public @NotNull Click.Preprocessor clickPreprocessor() { + return clickPreprocessor; + } + /** * Gets the player open inventory. * @@ -1726,6 +1734,19 @@ public class Player extends LivingEntity implements CommandSender, Localizable, return openInventory; } + private void tryCloseInventory(boolean fromClient) { + var closedInventory = getOpenInventory(); + if (closedInventory == null) return; + + didCloseInventory = fromClient; + + if (closedInventory.removeViewer(this)) { + if (closedInventory == getOpenInventory()) { + this.openInventory = null; + } + } + } + /** * Opens the specified Inventory, close the previous inventory if existing. * @@ -1736,21 +1757,12 @@ public class Player extends LivingEntity implements CommandSender, Localizable, InventoryOpenEvent inventoryOpenEvent = new InventoryOpenEvent(inventory, this); EventDispatcher.callCancellable(inventoryOpenEvent, () -> { - Inventory openInventory = getOpenInventory(); - if (openInventory != null) { - openInventory.removeViewer(this); - } + tryCloseInventory(false); Inventory newInventory = inventoryOpenEvent.getInventory(); - if (newInventory == null) { - // just close the inventory - return; + if (newInventory.addViewer(this)) { + this.openInventory = newInventory; } - - sendPacket(new OpenWindowPacket(newInventory.getWindowId(), - newInventory.getInventoryType().getWindowType(), newInventory.getTitle())); - newInventory.addViewer(this); - this.openInventory = newInventory; }); return !inventoryOpenEvent.isCancelled(); } @@ -1765,37 +1777,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable, @ApiStatus.Internal public void closeInventory(boolean fromClient) { - Inventory openInventory = getOpenInventory(); - - // Drop cursor item when closing inventory - ItemStack cursorItem; - if (openInventory == null) { - cursorItem = getInventory().getCursorItem(); - getInventory().setCursorItem(ItemStack.AIR); - } else { - cursorItem = openInventory.getCursorItem(this); - openInventory.setCursorItem(this, ItemStack.AIR); - } - if (!cursorItem.isAir()) { - // Add item to inventory if he hasn't been able to drop it - if (!dropItem(cursorItem)) { - getInventory().addItemStack(cursorItem); - } - } - - if (openInventory == getOpenInventory()) { - CloseWindowPacket closeWindowPacket; - if (openInventory == null) { - closeWindowPacket = new CloseWindowPacket((byte) 0); - } else { - closeWindowPacket = new CloseWindowPacket(openInventory.getWindowId()); - openInventory.removeViewer(this); // Clear cache - this.openInventory = null; - } - if (!fromClient) sendPacket(closeWindowPacket); - inventory.update(); - this.didCloseInventory = true; - } + tryCloseInventory(fromClient); + inventory.update(); } /** @@ -2304,62 +2287,62 @@ public class Player extends LivingEntity implements CommandSender, Localizable, @Override public @NotNull ItemStack getItemInMainHand() { - return inventory.getItemInMainHand(); + return inventory.getEquipment(EquipmentSlot.MAIN_HAND, getHeldSlot()); } @Override public void setItemInMainHand(@NotNull ItemStack itemStack) { - inventory.setItemInMainHand(itemStack); + inventory.setEquipment(EquipmentSlot.MAIN_HAND, getHeldSlot(), itemStack); } @Override public @NotNull ItemStack getItemInOffHand() { - return inventory.getItemInOffHand(); + return inventory.getEquipment(EquipmentSlot.OFF_HAND, getHeldSlot()); } @Override public void setItemInOffHand(@NotNull ItemStack itemStack) { - inventory.setItemInOffHand(itemStack); + inventory.setEquipment(EquipmentSlot.OFF_HAND, getHeldSlot(), itemStack); } @Override public @NotNull ItemStack getHelmet() { - return inventory.getHelmet(); + return inventory.getEquipment(EquipmentSlot.HELMET, getHeldSlot()); } @Override public void setHelmet(@NotNull ItemStack itemStack) { - inventory.setHelmet(itemStack); + inventory.setEquipment(EquipmentSlot.HELMET, getHeldSlot(), itemStack); } @Override public @NotNull ItemStack getChestplate() { - return inventory.getChestplate(); + return inventory.getEquipment(EquipmentSlot.CHESTPLATE, getHeldSlot()); } @Override public void setChestplate(@NotNull ItemStack itemStack) { - inventory.setChestplate(itemStack); + inventory.setEquipment(EquipmentSlot.CHESTPLATE, getHeldSlot(), itemStack); } @Override public @NotNull ItemStack getLeggings() { - return inventory.getLeggings(); + return inventory.getEquipment(EquipmentSlot.LEGGINGS, getHeldSlot()); } @Override public void setLeggings(@NotNull ItemStack itemStack) { - inventory.setLeggings(itemStack); + inventory.setEquipment(EquipmentSlot.LEGGINGS, getHeldSlot(), itemStack); } @Override public @NotNull ItemStack getBoots() { - return inventory.getBoots(); + return inventory.getEquipment(EquipmentSlot.BOOTS, getHeldSlot()); } @Override public void setBoots(@NotNull ItemStack itemStack) { - inventory.setBoots(itemStack); + inventory.setEquipment(EquipmentSlot.BOOTS, getHeldSlot(), itemStack); } @Override diff --git a/src/main/java/net/minestom/server/event/inventory/InventoryButtonClickEvent.java b/src/main/java/net/minestom/server/event/inventory/InventoryButtonClickEvent.java new file mode 100644 index 000000000..a249a4366 --- /dev/null +++ b/src/main/java/net/minestom/server/event/inventory/InventoryButtonClickEvent.java @@ -0,0 +1,48 @@ +package net.minestom.server.event.inventory; + +import net.minestom.server.entity.Player; +import net.minestom.server.event.trait.InventoryEvent; +import net.minestom.server.event.trait.PlayerInstanceEvent; +import net.minestom.server.inventory.Inventory; +import org.jetbrains.annotations.NotNull; + +/** + * Called when a player clicks an inventory button. + * See wiki.vg for slot number details. + */ +public class InventoryButtonClickEvent implements InventoryEvent, PlayerInstanceEvent { + + private final Inventory inventory; + private final Player player; + private final byte button; + + public InventoryButtonClickEvent(@NotNull Inventory inventory, @NotNull Player player, byte button) { + this.inventory = inventory; + this.player = player; + this.button = button; + } + + /** + * Gets the player who clicked the button in the inventory. + * + * @return the player who clicked + */ + @Override + public @NotNull Player getPlayer() { + return player; + } + + @NotNull + @Override + public Inventory getInventory() { + return inventory; + } + + /** + * Gets the inventory button number that the player clicked. This is different from inventory slots. + * @return the button clicked by the player + */ + public byte getButton() { + return button; + } +} diff --git a/src/main/java/net/minestom/server/event/inventory/InventoryClickEvent.java b/src/main/java/net/minestom/server/event/inventory/InventoryClickEvent.java index aa8a89366..ca47f20d1 100644 --- a/src/main/java/net/minestom/server/event/inventory/InventoryClickEvent.java +++ b/src/main/java/net/minestom/server/event/inventory/InventoryClickEvent.java @@ -1,89 +1,93 @@ package net.minestom.server.event.inventory; import net.minestom.server.entity.Player; +import net.minestom.server.event.trait.CancellableEvent; import net.minestom.server.event.trait.InventoryEvent; import net.minestom.server.event.trait.PlayerInstanceEvent; import net.minestom.server.inventory.Inventory; -import net.minestom.server.inventory.click.ClickType; -import net.minestom.server.item.ItemStack; +import net.minestom.server.inventory.PlayerInventory; +import net.minestom.server.inventory.click.Click; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; /** - * Called after {@link InventoryPreClickEvent}, this event cannot be cancelled and items related to the click - * are already moved. + * Called after {@link InventoryPreClickEvent} and before {@link InventoryPostClickEvent}. */ -public class InventoryClickEvent implements InventoryEvent, PlayerInstanceEvent { +public class InventoryClickEvent implements InventoryEvent, PlayerInstanceEvent, CancellableEvent { + private final PlayerInventory playerInventory; private final Inventory inventory; private final Player player; - private final int slot; - private final ClickType clickType; - private final ItemStack clickedItem; - private final ItemStack cursorItem; + private final Click.Info info; + private Click.Result changes; - public InventoryClickEvent(@Nullable Inventory inventory, @NotNull Player player, - int slot, @NotNull ClickType clickType, - @NotNull ItemStack clicked, @NotNull ItemStack cursor) { + private boolean cancelled; + + public InventoryClickEvent(@NotNull PlayerInventory playerInventory, @NotNull Inventory inventory, + @NotNull Player player, @NotNull Click.Info info, @NotNull Click.Result changes) { + this.playerInventory = playerInventory; this.inventory = inventory; this.player = player; - this.slot = slot; - this.clickType = clickType; - this.clickedItem = clicked; - this.cursorItem = cursor; + this.info = info; + this.changes = changes; } /** - * Gets the player who clicked in the inventory. + * Gets the player who is trying to click on the inventory. * - * @return the player who clicked in the inventory + * @return the player who clicked */ - @NotNull - public Player getPlayer() { + public @NotNull Player getPlayer() { return player; } /** - * Gets the clicked slot number. + * Gets the info about the click that occurred. This is enough to fully describe the click. * - * @return the clicked slot number + * @return the click info */ - public int getSlot() { - return slot; + public @NotNull Click.Info getClickInfo() { + return info; } /** - * Gets the click type. + * Gets the changes that will occur as a result of this click. * - * @return the click type + * @return the changes */ - @NotNull - public ClickType getClickType() { - return clickType; + public @NotNull Click.Result getChanges() { + return changes; } /** - * Gets the clicked item. + * Updates the changes that will occur as a result of this click. * - * @return the clicked item + * @param changes the new results */ - @NotNull - public ItemStack getClickedItem() { - return clickedItem; + public void setChanges(@NotNull Click.Result changes) { + this.changes = changes; } /** - * Gets the item in the player cursor. + * Gets the player inventory that was involved with the click. * - * @return the cursor item + * @return the player inventory */ - @NotNull - public ItemStack getCursorItem() { - return cursorItem; + public @NotNull PlayerInventory getPlayerInventory() { + return playerInventory; } @Override - public @Nullable Inventory getInventory() { + public @NotNull Inventory getInventory() { return inventory; } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } } diff --git a/src/main/java/net/minestom/server/event/inventory/InventoryCloseEvent.java b/src/main/java/net/minestom/server/event/inventory/InventoryCloseEvent.java index 9099c7dc4..2225d2313 100644 --- a/src/main/java/net/minestom/server/event/inventory/InventoryCloseEvent.java +++ b/src/main/java/net/minestom/server/event/inventory/InventoryCloseEvent.java @@ -14,9 +14,9 @@ public class InventoryCloseEvent implements InventoryEvent, PlayerInstanceEvent private final Inventory inventory; private final Player player; - private Inventory newInventory; + private @Nullable Inventory newInventory; - public InventoryCloseEvent(@Nullable Inventory inventory, @NotNull Player player) { + public InventoryCloseEvent(@NotNull Inventory inventory, @NotNull Player player) { this.inventory = inventory; this.player = player; } @@ -36,8 +36,7 @@ public class InventoryCloseEvent implements InventoryEvent, PlayerInstanceEvent * * @return the new inventory to open, null if there isn't any */ - @Nullable - public Inventory getNewInventory() { + public @Nullable Inventory getNewInventory() { return newInventory; } @@ -51,7 +50,7 @@ public class InventoryCloseEvent implements InventoryEvent, PlayerInstanceEvent } @Override - public @Nullable Inventory getInventory() { + public @NotNull Inventory getInventory() { return inventory; } } diff --git a/src/main/java/net/minestom/server/event/inventory/InventoryItemChangeEvent.java b/src/main/java/net/minestom/server/event/inventory/InventoryItemChangeEvent.java index 0629dd505..0df419e80 100644 --- a/src/main/java/net/minestom/server/event/inventory/InventoryItemChangeEvent.java +++ b/src/main/java/net/minestom/server/event/inventory/InventoryItemChangeEvent.java @@ -2,19 +2,14 @@ package net.minestom.server.event.inventory; import net.minestom.server.event.trait.InventoryEvent; import net.minestom.server.event.trait.RecursiveEvent; -import net.minestom.server.inventory.AbstractInventory; import net.minestom.server.inventory.Inventory; import net.minestom.server.item.ItemStack; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; /** - * Called when {@link AbstractInventory#safeItemInsert(int, ItemStack)} is being invoked. + * Called when a slot was changed in an inventory. * This event cannot be cancelled and items related to the change are already moved. - * - * @see PlayerInventoryItemChangeEvent */ -@SuppressWarnings("JavadocReference") public class InventoryItemChangeEvent implements InventoryEvent, RecursiveEvent { private final Inventory inventory; @@ -22,7 +17,7 @@ public class InventoryItemChangeEvent implements InventoryEvent, RecursiveEvent private final ItemStack previousItem; private final ItemStack newItem; - public InventoryItemChangeEvent(@Nullable Inventory inventory, int slot, + public InventoryItemChangeEvent(@NotNull Inventory inventory, int slot, @NotNull ItemStack previousItem, @NotNull ItemStack newItem) { this.inventory = inventory; this.slot = slot; @@ -58,7 +53,7 @@ public class InventoryItemChangeEvent implements InventoryEvent, RecursiveEvent } @Override - public @Nullable Inventory getInventory() { + public @NotNull Inventory getInventory() { return inventory; } } diff --git a/src/main/java/net/minestom/server/event/inventory/InventoryOpenEvent.java b/src/main/java/net/minestom/server/event/inventory/InventoryOpenEvent.java index bb50abd4a..3f4f73e7e 100644 --- a/src/main/java/net/minestom/server/event/inventory/InventoryOpenEvent.java +++ b/src/main/java/net/minestom/server/event/inventory/InventoryOpenEvent.java @@ -6,7 +6,6 @@ import net.minestom.server.event.trait.InventoryEvent; import net.minestom.server.event.trait.PlayerInstanceEvent; import net.minestom.server.inventory.Inventory; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; /** * Called when a player open an {@link Inventory}. @@ -15,12 +14,12 @@ import org.jetbrains.annotations.Nullable; */ public class InventoryOpenEvent implements InventoryEvent, PlayerInstanceEvent, CancellableEvent { - private Inventory inventory; private final Player player; + private Inventory inventory; private boolean cancelled; - public InventoryOpenEvent(@Nullable Inventory inventory, @NotNull Player player) { + public InventoryOpenEvent(@NotNull Inventory inventory, @NotNull Player player) { this.inventory = inventory; this.player = player; } @@ -36,13 +35,12 @@ public class InventoryOpenEvent implements InventoryEvent, PlayerInstanceEvent, } /** - * Gets the inventory to open, this could have been change by the {@link #setInventory(Inventory)}. + * Gets the inventory to open. * - * @return the inventory to open, null to just close the current inventory if any + * @return the inventory to open */ - @Nullable @Override - public Inventory getInventory() { + public @NotNull Inventory getInventory() { return inventory; } @@ -53,7 +51,7 @@ public class InventoryOpenEvent implements InventoryEvent, PlayerInstanceEvent, * * @param inventory the inventory to open */ - public void setInventory(@Nullable Inventory inventory) { + public void setInventory(@NotNull Inventory inventory) { this.inventory = inventory; } diff --git a/src/main/java/net/minestom/server/event/inventory/InventoryPostClickEvent.java b/src/main/java/net/minestom/server/event/inventory/InventoryPostClickEvent.java new file mode 100644 index 000000000..23a29fc2e --- /dev/null +++ b/src/main/java/net/minestom/server/event/inventory/InventoryPostClickEvent.java @@ -0,0 +1,60 @@ +package net.minestom.server.event.inventory; + +import net.minestom.server.entity.Player; +import net.minestom.server.event.trait.InventoryEvent; +import net.minestom.server.event.trait.PlayerInstanceEvent; +import net.minestom.server.inventory.Inventory; +import net.minestom.server.inventory.click.Click; +import org.jetbrains.annotations.NotNull; + +/** + * Called after {@link InventoryClickEvent}, this event cannot be cancelled and items related to the click + * are already moved. + */ +public class InventoryPostClickEvent implements InventoryEvent, PlayerInstanceEvent { + + private final Player player; + private final Inventory inventory; + private final Click.Info info; + private final Click.Result changes; + + public InventoryPostClickEvent(@NotNull Player player, @NotNull Inventory inventory, @NotNull Click.Info info, @NotNull Click.Result changes) { + this.player = player; + this.inventory = inventory; + this.info = info; + this.changes = changes; + } + + /** + * Gets the player who clicked in the inventory. + * + * @return the player who clicked in the inventory + */ + @NotNull + public Player getPlayer() { + return player; + } + + /** + * Gets the info about the click that was already processed. + * + * @return the click info + */ + public @NotNull Click.Info getClickInfo() { + return info; + } + + /** + * Gets the changes that occurred as a result of this click. + * + * @return the changes + */ + public @NotNull Click.Result getChanges() { + return changes; + } + + @Override + public @NotNull Inventory getInventory() { + return inventory; + } +} diff --git a/src/main/java/net/minestom/server/event/inventory/InventoryPreClickEvent.java b/src/main/java/net/minestom/server/event/inventory/InventoryPreClickEvent.java index 85fe79902..c9d2d56b7 100644 --- a/src/main/java/net/minestom/server/event/inventory/InventoryPreClickEvent.java +++ b/src/main/java/net/minestom/server/event/inventory/InventoryPreClickEvent.java @@ -5,35 +5,28 @@ import net.minestom.server.event.trait.CancellableEvent; import net.minestom.server.event.trait.InventoryEvent; import net.minestom.server.event.trait.PlayerInstanceEvent; import net.minestom.server.inventory.Inventory; -import net.minestom.server.inventory.click.ClickType; -import net.minestom.server.item.ItemStack; +import net.minestom.server.inventory.PlayerInventory; +import net.minestom.server.inventory.click.Click; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; /** * Called before {@link InventoryClickEvent}, used to potentially cancel the click. */ public class InventoryPreClickEvent implements InventoryEvent, PlayerInstanceEvent, CancellableEvent { + private final PlayerInventory playerInventory; private final Inventory inventory; private final Player player; - private final int slot; - private final ClickType clickType; - private ItemStack clickedItem; - private ItemStack cursorItem; + private Click.Info info; private boolean cancelled; - public InventoryPreClickEvent(@Nullable Inventory inventory, - @NotNull Player player, - int slot, @NotNull ClickType clickType, - @NotNull ItemStack clicked, @NotNull ItemStack cursor) { + public InventoryPreClickEvent(@NotNull PlayerInventory playerInventory, @NotNull Inventory inventory, + @NotNull Player player, @NotNull Click.Info info) { + this.playerInventory = playerInventory; this.inventory = inventory; this.player = player; - this.slot = slot; - this.clickType = clickType; - this.clickedItem = clicked; - this.cursorItem = cursor; + this.info = info; } /** @@ -41,66 +34,41 @@ public class InventoryPreClickEvent implements InventoryEvent, PlayerInstanceEve * * @return the player who clicked */ - @NotNull - public Player getPlayer() { + public @NotNull Player getPlayer() { return player; } /** - * Gets the clicked slot number. + * Gets the info about the click that occurred. This is enough to fully describe the click. * - * @return the clicked slot number + * @return the click info */ - public int getSlot() { - return slot; + public @NotNull Click.Info getClickInfo() { + return info; } /** - * Gets the click type. + * Updates the information about the click that occurred. This completely overrides the previous click, but it may + * require the inventory to be updated. * - * @return the click type + * @param info the new click info */ - @NotNull - public ClickType getClickType() { - return clickType; + public void setClickInfo(@NotNull Click.Info info) { + this.info = info; } /** - * Gets the item who have been clicked. + * Gets the player inventory that was involved with the click. * - * @return the clicked item + * @return the player inventory */ - @NotNull - public ItemStack getClickedItem() { - return clickedItem; + public @NotNull PlayerInventory getPlayerInventory() { + return playerInventory; } - /** - * Changes the clicked item. - * - * @param clickedItem the clicked item - */ - public void setClickedItem(@NotNull ItemStack clickedItem) { - this.clickedItem = clickedItem; - } - - /** - * Gets the item who was in the player cursor. - * - * @return the cursor item - */ - @NotNull - public ItemStack getCursorItem() { - return cursorItem; - } - - /** - * Changes the cursor item. - * - * @param cursorItem the cursor item - */ - public void setCursorItem(@NotNull ItemStack cursorItem) { - this.cursorItem = cursorItem; + @Override + public @NotNull Inventory getInventory() { + return inventory; } @Override @@ -112,9 +80,4 @@ public class InventoryPreClickEvent implements InventoryEvent, PlayerInstanceEve public void setCancelled(boolean cancel) { this.cancelled = cancel; } - - @Override - public @Nullable Inventory getInventory() { - return inventory; - } } diff --git a/src/main/java/net/minestom/server/event/inventory/PlayerInventoryItemChangeEvent.java b/src/main/java/net/minestom/server/event/inventory/PlayerInventoryItemChangeEvent.java deleted file mode 100644 index c73990c80..000000000 --- a/src/main/java/net/minestom/server/event/inventory/PlayerInventoryItemChangeEvent.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.minestom.server.event.inventory; - -import net.minestom.server.entity.Player; -import net.minestom.server.event.trait.PlayerInstanceEvent; -import net.minestom.server.inventory.AbstractInventory; -import net.minestom.server.inventory.PlayerInventory; -import net.minestom.server.item.ItemStack; -import org.jetbrains.annotations.NotNull; - -/** - * Called when {@link AbstractInventory#safeItemInsert(int, ItemStack)} is being invoked on a {@link PlayerInventory}. - * This event cannot be cancelled and items related to the change are already moved. - *

- * When this event is being called, {@link InventoryItemChangeEvent} listeners will also be triggered, so you can - * listen only for an ancestor event and check whether it is an instance of that class. - */ -@SuppressWarnings("JavadocReference") -public class PlayerInventoryItemChangeEvent extends InventoryItemChangeEvent implements PlayerInstanceEvent { - - private final Player player; - - public PlayerInventoryItemChangeEvent(@NotNull Player player, int slot, @NotNull ItemStack previousItem, @NotNull ItemStack newItem) { - super(null, slot, previousItem, newItem); - this.player = player; - } - - @Override - public @NotNull Player getPlayer() { - return player; - } -} diff --git a/src/main/java/net/minestom/server/event/player/PlayerSwapItemEvent.java b/src/main/java/net/minestom/server/event/player/PlayerSwapItemEvent.java deleted file mode 100644 index 477bb74fd..000000000 --- a/src/main/java/net/minestom/server/event/player/PlayerSwapItemEvent.java +++ /dev/null @@ -1,78 +0,0 @@ -package net.minestom.server.event.player; - -import net.minestom.server.entity.Player; -import net.minestom.server.event.trait.CancellableEvent; -import net.minestom.server.event.trait.PlayerInstanceEvent; -import net.minestom.server.item.ItemStack; -import org.jetbrains.annotations.NotNull; - -/** - * Called when a player is trying to swap his main and off hand item. - */ -public class PlayerSwapItemEvent implements PlayerInstanceEvent, CancellableEvent { - - private final Player player; - private ItemStack mainHandItem; - private ItemStack offHandItem; - - private boolean cancelled; - - public PlayerSwapItemEvent(@NotNull Player player, @NotNull ItemStack mainHandItem, @NotNull ItemStack offHandItem) { - this.player = player; - this.mainHandItem = mainHandItem; - this.offHandItem = offHandItem; - } - - /** - * Gets the item which will be in player main hand after the event. - * - * @return the item in main hand - */ - @NotNull - public ItemStack getMainHandItem() { - return mainHandItem; - } - - /** - * Changes the item which will be in the player main hand. - * - * @param mainHandItem the main hand item - */ - public void setMainHandItem(@NotNull ItemStack mainHandItem) { - this.mainHandItem = mainHandItem; - } - - /** - * Gets the item which will be in player off hand after the event. - * - * @return the item in off hand - */ - @NotNull - public ItemStack getOffHandItem() { - return offHandItem; - } - - /** - * Changes the item which will be in the player off hand. - * - * @param offHandItem the off hand item - */ - public void setOffHandItem(@NotNull ItemStack offHandItem) { - this.offHandItem = offHandItem; - } - - @Override - public boolean isCancelled() { - return cancelled; - } - - @Override - public void setCancelled(boolean cancel) { - this.cancelled = cancel; - } - - @Override - public @NotNull Player getPlayer() { - return player; - } -} diff --git a/src/main/java/net/minestom/server/event/trait/InventoryEvent.java b/src/main/java/net/minestom/server/event/trait/InventoryEvent.java index 726741531..2c887a635 100644 --- a/src/main/java/net/minestom/server/event/trait/InventoryEvent.java +++ b/src/main/java/net/minestom/server/event/trait/InventoryEvent.java @@ -2,7 +2,7 @@ package net.minestom.server.event.trait; import net.minestom.server.event.Event; import net.minestom.server.inventory.Inventory; -import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; /** * Represents any event inside an {@link Inventory}. @@ -12,7 +12,7 @@ public interface InventoryEvent extends Event { /** * Gets the inventory. * - * @return the inventory, null if this is a player's inventory + * @return the inventory (may be a player inventory) */ - @Nullable Inventory getInventory(); + @NotNull Inventory getInventory(); } diff --git a/src/main/java/net/minestom/server/inventory/AbstractInventory.java b/src/main/java/net/minestom/server/inventory/AbstractInventory.java deleted file mode 100644 index 99cfc2fb0..000000000 --- a/src/main/java/net/minestom/server/inventory/AbstractInventory.java +++ /dev/null @@ -1,258 +0,0 @@ -package net.minestom.server.inventory; - -import net.minestom.server.event.EventDispatcher; -import net.minestom.server.event.inventory.InventoryItemChangeEvent; -import net.minestom.server.event.inventory.PlayerInventoryItemChangeEvent; -import net.minestom.server.inventory.click.InventoryClickProcessor; -import net.minestom.server.inventory.condition.InventoryCondition; -import net.minestom.server.item.ItemStack; -import net.minestom.server.tag.TagHandler; -import net.minestom.server.tag.Taggable; -import net.minestom.server.utils.MathUtils; -import net.minestom.server.utils.validate.Check; -import org.jetbrains.annotations.NotNull; - -import java.lang.invoke.MethodHandles; -import java.lang.invoke.VarHandle; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.UnaryOperator; - -/** - * Represents an inventory where items can be modified/retrieved. - */ -public sealed abstract class AbstractInventory implements InventoryClickHandler, Taggable - permits Inventory, PlayerInventory { - - private static final VarHandle ITEM_UPDATER = MethodHandles.arrayElementVarHandle(ItemStack[].class); - - private final int size; - protected final ItemStack[] itemStacks; - - // list of conditions/callbacks assigned to this inventory - protected final List inventoryConditions = new CopyOnWriteArrayList<>(); - // the click processor which process all the clicks in the inventory - protected final InventoryClickProcessor clickProcessor = new InventoryClickProcessor(); - - private final TagHandler tagHandler = TagHandler.newHandler(); - - protected AbstractInventory(int size) { - this.size = size; - this.itemStacks = new ItemStack[getSize()]; - Arrays.fill(itemStacks, ItemStack.AIR); - } - - /** - * Sets an {@link ItemStack} at the specified slot and send relevant update to the viewer(s). - * - * @param slot the slot to set the item - * @param itemStack the item to set - */ - public synchronized void setItemStack(int slot, @NotNull ItemStack itemStack) { - Check.argCondition(!MathUtils.isBetween(slot, 0, getSize()), - "Inventory does not have the slot " + slot); - safeItemInsert(slot, itemStack); - } - - /** - * Inserts safely an item into the inventory. - *

- * This will update the slot for all viewers and warn the inventory that - * the window items packet is not up-to-date. - * - * @param slot the internal slot id - * @param itemStack the item to insert (use air instead of null) - * @throws IllegalArgumentException if the slot {@code slot} does not exist - */ - protected final void safeItemInsert(int slot, @NotNull ItemStack itemStack, boolean sendPacket) { - ItemStack previous; - synchronized (this) { - Check.argCondition( - !MathUtils.isBetween(slot, 0, getSize()), - "The slot {0} does not exist in this inventory", - slot - ); - previous = itemStacks[slot]; - if (itemStack.equals(previous)) return; // Avoid sending updates if the item has not changed - UNSAFE_itemInsert(slot, itemStack, sendPacket); - } - if (this instanceof PlayerInventory inv) { - EventDispatcher.call(new PlayerInventoryItemChangeEvent(inv.player, slot, previous, itemStack)); - } else if (this instanceof Inventory inv) { - EventDispatcher.call(new InventoryItemChangeEvent(inv, slot, previous, itemStack)); - } - } - - protected final void safeItemInsert(int slot, @NotNull ItemStack itemStack) { - safeItemInsert(slot, itemStack, true); - } - - protected abstract void UNSAFE_itemInsert(int slot, @NotNull ItemStack itemStack, boolean sendPacket); - - public synchronized @NotNull T processItemStack(@NotNull ItemStack itemStack, - @NotNull TransactionType type, - @NotNull TransactionOption option) { - return option.fill(type, this, itemStack); - } - - public synchronized @NotNull List<@NotNull T> processItemStacks(@NotNull List<@NotNull ItemStack> itemStacks, - @NotNull TransactionType type, - @NotNull TransactionOption option) { - List result = new ArrayList<>(itemStacks.size()); - itemStacks.forEach(itemStack -> { - T transactionResult = processItemStack(itemStack, type, option); - result.add(transactionResult); - }); - return result; - } - - /** - * Adds an {@link ItemStack} to the inventory and sends relevant update to the viewer(s). - * - * @param itemStack the item to add - * @param option the transaction option - * @return true if the item has been successfully added, false otherwise - */ - public @NotNull T addItemStack(@NotNull ItemStack itemStack, @NotNull TransactionOption option) { - return processItemStack(itemStack, TransactionType.ADD, option); - } - - public boolean addItemStack(@NotNull ItemStack itemStack) { - return addItemStack(itemStack, TransactionOption.ALL_OR_NOTHING); - } - - /** - * Adds {@link ItemStack}s to the inventory and sends relevant updates to the viewer(s). - * - * @param itemStacks items to add - * @param option the transaction option - * @return the operation results - */ - public @NotNull List<@NotNull T> addItemStacks(@NotNull List<@NotNull ItemStack> itemStacks, - @NotNull TransactionOption option) { - return processItemStacks(itemStacks, TransactionType.ADD, option); - } - - /** - * Takes an {@link ItemStack} from the inventory and sends relevant update to the viewer(s). - * - * @param itemStack the item to take - * @return true if the item has been successfully fully taken, false otherwise - */ - public @NotNull T takeItemStack(@NotNull ItemStack itemStack, @NotNull TransactionOption option) { - return processItemStack(itemStack, TransactionType.TAKE, option); - } - - /** - * Takes {@link ItemStack}s from the inventory and sends relevant updates to the viewer(s). - * - * @param itemStacks items to take - * @return the operation results - */ - public @NotNull List<@NotNull T> takeItemStacks(@NotNull List<@NotNull ItemStack> itemStacks, - @NotNull TransactionOption option) { - return processItemStacks(itemStacks, TransactionType.TAKE, option); - } - - public synchronized void replaceItemStack(int slot, @NotNull UnaryOperator<@NotNull ItemStack> operator) { - var currentItem = getItemStack(slot); - setItemStack(slot, operator.apply(currentItem)); - } - - /** - * Clears the inventory and send relevant update to the viewer(s). - */ - public synchronized void clear() { - // Clear the item array - for (int i = 0; i < size; i++) { - safeItemInsert(i, ItemStack.AIR, false); - } - // Send the cleared inventory to viewers - update(); - } - - public abstract void update(); - - /** - * Gets the {@link ItemStack} at the specified slot. - * - * @param slot the slot to check - * @return the item in the slot {@code slot} - */ - public @NotNull ItemStack getItemStack(int slot) { - return (ItemStack) ITEM_UPDATER.getVolatile(itemStacks, slot); - } - - /** - * Gets all the {@link ItemStack} in the inventory. - *

- * Be aware that the returned array does not need to be the original one, - * meaning that modifying it directly may not work. - * - * @return an array containing all the inventory's items - */ - public @NotNull ItemStack[] getItemStacks() { - return itemStacks.clone(); - } - - /** - * Gets the size of the inventory. - * - * @return the inventory's size - */ - public int getSize() { - return size; - } - - /** - * Gets the size of the "inner inventory" (which includes only "usable" slots). - * - * @return inner inventory's size - */ - public int getInnerSize() { - return getSize(); - } - - /** - * Gets all the {@link InventoryCondition} of this inventory. - * - * @return a modifiable {@link List} containing all the inventory conditions - */ - public @NotNull List<@NotNull InventoryCondition> getInventoryConditions() { - return inventoryConditions; - } - - /** - * Adds a new {@link InventoryCondition} to this inventory. - * - * @param inventoryCondition the inventory condition to add - */ - public void addInventoryCondition(@NotNull InventoryCondition inventoryCondition) { - this.inventoryConditions.add(inventoryCondition); - } - - /** - * Places all the items of {@code itemStacks} into the internal array. - * - * @param itemStacks the array to copy the content from - * @throws IllegalArgumentException if the size of the array is not equal to {@link #getSize()} - * @throws NullPointerException if {@code itemStacks} contains one null element or more - */ - public void copyContents(@NotNull ItemStack[] itemStacks) { - Check.argCondition(itemStacks.length != getSize(), - "The size of the array has to be of the same size as the inventory: " + getSize()); - - for (int i = 0; i < itemStacks.length; i++) { - final ItemStack itemStack = itemStacks[i]; - Check.notNull(itemStack, "The item array cannot contain any null element!"); - setItemStack(i, itemStack); - } - } - - @Override - public @NotNull TagHandler tagHandler() { - return tagHandler; - } -} diff --git a/src/main/java/net/minestom/server/inventory/ContainerInventory.java b/src/main/java/net/minestom/server/inventory/ContainerInventory.java new file mode 100644 index 000000000..8ae0274c6 --- /dev/null +++ b/src/main/java/net/minestom/server/inventory/ContainerInventory.java @@ -0,0 +1,172 @@ +package net.minestom.server.inventory; + +import net.kyori.adventure.text.Component; +import net.minestom.server.entity.Player; +import net.minestom.server.event.EventDispatcher; +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.item.ItemStack; +import net.minestom.server.network.packet.server.play.OpenWindowPacket; +import net.minestom.server.network.packet.server.play.WindowPropertyPacket; +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}. + *

+ * You can create one with {@link ContainerInventory#ContainerInventory(InventoryType, String)} or by making your own subclass. + * It can then be opened using {@link Player#openInventory(Inventory)}. + */ +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 + * @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) { + PlayerInventory playerInventory = player.getInventory(); + + InventoryPreClickEvent preClickEvent = new InventoryPreClickEvent(playerInventory, inventory, player, info); + EventDispatcher.call(preClickEvent); + + Click.Info newInfo = preClickEvent.getClickInfo(); + + if (!preClickEvent.isCancelled()) { + Click.Getter getter = new Click.Getter(inventory::getItemStack, playerInventory::getItemStack, playerInventory.getCursorItem(), inventory.getSize()); + Click.Result changes = processor.apply(newInfo, getter); + + InventoryClickEvent clickEvent = new InventoryClickEvent(playerInventory, inventory, player, newInfo, changes); + EventDispatcher.call(clickEvent); + + if (!clickEvent.isCancelled()) { + Click.Result newChanges = clickEvent.getChanges(); + + apply(newChanges, player, inventory); + + var postClickEvent = new InventoryPostClickEvent(player, inventory, newInfo, newChanges); + EventDispatcher.call(postClickEvent); + + if (!info.equals(newInfo) || !changes.equals(newChanges)) { + inventory.update(player); + if (inventory != playerInventory) { + playerInventory.update(player); + } + } + + return newChanges; + } + } + + inventory.update(player); + if (inventory != playerInventory) { + playerInventory.update(player); + } + return null; + } + + public static void apply(@NotNull Click.Result result, @NotNull Player player, @NotNull Inventory inventory) { + for (var entry : result.changes().entrySet()) { + inventory.setItemStack(entry.getKey(), entry.getValue()); + } + + for (var entry : result.playerInventoryChanges().entrySet()) { + player.getInventory().setItemStack(entry.getKey(), entry.getValue()); + } + + if (result.newCursorItem() != null) { + player.getInventory().setCursorItem(result.newCursorItem()); + } + + if (result.sideEffects() instanceof Click.SideEffect.DropFromPlayer drop) { + for (ItemStack item : drop.items()) { + player.dropItem(item); + } + } + } + + private static final AtomicInteger ID_COUNTER = new AtomicInteger(); + + private final byte id; + private final InventoryType inventoryType; + private Component title; + + public ContainerInventory(@NotNull InventoryType inventoryType, @NotNull Component title) { + super(inventoryType.getSize()); + this.id = generateId(); + this.inventoryType = inventoryType; + this.title = title; + } + + public ContainerInventory(@NotNull InventoryType inventoryType, @NotNull String title) { + this(inventoryType, Component.text(title)); + } + + private static byte generateId() { + return (byte) ID_COUNTER.updateAndGet(i -> i + 1 >= 128 ? 1 : i + 1); + } + + /** + * Gets the inventory type of this inventory. + * + * @return the inventory type + */ + public @NotNull InventoryType getInventoryType() { + return inventoryType; + } + + /** + * Gets the inventory title of this inventory. + * + * @return the inventory title + */ + public @NotNull Component getTitle() { + return title; + } + + /** + * Changes the inventory title of this inventory. + * + * @param title the new inventory title + */ + public void setTitle(@NotNull Component title) { + this.title = title; + + // Reopen and update this inventory with the new title + sendPacketToViewers(new OpenWindowPacket(getWindowId(), getInventoryType().getWindowType(), title)); + update(); + } + + @Override + public byte getWindowId() { + return id; + } + + @Override + public boolean addViewer(@NotNull Player player) { + if (!this.viewers.add(player)) return false; + + player.sendPacket(new OpenWindowPacket(getWindowId(), inventoryType.getWindowType(), getTitle())); + update(player); + return true; + } + + /** + * Sends a window property to all viewers. + * + * @param property the property to send + * @param value the value of the property + * @see https://wiki.vg/Protocol#Set_Container_Property + */ + protected void sendProperty(@NotNull InventoryProperty property, short value) { + sendPacketToViewers(new WindowPropertyPacket(getWindowId(), property.getProperty(), value)); + } + +} diff --git a/src/main/java/net/minestom/server/inventory/EquipmentHandler.java b/src/main/java/net/minestom/server/inventory/EquipmentHandler.java index 576dddd4d..003cb61b5 100644 --- a/src/main/java/net/minestom/server/inventory/EquipmentHandler.java +++ b/src/main/java/net/minestom/server/inventory/EquipmentHandler.java @@ -163,10 +163,19 @@ public interface EquipmentHandler { * @param slot the slot of the equipment */ default void syncEquipment(@NotNull EquipmentSlot slot) { + syncEquipment(slot, getEquipment(slot)); + } + + /** + * Sends a specific equipment to viewers. + * + * @param slot the slot of the equipment + * @param itemStack the item to be sent for the slot + */ + default void syncEquipment(@NotNull EquipmentSlot slot, @NotNull ItemStack itemStack) { Check.stateCondition(!(this instanceof Entity), "Only accessible for Entity"); Entity entity = (Entity) this; - final ItemStack itemStack = getEquipment(slot); entity.sendPacketToViewers(new EntityEquipmentPacket(entity.getEntityId(), Map.of(slot, itemStack))); } diff --git a/src/main/java/net/minestom/server/inventory/Inventory.java b/src/main/java/net/minestom/server/inventory/Inventory.java index be9c688fa..70c45f5b6 100644 --- a/src/main/java/net/minestom/server/inventory/Inventory.java +++ b/src/main/java/net/minestom/server/inventory/Inventory.java @@ -1,394 +1,146 @@ package net.minestom.server.inventory; -import net.kyori.adventure.text.Component; import net.minestom.server.Viewable; import net.minestom.server.entity.Player; -import net.minestom.server.inventory.click.ClickType; -import net.minestom.server.inventory.click.InventoryClickResult; +import net.minestom.server.inventory.click.Click; import net.minestom.server.item.ItemStack; -import net.minestom.server.network.packet.server.play.OpenWindowPacket; -import net.minestom.server.network.packet.server.play.SetSlotPacket; -import net.minestom.server.network.packet.server.play.WindowItemsPacket; -import net.minestom.server.network.packet.server.play.WindowPropertyPacket; -import net.minestom.server.utils.inventory.PlayerInventoryUtils; +import net.minestom.server.tag.Taggable; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import java.util.Collections; import java.util.List; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.UnaryOperator; /** - * Represents an inventory which can be viewed by a collection of {@link Player}. - *

- * You can create one with {@link Inventory#Inventory(InventoryType, String)} or by making your own subclass. - * It can then be opened using {@link Player#openInventory(Inventory)}. + * Represents a generic inventory that can be interacted with. */ -public non-sealed class Inventory extends AbstractInventory implements Viewable { - private static final AtomicInteger ID_COUNTER = new AtomicInteger(); - - // the id of this inventory - private final byte id; - // the type of this inventory - private final InventoryType inventoryType; - // the title of this inventory - private Component title; - - private final int offset; - - // the players currently viewing this inventory - private final Set viewers = new CopyOnWriteArraySet<>(); - private final Set unmodifiableViewers = Collections.unmodifiableSet(viewers); - // (player -> cursor item) map, used by the click listeners - private final ConcurrentHashMap cursorPlayersItem = new ConcurrentHashMap<>(); - - public Inventory(@NotNull InventoryType inventoryType, @NotNull Component title) { - super(inventoryType.getSize()); - this.id = generateId(); - this.inventoryType = inventoryType; - this.title = title; - - this.offset = getSize(); - } - - public Inventory(@NotNull InventoryType inventoryType, @NotNull String title) { - this(inventoryType, Component.text(title)); - } - - private static byte generateId() { - return (byte) ID_COUNTER.updateAndGet(i -> i + 1 >= 128 ? 1 : i + 1); - } +public sealed interface Inventory extends Taggable, Viewable permits InventoryImpl { /** - * Gets the inventory type. - * - * @return the inventory type + * Gets the size of this inventory. This should be a constant number. + * @return the size */ - public @NotNull InventoryType getInventoryType() { - return inventoryType; - } + int getSize(); /** - * Gets the inventory title. + * Gets the {@link ItemStack} at the specified slot. * - * @return the inventory title + * @param slot the slot to check + * @return the item in the slot {@code slot} */ - public @NotNull Component getTitle() { - return title; - } + @NotNull ItemStack getItemStack(int slot); /** - * Changes the inventory title. + * Sets an {@link ItemStack} at the specified slot and send relevant update to the viewer(s). * - * @param title the new inventory title + * @param slot the slot to set the item + * @param itemStack the item to set */ - public void setTitle(@NotNull Component title) { - this.title = title; - // Re-open the inventory - sendPacketToViewers(new OpenWindowPacket(getWindowId(), getInventoryType().getWindowType(), title)); - // Send inventory items - update(); - } + void setItemStack(int slot, @NotNull ItemStack itemStack); /** - * Gets this window id. + * Gets the window ID of this window, as a byte. + * + * @return the window ID + */ + byte getWindowId(); + + /** + * Handles the provided click from the given player, returning the results after it is applied. If the results are + * null, this indicates that the click was cancelled or was otherwise not processed. + * + * @param player the player that clicked + * @param info the information about the player's click + * @return the results of the click, or null if the click was cancelled or otherwise was not handled + */ + @Nullable Click.Result handleClick(@NotNull Player player, @NotNull Click.Info info); + + /** + * Gets all the {@link ItemStack} in the inventory. *

- * This is the id that the client will send to identify the affected inventory, mostly used by packets. + * Be aware that the returned array does not need to be the original one, + * meaning that modifying it directly may not work. * - * @return the window id + * @return an array containing all the inventory's items */ - public byte getWindowId() { - return id; - } - - @Override - public synchronized void clear() { - this.cursorPlayersItem.clear(); - super.clear(); - } + @NotNull ItemStack[] getItemStacks(); /** - * Refreshes the inventory for all viewers. + * Places all the items of {@code itemStacks} into the internal array. + * + * @param itemStacks the array to copy the content from + * @throws IllegalArgumentException if the size of the array is not equal to {@link #getSize()} + * @throws NullPointerException if {@code itemStacks} contains one null element or more */ - @Override - public void update() { - this.viewers.forEach(p -> p.sendPacket(createNewWindowItemsPacket(p))); - } + void copyContents(@NotNull ItemStack[] itemStacks); /** - * Refreshes the inventory for a specific viewer. - *

- * The player needs to be a viewer, otherwise nothing is sent. - * - * @param player the player to update the inventory + * Clears the inventory and send relevant update to the viewer(s). */ - public void update(@NotNull Player player) { - if (!isViewer(player)) return; - player.sendPacket(createNewWindowItemsPacket(player)); - } - - @Override - public @NotNull Set getViewers() { - return unmodifiableViewers; - } + void clear(); /** - * This will not open the inventory for {@code player}, use {@link Player#openInventory(Inventory)}. - * - * @param player the viewer to add - * @return true if the player has successfully been added + * Updates the inventory for all viewers. */ - @Override - public boolean addViewer(@NotNull Player player) { - final boolean result = this.viewers.add(player); - update(player); - return result; - } + void update(); /** - * This will not close the inventory for {@code player}, use {@link Player#closeInventory()}. + * Updates the inventory for a specific viewer. * - * @param player the viewer to remove - * @return true if the player has successfully been removed + * @param player the player to update the inventory for */ - @Override - public boolean removeViewer(@NotNull Player player) { - final boolean result = this.viewers.remove(player); - setCursorItem(player, ItemStack.AIR); - this.clickProcessor.clearCache(player); - return result; - } + void update(@NotNull Player player); /** - * Gets the cursor item of a viewer. + * Replaces the item in the slot according to the operator. * - * @param player the player to get the cursor item from - * @return the player cursor item, air item if the player is not a viewer + * @param slot the slot to replace + * @param operator the operator to apply to the slot */ - public @NotNull ItemStack getCursorItem(@NotNull Player player) { - return cursorPlayersItem.getOrDefault(player, ItemStack.AIR); - } + void replaceItemStack(int slot, @NotNull UnaryOperator<@NotNull ItemStack> operator); + + @NotNull T processItemStack(@NotNull ItemStack itemStack, + @NotNull TransactionType type, + @NotNull TransactionOption option); + + @NotNull List<@NotNull T> processItemStacks(@NotNull List<@NotNull ItemStack> itemStacks, + @NotNull TransactionType type, @NotNull TransactionOption option); /** - * Changes the cursor item of a viewer, - * does nothing if player is not a viewer. + * Adds an {@link ItemStack} to the inventory and sends relevant update to the viewer(s). * - * @param player the player to change the cursor item - * @param cursorItem the new player cursor item + * @param itemStack the item to add + * @param option the transaction option + * @return true if the item has been successfully added, false otherwise */ - public void setCursorItem(@NotNull Player player, @NotNull ItemStack cursorItem) { - final ItemStack currentCursorItem = cursorPlayersItem.getOrDefault(player, ItemStack.AIR); - if (!currentCursorItem.equals(cursorItem)) { - player.sendPacket(SetSlotPacket.createCursorPacket(cursorItem)); - } - if (!cursorItem.isAir()) { - this.cursorPlayersItem.put(player, cursorItem); - } else { - this.cursorPlayersItem.remove(player); - } - } + @NotNull T addItemStack(@NotNull ItemStack itemStack, @NotNull TransactionOption option); - @Override - protected void UNSAFE_itemInsert(int slot, @NotNull ItemStack itemStack, boolean sendPacket) { - itemStacks[slot] = itemStack; - if (sendPacket) sendPacketToViewers(new SetSlotPacket(getWindowId(), 0, (short) slot, itemStack)); - } - - private @NotNull WindowItemsPacket createNewWindowItemsPacket(Player player) { - return new WindowItemsPacket(getWindowId(), 0, List.of(getItemStacks()), cursorPlayersItem.getOrDefault(player, ItemStack.AIR)); - } + boolean addItemStack(@NotNull ItemStack itemStack); /** - * Sends a window property to all viewers. + * Adds {@link ItemStack}s to the inventory and sends relevant updates to the viewer(s). * - * @param property the property to send - * @param value the value of the property - * @see https://wiki.vg/Protocol#Window_Property + * @param itemStacks items to add + * @param option the transaction option + * @return the operation results */ - protected void sendProperty(@NotNull InventoryProperty property, short value) { - sendPacketToViewers(new WindowPropertyPacket(getWindowId(), property.getProperty(), value)); - } + @NotNull List<@NotNull T> addItemStacks(@NotNull List<@NotNull ItemStack> itemStacks, @NotNull TransactionOption option); - @Override - public boolean leftClick(@NotNull Player player, int slot) { - final PlayerInventory playerInventory = player.getInventory(); - final ItemStack cursor = getCursorItem(player); - final boolean isInWindow = isClickInWindow(slot); - final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset); - final ItemStack clicked = isInWindow ? getItemStack(slot) : playerInventory.getItemStack(clickSlot); - final InventoryClickResult clickResult = clickProcessor.leftClick(player, - isInWindow ? this : playerInventory, clickSlot, clicked, cursor); - if (clickResult.isCancel()) { - updateAll(player); - return false; - } - if (isInWindow) { - setItemStack(slot, clickResult.getClicked()); - } else { - playerInventory.setItemStack(clickSlot, clickResult.getClicked()); - } - this.cursorPlayersItem.put(player, clickResult.getCursor()); - callClickEvent(player, isInWindow ? this : null, slot, ClickType.LEFT_CLICK, clicked, cursor); - return true; - } + /** + * Takes an {@link ItemStack} from the inventory and sends relevant update to the viewer(s). + * + * @param itemStack the item to take + * @return true if the item has been successfully fully taken, false otherwise + */ + @NotNull T takeItemStack(@NotNull ItemStack itemStack, @NotNull TransactionOption option); - @Override - public boolean rightClick(@NotNull Player player, int slot) { - final PlayerInventory playerInventory = player.getInventory(); - final ItemStack cursor = getCursorItem(player); - final boolean isInWindow = isClickInWindow(slot); - final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset); - final ItemStack clicked = isInWindow ? getItemStack(slot) : playerInventory.getItemStack(clickSlot); - final InventoryClickResult clickResult = clickProcessor.rightClick(player, - isInWindow ? this : playerInventory, clickSlot, clicked, cursor); - if (clickResult.isCancel()) { - updateAll(player); - return false; - } - if (isInWindow) { - setItemStack(slot, clickResult.getClicked()); - } else { - playerInventory.setItemStack(clickSlot, clickResult.getClicked()); - } - this.cursorPlayersItem.put(player, clickResult.getCursor()); - callClickEvent(player, isInWindow ? this : null, slot, ClickType.RIGHT_CLICK, clicked, cursor); - return true; - } + /** + * Takes {@link ItemStack}s from the inventory and sends relevant updates to the viewer(s). + * + * @param itemStacks items to take + * @return the operation results + */ + @NotNull List<@NotNull T> takeItemStacks(@NotNull List<@NotNull ItemStack> itemStacks, @NotNull TransactionOption option); - @Override - public boolean shiftClick(@NotNull Player player, int slot) { - final PlayerInventory playerInventory = player.getInventory(); - final boolean isInWindow = isClickInWindow(slot); - final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset); - final ItemStack clicked = isInWindow ? getItemStack(slot) : playerInventory.getItemStack(clickSlot); - final ItemStack cursor = getCursorItem(player); // Isn't used in the algorithm - final InventoryClickResult clickResult = clickProcessor.shiftClick( - isInWindow ? this : playerInventory, - isInWindow ? playerInventory : this, - 0, isInWindow ? playerInventory.getInnerSize() : getInnerSize(), 1, - player, clickSlot, clicked, cursor); - if (clickResult.isCancel()) { - updateAll(player); - return false; - } - if (isInWindow) { - setItemStack(slot, clickResult.getClicked()); - } else { - playerInventory.setItemStack(clickSlot, clickResult.getClicked()); - } - updateAll(player); // FIXME: currently not properly client-predicted - this.cursorPlayersItem.put(player, clickResult.getCursor()); - return true; - } - - @Override - public boolean changeHeld(@NotNull Player player, int slot, int key) { - final int convertedKey = key == 40 ? PlayerInventoryUtils.OFFHAND_SLOT : key; - final PlayerInventory playerInventory = player.getInventory(); - final boolean isInWindow = isClickInWindow(slot); - final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset); - final ItemStack clicked = isInWindow ? getItemStack(slot) : playerInventory.getItemStack(clickSlot); - final ItemStack heldItem = playerInventory.getItemStack(convertedKey); - final InventoryClickResult clickResult = clickProcessor.changeHeld(player, - isInWindow ? this : playerInventory, clickSlot, convertedKey, clicked, heldItem); - if (clickResult.isCancel()) { - updateAll(player); - return false; - } - if (isInWindow) { - setItemStack(slot, clickResult.getClicked()); - } else { - playerInventory.setItemStack(clickSlot, clickResult.getClicked()); - } - playerInventory.setItemStack(convertedKey, clickResult.getCursor()); - callClickEvent(player, isInWindow ? this : null, slot, ClickType.CHANGE_HELD, clicked, getCursorItem(player)); - return true; - } - - @Override - public boolean middleClick(@NotNull Player player, int slot) { - // TODO - update(player); - return false; - } - - @Override - public boolean drop(@NotNull Player player, boolean all, int slot, int button) { - final PlayerInventory playerInventory = player.getInventory(); - final boolean isInWindow = isClickInWindow(slot); - final boolean outsideDrop = slot == -999; - final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset); - final ItemStack clicked = outsideDrop ? - ItemStack.AIR : (isInWindow ? getItemStack(slot) : playerInventory.getItemStack(clickSlot)); - final ItemStack cursor = getCursorItem(player); - final InventoryClickResult clickResult = clickProcessor.drop(player, - isInWindow ? this : playerInventory, all, clickSlot, button, clicked, cursor); - if (clickResult.isCancel()) { - updateAll(player); - return false; - } - final ItemStack resultClicked = clickResult.getClicked(); - if (!outsideDrop && resultClicked != null) { - if (isInWindow) { - setItemStack(slot, resultClicked); - } else { - playerInventory.setItemStack(clickSlot, resultClicked); - } - } - this.cursorPlayersItem.put(player, clickResult.getCursor()); - return true; - } - - @Override - public boolean dragging(@NotNull Player player, int slot, int button) { - final PlayerInventory playerInventory = player.getInventory(); - final boolean isInWindow = isClickInWindow(slot); - final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset); - final ItemStack clicked = slot != -999 ? - (isInWindow ? getItemStack(slot) : playerInventory.getItemStack(clickSlot)) : - ItemStack.AIR; - final ItemStack cursor = getCursorItem(player); - final InventoryClickResult clickResult = clickProcessor.dragging(player, - slot != -999 ? (isInWindow ? this : playerInventory) : null, - clickSlot, button, - clicked, cursor); - if (clickResult == null || clickResult.isCancel()) { - updateAll(player); - return false; - } - this.cursorPlayersItem.put(player, clickResult.getCursor()); - updateAll(player); // FIXME: currently not properly client-predicted - return true; - } - - @Override - public boolean doubleClick(@NotNull Player player, int slot) { - final PlayerInventory playerInventory = player.getInventory(); - final boolean isInWindow = isClickInWindow(slot); - final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset); - final ItemStack clicked = slot != -999 ? - (isInWindow ? getItemStack(slot) : playerInventory.getItemStack(clickSlot)) : - ItemStack.AIR; - final ItemStack cursor = getCursorItem(player); - final InventoryClickResult clickResult = clickProcessor.doubleClick(isInWindow ? this : playerInventory, - this, player, clickSlot, clicked, cursor); - if (clickResult.isCancel()) { - updateAll(player); - return false; - } - this.cursorPlayersItem.put(player, clickResult.getCursor()); - updateAll(player); // FIXME: currently not properly client-predicted - return true; - } - - private boolean isClickInWindow(int slot) { - return slot < getSize(); - } - - private void updateAll(Player player) { - player.getInventory().update(); - update(player); - } } diff --git a/src/main/java/net/minestom/server/inventory/InventoryClickHandler.java b/src/main/java/net/minestom/server/inventory/InventoryClickHandler.java deleted file mode 100644 index 5fa175416..000000000 --- a/src/main/java/net/minestom/server/inventory/InventoryClickHandler.java +++ /dev/null @@ -1,83 +0,0 @@ -package net.minestom.server.inventory; - -import net.minestom.server.entity.Player; -import net.minestom.server.event.EventDispatcher; -import net.minestom.server.event.inventory.InventoryClickEvent; -import net.minestom.server.inventory.click.ClickType; -import net.minestom.server.item.ItemStack; -import org.jetbrains.annotations.NotNull; - -/** - * Represents an inventory which can receive click input. - * All methods returning boolean returns true if the action is successful, false otherwise. - *

- * See https://wiki.vg/Protocol#Click_Window for more information. - */ -public sealed interface InventoryClickHandler permits AbstractInventory { - - /** - * Called when a {@link Player} left click in the inventory. Can also be to drop the cursor item - * - * @param player the player who clicked - * @param slot the slot number - * @return true if the click hasn't been cancelled, false otherwise - */ - boolean leftClick(@NotNull Player player, int slot); - - /** - * Called when a {@link Player} right click in the inventory. Can also be to drop the cursor item - * - * @param player the player who clicked - * @param slot the slot number - * @return true if the click hasn't been cancelled, false otherwise - */ - boolean rightClick(@NotNull Player player, int slot); - - /** - * Called when a {@link Player} shift click in the inventory - * - * @param player the player who clicked - * @param slot the slot number - * @return true if the click hasn't been cancelled, false otherwise - */ - boolean shiftClick(@NotNull Player player, int slot); // shift + left/right click have the same behavior - - /** - * Called when a {@link Player} held click in the inventory - * - * @param player the player who clicked - * @param slot the slot number - * @param key the held slot (0-8) pressed - * @return true if the click hasn't been cancelled, false otherwise - */ - boolean changeHeld(@NotNull Player player, int slot, int key); - - boolean middleClick(@NotNull Player player, int slot); - - /** - * Called when a {@link Player} press the drop button - * - * @param player the player who clicked - * @param all - * @param slot the slot number - * @param button -999 if clicking outside, normal if he is not - * @return true if the drop hasn't been cancelled, false otherwise - */ - boolean drop(@NotNull Player player, boolean all, int slot, int button); - - boolean dragging(@NotNull Player player, int slot, int button); - - /** - * Called when a {@link Player} double click in the inventory - * - * @param player the player who clicked - * @param slot the slot number - * @return true if the click hasn't been cancelled, false otherwise - */ - boolean doubleClick(@NotNull Player player, int slot); - - default void callClickEvent(@NotNull Player player, Inventory inventory, int slot, - @NotNull ClickType clickType, @NotNull ItemStack clicked, @NotNull ItemStack cursor) { - EventDispatcher.call(new InventoryClickEvent(inventory, player, slot, clickType, clicked, cursor)); - } -} diff --git a/src/main/java/net/minestom/server/inventory/InventoryImpl.java b/src/main/java/net/minestom/server/inventory/InventoryImpl.java new file mode 100644 index 000000000..9a3269369 --- /dev/null +++ b/src/main/java/net/minestom/server/inventory/InventoryImpl.java @@ -0,0 +1,298 @@ +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; +import net.minestom.server.network.packet.server.play.WindowItemsPacket; +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; +import java.util.*; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.UnaryOperator; +import java.util.stream.IntStream; + +sealed abstract class InventoryImpl implements Inventory permits ContainerInventory, PlayerInventory { + + private static final VarHandle ITEM_UPDATER = MethodHandles.arrayElementVarHandle(ItemStack[].class); + + private final int size; + protected final ItemStack[] itemStacks; + + private final TagHandler tagHandler = TagHandler.newHandler(); + + protected final ReentrantLock lock = new ReentrantLock(); + + // the players currently viewing this inventory + protected final Set viewers = new CopyOnWriteArraySet<>(); + protected final Set unmodifiableViewers = Collections.unmodifiableSet(viewers); + + protected InventoryImpl(int size) { + this.size = size; + this.itemStacks = new ItemStack[size]; + Arrays.fill(itemStacks, ItemStack.AIR); + } + + @Override + public int getSize() { + return size; + } + + @Override + public @NotNull TagHandler tagHandler() { + return tagHandler; + } + + @Override + public @NotNull Set getViewers() { + return unmodifiableViewers; + } + + @Override + public boolean addViewer(@NotNull Player player) { + if (!this.viewers.add(player)) return false; + + update(player); + return true; + } + + @Override + public boolean removeViewer(@NotNull Player player) { + if (!this.viewers.remove(player)) return false; + + ItemStack cursorItem = player.getInventory().getCursorItem(); + player.getInventory().setCursorItem(ItemStack.AIR); + + if (!cursorItem.isAir()) { + // Drop the item if it can not be added back to the inventory + if (!player.getInventory().addItemStack(cursorItem)) { + player.dropItem(cursorItem); + } + } + + player.clickPreprocessor().clearCache(); + if (player.didCloseInventory()) { + player.UNSAFE_changeDidCloseInventory(false); + } else { + player.sendPacket(new CloseWindowPacket(getWindowId())); + } + return true; + } + + /** + * Updates the provided slot for this inventory's viewers. + * + * @param slot the slot to update + * @param itemStack the item treated as in the slot + */ + protected void updateSlot(int slot, @NotNull ItemStack itemStack) { + sendPacketToViewers(new SetSlotPacket(getWindowId(), 0, (short) slot, itemStack)); + } + + @Override + public void update() { + this.viewers.forEach(this::update); + } + + @Override + public void update(@NotNull Player player) { + 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); + } + + @Override + public @NotNull ItemStack[] getItemStacks() { + return itemStacks.clone(); + } + + @Override + public void copyContents(@NotNull ItemStack[] itemStacks) { + Check.argCondition(itemStacks.length != getSize(), + "The size of the array has to be of the same size as the inventory: " + getSize()); + + for (int i = 0; i < itemStacks.length; i++) { + final ItemStack itemStack = itemStacks[i]; + Check.notNull(itemStack, "The item array cannot contain any null element!"); + setItemStack(i, itemStack); + } + } + + @Override + public void setItemStack(int slot, @NotNull ItemStack itemStack) { + Check.argCondition(!MathUtils.isBetween(slot, 0, getSize()), + "Inventory does not have the slot " + slot); + safeItemInsert(slot, itemStack); + } + + protected final void safeItemInsert(int slot, @NotNull ItemStack itemStack) { + safeItemInsert(slot, itemStack, true); + } + + /** + * Inserts safely an item into the inventory. + *

+ * This will update the slot for all viewers and warn the inventory that + * the window items packet is not up-to-date. + * + * @param slot the internal slot id + * @param itemStack the item to insert (use air instead of null) + * @throws IllegalArgumentException if the slot {@code slot} does not exist + */ + 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 + + UNSAFE_itemInsert(slot, itemStack); + if (sendPacket) updateSlot(slot, itemStack); + + EventDispatcher.call(new InventoryItemChangeEvent(this, slot, previous, itemStack)); + } finally { + lock.unlock(); + } + } + + protected void UNSAFE_itemInsert(int slot, @NotNull ItemStack itemStack) { + itemStacks[slot] = itemStack; + } + + @Override + public void replaceItemStack(int slot, @NotNull UnaryOperator<@NotNull ItemStack> operator) { + lock.lock(); + + try { + var currentItem = getItemStack(slot); + setItemStack(slot, operator.apply(currentItem)); + } finally { + lock.unlock(); + } + } + + @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); + } + // Send the cleared inventory to viewers + update(); + } finally { + lock.unlock(); + } + } + + @Override + public @NotNull T processItemStack(@NotNull ItemStack itemStack, + @NotNull TransactionType type, + @NotNull TransactionOption option) { + lock.lock(); + try { + return option.fill(type, this, itemStack); + } finally { + lock.unlock(); + } + } + + @Override + public @NotNull List<@NotNull T> processItemStacks(@NotNull List<@NotNull ItemStack> itemStacks, + @NotNull TransactionType type, + @NotNull TransactionOption option) { + List result = new ArrayList<>(itemStacks.size()); + + lock.lock(); + try { + for (ItemStack item : itemStacks) { + result.add(processItemStack(item, type, option)); + } + } finally { + lock.unlock(); + } + return result; + } + + @Override + public @NotNull T addItemStack(@NotNull ItemStack itemStack, @NotNull TransactionOption option) { + List slots = IntStream.range(0, getSize()).boxed().toList(); + return processItemStack(itemStack, TransactionType.add(slots, slots), option); + } + + @Override + public boolean addItemStack(@NotNull ItemStack itemStack) { + return addItemStack(itemStack, TransactionOption.ALL_OR_NOTHING); + } + + @Override + public @NotNull List<@NotNull T> addItemStacks(@NotNull List<@NotNull ItemStack> itemStacks, + @NotNull TransactionOption option) { + List result = new ArrayList<>(itemStacks.size()); + + lock.lock(); + try { + for (ItemStack item : itemStacks) { + result.add(addItemStack(item, option)); + } + } finally { + lock.unlock(); + } + return result; + } + + @Override + public @NotNull T takeItemStack(@NotNull ItemStack itemStack, @NotNull TransactionOption option) { + return processItemStack(itemStack, TransactionType.take(IntStream.range(0, getSize()).boxed().toList()), option); + } + + @Override + public @NotNull List<@NotNull T> takeItemStacks(@NotNull List<@NotNull ItemStack> itemStacks, + @NotNull TransactionOption option) { + List result = new ArrayList<>(itemStacks.size()); + + lock.lock(); + try { + for (ItemStack item : itemStacks) { + result.add(takeItemStack(item, option)); + } + } finally { + lock.unlock(); + } + return result; + } + +} diff --git a/src/main/java/net/minestom/server/inventory/InventoryType.java b/src/main/java/net/minestom/server/inventory/InventoryType.java index 9b77808c0..804cc4619 100644 --- a/src/main/java/net/minestom/server/inventory/InventoryType.java +++ b/src/main/java/net/minestom/server/inventory/InventoryType.java @@ -1,7 +1,7 @@ package net.minestom.server.inventory; /** - * Represents a type of {@link Inventory} + * Represents a type of {@link ContainerInventory} */ public enum InventoryType { diff --git a/src/main/java/net/minestom/server/inventory/PlayerInventory.java b/src/main/java/net/minestom/server/inventory/PlayerInventory.java index 988ecdc0e..d7a08bed7 100644 --- a/src/main/java/net/minestom/server/inventory/PlayerInventory.java +++ b/src/main/java/net/minestom/server/inventory/PlayerInventory.java @@ -4,327 +4,230 @@ import net.minestom.server.entity.EquipmentSlot; import net.minestom.server.entity.Player; import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.item.EntityEquipEvent; -import net.minestom.server.inventory.click.ClickType; -import net.minestom.server.inventory.click.InventoryClickResult; +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; import static net.minestom.server.utils.inventory.PlayerInventoryUtils.*; /** * Represents the inventory of a {@link Player}, retrieved with {@link Player#getInventory()}. */ -public non-sealed class PlayerInventory extends AbstractInventory implements EquipmentHandler { - public static final int INVENTORY_SIZE = 46; - public static final int INNER_INVENTORY_SIZE = 36; +public non-sealed class PlayerInventory extends InventoryImpl { + + public static @NotNull IntStream getInnerShiftClickSlots(int size) { + return IntStream.range(0, 36).map(i -> i + size); + } + + public static @NotNull IntStream getInnerDoubleClickSlots(int size) { + return IntStream.range(0, 36).map(i -> i + size); + } + + private static int getSlotIndex(@NotNull EquipmentSlot slot, int heldSlot) { + return switch (slot) { + case HELMET, CHESTPLATE, LEGGINGS, BOOTS -> slot.armorSlot(); + case OFF_HAND -> OFF_HAND_SLOT; + case MAIN_HAND -> heldSlot; + }; + } + + private static @Nullable EquipmentSlot fromSlotIndex(int slot, int heldSlot) { + return switch (slot) { + case HELMET_SLOT -> EquipmentSlot.HELMET; + case CHESTPLATE_SLOT -> EquipmentSlot.CHESTPLATE; + case LEGGINGS_SLOT -> EquipmentSlot.LEGGINGS; + case BOOTS_SLOT -> EquipmentSlot.BOOTS; + case OFF_HAND_SLOT -> EquipmentSlot.OFF_HAND; + default -> slot == heldSlot ? EquipmentSlot.MAIN_HAND : null; + }; + } + + private static final List FILL_ADD_SLOTS = IntStream.concat( + IntStream.of(OFF_HAND_SLOT), + IntStream.range(0, 36) + ).boxed().toList(); + + private static final List AIR_ADD_SLOTS = IntStream.range(0, 36).boxed().toList(); + + private static final List TAKE_SLOTS = Stream.of( + IntStream.range(0, 36), + IntStream.of(OFF_HAND_SLOT), + IntStream.range(36, 45) + ).flatMapToInt(i -> i).boxed().toList(); - protected final Player player; private ItemStack cursorItem = ItemStack.AIR; - public PlayerInventory(@NotNull Player player) { + public PlayerInventory() { super(INVENTORY_SIZE); - this.player = player; } @Override - public synchronized void clear() { - cursorItem = ItemStack.AIR; - super.clear(); - // Update equipments - this.player.sendPacketToViewersAndSelf(player.getEquipmentsPacket()); - } - - @Override - public int getInnerSize() { - return INNER_INVENTORY_SIZE; - } - - @Override - public @NotNull ItemStack getItemInMainHand() { - return getItemStack(player.getHeldSlot()); - } - - @Override - public void setItemInMainHand(@NotNull ItemStack itemStack) { - safeItemInsert(player.getHeldSlot(), itemStack); - } - - @Override - public @NotNull ItemStack getItemInOffHand() { - return getItemStack(OFFHAND_SLOT); - } - - @Override - public void setItemInOffHand(@NotNull ItemStack itemStack) { - safeItemInsert(OFFHAND_SLOT, itemStack); - } - - @Override - public @NotNull ItemStack getHelmet() { - return getItemStack(HELMET_SLOT); - } - - @Override - public void setHelmet(@NotNull ItemStack itemStack) { - safeItemInsert(HELMET_SLOT, itemStack); - } - - @Override - public @NotNull ItemStack getChestplate() { - return getItemStack(CHESTPLATE_SLOT); - } - - @Override - public void setChestplate(@NotNull ItemStack itemStack) { - safeItemInsert(CHESTPLATE_SLOT, itemStack); - } - - @Override - public @NotNull ItemStack getLeggings() { - return getItemStack(LEGGINGS_SLOT); - } - - @Override - public void setLeggings(@NotNull ItemStack itemStack) { - safeItemInsert(LEGGINGS_SLOT, itemStack); - } - - @Override - public @NotNull ItemStack getBoots() { - return getItemStack(BOOTS_SLOT); - } - - @Override - public void setBoots(@NotNull ItemStack itemStack) { - safeItemInsert(BOOTS_SLOT, itemStack); + public byte getWindowId() { + return 0; } /** - * Refreshes the player inventory by sending a {@link WindowItemsPacket} containing all. - * the inventory items - */ - @Override - public void update() { - this.player.sendPacket(createWindowItemsPacket()); - } - - /** - * Gets the item in player cursor. - * - * @return the cursor item + * Gets the cursor item of this inventory + * @return the cursor item that is shared between all viewers */ public @NotNull ItemStack getCursorItem() { return cursorItem; } /** - * Changes the player cursor item. - * - * @param cursorItem the new cursor item + * Sets the cursor item for all viewers of this inventory. + * @param cursorItem the new item (will not update if same as current) */ public void setCursorItem(@NotNull ItemStack cursorItem) { + setCursorItem(cursorItem, true); + } + + /** + * Sets the cursor item for all viewers of this inventory. + * @param cursorItem the new item (will not update if same as current) + * @param sendPacket whether or not to send a packet + */ + public void setCursorItem(@NotNull ItemStack cursorItem, boolean sendPacket) { if (this.cursorItem.equals(cursorItem)) return; - this.cursorItem = cursorItem; - final SetSlotPacket setSlotPacket = SetSlotPacket.createCursorPacket(cursorItem); - this.player.sendPacket(setSlotPacket); + + lock.lock(); + try { + this.cursorItem = cursorItem; + } finally { + lock.unlock(); + } + + if (!sendPacket) return; + sendPacketToViewers(SetSlotPacket.createCursorPacket(cursorItem)); } @Override - protected void UNSAFE_itemInsert(int slot, @NotNull ItemStack itemStack, boolean sendPacket) { - final EquipmentSlot equipmentSlot = switch (slot) { - case HELMET_SLOT -> EquipmentSlot.HELMET; - case CHESTPLATE_SLOT -> EquipmentSlot.CHESTPLATE; - case LEGGINGS_SLOT -> EquipmentSlot.LEGGINGS; - case BOOTS_SLOT -> EquipmentSlot.BOOTS; - case OFFHAND_SLOT -> EquipmentSlot.OFF_HAND; - default -> slot == player.getHeldSlot() ? EquipmentSlot.MAIN_HAND : null; - }; - if (equipmentSlot != null) { + public void updateSlot(int slot, @NotNull ItemStack itemStack) { + SetSlotPacket defaultPacket = new SetSlotPacket(getWindowId(), 0, (short) PlayerInventoryUtils.minestomToProtocol(slot), itemStack); + + for (Player player : getViewers()) { + Inventory open = player.getOpenInventory(); + if (open != null && slot >= 0 && slot < INNER_SIZE) { + player.sendPacket(new SetSlotPacket(open.getWindowId(), 0, (short) PlayerInventoryUtils.minestomToProtocol(slot, open.getSize()), itemStack)); + } else if (open == null || slot == OFF_HAND_SLOT) { + player.sendPacket(defaultPacket); + } + + var equipmentSlot = fromSlotIndex(slot, player.getHeldSlot()); + if (equipmentSlot == null) continue; + + player.syncEquipment(equipmentSlot, itemStack); + } + } + + @Override + public void update(@NotNull Player player) { + ItemStack[] local = getItemStacks(); + ItemStack[] mapped = new ItemStack[getSize()]; + + for (int slot = 0; slot < getSize(); slot++) { + mapped[PlayerInventoryUtils.minestomToProtocol(slot)] = local[slot]; + } + + player.sendPacket(new WindowItemsPacket(getWindowId(), 0, List.of(mapped), getCursorItem())); + } + + @Override + protected void UNSAFE_itemInsert(int slot, @NotNull ItemStack itemStack) { + for (var player : getViewers()) { + final EquipmentSlot equipmentSlot = fromSlotIndex(slot, player.getHeldSlot()); + if (equipmentSlot == null) continue; + EntityEquipEvent entityEquipEvent = new EntityEquipEvent(player, itemStack, equipmentSlot); EventDispatcher.call(entityEquipEvent); itemStack = entityEquipEvent.getEquippedItem(); } - this.itemStacks[slot] = itemStack; - if (sendPacket) { - // Sync equipment - if (equipmentSlot != null) this.player.syncEquipment(equipmentSlot); - // Refresh slot - sendSlotRefresh((short) convertToPacketSlot(slot), itemStack); - } - } - - /** - * Refreshes an inventory slot. - * - * @param slot the packet slot, - * see {@link net.minestom.server.utils.inventory.PlayerInventoryUtils#convertToPacketSlot(int)} - * @param itemStack the item stack in the slot - */ - protected void sendSlotRefresh(short slot, ItemStack itemStack) { - var openInventory = player.getOpenInventory(); - if (openInventory != null && slot >= OFFSET && slot < OFFSET + INNER_INVENTORY_SIZE) { - this.player.sendPacket(new SetSlotPacket(openInventory.getWindowId(), 0, (short) (slot + openInventory.getSize() - OFFSET), itemStack)); - } else if (openInventory == null || slot == OFFHAND_SLOT) { - this.player.sendPacket(new SetSlotPacket((byte) 0, 0, slot, itemStack)); - } - } - - /** - * Gets a {@link WindowItemsPacket} with all the items in the inventory. - * - * @return a {@link WindowItemsPacket} with inventory items - */ - private WindowItemsPacket createWindowItemsPacket() { - ItemStack[] convertedSlots = new ItemStack[INVENTORY_SIZE]; - for (int i = 0; i < itemStacks.length; i++) { - final int slot = convertToPacketSlot(i); - convertedSlots[slot] = itemStacks[i]; - } - return new WindowItemsPacket((byte) 0, 0, List.of(convertedSlots), cursorItem); + super.UNSAFE_itemInsert(slot, itemStack); } @Override - public boolean leftClick(@NotNull Player player, int slot) { - final int convertedSlot = convertPlayerInventorySlot(slot, OFFSET); - final ItemStack cursor = getCursorItem(); - final ItemStack clicked = getItemStack(convertedSlot); - final InventoryClickResult clickResult = clickProcessor.leftClick(player, this, convertedSlot, clicked, cursor); - if (clickResult.isCancel()) { - update(); - return false; + public void clear() { + lock.lock(); + try { + super.clear(); + + for (var player : getViewers()) { + player.sendPacketToViewersAndSelf(player.getEquipmentsPacket()); + } + } finally { + lock.unlock(); } - setItemStack(convertedSlot, clickResult.getClicked()); - setCursorItem(clickResult.getCursor()); - callClickEvent(player, null, convertedSlot, ClickType.LEFT_CLICK, clicked, cursor); - return true; } @Override - public boolean rightClick(@NotNull Player player, int slot) { - final int convertedSlot = convertPlayerInventorySlot(slot, OFFSET); - final ItemStack cursor = getCursorItem(); - final ItemStack clicked = getItemStack(convertedSlot); - final InventoryClickResult clickResult = clickProcessor.rightClick(player, this, convertedSlot, clicked, cursor); - if (clickResult.isCancel()) { - update(); - return false; - } - setItemStack(convertedSlot, clickResult.getClicked()); - setCursorItem(clickResult.getCursor()); - callClickEvent(player, null, convertedSlot, ClickType.RIGHT_CLICK, clicked, cursor); - return true; + public @Nullable Click.Result handleClick(@NotNull Player player, @NotNull Click.Info info) { + var processor = ClickProcessors.standard( + (getter, item, slot) -> { + List slots = new ArrayList<>(); + + var 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); + } + + public @NotNull ItemStack getEquipment(@NotNull EquipmentSlot slot, int heldSlot) { + return getItemStack(getSlotIndex(slot, heldSlot)); + } + + public void setEquipment(@NotNull EquipmentSlot slot, int heldSlot, @NotNull ItemStack newValue) { + setItemStack(getSlotIndex(slot, heldSlot), newValue); } @Override - public boolean middleClick(@NotNull Player player, int slot) { - // TODO - update(); - return false; + public @NotNull T addItemStack(@NotNull ItemStack itemStack, @NotNull TransactionOption option) { + return processItemStack(itemStack, TransactionType.add(FILL_ADD_SLOTS, AIR_ADD_SLOTS), option); } @Override - public boolean drop(@NotNull Player player, boolean all, int slot, int button) { - final int convertedSlot = convertPlayerInventorySlot(slot, OFFSET); - final ItemStack cursor = getCursorItem(); - final boolean outsideDrop = slot == -999; - final ItemStack clicked = outsideDrop ? ItemStack.AIR : getItemStack(convertedSlot); - final InventoryClickResult clickResult = clickProcessor.drop(player, this, - all, convertedSlot, button, clicked, cursor); - if (clickResult.isCancel()) { - update(); - return false; - } - final ItemStack resultClicked = clickResult.getClicked(); - if (resultClicked != null && !outsideDrop) { - setItemStack(convertedSlot, resultClicked); - } - setCursorItem(clickResult.getCursor()); - return true; + public @NotNull T takeItemStack(@NotNull ItemStack itemStack, @NotNull TransactionOption option) { + return processItemStack(itemStack, TransactionType.take(TAKE_SLOTS), option); } - @Override - public boolean shiftClick(@NotNull Player player, int slot) { - final int convertedSlot = convertPlayerInventorySlot(slot, OFFSET); - final ItemStack cursor = getCursorItem(); - final ItemStack clicked = getItemStack(convertedSlot); - final boolean hotBarClick = convertSlot(slot, OFFSET) < 9; - final int start = hotBarClick ? 9 : 0; - final int end = hotBarClick ? getSize() - 9 : 8; - final InventoryClickResult clickResult = clickProcessor.shiftClick( - this, this, - start, end, 1, - player, convertedSlot, clicked, cursor); - if (clickResult.isCancel()) { - update(); - return false; - } - setItemStack(convertedSlot, clickResult.getClicked()); - setCursorItem(clickResult.getCursor()); - update(); // FIXME: currently not properly client-predicted - return true; - } - - @Override - public boolean changeHeld(@NotNull Player player, int slot, int key) { - final int convertedKey = key == 40 ? OFFHAND_SLOT : key; - final ItemStack cursorItem = getCursorItem(); - if (!cursorItem.isAir()) return false; - final int convertedSlot = convertPlayerInventorySlot(slot, OFFSET); - final ItemStack heldItem = getItemStack(convertedKey); - final ItemStack clicked = getItemStack(convertedSlot); - final InventoryClickResult clickResult = clickProcessor.changeHeld(player, this, convertedSlot, convertedKey, clicked, heldItem); - if (clickResult.isCancel()) { - update(); - return false; - } - setItemStack(convertedSlot, clickResult.getClicked()); - setItemStack(convertedKey, clickResult.getCursor()); - callClickEvent(player, null, convertedSlot, ClickType.CHANGE_HELD, clicked, cursorItem); - return true; - } - - @Override - public boolean dragging(@NotNull Player player, int slot, int button) { - final ItemStack cursor = getCursorItem(); - final ItemStack clicked = slot != -999 ? getItemStackFromPacketSlot(slot) : ItemStack.AIR; - final InventoryClickResult clickResult = clickProcessor.dragging(player, this, - convertPlayerInventorySlot(slot, OFFSET), button, clicked, cursor); - if (clickResult == null || clickResult.isCancel()) { - update(); - return false; - } - setCursorItem(clickResult.getCursor()); - update(); // FIXME: currently not properly client-predicted - return true; - } - - @Override - public boolean doubleClick(@NotNull Player player, int slot) { - final int convertedSlot = convertPlayerInventorySlot(slot, OFFSET); - final ItemStack cursor = getCursorItem(); - final ItemStack clicked = getItemStack(convertedSlot); - final InventoryClickResult clickResult = clickProcessor.doubleClick(this, this, player, convertedSlot, clicked, cursor); - if (clickResult.isCancel()) { - update(); - return false; - } - setCursorItem(clickResult.getCursor()); - update(); // FIXME: currently not properly client-predicted - return true; - } - - private void setItemStackFromPacketSlot(int slot, @NotNull ItemStack itemStack) { - final int convertedSlot = convertPlayerInventorySlot(slot, OFFSET); - setItemStack(convertedSlot, itemStack); - } - - private ItemStack getItemStackFromPacketSlot(int slot) { - final int convertedSlot = convertPlayerInventorySlot(slot, OFFSET); - return itemStacks[convertedSlot]; - } } diff --git a/src/main/java/net/minestom/server/inventory/TransactionOperator.java b/src/main/java/net/minestom/server/inventory/TransactionOperator.java new file mode 100644 index 000000000..df298e6fe --- /dev/null +++ b/src/main/java/net/minestom/server/inventory/TransactionOperator.java @@ -0,0 +1,116 @@ +package net.minestom.server.inventory; + +import it.unimi.dsi.fastutil.Pair; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.StackingRule; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.BiPredicate; + +/** + * A transaction operator is a simpler operation that takes two items and returns two items. + *
+ * This allows a significant amount of logic reuse, since many operations are just the {@link #flip(TransactionOperator) flipped} + * version of others. + */ +public interface TransactionOperator { + + /** + * Creates a new operator that filters the given one using the provided predicate + */ + static @NotNull TransactionOperator filter(@NotNull TransactionOperator operator, @NotNull BiPredicate predicate) { + return (left, right) -> predicate.test(left, right) ? operator.apply(left, right) : null; + } + + /** + * Creates a new operator that flips the left and right before providing it to the given operator. + */ + static @NotNull TransactionOperator flip(@NotNull TransactionOperator operator) { + return (left, right) -> { + var pair = operator.apply(right, left); + return pair != null ? Pair.of(pair.right(), pair.left()) : null; + }; + } + + /** + * Provides operators that try to stack up to the provided number of items to the left. + */ + static @NotNull TransactionOperator stackLeftN(int count) { + return (left, right) -> { + final StackingRule rule = StackingRule.get(); + + // Quick exit if the right is air (nothing can be transferred anyway) + // If the left is air then we know it can be transferred, but it can also be transferred if they're stackable + // and left isn't full, even if left isn't air. + if (right.isAir() || (!left.isAir() && !(rule.canBeStacked(left, right) && rule.getAmount(left) < rule.getMaxSize(left)))) { + return null; + } + + int leftAmount = left.isAir() ? 0 : rule.getAmount(left); + int rightAmount = rule.getAmount(right); + + int addedAmount = Math.min(Math.min(rightAmount, count), rule.getMaxSize(left) - leftAmount); + + if (addedAmount == 0) return null; + + return Pair.of(rule.apply(left.isAir() ? right : left, leftAmount + addedAmount), rule.apply(right, rightAmount - addedAmount)); + }; + } + + /** + * Stacks as many items to the left as possible, including if the left is an air item.
+ * This will not swap the items if they are of different types. + */ + TransactionOperator STACK_LEFT = (left, right) -> { + final StackingRule rule = StackingRule.get(); + + // Quick exit if the right is air (nothing can be transferred anyway) + // If the left is air then we know it can be transferred, but it can also be transferred if they're stackable + // and left isn't full, even if left isn't air. + if (right.isAir() || (!left.isAir() && !(rule.canBeStacked(left, right) && rule.getAmount(left) < rule.getMaxSize(left)))) { + return null; + } + + int leftAmount = left.isAir() ? 0 : rule.getAmount(left); + int rightAmount = rule.getAmount(right); + + int addedAmount = Math.min(rightAmount, rule.getMaxSize(left) - leftAmount); + + return Pair.of(rule.apply(left.isAir() ? right : left, leftAmount + addedAmount), rule.apply(right, rightAmount - addedAmount)); + }; + + /** + * Stacks as many items to the right as possible. This is the flipped version of {@link #STACK_LEFT}. + */ + TransactionOperator STACK_RIGHT = flip(STACK_LEFT); + + /** + * Takes as many items as possible from both stacks, if the given items are stackable. + * This is a symmetric operation. + */ + TransactionOperator TAKE = (left, right) -> { + final StackingRule rule = StackingRule.get(); + + if (right.isAir() || !rule.canBeStacked(left, right)) { + return null; + } + + int leftAmount = rule.getAmount(left); + int rightAmount = rule.getAmount(right); + + int subtracted = Math.min(leftAmount, rightAmount); + + return Pair.of(rule.apply(left, leftAmount - subtracted), rule.apply(right, rightAmount - subtracted)); + }; + + /** + * Applies this operation to the two given items. + * They are unnamed as to abstract the operations performed from inventories. + * @param left the "left" item + * @param right the "right" item + * @return the resulting pair, or null to indicate no changes + */ + @Nullable Pair apply(@NotNull ItemStack left, @NotNull ItemStack right); + +} diff --git a/src/main/java/net/minestom/server/inventory/TransactionOption.java b/src/main/java/net/minestom/server/inventory/TransactionOption.java index 577784ebd..4fe1417ca 100644 --- a/src/main/java/net/minestom/server/inventory/TransactionOption.java +++ b/src/main/java/net/minestom/server/inventory/TransactionOption.java @@ -1,32 +1,30 @@ package net.minestom.server.inventory; +import it.unimi.dsi.fastutil.Pair; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import net.minestom.server.item.ItemStack; import org.jetbrains.annotations.NotNull; -import java.util.Map; - @FunctionalInterface public interface TransactionOption { /** - * Place as much as the item as possible. - *

- * The remaining, can be air. + * Performs as much of the operation as is possible. + * Returns the remaining item in the operation (can be air). */ TransactionOption ALL = (inventory, result, itemChangesMap) -> { - itemChangesMap.forEach(inventory::safeItemInsert); + itemChangesMap.forEach(inventory::setItemStack); return result; }; /** - * Only place the item if can be fully added. - *

- * Returns true if the item has been added, false if nothing changed. + * Performs the operation atomically (only if the operation resulted in air), returning whether or not the operation + * was performed. */ TransactionOption ALL_OR_NOTHING = (inventory, result, itemChangesMap) -> { if (result.isAir()) { // Item can be fully placed inside the inventory, do so - itemChangesMap.forEach(inventory::safeItemInsert); + itemChangesMap.forEach(inventory::setItemStack); return true; } else { // Inventory cannot accept the item fully @@ -35,20 +33,14 @@ public interface TransactionOption { }; /** - * Loop through the inventory items without changing anything. - *

- * Returns true if the item can be fully added, false otherwise. + * Discards the result of the operation, returning whether or not the operation could have finished. */ TransactionOption DRY_RUN = (inventory, result, itemChangesMap) -> result.isAir(); - @NotNull T fill(@NotNull AbstractInventory inventory, - @NotNull ItemStack result, - @NotNull Map<@NotNull Integer, @NotNull ItemStack> itemChangesMap); + @NotNull T fill(@NotNull Inventory inventory, @NotNull ItemStack result, @NotNull Int2ObjectMap itemChangesMap); - default @NotNull T fill(@NotNull TransactionType type, - @NotNull AbstractInventory inventory, - @NotNull ItemStack itemStack) { - var pair = type.process(inventory, itemStack); - return fill(inventory, pair.left(), pair.right()); + default @NotNull T fill(@NotNull TransactionType type, @NotNull Inventory inventory, @NotNull ItemStack itemStack) { + Pair> result = type.process(itemStack, inventory::getItemStack); + return fill(inventory, result.left(), result.right()); } } diff --git a/src/main/java/net/minestom/server/inventory/TransactionType.java b/src/main/java/net/minestom/server/inventory/TransactionType.java index 253b4a4e3..2ab3e7c3d 100644 --- a/src/main/java/net/minestom/server/inventory/TransactionType.java +++ b/src/main/java/net/minestom/server/inventory/TransactionType.java @@ -1,124 +1,86 @@ package net.minestom.server.inventory; import it.unimi.dsi.fastutil.Pair; +import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import net.minestom.server.item.ItemStack; -import net.minestom.server.item.StackingRule; import org.jetbrains.annotations.NotNull; -import java.util.Map; +import java.util.List; +import java.util.function.IntFunction; /** - * Represents a type of transaction that you can apply to an {@link AbstractInventory}. + * Represents a type of transaction that you can apply to an {@link Inventory}. */ -@FunctionalInterface public interface TransactionType { + /** + * Applies a transaction operator to a given list of slots, turning it into a TransactionType. + */ + static @NotNull TransactionType general(@NotNull TransactionOperator operator, @NotNull List slots) { + return (item, getter) -> { + Int2ObjectMap map = new Int2ObjectArrayMap<>(); + for (int slot : slots) { + ItemStack slotItem = getter.apply(slot); + + Pair result = operator.apply(slotItem, item); + if (result == null) continue; + + map.put(slot, result.first()); + item = result.second(); + } + + return Pair.of(item, map); + }; + } + + /** + * Joins two transaction types consecutively. + * This will use the same getter in both cases, so ensure that any potential overlap between the transaction types + * will not result in unexpected behaviour (e.g. item duping). + */ + static @NotNull TransactionType join(@NotNull TransactionType first, @NotNull TransactionType second) { + return (item, getter) -> { + // Calculate results + Pair> f = first.process(item, getter); + Pair> s = second.process(f.left(), getter); + + // Join results + Int2ObjectMap map = new Int2ObjectArrayMap<>(); + map.putAll(f.right()); + map.putAll(s.right()); + return Pair.of(s.left(), map); + }; + } + /** * Adds an item to the inventory. * Can either take an air slot or be stacked. + * + * @param fill the list of slots that will be added to if they already have some of the item in it + * @param air the list of slots that will be added to if they have air (may be different from {@code fill}). */ - TransactionType ADD = (inventory, itemStack, slotPredicate, start, end, step) -> { - Int2ObjectMap itemChangesMap = new Int2ObjectOpenHashMap<>(); - final StackingRule stackingRule = StackingRule.get(); - // Check filled slot (not air) - for (int i = start; i < end; i += step) { - ItemStack inventoryItem = inventory.getItemStack(i); - if (inventoryItem.isAir()) { - continue; - } - if (stackingRule.canBeStacked(itemStack, inventoryItem)) { - final int itemAmount = stackingRule.getAmount(inventoryItem); - final int maxSize = stackingRule.getMaxSize(inventoryItem); - if (itemAmount >= maxSize) continue; - if (!slotPredicate.test(i, inventoryItem)) { - // Cancelled transaction - continue; - } - - final int itemStackAmount = stackingRule.getAmount(itemStack); - final int totalAmount = itemStackAmount + itemAmount; - if (!stackingRule.canApply(itemStack, totalAmount)) { - // Slot cannot accept the whole item, reduce amount to 'itemStack' - itemChangesMap.put(i, stackingRule.apply(inventoryItem, maxSize)); - itemStack = stackingRule.apply(itemStack, totalAmount - maxSize); - } else { - // Slot can accept the whole item - itemChangesMap.put(i, stackingRule.apply(inventoryItem, totalAmount)); - itemStack = stackingRule.apply(itemStack, 0); - break; - } - } - } - // Check air slot to fill - for (int i = start; i < end; i += step) { - ItemStack inventoryItem = inventory.getItemStack(i); - if (!inventoryItem.isAir()) continue; - if (!slotPredicate.test(i, inventoryItem)) { - // Cancelled transaction - continue; - } - // Fill the slot - itemChangesMap.put(i, itemStack); - itemStack = stackingRule.apply(itemStack, 0); - break; - } - return Pair.of(itemStack, itemChangesMap); - }; + static @NotNull TransactionType add(@NotNull List fill, @NotNull List air) { + var first = general((slotItem, extra) -> !slotItem.isAir() ? TransactionOperator.STACK_LEFT.apply(slotItem, extra) : null, fill); + var second = general((slotItem, extra) -> slotItem.isAir() ? TransactionOperator.STACK_LEFT.apply(slotItem, extra) : null, air); + return TransactionType.join(first, second); + } /** * Takes an item from the inventory. * Can either transform items to air or reduce their amount. + * @param takeSlots the ordered list of slots that will be taken from (if possible) */ - TransactionType TAKE = (inventory, itemStack, slotPredicate, start, end, step) -> { - Int2ObjectMap itemChangesMap = new Int2ObjectOpenHashMap<>(); - final StackingRule stackingRule = StackingRule.get(); - for (int i = start; i < end; i += step) { - final ItemStack inventoryItem = inventory.getItemStack(i); - if (inventoryItem.isAir()) continue; - if (stackingRule.canBeStacked(itemStack, inventoryItem)) { - if (!slotPredicate.test(i, inventoryItem)) { - // Cancelled transaction - continue; - } - - final int itemAmount = stackingRule.getAmount(inventoryItem); - final int itemStackAmount = stackingRule.getAmount(itemStack); - if (itemStackAmount < itemAmount) { - itemChangesMap.put(i, stackingRule.apply(inventoryItem, itemAmount - itemStackAmount)); - itemStack = stackingRule.apply(itemStack, 0); - break; - } - itemChangesMap.put(i, stackingRule.apply(inventoryItem, 0)); - itemStack = stackingRule.apply(itemStack, itemStackAmount - itemAmount); - if (stackingRule.getAmount(itemStack) == 0) { - itemStack = stackingRule.apply(itemStack, 0); - break; - } - } - } - return Pair.of(itemStack, itemChangesMap); - }; - - @NotNull Pair> process(@NotNull AbstractInventory inventory, - @NotNull ItemStack itemStack, - @NotNull SlotPredicate slotPredicate, - int start, int end, int step); - - default @NotNull Pair> process(@NotNull AbstractInventory inventory, - @NotNull ItemStack itemStack, - @NotNull SlotPredicate slotPredicate) { - return process(inventory, itemStack, slotPredicate, 0, inventory.getInnerSize(), 1); + static @NotNull TransactionType take(@NotNull List takeSlots) { + return general(TransactionOperator.TAKE, takeSlots); } - default @NotNull Pair> process(@NotNull AbstractInventory inventory, - @NotNull ItemStack itemStack) { - return process(inventory, itemStack, (slot, itemStack1) -> true); - } + /** + * Processes the provided item into the given inventory. + * @param itemStack the item to process + * @param inventory the inventory function; must support #get and #put operations. + * @return the remaining portion of the processed item + */ + @NotNull Pair> process(@NotNull ItemStack itemStack, @NotNull IntFunction inventory); - @FunctionalInterface - interface SlotPredicate { - boolean test(int slot, @NotNull ItemStack itemStack); - } } diff --git a/src/main/java/net/minestom/server/inventory/click/Click.java b/src/main/java/net/minestom/server/inventory/click/Click.java new file mode 100644 index 000000000..159a00e3f --- /dev/null +++ b/src/main/java/net/minestom/server/inventory/click/Click.java @@ -0,0 +1,280 @@ +package net.minestom.server.inventory.click; + +import net.minestom.server.inventory.Inventory; +import net.minestom.server.inventory.PlayerInventory; +import net.minestom.server.item.ItemStack; +import net.minestom.server.network.packet.client.play.ClientClickWindowPacket; +import net.minestom.server.utils.inventory.PlayerInventoryUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.IntFunction; + +public final class Click { + + /** + * Contains information about a click. These are equal to the packet slot IDs from the Minecraft protocol.. + * The inventory used should be known from context. + */ + public sealed interface Info { + + record Left(int slot) implements Info {} + record Right(int slot) implements Info {} + record Middle(int slot) implements Info {} // Creative only + + record LeftShift(int slot) implements Info {} + record RightShift(int slot) implements Info {} + + record Double(int slot) implements Info {} + + record LeftDrag(List slots) implements Info {} + record RightDrag(List slots) implements Info {} + record MiddleDrag(List slots) implements Info {} // Creative only + + record LeftDropCursor() implements Info {} + record RightDropCursor() implements Info {} + record MiddleDropCursor() implements Info {} + + record DropSlot(int slot, boolean all) implements Info {} + + record HotbarSwap(int hotbarSlot, int clickedSlot) implements Info {} + record OffhandSwap(int slot) implements Info {} + + record CreativeSetItem(int slot, @NotNull ItemStack item) implements Info {} + record CreativeDropItem(@NotNull ItemStack item) implements Info {} + + } + + /** + * Preprocesses click packets, turning them into {@link Info} instances for further processing. + */ + public static final class Preprocessor { + + private final List leftDrag = new ArrayList<>(); + private final List rightDrag = new ArrayList<>(); + private final List middleDrag = new ArrayList<>(); + + public void clearCache() { + leftDrag.clear(); + rightDrag.clear(); + middleDrag.clear(); + } + + /** + * Processes the provided click packet, turning it into a {@link Info}. + * This will do simple verification of the packet before sending it to {@link #process(ClientClickWindowPacket.ClickType, int, byte, boolean)}. + * + * @param packet the raw click packet + * @param isCreative whether or not the player is in creative mode (used for ignoring some actions) + * @return the information about the click, or nothing if there was no immediately usable information + */ + public @Nullable Click.Info process(@NotNull ClientClickWindowPacket packet, @NotNull Inventory inventory, boolean isCreative) { + final int originalSlot = packet.slot(); + final byte button = packet.button(); + final ClientClickWindowPacket.ClickType type = packet.clickType(); + + int slot = inventory instanceof PlayerInventory ? PlayerInventoryUtils.protocolToMinestom(originalSlot) : originalSlot; + if (originalSlot == -999) slot = -999; + + boolean creativeRequired = switch (type) { + case CLONE -> true; + case QUICK_CRAFT -> button == 8 || button == 9 || button == 10; + default -> false; + }; + + if (creativeRequired && !isCreative) return null; + + int maxSize = inventory.getSize() + (inventory instanceof PlayerInventory ? 0 : PlayerInventoryUtils.INNER_SIZE); + return process(type, slot, button, slot >= 0 && slot < maxSize); + } + + /** + * Processes a packet into click info. + * + * @param type the type of the click + * @param slot the clicked slot + * @param button the sent button + * @param valid whether or not {@code slot} fits within the inventory (may be unused, depending on click) + * @return the information about the click, or nothing if there was no immediately usable information + */ + public @Nullable Click.Info process(@NotNull ClientClickWindowPacket.ClickType type, + int slot, byte button, boolean valid) { + return switch (type) { + case PICKUP -> { + if (slot == -999) { + yield switch (button) { + case 0 -> new Info.LeftDropCursor(); + case 1 -> new Info.RightDropCursor(); + case 2 -> new Info.MiddleDropCursor(); + default -> null; + }; + } + + if (!valid) yield null; + + yield switch (button) { + case 0 -> new Info.Left(slot); + case 1 -> new Info.Right(slot); + default -> null; + }; + } + case QUICK_MOVE -> { + if (!valid) yield null; + yield button == 0 ? new Info.LeftShift(slot) : new Info.RightShift(slot); + } + case SWAP -> { + if (!valid) { + yield null; + } else if (button >= 0 && button < 9) { + yield new Info.HotbarSwap(button, slot); + } else if (button == 40) { + yield new Info.OffhandSwap(slot); + } else { + yield null; + } + } + case CLONE -> valid ? new Info.Middle(slot) : null; + case THROW -> valid ? new Info.DropSlot(slot, button == 1) : null; + case QUICK_CRAFT -> { + // Handle drag finishes + if (button == 2) { + var list = List.copyOf(leftDrag); + leftDrag.clear(); + yield new Info.LeftDrag(list); + } else if (button == 6) { + var list = List.copyOf(rightDrag); + rightDrag.clear(); + yield new Info.RightDrag(list); + } else if (button == 10) { + var list = List.copyOf(middleDrag); + middleDrag.clear(); + yield new Info.MiddleDrag(list); + } + + Consumer> tryAdd = list -> { + if (valid && !list.contains(slot)) { + list.add(slot); + } + }; + + switch (button) { + case 0 -> leftDrag.clear(); + case 4 -> rightDrag.clear(); + case 8 -> middleDrag.clear(); + + case 1 -> tryAdd.accept(leftDrag); + case 5 -> tryAdd.accept(rightDrag); + case 9 -> tryAdd.accept(middleDrag); + } + + yield null; + } + case PICKUP_ALL -> valid ? new Info.Double(slot) : null; + }; + } + + } + + public record Getter(@NotNull IntFunction main, @NotNull IntFunction player, + @NotNull ItemStack cursor, int mainSize) { + + public @NotNull ItemStack get(int slot) { + if (slot < mainSize()) { + return main.apply(slot); + } else { + return player.apply(PlayerInventoryUtils.protocolToMinestom(slot, mainSize())); + } + } + + public @NotNull Click.Setter setter() { + return new Setter(mainSize); + } + } + + public static class Setter { + + private final Map main = new HashMap<>(); + private final Map player = new HashMap<>(); + private @Nullable ItemStack cursor; + private @Nullable SideEffect sideEffect; + + private final int clickedSize; + + Setter(int clickedSize) { + this.clickedSize = clickedSize; + } + + public @NotNull Setter set(int slot, @NotNull ItemStack item) { + if (slot >= clickedSize) { + int converted = PlayerInventoryUtils.protocolToMinestom(slot, clickedSize); + return setPlayer(converted, item); + } else { + main.put(slot, item); + return this; + } + } + + public @NotNull Setter setPlayer(int slot, @NotNull ItemStack item) { + player.put(slot, item); + return this; + } + + public @NotNull Setter cursor(@Nullable ItemStack newCursorItem) { + this.cursor = newCursorItem; + return this; + } + + public @NotNull Setter sideEffects(@Nullable SideEffect sideEffect) { + this.sideEffect = sideEffect; + return this; + } + + public @NotNull Click.Result build() { + return new Result(main, player, cursor, sideEffect); + } + + } + + /** + * Stores changes that occurred or will occur as the result of a click. + * @param changes the map of changes that will occur to the inventory + * @param playerInventoryChanges the map of changes that will occur to the player inventory + * @param newCursorItem the player's cursor item after this click. Null indicates no change + * @param sideEffects the side effects of this click + */ + public record Result(@NotNull Map changes, @NotNull Map playerInventoryChanges, + @Nullable ItemStack newCursorItem, @Nullable Click.SideEffect sideEffects) { + + public static @NotNull Result nothing() { + return new Result(Map.of(), Map.of(), null, null); + } + + public Result { + changes = Map.copyOf(changes); + playerInventoryChanges = Map.copyOf(playerInventoryChanges); + } + + } + + /** + * Represents side effects that may occur as the result of an inventory click. + */ + public sealed interface SideEffect { + + record DropFromPlayer(@NotNull List items) implements SideEffect { + + public DropFromPlayer { + items = List.copyOf(items); + } + + public DropFromPlayer(@NotNull ItemStack @NotNull ... items) { + this(List.of(items)); + } + } + } +} diff --git a/src/main/java/net/minestom/server/inventory/click/ClickProcessors.java b/src/main/java/net/minestom/server/inventory/click/ClickProcessors.java new file mode 100644 index 000000000..ec38f5b77 --- /dev/null +++ b/src/main/java/net/minestom/server/inventory/click/ClickProcessors.java @@ -0,0 +1,219 @@ +package net.minestom.server.inventory.click; + +import it.unimi.dsi.fastutil.Pair; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import net.minestom.server.inventory.TransactionOperator; +import net.minestom.server.inventory.TransactionType; +import net.minestom.server.item.ItemStack; +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.List; +import java.util.function.BiFunction; +import java.util.stream.IntStream; + +/** + * Provides standard implementations of most click functions. + */ +public final class ClickProcessors { + + private static final @NotNull StackingRule RULE = StackingRule.get(); + + public static @NotNull Click.Result leftClick(int slot, @NotNull Click.Getter getter) { + ItemStack cursor = getter.cursor(); + ItemStack clickedItem = getter.get(slot); + + Pair pair = TransactionOperator.STACK_LEFT.apply(clickedItem, cursor); + if (pair != null) { // Stackable items, combine their counts + return getter.setter().set(slot, pair.left()).cursor(pair.right()).build(); + } else if (!RULE.canBeStacked(cursor, clickedItem)) { // If they're unstackable, switch them + return getter.setter().set(slot, cursor).cursor(clickedItem).build(); + } else { + return Click.Result.nothing(); + } + } + + public static @NotNull Click.Result rightClick(int slot, @NotNull Click.Getter getter) { + ItemStack cursor = getter.cursor(); + ItemStack clickedItem = getter.get(slot); + + if (cursor.isAir() && clickedItem.isAir()) return Click.Result.nothing(); // Both are air, no changes + + if (cursor.isAir()) { // Take half (rounded up) of the clicked item + int newAmount = (int) Math.ceil(RULE.getAmount(clickedItem) / 2d); + Pair cursorSlot = TransactionOperator.stackLeftN(newAmount).apply(cursor, clickedItem); + return cursorSlot == null ? Click.Result.nothing() : + getter.setter().cursor(cursorSlot.left()).set(slot, cursorSlot.right()).build(); + } else if (clickedItem.isAir() || RULE.canBeStacked(clickedItem, cursor)) { // Can add, transfer one over + Pair slotCursor = TransactionOperator.stackLeftN(1).apply(clickedItem, cursor); + return slotCursor == null ? Click.Result.nothing() : + getter.setter().set(slot, slotCursor.left()).cursor(slotCursor.right()).build(); + } else { // Two existing of items of different types, so switch + return getter.setter().cursor(clickedItem).set(slot, cursor).build(); + } + } + + public static @NotNull Click.Result middleClick(int slot, @NotNull Click.Getter getter) { + var item = getter.get(slot); + if (getter.cursor().isAir() && !item.isAir()) { + return getter.setter().cursor(RULE.apply(item, RULE.getMaxSize(item))).build(); + } else { + return Click.Result.nothing(); + } + } + + public static @NotNull Click.Result shiftClick(int slot, @NotNull List slots, @NotNull Click.Getter getter) { + ItemStack clicked = getter.get(slot); + + slots = new ArrayList<>(slots); + slots.removeIf(i -> i == slot); + + Pair> result = TransactionType.add(slots, slots).process(clicked, getter::get); + Click.Setter setter = getter.setter(); + result.right().forEach(setter::set); + + return !result.left().equals(clicked) ? + setter.set(slot, result.left()).build() : + setter.build(); + } + + public static @NotNull Click.Result doubleClick(@NotNull List slots, @NotNull Click.Getter getter) { + var cursor = getter.cursor(); + if (cursor.isAir()) Click.Result.nothing(); + + var unstacked = TransactionType.general(TransactionOperator.filter(TransactionOperator.STACK_RIGHT, (left, right) -> RULE.getAmount(left) < RULE.getMaxSize(left)), slots); + var stacked = TransactionType.general(TransactionOperator.filter(TransactionOperator.STACK_RIGHT, (left, right) -> RULE.getAmount(left) == RULE.getMaxSize(left)), slots); + + Pair> result = TransactionType.join(unstacked, stacked).process(cursor, getter::get); + Click.Setter setter = getter.setter(); + result.right().forEach(setter::set); + + return !result.left().equals(cursor) ? + setter.cursor(result.left()).build() : + setter.build(); + } + + public static @NotNull Click.Result dragClick(int countPerSlot, @NotNull List slots, @NotNull Click.Getter getter) { + var cursor = getter.cursor(); + if (cursor.isAir()) return Click.Result.nothing(); + + Pair> result = TransactionType.general(TransactionOperator.stackLeftN(countPerSlot), slots).process(cursor, getter::get); + Click.Setter setter = getter.setter(); + result.right().forEach(setter::set); + + return !result.left().equals(cursor) ? + setter.cursor(result.left()).build() : + setter.build(); + } + + public static @NotNull Click.Result middleDragClick(@NotNull List slots, @NotNull Click.Getter getter) { + var cursor = getter.cursor(); + + Click.Setter setter = getter.setter(); + + for (int slot : slots) { + if (getter.get(slot).isAir()) { + setter.set(slot, cursor); + } + } + + return setter.build(); + } + + public static @NotNull Click.Result dropFromCursor(int amount, @NotNull Click.Getter getter) { + var cursor = getter.cursor(); + if (cursor.isAir()) Click.Result.nothing(); // Do nothing + + var pair = TransactionOperator.stackLeftN(amount).apply(ItemStack.AIR, cursor); + if (pair == null) return Click.Result.nothing(); + + return getter.setter().cursor(pair.right()) + .sideEffects(new Click.SideEffect.DropFromPlayer(pair.left())) + .build(); + } + + public static @NotNull Click.Result dropFromSlot(int slot, int amount, @NotNull Click.Getter getter) { + var item = getter.get(slot); + if (item.isAir()) return Click.Result.nothing(); // Do nothing + + var pair = TransactionOperator.stackLeftN(amount).apply(ItemStack.AIR, item); + if (pair == null) return Click.Result.nothing(); + + return getter.setter().set(slot, pair.right()) + .sideEffects(new Click.SideEffect.DropFromPlayer(pair.left())) + .build(); + } + + /** + * Handles clicks, given a shift click provider and a double click provider.
+ * When shift clicks or double clicks need to be handled, the slots provided from the relevant handler will be + * checked in their given order.
+ * For example, double clicking will collect items of the same type as the cursor; the slots provided by the double + * click slot provider will be checked sequentially and used if they have the same type as + * @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) { + 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.LeftDrag(List slots) -> { + int cursorAmount = RULE.getAmount(getter.cursor()); + int amount = (int) Math.floor(cursorAmount / (double) slots.size()); + yield dragClick(amount, slots, getter); + } + 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.LeftDropCursor() -> dropFromCursor(getter.cursor().amount(), getter); + case Click.Info.RightDropCursor() -> dropFromCursor(1, getter); + case Click.Info.MiddleDropCursor() -> Click.Result.nothing(); + case Click.Info.HotbarSwap(int hotbarSlot, int clickedSlot) -> { + var hotbarItem = getter.player().apply(hotbarSlot); + var selectedItem = getter.get(clickedSlot); + if (hotbarItem.equals(selectedItem)) yield Click.Result.nothing(); + + yield getter.setter().setPlayer(hotbarSlot, selectedItem).set(clickedSlot, hotbarItem).build(); + } + case Click.Info.OffhandSwap(int slot) -> { + var offhandItem = getter.player().apply(PlayerInventoryUtils.OFF_HAND_SLOT); + var selectedItem = getter.get(slot); + if (offhandItem.equals(selectedItem)) yield Click.Result.nothing(); + + 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(); + }; + } + + /** + * 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, + * shift clicking an item out of an inventory can only put it in the player's inner inventory slots, and will never + * put the item anywhere else in the inventory or the player's inventory.
+ */ + @FunctionalInterface + public interface SlotSuggestor { + + /** + * Suggests slots to be used for this operation. + * @param builder the result builder + * @param item the item clicked + * @param slot the slot of the clicked item + * @return the list of slots, in order of priority, to be used for this operation + */ + @NotNull IntStream get(@NotNull Click.Getter builder, @NotNull ItemStack item, int slot); + + default @NotNull List getList(@NotNull Click.Getter builder, @NotNull ItemStack item, int slot) { + return get(builder, item, slot).boxed().toList(); + } + } + +} diff --git a/src/main/java/net/minestom/server/inventory/click/ClickType.java b/src/main/java/net/minestom/server/inventory/click/ClickType.java deleted file mode 100644 index c6fe8eda8..000000000 --- a/src/main/java/net/minestom/server/inventory/click/ClickType.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.minestom.server.inventory.click; - -public enum ClickType { - - LEFT_CLICK, - RIGHT_CLICK, - CHANGE_HELD, - - START_SHIFT_CLICK, - SHIFT_CLICK, - - START_LEFT_DRAGGING, - START_RIGHT_DRAGGING, - - LEFT_DRAGGING, - RIGHT_DRAGGING, - - END_LEFT_DRAGGING, - END_RIGHT_DRAGGING, - - START_DOUBLE_CLICK, - DOUBLE_CLICK, - - DROP - -} diff --git a/src/main/java/net/minestom/server/inventory/click/InventoryClickProcessor.java b/src/main/java/net/minestom/server/inventory/click/InventoryClickProcessor.java deleted file mode 100644 index 634a9ef2a..000000000 --- a/src/main/java/net/minestom/server/inventory/click/InventoryClickProcessor.java +++ /dev/null @@ -1,476 +0,0 @@ -package net.minestom.server.inventory.click; - -import net.minestom.server.entity.EquipmentSlot; -import net.minestom.server.entity.Player; -import net.minestom.server.event.EventDispatcher; -import net.minestom.server.event.inventory.InventoryClickEvent; -import net.minestom.server.event.inventory.InventoryPreClickEvent; -import net.minestom.server.inventory.AbstractInventory; -import net.minestom.server.inventory.Inventory; -import net.minestom.server.inventory.PlayerInventory; -import net.minestom.server.inventory.TransactionType; -import net.minestom.server.inventory.condition.InventoryCondition; -import net.minestom.server.inventory.condition.InventoryConditionResult; -import net.minestom.server.item.ItemStack; -import net.minestom.server.item.Material; -import net.minestom.server.item.StackingRule; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.BiFunction; - -@ApiStatus.Internal -public final class InventoryClickProcessor { - // Dragging maps - private final Map> leftDraggingMap = new ConcurrentHashMap<>(); - private final Map> rightDraggingMap = new ConcurrentHashMap<>(); - - public @NotNull InventoryClickResult leftClick(@NotNull Player player, @NotNull AbstractInventory inventory, - int slot, - @NotNull ItemStack clicked, @NotNull ItemStack cursor) { - final var result = startCondition(player, inventory, slot, ClickType.LEFT_CLICK, clicked, cursor); - if (result.isCancel()) return result; - clicked = result.getClicked(); - cursor = result.getCursor(); - final StackingRule rule = StackingRule.get(); - if (rule.canBeStacked(cursor, clicked)) { - // Try to stack items - final int totalAmount = rule.getAmount(cursor) + rule.getAmount(clicked); - final int maxSize = rule.getMaxSize(cursor); - if (!rule.canApply(clicked, totalAmount)) { - // Size is too big, stack as much as possible into clicked - result.setCursor(rule.apply(cursor, totalAmount - maxSize)); - result.setClicked(rule.apply(clicked, maxSize)); - } else { - // Merge cursor item clicked - result.setCursor(rule.apply(cursor, 0)); - result.setClicked(rule.apply(clicked, totalAmount)); - } - } else { - // Items are not compatible, swap them - result.setCursor(clicked); - result.setClicked(cursor); - } - return result; - } - - public @NotNull InventoryClickResult rightClick(@NotNull Player player, @NotNull AbstractInventory inventory, - int slot, - @NotNull ItemStack clicked, @NotNull ItemStack cursor) { - final var result = startCondition(player, inventory, slot, ClickType.RIGHT_CLICK, clicked, cursor); - if (result.isCancel()) return result; - clicked = result.getClicked(); - cursor = result.getCursor(); - final StackingRule rule = StackingRule.get(); - if (rule.canBeStacked(clicked, cursor)) { - // Items can be stacked - final int amount = rule.getAmount(clicked) + 1; - if (!rule.canApply(clicked, amount)) { - // Size too large, stop here - return result; - } else { - // Add 1 to clicked - result.setCursor(rule.apply(cursor, operand -> operand - 1)); - result.setClicked(rule.apply(clicked, amount)); - } - } else { - // Items cannot be stacked - if (cursor.isAir()) { - // Take half of clicked - final int amount = (int) Math.ceil((double) rule.getAmount(clicked) / 2d); - result.setCursor(rule.apply(clicked, amount)); - result.setClicked(rule.apply(clicked, operand -> operand / 2)); - } else { - if (clicked.isAir()) { - // Put 1 to clicked - result.setCursor(rule.apply(cursor, operand -> operand - 1)); - result.setClicked(rule.apply(cursor, 1)); - } else { - // Swap items - result.setCursor(clicked); - result.setClicked(cursor); - } - } - } - return result; - } - - public @NotNull InventoryClickResult changeHeld(@NotNull Player player, @NotNull AbstractInventory inventory, - int slot, int key, - @NotNull ItemStack clicked, @NotNull ItemStack cursor) { - // Verify the clicked item - InventoryClickResult clickResult = startCondition(player, inventory, slot, ClickType.CHANGE_HELD, clicked, cursor); - if (clickResult.isCancel()) return clickResult; - // Verify the destination (held bar) - clickResult = startCondition(player, player.getInventory(), key, ClickType.CHANGE_HELD, clicked, cursor); - if (clickResult.isCancel()) return clickResult; - // Swap items - clickResult.setClicked(cursor); - clickResult.setCursor(clicked); - return clickResult; - } - - public @NotNull InventoryClickResult shiftClick(@NotNull AbstractInventory inventory, @NotNull AbstractInventory targetInventory, - int start, int end, int step, - @NotNull Player player, int slot, - @NotNull ItemStack clicked, @NotNull ItemStack cursor) { - InventoryClickResult clickResult = startCondition(player, inventory, slot, ClickType.START_SHIFT_CLICK, clicked, cursor); - if (clickResult.isCancel()) return clickResult; - if (clicked.isAir()) return clickResult.cancelled(); - - // Handle armor equip - if (inventory instanceof PlayerInventory && targetInventory instanceof PlayerInventory) { - final Material material = clicked.material(); - final EquipmentSlot equipmentSlot = material.registry().equipmentSlot(); - if (equipmentSlot != null) { - // Shift-click equip - final ItemStack currentArmor = player.getEquipment(equipmentSlot); - if (currentArmor.isAir()) { - final int armorSlot = equipmentSlot.armorSlot(); - InventoryClickResult result = startCondition(player, targetInventory, armorSlot, ClickType.SHIFT_CLICK, clicked, cursor); - if (result.isCancel()) return clickResult; - result.setClicked(ItemStack.AIR); - result.setCursor(cursor); - player.setEquipment(equipmentSlot, clicked); - return result; - } - } - } - - clickResult.setCancel(true); - final var pair = TransactionType.ADD.process(targetInventory, clicked, (index, itemStack) -> { - if (inventory == targetInventory && index == slot) - return false; // Prevent item lose/duplication - InventoryClickResult result = startCondition(player, targetInventory, index, ClickType.SHIFT_CLICK, itemStack, cursor); - if (result.isCancel()) { - return false; - } - clickResult.setCancel(false); - return true; - }, start, end, step); - - final ItemStack itemResult = pair.left(); - final Map itemChangesMap = pair.right(); - itemChangesMap.forEach((Integer s, ItemStack itemStack) -> { - targetInventory.setItemStack(s, itemStack); - callClickEvent(player, targetInventory, s, ClickType.SHIFT_CLICK, itemStack, cursor); - }); - clickResult.setClicked(itemResult); - return clickResult; - } - - public @Nullable InventoryClickResult dragging(@NotNull Player player, @Nullable AbstractInventory inventory, - int slot, int button, - @NotNull ItemStack clicked, @NotNull ItemStack cursor) { - InventoryClickResult clickResult = null; - final StackingRule stackingRule = StackingRule.get(); - if (slot != -999) { - // Add slot - if (button == 1) { - // Add left - List left = leftDraggingMap.get(player); - if (left == null) return null; - left.add(new DragData(slot, inventory)); - } else if (button == 5) { - // Add right - List right = rightDraggingMap.get(player); - if (right == null) return null; - right.add(new DragData(slot, inventory)); - } else if (button == 9) { - // Add middle - // TODO - } - } else { - // Drag instruction - if (button == 0) { - // Start left - clickResult = startCondition(player, inventory, slot, ClickType.START_LEFT_DRAGGING, clicked, cursor); - if (!clickResult.isCancel()) this.leftDraggingMap.put(player, new ArrayList<>()); - } else if (button == 2) { - // End left - final List slots = leftDraggingMap.remove(player); - if (slots == null) return null; - final int slotCount = slots.size(); - final int cursorAmount = stackingRule.getAmount(cursor); - if (slotCount > cursorAmount) return null; - for (DragData s : slots) { - // Apply each drag element - final ItemStack slotItem = s.inventory.getItemStack(s.slot); - clickResult = startCondition(player, s.inventory, s.slot, ClickType.LEFT_DRAGGING, slotItem, cursor); - if (clickResult.isCancel()) { - return clickResult; - } - } - clickResult = startCondition(player, inventory, slot, ClickType.END_LEFT_DRAGGING, clicked, cursor); - if (clickResult.isCancel()) return clickResult; - // Should be size of each defined slot (if not full) - final int slotSize = (int) ((float) cursorAmount / (float) slotCount); - // Place all waiting drag action - int finalCursorAmount = cursorAmount; - for (DragData dragData : slots) { - final var inv = dragData.inventory; - final int s = dragData.slot; - ItemStack slotItem = inv.getItemStack(s); - final int amount = stackingRule.getAmount(slotItem); - if (stackingRule.canBeStacked(cursor, slotItem)) { - if (stackingRule.canApply(slotItem, amount + slotSize)) { - // Append divided amount to slot - slotItem = stackingRule.apply(slotItem, a -> a + slotSize); - finalCursorAmount -= slotSize; - } else { - // Amount too big, fill as much as possible - final int maxSize = stackingRule.getMaxSize(cursor); - final int removedAmount = maxSize - amount; - slotItem = stackingRule.apply(slotItem, maxSize); - finalCursorAmount -= removedAmount; - } - } else if (slotItem.isAir()) { - // Slot is empty, add divided amount - slotItem = stackingRule.apply(cursor, slotSize); - finalCursorAmount -= slotSize; - } - inv.setItemStack(s, slotItem); - callClickEvent(player, inv, s, ClickType.LEFT_DRAGGING, slotItem, cursor); - } - // Update the cursor - clickResult.setCursor(stackingRule.apply(cursor, finalCursorAmount)); - } else if (button == 4) { - // Start right - clickResult = startCondition(player, inventory, slot, ClickType.START_RIGHT_DRAGGING, clicked, cursor); - if (!clickResult.isCancel()) this.rightDraggingMap.put(player, new ArrayList<>()); - } else if (button == 6) { - // End right - final List slots = rightDraggingMap.remove(player); - if (slots == null) return null; - final int size = slots.size(); - int cursorAmount = stackingRule.getAmount(cursor); - if (size > cursorAmount) return null; - // Verify if each slot can be modified (or cancel the whole drag) - for (DragData s : slots) { - final ItemStack slotItem = s.inventory.getItemStack(s.slot); - clickResult = startCondition(player, s.inventory, s.slot, ClickType.RIGHT_DRAGGING, slotItem, cursor); - if (clickResult.isCancel()) { - return clickResult; - } - } - clickResult = startCondition(player, inventory, slot, ClickType.END_RIGHT_DRAGGING, clicked, cursor); - if (clickResult.isCancel()) return clickResult; - // Place all waiting drag action - int finalCursorAmount = cursorAmount; - for (DragData dragData : slots) { - final var inv = dragData.inventory; - final int s = dragData.slot; - ItemStack slotItem = inv.getItemStack(s); - if (stackingRule.canBeStacked(cursor, slotItem)) { - // Compatible item in the slot, increment by 1 - final int amount = stackingRule.getAmount(slotItem) + 1; - if (stackingRule.canApply(slotItem, amount)) { - slotItem = stackingRule.apply(slotItem, amount); - finalCursorAmount -= 1; - } - } else if (slotItem.isAir()) { - // No item at the slot, place one - slotItem = stackingRule.apply(cursor, 1); - finalCursorAmount -= 1; - } - inv.setItemStack(s, slotItem); - callClickEvent(player, inv, s, ClickType.RIGHT_DRAGGING, slotItem, cursor); - } - // Update the cursor - clickResult.setCursor(stackingRule.apply(cursor, finalCursorAmount)); - } - } - return clickResult; - } - - public @NotNull InventoryClickResult doubleClick(@NotNull AbstractInventory clickedInventory, @NotNull AbstractInventory inventory, @NotNull Player player, int slot, - @NotNull ItemStack clicked, @NotNull ItemStack cursor) { - InventoryClickResult clickResult = startCondition(player, clickedInventory, slot, ClickType.START_DOUBLE_CLICK, clicked, cursor); - if (clickResult.isCancel()) return clickResult; - if (cursor.isAir()) return clickResult.cancelled(); - - final StackingRule rule = StackingRule.get(); - final int amount = rule.getAmount(cursor); - final int maxSize = rule.getMaxSize(cursor); - final int remainingAmount = maxSize - amount; - if (remainingAmount == 0) { - // Item is already full - return clickResult; - } - final BiFunction func = (inv, rest) -> { - var pair = TransactionType.TAKE.process(inv, rest, (index, itemStack) -> { - if (index == slot) // Prevent item lose/duplication - return false; - final InventoryClickResult result = startCondition(player, inv, index, ClickType.DOUBLE_CLICK, itemStack, cursor); - return !result.isCancel(); - }); - final ItemStack itemResult = pair.left(); - var itemChangesMap = pair.right(); - itemChangesMap.forEach((Integer s, ItemStack itemStack) -> { - inv.setItemStack(s, itemStack); - callClickEvent(player, inv, s, ClickType.DOUBLE_CLICK, itemStack, cursor); - }); - return itemResult; - }; - - ItemStack remain = rule.apply(cursor, remainingAmount); - final var playerInventory = player.getInventory(); - // Retrieve remain - if (Objects.equals(clickedInventory, inventory)) { - // Clicked inside inventory - remain = func.apply(inventory, remain); - if (!remain.isAir()) { - remain = func.apply(playerInventory, remain); - } - } else if (clickedInventory == playerInventory) { - // Clicked inside player inventory, but with another inventory open - remain = func.apply(playerInventory, remain); - if (!remain.isAir()) { - remain = func.apply(inventory, remain); - } - } else { - // Clicked inside player inventory - remain = func.apply(playerInventory, remain); - } - - // Update cursor based on the remaining - if (remain.isAir()) { - // Item has been filled - clickResult.setCursor(rule.apply(cursor, maxSize)); - } else { - final int tookAmount = remainingAmount - rule.getAmount(remain); - clickResult.setCursor(rule.apply(cursor, amount + tookAmount)); - } - return clickResult; - } - - public @NotNull InventoryClickResult drop(@NotNull Player player, @NotNull AbstractInventory inventory, - boolean all, int slot, int button, - @NotNull ItemStack clicked, @NotNull ItemStack cursor) { - final InventoryClickResult clickResult = startCondition(player, inventory, slot, ClickType.DROP, clicked, cursor); - if (clickResult.isCancel()) return clickResult; - - final StackingRule rule = StackingRule.get(); - - ItemStack resultClicked = clicked; - ItemStack resultCursor = cursor; - - if (slot == -999) { - // Click outside - if (button == 0) { - // Left (drop all) - final int amount = rule.getAmount(resultCursor); - final ItemStack dropItem = rule.apply(resultCursor, amount); - final boolean dropResult = player.dropItem(dropItem); - clickResult.setCancel(!dropResult); - if (dropResult) { - resultCursor = rule.apply(resultCursor, 0); - } - } else if (button == 1) { - // Right (drop 1) - final ItemStack dropItem = rule.apply(resultCursor, 1); - final boolean dropResult = player.dropItem(dropItem); - clickResult.setCancel(!dropResult); - if (dropResult) { - final int amount = rule.getAmount(resultCursor); - final int newAmount = amount - 1; - resultCursor = rule.apply(resultCursor, newAmount); - } - } - - } else if (!all) { - if (button == 0) { - // Drop key Q (drop 1) - final ItemStack dropItem = rule.apply(resultClicked, 1); - final boolean dropResult = player.dropItem(dropItem); - clickResult.setCancel(!dropResult); - if (dropResult) { - final int amount = rule.getAmount(resultClicked); - final int newAmount = amount - 1; - resultClicked = rule.apply(resultClicked, newAmount); - } - } else if (button == 1) { - // Ctrl + Drop key Q (drop all) - final int amount = rule.getAmount(resultClicked); - final ItemStack dropItem = rule.apply(resultClicked, amount); - final boolean dropResult = player.dropItem(dropItem); - clickResult.setCancel(!dropResult); - if (dropResult) { - resultClicked = rule.apply(resultClicked, 0); - } - } - } - - clickResult.setClicked(resultClicked); - clickResult.setCursor(resultCursor); - - return clickResult; - } - - private @NotNull InventoryClickResult startCondition(@NotNull Player player, - @Nullable AbstractInventory inventory, - int slot, @NotNull ClickType clickType, - @NotNull ItemStack clicked, @NotNull ItemStack cursor) { - final InventoryClickResult clickResult = new InventoryClickResult(clicked, cursor); - final Inventory eventInventory = inventory instanceof Inventory ? (Inventory) inventory : null; - - // Reset the didCloseInventory field - // Wait for inventory conditions + events to possibly close the inventory - player.UNSAFE_changeDidCloseInventory(false); - // InventoryPreClickEvent - { - InventoryPreClickEvent inventoryPreClickEvent = new InventoryPreClickEvent(eventInventory, player, slot, clickType, - clickResult.getClicked(), clickResult.getCursor()); - EventDispatcher.call(inventoryPreClickEvent); - clickResult.setCursor(inventoryPreClickEvent.getCursorItem()); - clickResult.setClicked(inventoryPreClickEvent.getClickedItem()); - if (inventoryPreClickEvent.isCancelled()) { - clickResult.setCancel(true); - } - } - // Inventory conditions - { - if (inventory != null) { - final List inventoryConditions = inventory.getInventoryConditions(); - if (!inventoryConditions.isEmpty()) { - for (InventoryCondition inventoryCondition : inventoryConditions) { - var result = new InventoryConditionResult(clickResult.getClicked(), clickResult.getCursor()); - inventoryCondition.accept(player, slot, clickType, result); - - clickResult.setCursor(result.getCursorItem()); - clickResult.setClicked(result.getClickedItem()); - if (result.isCancel()) { - clickResult.setCancel(true); - } - } - // Cancel the click if the inventory has been closed by Player#closeInventory within an inventory listener - if (player.didCloseInventory()) { - clickResult.setCancel(true); - player.UNSAFE_changeDidCloseInventory(false); - } - } - } - } - return clickResult; - } - - private void callClickEvent(@NotNull Player player, @Nullable AbstractInventory inventory, int slot, - @NotNull ClickType clickType, @NotNull ItemStack clicked, @NotNull ItemStack cursor) { - final Inventory eventInventory = inventory instanceof Inventory ? (Inventory) inventory : null; - EventDispatcher.call(new InventoryClickEvent(eventInventory, player, slot, clickType, clicked, cursor)); - } - - public void clearCache(@NotNull Player player) { - this.leftDraggingMap.remove(player); - this.rightDraggingMap.remove(player); - } - - private record DragData(int slot, AbstractInventory inventory) { - } -} diff --git a/src/main/java/net/minestom/server/inventory/click/InventoryClickResult.java b/src/main/java/net/minestom/server/inventory/click/InventoryClickResult.java deleted file mode 100644 index 5a5384565..000000000 --- a/src/main/java/net/minestom/server/inventory/click/InventoryClickResult.java +++ /dev/null @@ -1,43 +0,0 @@ -package net.minestom.server.inventory.click; - -import net.minestom.server.item.ItemStack; - -public final class InventoryClickResult { - private ItemStack clicked; - private ItemStack cursor; - private boolean cancel; - - public InventoryClickResult(ItemStack clicked, ItemStack cursor) { - this.clicked = clicked; - this.cursor = cursor; - } - - public ItemStack getClicked() { - return clicked; - } - - void setClicked(ItemStack clicked) { - this.clicked = clicked; - } - - public ItemStack getCursor() { - return cursor; - } - - void setCursor(ItemStack cursor) { - this.cursor = cursor; - } - - public boolean isCancel() { - return cancel; - } - - void setCancel(boolean cancel) { - this.cancel = cancel; - } - - InventoryClickResult cancelled() { - setCancel(true); - return this; - } -} diff --git a/src/main/java/net/minestom/server/inventory/condition/InventoryCondition.java b/src/main/java/net/minestom/server/inventory/condition/InventoryCondition.java deleted file mode 100644 index 121adade1..000000000 --- a/src/main/java/net/minestom/server/inventory/condition/InventoryCondition.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.minestom.server.inventory.condition; - -import net.minestom.server.entity.Player; -import net.minestom.server.inventory.AbstractInventory; -import net.minestom.server.inventory.click.ClickType; - -/** - * Can be added to any {@link AbstractInventory} - * using {@link net.minestom.server.inventory.Inventory#addInventoryCondition(InventoryCondition)} - * or {@link net.minestom.server.inventory.PlayerInventory#addInventoryCondition(InventoryCondition)} - * in order to listen to any issued clicks. - */ -@FunctionalInterface -public interface InventoryCondition { - - /** - * Called when a {@link Player} clicks in the inventory where this {@link InventoryCondition} has been added to. - * - * @param player the player who clicked in the inventory - * @param slot the slot clicked, can be -999 if the click is out of the inventory - * @param clickType the click type - * @param inventoryConditionResult the result of this callback - */ - void accept(Player player, int slot, ClickType clickType, InventoryConditionResult inventoryConditionResult); -} diff --git a/src/main/java/net/minestom/server/inventory/condition/InventoryConditionResult.java b/src/main/java/net/minestom/server/inventory/condition/InventoryConditionResult.java deleted file mode 100644 index 2399c12d8..000000000 --- a/src/main/java/net/minestom/server/inventory/condition/InventoryConditionResult.java +++ /dev/null @@ -1,41 +0,0 @@ -package net.minestom.server.inventory.condition; - -import net.minestom.server.item.ItemStack; - -/** - * Used by {@link InventoryCondition} to step in inventory click processing. - */ -public class InventoryConditionResult { - - private ItemStack clickedItem, cursorItem; - private boolean cancel; - - public InventoryConditionResult(ItemStack clickedItem, ItemStack cursorItem) { - this.clickedItem = clickedItem; - this.cursorItem = cursorItem; - } - - public ItemStack getClickedItem() { - return clickedItem; - } - - public void setClickedItem(ItemStack clickedItem) { - this.clickedItem = clickedItem; - } - - public ItemStack getCursorItem() { - return cursorItem; - } - - public void setCursorItem(ItemStack cursorItem) { - this.cursorItem = cursorItem; - } - - public boolean isCancel() { - return cancel; - } - - public void setCancel(boolean cancel) { - this.cancel = cancel; - } -} diff --git a/src/main/java/net/minestom/server/inventory/type/AnvilInventory.java b/src/main/java/net/minestom/server/inventory/type/AnvilInventory.java index 8a7c9f2c1..0f5c419dc 100644 --- a/src/main/java/net/minestom/server/inventory/type/AnvilInventory.java +++ b/src/main/java/net/minestom/server/inventory/type/AnvilInventory.java @@ -1,12 +1,12 @@ package net.minestom.server.inventory.type; import net.kyori.adventure.text.Component; -import net.minestom.server.inventory.Inventory; +import net.minestom.server.inventory.ContainerInventory; import net.minestom.server.inventory.InventoryProperty; import net.minestom.server.inventory.InventoryType; import org.jetbrains.annotations.NotNull; -public class AnvilInventory extends Inventory { +public class AnvilInventory extends ContainerInventory { private short repairCost; diff --git a/src/main/java/net/minestom/server/inventory/type/BeaconInventory.java b/src/main/java/net/minestom/server/inventory/type/BeaconInventory.java index 30551dae6..0915d1841 100644 --- a/src/main/java/net/minestom/server/inventory/type/BeaconInventory.java +++ b/src/main/java/net/minestom/server/inventory/type/BeaconInventory.java @@ -1,13 +1,13 @@ package net.minestom.server.inventory.type; import net.kyori.adventure.text.Component; -import net.minestom.server.inventory.Inventory; +import net.minestom.server.inventory.ContainerInventory; import net.minestom.server.inventory.InventoryProperty; import net.minestom.server.inventory.InventoryType; import net.minestom.server.potion.PotionEffect; import org.jetbrains.annotations.NotNull; -public class BeaconInventory extends Inventory { +public class BeaconInventory extends ContainerInventory { private short powerLevel; private PotionEffect firstPotionEffect; diff --git a/src/main/java/net/minestom/server/inventory/type/BrewingStandInventory.java b/src/main/java/net/minestom/server/inventory/type/BrewingStandInventory.java index ed0bc7d78..7dc1e68bd 100644 --- a/src/main/java/net/minestom/server/inventory/type/BrewingStandInventory.java +++ b/src/main/java/net/minestom/server/inventory/type/BrewingStandInventory.java @@ -1,12 +1,12 @@ package net.minestom.server.inventory.type; import net.kyori.adventure.text.Component; -import net.minestom.server.inventory.Inventory; +import net.minestom.server.inventory.ContainerInventory; import net.minestom.server.inventory.InventoryProperty; import net.minestom.server.inventory.InventoryType; import org.jetbrains.annotations.NotNull; -public class BrewingStandInventory extends Inventory { +public class BrewingStandInventory extends ContainerInventory { private short brewTime; private short fuelTime; diff --git a/src/main/java/net/minestom/server/inventory/type/EnchantmentTableInventory.java b/src/main/java/net/minestom/server/inventory/type/EnchantmentTableInventory.java index bd855c32a..3d0ca7473 100644 --- a/src/main/java/net/minestom/server/inventory/type/EnchantmentTableInventory.java +++ b/src/main/java/net/minestom/server/inventory/type/EnchantmentTableInventory.java @@ -1,13 +1,13 @@ package net.minestom.server.inventory.type; import net.kyori.adventure.text.Component; -import net.minestom.server.inventory.Inventory; +import net.minestom.server.inventory.ContainerInventory; import net.minestom.server.inventory.InventoryProperty; import net.minestom.server.inventory.InventoryType; import net.minestom.server.item.Enchantment; import org.jetbrains.annotations.NotNull; -public class EnchantmentTableInventory extends Inventory { +public class EnchantmentTableInventory extends ContainerInventory { private final short[] levelRequirements = new short[EnchantmentSlot.values().length]; private short seed; 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 9e357da99..92fc59a03 100644 --- a/src/main/java/net/minestom/server/inventory/type/FurnaceInventory.java +++ b/src/main/java/net/minestom/server/inventory/type/FurnaceInventory.java @@ -1,12 +1,18 @@ package net.minestom.server.inventory.type; import net.kyori.adventure.text.Component; -import net.minestom.server.inventory.Inventory; +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; -public class FurnaceInventory extends Inventory { +import java.util.stream.IntStream; + +public class FurnaceInventory extends ContainerInventory { private short remainingFuelTick; private short maximumFuelBurnTime; @@ -21,6 +27,32 @@ public class FurnaceInventory extends Inventory { 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. * diff --git a/src/main/java/net/minestom/server/inventory/type/VillagerInventory.java b/src/main/java/net/minestom/server/inventory/type/VillagerInventory.java index 048fa5813..36637d5ea 100644 --- a/src/main/java/net/minestom/server/inventory/type/VillagerInventory.java +++ b/src/main/java/net/minestom/server/inventory/type/VillagerInventory.java @@ -2,7 +2,7 @@ package net.minestom.server.inventory.type; import net.kyori.adventure.text.Component; import net.minestom.server.entity.Player; -import net.minestom.server.inventory.Inventory; +import net.minestom.server.inventory.ContainerInventory; import net.minestom.server.inventory.InventoryType; import net.minestom.server.network.packet.server.CachedPacket; import net.minestom.server.network.packet.server.play.TradeListPacket; @@ -12,7 +12,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -public class VillagerInventory extends Inventory { +public class VillagerInventory extends ContainerInventory { private final CachedPacket tradeCache = new CachedPacket(this::createTradePacket); private final List trades = new ArrayList<>(); private int villagerLevel; @@ -82,14 +82,12 @@ public class VillagerInventory extends Inventory { public void update() { super.update(); this.tradeCache.invalidate(); - sendPacketToViewers(tradeCache); } @Override - public boolean addViewer(@NotNull Player player) { - final boolean result = super.addViewer(player); - if (result) player.sendPacket(tradeCache); - return result; + public void update(@NotNull Player player) { + super.update(player); + player.sendPacket(tradeCache); } private TradeListPacket createTradePacket() { diff --git a/src/main/java/net/minestom/server/item/ItemStack.java b/src/main/java/net/minestom/server/item/ItemStack.java index 4a3ca866b..7d0beeb82 100644 --- a/src/main/java/net/minestom/server/item/ItemStack.java +++ b/src/main/java/net/minestom/server/item/ItemStack.java @@ -5,6 +5,7 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.event.HoverEventSource; import net.minestom.server.adventure.MinestomAdventure; +import net.minestom.server.inventory.ContainerInventory; import net.minestom.server.tag.Tag; import net.minestom.server.tag.TagHandler; import net.minestom.server.tag.TagReadable; @@ -20,7 +21,7 @@ import java.util.function.UnaryOperator; /** * Represents an immutable item to be placed inside {@link net.minestom.server.inventory.PlayerInventory}, - * {@link net.minestom.server.inventory.Inventory} or even on the ground {@link net.minestom.server.entity.ItemEntity}. + * {@link ContainerInventory} or even on the ground {@link net.minestom.server.entity.ItemEntity}. *

* An item stack cannot be null, {@link ItemStack#AIR} should be used instead. */ diff --git a/src/main/java/net/minestom/server/listener/BlockPlacementListener.java b/src/main/java/net/minestom/server/listener/BlockPlacementListener.java index 46980b6ac..9da15ee9a 100644 --- a/src/main/java/net/minestom/server/listener/BlockPlacementListener.java +++ b/src/main/java/net/minestom/server/listener/BlockPlacementListener.java @@ -170,7 +170,7 @@ public class BlockPlacementListener { if (playerBlockPlaceEvent.doesConsumeBlock()) { // Consume the block in the player's hand final ItemStack newUsedItem = usedItem.consume(1); - playerInventory.setItemInHand(hand, newUsedItem); + player.setItemInHand(hand, newUsedItem); } else { // Prevent invisible item on client playerInventory.update(); diff --git a/src/main/java/net/minestom/server/listener/BookListener.java b/src/main/java/net/minestom/server/listener/BookListener.java index 936a39448..1630a1391 100644 --- a/src/main/java/net/minestom/server/listener/BookListener.java +++ b/src/main/java/net/minestom/server/listener/BookListener.java @@ -5,12 +5,14 @@ import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.book.EditBookEvent; import net.minestom.server.item.ItemStack; import net.minestom.server.network.packet.client.play.ClientEditBookPacket; -import net.minestom.server.utils.inventory.PlayerInventoryUtils; public class BookListener { public static void listener(ClientEditBookPacket packet, Player player) { - int slot = PlayerInventoryUtils.convertClientInventorySlot(packet.slot()); + int slot = packet.slot(); + if (slot < 0 || slot > 8) return; + + // Do not need to convert slot as hotbar slots correspond to Minestom inventory slots ItemStack itemStack = player.getInventory().getItemStack(slot); EventDispatcher.call(new EditBookEvent(player, itemStack, packet.pages(), packet.title())); } diff --git a/src/main/java/net/minestom/server/listener/CreativeInventoryActionListener.java b/src/main/java/net/minestom/server/listener/CreativeInventoryActionListener.java index 1554fd5bf..87fd8092d 100644 --- a/src/main/java/net/minestom/server/listener/CreativeInventoryActionListener.java +++ b/src/main/java/net/minestom/server/listener/CreativeInventoryActionListener.java @@ -1,35 +1,24 @@ package net.minestom.server.listener; import net.minestom.server.entity.Player; -import net.minestom.server.inventory.PlayerInventory; +import net.minestom.server.inventory.click.Click; import net.minestom.server.item.ItemStack; import net.minestom.server.network.packet.client.play.ClientCreativeInventoryActionPacket; import net.minestom.server.utils.inventory.PlayerInventoryUtils; -import java.util.Objects; - public final class CreativeInventoryActionListener { public static void listener(ClientCreativeInventoryActionPacket packet, Player player) { if (!player.isCreative()) return; - short slot = packet.slot(); - final ItemStack item = packet.item(); - if (slot == -1) { - // Drop item - player.dropItem(item); - return; + + ItemStack item = packet.item(); + + if (packet.slot() == -1) { // -1 here indicates a drop + player.getInventory().handleClick(player, new Click.Info.CreativeDropItem(item)); } - // Bounds check - // 0 is crafting result inventory slot, ignore attempts to place into it - if (slot < 1 || slot > PlayerInventoryUtils.OFFHAND_SLOT) { - return; - } - // Set item - slot = (short) PlayerInventoryUtils.convertPlayerInventorySlot(slot, PlayerInventoryUtils.OFFSET); - PlayerInventory inventory = player.getInventory(); - if (Objects.equals(inventory.getItemStack(slot), item)) { - // Item is already present, ignore - return; - } - inventory.setItemStack(slot, item); + + int slot = PlayerInventoryUtils.protocolToMinestom(packet.slot()); + if (slot == -1) return; // -1 after conversion indicates an invalid slot + + player.getInventory().handleClick(player, new Click.Info.CreativeSetItem(slot, item)); } } diff --git a/src/main/java/net/minestom/server/listener/PlayerDiggingListener.java b/src/main/java/net/minestom/server/listener/PlayerDiggingListener.java index 9c3beec2c..231fb8139 100644 --- a/src/main/java/net/minestom/server/listener/PlayerDiggingListener.java +++ b/src/main/java/net/minestom/server/listener/PlayerDiggingListener.java @@ -10,13 +10,11 @@ import net.minestom.server.event.item.ItemUpdateStateEvent; import net.minestom.server.event.player.PlayerCancelDiggingEvent; import net.minestom.server.event.player.PlayerFinishDiggingEvent; import net.minestom.server.event.player.PlayerStartDiggingEvent; -import net.minestom.server.event.player.PlayerSwapItemEvent; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockFace; -import net.minestom.server.inventory.PlayerInventory; +import net.minestom.server.inventory.click.Click; import net.minestom.server.item.ItemStack; -import net.minestom.server.item.StackingRule; import net.minestom.server.network.packet.client.play.ClientPlayerDiggingPacket; import net.minestom.server.network.packet.server.play.AcknowledgeBlockChangePacket; import net.minestom.server.network.packet.server.play.BlockEntityDataPacket; @@ -43,13 +41,13 @@ public final class PlayerDiggingListener { if (!instance.isChunkLoaded(blockPosition)) return; diggingResult = finishDigging(player, instance, blockPosition, packet.blockFace()); } else if (status == ClientPlayerDiggingPacket.Status.DROP_ITEM_STACK) { - dropStack(player); + player.getInventory().handleClick(player, new Click.Info.DropSlot(player.getHeldSlot(), true)); } else if (status == ClientPlayerDiggingPacket.Status.DROP_ITEM) { - dropSingle(player); + player.getInventory().handleClick(player, new Click.Info.DropSlot(player.getHeldSlot(), false)); } else if (status == ClientPlayerDiggingPacket.Status.UPDATE_ITEM_STATE) { updateItemState(player); } else if (status == ClientPlayerDiggingPacket.Status.SWAP_ITEM_HAND) { - swapItemHand(player); + player.getInventory().handleClick(player, new Click.Info.OffhandSwap(player.getHeldSlot())); } // Acknowledge start/cancel/finish digging status if (diggingResult != null) { @@ -124,26 +122,6 @@ public final class PlayerDiggingListener { return false; } - private static void dropStack(Player player) { - final ItemStack droppedItemStack = player.getInventory().getItemInMainHand(); - dropItem(player, droppedItemStack, ItemStack.AIR); - } - - private static void dropSingle(Player player) { - final ItemStack handItem = player.getInventory().getItemInMainHand(); - final StackingRule stackingRule = StackingRule.get(); - final int handAmount = stackingRule.getAmount(handItem); - if (handAmount <= 1) { - // Drop the whole item without copy - dropItem(player, handItem, ItemStack.AIR); - } else { - // Drop a single item - dropItem(player, - stackingRule.apply(handItem, 1), // Single dropped item - stackingRule.apply(handItem, handAmount - 1)); // Updated hand - } - } - private static void updateItemState(Player player) { LivingEntityMeta meta = player.getLivingEntityMeta(); if (meta == null || !meta.isHandActive()) return; @@ -162,17 +140,6 @@ public final class PlayerDiggingListener { } } - private static void swapItemHand(Player player) { - final PlayerInventory inventory = player.getInventory(); - final ItemStack mainHand = inventory.getItemInMainHand(); - final ItemStack offHand = inventory.getItemInOffHand(); - PlayerSwapItemEvent swapItemEvent = new PlayerSwapItemEvent(player, offHand, mainHand); - EventDispatcher.callCancellable(swapItemEvent, () -> { - inventory.setItemInMainHand(swapItemEvent.getMainHandItem()); - inventory.setItemInOffHand(swapItemEvent.getOffHandItem()); - }); - } - private static DiggingResult breakBlock(Instance instance, Player player, Point blockPosition, Block previousBlock, BlockFace blockFace) { @@ -191,16 +158,6 @@ public final class PlayerDiggingListener { return new DiggingResult(updatedBlock, success); } - private static void dropItem(@NotNull Player player, - @NotNull ItemStack droppedItem, @NotNull ItemStack handItem) { - final PlayerInventory playerInventory = player.getInventory(); - if (player.dropItem(droppedItem)) { - playerInventory.setItemInMainHand(handItem); - } else { - playerInventory.update(); - } - } - private record DiggingResult(Block block, boolean success) { } } diff --git a/src/main/java/net/minestom/server/listener/UseItemListener.java b/src/main/java/net/minestom/server/listener/UseItemListener.java index 4ffb8dd45..93b6443e2 100644 --- a/src/main/java/net/minestom/server/listener/UseItemListener.java +++ b/src/main/java/net/minestom/server/listener/UseItemListener.java @@ -14,9 +14,8 @@ import net.minestom.server.network.packet.client.play.ClientUseItemPacket; public class UseItemListener { public static void useItemListener(ClientUseItemPacket packet, Player player) { - final PlayerInventory inventory = player.getInventory(); final Player.Hand hand = packet.hand(); - ItemStack itemStack = hand == Player.Hand.MAIN ? inventory.getItemInMainHand() : inventory.getItemInOffHand(); + ItemStack itemStack = player.getItemInHand(hand); //itemStack.onRightClick(player, hand); PlayerUseItemEvent useItemEvent = new PlayerUseItemEvent(player, hand, itemStack); EventDispatcher.call(useItemEvent); @@ -33,10 +32,10 @@ public class UseItemListener { // Equip armor with right click final EquipmentSlot equipmentSlot = material.registry().equipmentSlot(); if (equipmentSlot != null) { - final ItemStack currentlyEquipped = playerInventory.getEquipment(equipmentSlot); + final ItemStack currentlyEquipped = player.getEquipment(equipmentSlot); if (currentlyEquipped.isAir()) { - playerInventory.setEquipment(equipmentSlot, itemStack); - playerInventory.setItemInHand(hand, currentlyEquipped); + player.setEquipment(equipmentSlot, itemStack); + player.setItemInHand(hand, currentlyEquipped); } } diff --git a/src/main/java/net/minestom/server/listener/WindowListener.java b/src/main/java/net/minestom/server/listener/WindowListener.java index dfdf2e675..357bc1771 100644 --- a/src/main/java/net/minestom/server/listener/WindowListener.java +++ b/src/main/java/net/minestom/server/listener/WindowListener.java @@ -2,79 +2,29 @@ package net.minestom.server.listener; import net.minestom.server.entity.Player; import net.minestom.server.event.EventDispatcher; +import net.minestom.server.event.inventory.InventoryButtonClickEvent; import net.minestom.server.event.inventory.InventoryCloseEvent; -import net.minestom.server.inventory.AbstractInventory; import net.minestom.server.inventory.Inventory; -import net.minestom.server.inventory.PlayerInventory; -import net.minestom.server.item.ItemStack; import net.minestom.server.network.packet.client.common.ClientPongPacket; +import net.minestom.server.network.packet.client.play.ClientClickWindowButtonPacket; import net.minestom.server.network.packet.client.play.ClientClickWindowPacket; import net.minestom.server.network.packet.client.play.ClientCloseWindowPacket; import net.minestom.server.network.packet.server.common.PingPacket; -import net.minestom.server.network.packet.server.play.SetSlotPacket; public class WindowListener { public static void clickWindowListener(ClientClickWindowPacket packet, Player player) { final int windowId = packet.windowId(); - final AbstractInventory inventory = windowId == 0 ? player.getInventory() : player.getOpenInventory(); - if (inventory == null) { - // Invalid packet - return; + final Inventory inventory = windowId == 0 ? player.getInventory() : player.getOpenInventory(); + + // Prevent some invalid packets + if (inventory == null || packet.slot() == -1) return; + + var info = player.clickPreprocessor().process(packet, inventory, player.isCreative()); + if (info != null) { + inventory.handleClick(player, info); } - final short slot = packet.slot(); - final byte button = packet.button(); - final ClientClickWindowPacket.ClickType clickType = packet.clickType(); - - boolean successful = false; - - // prevent click in a non-interactive slot (why does it exist?) - if (slot == -1) { - return; - } - if (clickType == ClientClickWindowPacket.ClickType.PICKUP) { - if (button == 0) { - if (slot != -999) { - successful = inventory.leftClick(player, slot); - } else { - successful = inventory.drop(player, true, slot, button); - } - } else if (button == 1) { - if (slot != -999) { - successful = inventory.rightClick(player, slot); - } else { - successful = inventory.drop(player, false, slot, button); - } - } - } else if (clickType == ClientClickWindowPacket.ClickType.QUICK_MOVE) { - successful = inventory.shiftClick(player, slot); - } else if (clickType == ClientClickWindowPacket.ClickType.SWAP) { - successful = inventory.changeHeld(player, slot, button); - } else if (clickType == ClientClickWindowPacket.ClickType.CLONE) { - successful = player.isCreative(); - if (successful) { - setCursor(player, inventory, packet.clickedItem()); - } - } else if (clickType == ClientClickWindowPacket.ClickType.THROW) { - successful = inventory.drop(player, false, slot, button); - } else if (clickType == ClientClickWindowPacket.ClickType.QUICK_CRAFT) { - successful = inventory.dragging(player, slot, button); - } else if (clickType == ClientClickWindowPacket.ClickType.PICKUP_ALL) { - successful = inventory.doubleClick(player, slot); - } - - // Prevent ghost item when the click is cancelled - if (!successful) { - player.getInventory().update(); - if (inventory instanceof Inventory) { - ((Inventory) inventory).update(player); - } - } - - // Prevent the player from picking a ghost item in cursor - refreshCursorItem(player, inventory); - // (Why is the ping packet necessary?) player.sendPacket(new PingPacket((1 << 30) | (windowId << 16))); } @@ -85,7 +35,10 @@ public class WindowListener { public static void closeWindowListener(ClientCloseWindowPacket packet, Player player) { // if windowId == 0 then it is player's inventory, meaning that they hadn't been any open inventory packet - InventoryCloseEvent inventoryCloseEvent = new InventoryCloseEvent(player.getOpenInventory(), player); + var openInventory = player.getOpenInventory(); + if (openInventory == null) openInventory = player.getInventory(); + + InventoryCloseEvent inventoryCloseEvent = new InventoryCloseEvent(openInventory, player); EventDispatcher.call(inventoryCloseEvent); player.closeInventory(true); @@ -95,30 +48,12 @@ public class WindowListener { player.openInventory(newInventory); } - /** - * @param player the player to refresh the cursor item - * @param inventory the player open inventory, null if not any (could be player inventory) - */ - private static void refreshCursorItem(Player player, AbstractInventory inventory) { - ItemStack cursorItem; - if (inventory instanceof PlayerInventory playerInventory) { - cursorItem = playerInventory.getCursorItem(); - } else if (inventory instanceof Inventory standardInventory) { - cursorItem = standardInventory.getCursorItem(player); - } else { - throw new RuntimeException("Invalid inventory: " + inventory.getClass()); - } - final SetSlotPacket setSlotPacket = SetSlotPacket.createCursorPacket(cursorItem); - player.sendPacket(setSlotPacket); + public static void buttonClickListener(ClientClickWindowButtonPacket packet, Player player) { + var openInventory = player.getOpenInventory(); + if (openInventory == null) openInventory = player.getInventory(); + + InventoryButtonClickEvent event = new InventoryButtonClickEvent(openInventory, player, packet.buttonId()); + EventDispatcher.call(event); } - private static void setCursor(Player player, AbstractInventory inventory, ItemStack itemStack) { - if (inventory instanceof PlayerInventory playerInventory) { - playerInventory.setCursorItem(itemStack); - } else if (inventory instanceof Inventory standardInventory) { - standardInventory.setCursorItem(player, itemStack); - } else { - throw new RuntimeException("Invalid inventory: " + inventory.getClass()); - } - } } diff --git a/src/main/java/net/minestom/server/listener/manager/PacketListenerManager.java b/src/main/java/net/minestom/server/listener/manager/PacketListenerManager.java index 3a45935f8..5e15289ed 100644 --- a/src/main/java/net/minestom/server/listener/manager/PacketListenerManager.java +++ b/src/main/java/net/minestom/server/listener/manager/PacketListenerManager.java @@ -64,6 +64,7 @@ public final class PacketListenerManager { setPlayListener(ClientChatMessagePacket.class, ChatMessageListener::chatMessageListener); setPlayListener(ClientClickWindowPacket.class, WindowListener::clickWindowListener); setPlayListener(ClientCloseWindowPacket.class, WindowListener::closeWindowListener); + setPlayListener(ClientClickWindowButtonPacket.class, WindowListener::buttonClickListener); setPlayListener(ClientConfigurationAckPacket.class, PlayConfigListener::configAckListener); setPlayListener(ClientPongPacket.class, WindowListener::pong); setPlayListener(ClientEntityActionPacket.class, EntityActionListener::listener); diff --git a/src/main/java/net/minestom/server/utils/inventory/PlayerInventoryUtils.java b/src/main/java/net/minestom/server/utils/inventory/PlayerInventoryUtils.java index 5c240b75e..793753f6e 100644 --- a/src/main/java/net/minestom/server/utils/inventory/PlayerInventoryUtils.java +++ b/src/main/java/net/minestom/server/utils/inventory/PlayerInventoryUtils.java @@ -1,8 +1,19 @@ package net.minestom.server.utils.inventory; + +/** + * Minestom uses different slot IDs for player inventories as the Minecraft protocol uses a strange system (e.g. the + * crafting result is the first slot).
+ * These can be mapped 1:1 to and from protocol slots using {@link #minestomToProtocol(int)} and {@link #protocolToMinestom(int)}.
+ * + * Read about protocol slot IDs here. + */ public final class PlayerInventoryUtils { - public static final int OFFSET = 9; + public static final int INVENTORY_SIZE = 46; + public static final int INNER_SIZE = 36; + + public static final int PROTOCOL_OFFSET = 9; public static final int CRAFT_RESULT = 36; public static final int CRAFT_SLOT_1 = 37; @@ -14,21 +25,49 @@ public final class PlayerInventoryUtils { public static final int CHESTPLATE_SLOT = 42; public static final int LEGGINGS_SLOT = 43; public static final int BOOTS_SLOT = 44; - public static final int OFFHAND_SLOT = 45; + public static final int OFF_HAND_SLOT = 45; private PlayerInventoryUtils() { } /** - * Converts a packet slot to an internal one. - * - * @param slot the packet slot - * @param offset the slot count separating the up part of the inventory to the bottom part (armor/craft in PlayerInventory, inventory slots in others) - * the offset for the player inventory is {@link #OFFSET} - * @return a packet which can be use internally with Minestom + * Converts a Minestom slot ID to a Minecraft protocol slot ID.
+ * This is the inverse of {@link #protocolToMinestom(int)}. + * @param slot the internal slot ID to convert + * @return the protocol slot ID, or -1 if the given slot could not be converted */ - public static int convertPlayerInventorySlot(int slot, int offset) { + public static int minestomToProtocol(int slot) { + return switch (slot) { + case CRAFT_RESULT -> 0; + case CRAFT_SLOT_1 -> 1; + case CRAFT_SLOT_2 -> 2; + case CRAFT_SLOT_3 -> 3; + case CRAFT_SLOT_4 -> 4; + case HELMET_SLOT -> 5; + case CHESTPLATE_SLOT -> 6; + case LEGGINGS_SLOT -> 7; + case BOOTS_SLOT -> 8; + case OFF_HAND_SLOT -> OFF_HAND_SLOT; + default -> { + if (slot >= 0 && slot <= 8) { + yield slot + 36; + } else if (slot >= 9 && slot <= 35) { + yield slot; + } else { + yield -1; // Unknown slot ID + } + } + }; + } + + /** + * Converts a Minecraft protocol slot ID to a Minestom slot ID.
+ * This is the inverse of {@link #minestomToProtocol(int)}. + * @param slot the protocol slot ID to convert + * @return the Minestom slot ID, or -1 if the given slot could not be converted + */ + public static int protocolToMinestom(int slot) { return switch (slot) { case 0 -> CRAFT_RESULT; case 1 -> CRAFT_SLOT_1; @@ -39,57 +78,47 @@ public final class PlayerInventoryUtils { case 6 -> CHESTPLATE_SLOT; case 7 -> LEGGINGS_SLOT; case 8 -> BOOTS_SLOT; - default -> convertSlot(slot, offset); + case OFF_HAND_SLOT -> OFF_HAND_SLOT; + default -> { + if (slot >= 36 && slot <= 44) { + yield slot - 36; + } else if (slot >= 9 && slot <= 35) { + yield slot; + } else { + yield -1; // Unknown slot ID + } + } }; - - } - - public static int convertSlot(int slot, int offset) { - final int rowSize = 9; - slot -= offset; - if (slot >= rowSize * 3 && slot < rowSize * 4) { - slot = slot % 9; - } else { - slot = slot + rowSize; - } - return slot; - } - - - /** - * Used to convert internal slot to one used in packets - * - * @param slot the internal slot - * @return a slot id which can be used for packets - */ - public static int convertToPacketSlot(int slot) { - if (slot > -1 && slot < 9) { // Held bar 0-8 - slot = slot + 36; - } else if (slot > 8 && slot < 36) { // Inventory 9-35 - slot = slot; - } else if (slot >= CRAFT_RESULT && slot <= CRAFT_SLOT_4) { // Crafting 36-40 - slot = slot - 36; - } else if (slot >= HELMET_SLOT && slot <= BOOTS_SLOT) { // Armor 41-44 - slot = slot - 36; - } else if (slot == OFFHAND_SLOT) { // Off hand - slot = 45; - } - return slot; } /** - * Used to convert the clients inventory slot to a Minestom slot. - * The client's inventory does not count the crafting slots. + * Converts the given slot into a protocol ID directly after the provided inventory. + * This is intended for when a player's inner inventory is interacted with while a player has another inventory + * open.
+ * This is the inverse of {@link #protocolToMinestom(int, int)}. * - * @param slot the client slot - * @return a slot which can be used internally with Minestom + * @param slot the player slot that was interacted with + * @param openInventorySize the size of the inventory opened by the player (not the player's inventory) + * @return the protocol slot ID */ - public static int convertClientInventorySlot(int slot) { - if (slot == 36) return BOOTS_SLOT; - if (slot == 37) return LEGGINGS_SLOT; - if (slot == 38) return CHESTPLATE_SLOT; - if (slot == 39) return HELMET_SLOT; - if (slot == 40) return OFFHAND_SLOT; - return slot; + public static int minestomToProtocol(int slot, int openInventorySize) { + return PlayerInventoryUtils.minestomToProtocol(slot) + openInventorySize - PROTOCOL_OFFSET; } -} + + /** + * Converts the given protocol ID that is directly after the provided inventory's slots into a player inventory slot + * ID. This is intended for when a player's inner inventory is interacted with while a player has another inventory + * open.
+ * This is the inverse of {@link #minestomToProtocol(int, int)}. + * + * @param slot the protocol slot ID, situated directly after the slot IDs for the open inventory + * @param openInventorySize the size of the inventory opened by the player (not the player's inventory) + * @return the player slot ID + */ + public static int protocolToMinestom(int slot, int openInventorySize) { + if (slot < openInventorySize) return -1; + + return PlayerInventoryUtils.protocolToMinestom(slot - openInventorySize + PROTOCOL_OFFSET); + } + +} \ No newline at end of file diff --git a/src/test/java/net/minestom/server/inventory/InventoryCloseStateTest.java b/src/test/java/net/minestom/server/inventory/InventoryCloseStateTest.java index 21d92088f..92464906b 100644 --- a/src/test/java/net/minestom/server/inventory/InventoryCloseStateTest.java +++ b/src/test/java/net/minestom/server/inventory/InventoryCloseStateTest.java @@ -22,7 +22,7 @@ public class InventoryCloseStateTest { assertEquals(instance, player.getInstance()); var packetTracker = connection.trackIncoming(CloseWindowPacket.class); - var inventory = new Inventory(InventoryType.CHEST_2_ROW, Component.text("Test")); + var inventory = new ContainerInventory(InventoryType.CHEST_2_ROW, Component.text("Test")); player.openInventory(inventory); player.closeInventory(); // Closes the inventory server-side, should send a CloseWindowPacket player.openInventory(inventory); diff --git a/src/test/java/net/minestom/server/inventory/InventoryIntegrationTest.java b/src/test/java/net/minestom/server/inventory/InventoryIntegrationTest.java index 68efd7a19..6bff7043a 100644 --- a/src/test/java/net/minestom/server/inventory/InventoryIntegrationTest.java +++ b/src/test/java/net/minestom/server/inventory/InventoryIntegrationTest.java @@ -5,7 +5,7 @@ import net.minestom.server.utils.inventory.PlayerInventoryUtils; import net.minestom.testing.Env; import net.minestom.testing.EnvTest; import net.minestom.server.coordinate.Pos; -import net.minestom.server.event.item.ItemDropEvent; +import net.minestom.server.event.inventory.InventoryItemChangeEvent; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.network.packet.server.play.EntityEquipmentPacket; @@ -27,7 +27,7 @@ public class InventoryIntegrationTest { var player = connection.connect(instance, new Pos(0, 42, 0)).join(); assertEquals(instance, player.getInstance()); - Inventory inventory = new Inventory(InventoryType.CHEST_6_ROW, Component.empty()); + ContainerInventory inventory = new ContainerInventory(InventoryType.CHEST_6_ROW, Component.empty()); player.openInventory(inventory); assertEquals(inventory, player.getOpenInventory()); @@ -51,20 +51,20 @@ public class InventoryIntegrationTest { var player = connection.connect(instance, new Pos(0, 42, 0)).join(); assertEquals(instance, player.getInstance()); - Inventory inventory = new Inventory(InventoryType.CHEST_6_ROW, Component.empty()); + ContainerInventory inventory = new ContainerInventory(InventoryType.CHEST_6_ROW, Component.empty()); player.openInventory(inventory); assertEquals(inventory, player.getOpenInventory()); var packetTracker = connection.trackIncoming(SetSlotPacket.class); - inventory.setCursorItem(player, MAGIC_STACK); + player.getInventory().setCursorItem(MAGIC_STACK); packetTracker.assertSingle(slot -> assertEquals(MAGIC_STACK, slot.itemStack())); // Setting a slot should send a packet packetTracker = connection.trackIncoming(SetSlotPacket.class); - inventory.setCursorItem(player, MAGIC_STACK); + player.getInventory().setCursorItem(MAGIC_STACK); packetTracker.assertEmpty(); // Setting the same slot to the same ItemStack should not send another packet packetTracker = connection.trackIncoming(SetSlotPacket.class); - inventory.setCursorItem(player, ItemStack.AIR); + player.getInventory().setCursorItem(ItemStack.AIR); packetTracker.assertSingle(slot -> assertEquals(ItemStack.AIR, slot.itemStack())); // Setting a slot should send a packet } @@ -75,7 +75,7 @@ public class InventoryIntegrationTest { var player = connection.connect(instance, new Pos(0, 42, 0)).join(); assertEquals(instance, player.getInstance()); - Inventory inventory = new Inventory(InventoryType.CHEST_6_ROW, Component.empty()); + ContainerInventory inventory = new ContainerInventory(InventoryType.CHEST_6_ROW, Component.empty()); player.openInventory(inventory); assertEquals(inventory, player.getOpenInventory()); @@ -85,7 +85,7 @@ public class InventoryIntegrationTest { inventory.setItemStack(3, MAGIC_STACK); inventory.setItemStack(19, MAGIC_STACK); inventory.setItemStack(40, MAGIC_STACK); - inventory.setCursorItem(player, MAGIC_STACK); + player.getInventory().setCursorItem(MAGIC_STACK); setSlotTracker.assertCount(5); @@ -116,7 +116,7 @@ public class InventoryIntegrationTest { var instance = env.createFlatInstance(); var connection = env.createConnection(); var player = connection.connect(instance, new Pos(0, 42, 0)).join(); - final var inventory = new Inventory(InventoryType.CHEST_1_ROW, "title"); + final var inventory = new ContainerInventory(InventoryType.CHEST_1_ROW, "title"); player.openInventory(inventory); assertSame(inventory, player.getOpenInventory()); player.closeInventory(); @@ -124,24 +124,24 @@ public class InventoryIntegrationTest { } @Test - public void openInventoryOnItemDropFromInventoryClosingTest(Env env) { + public void openInventoryOnItemAddFromInventoryClosingTest(Env env) { var instance = env.createFlatInstance(); var connection = env.createConnection(); var player = connection.connect(instance, new Pos(0, 42, 0)).join(); - var listener = env.listen(ItemDropEvent.class); - final var firstInventory = new Inventory(InventoryType.CHEST_1_ROW, "title"); + var listener = env.listen(InventoryItemChangeEvent.class); + final var firstInventory = new ContainerInventory(InventoryType.CHEST_1_ROW, "title"); player.openInventory(firstInventory); assertSame(firstInventory, player.getOpenInventory()); - firstInventory.setCursorItem(player, ItemStack.of(Material.STONE)); + player.getInventory().setCursorItem(ItemStack.of(Material.STONE)); listener.followup(); player.closeInventory(); assertNull(player.getOpenInventory()); player.openInventory(firstInventory); - firstInventory.setCursorItem(player, ItemStack.of(Material.STONE)); - final var secondInventory = new Inventory(InventoryType.CHEST_1_ROW, "title"); - listener.followup(event -> event.getPlayer().openInventory(secondInventory)); + player.getInventory().setCursorItem(ItemStack.of(Material.STONE)); + final var secondInventory = new ContainerInventory(InventoryType.CHEST_1_ROW, "title"); + listener.followup(event -> player.openInventory(secondInventory)); player.closeInventory(); assertSame(secondInventory, player.getOpenInventory()); } @@ -156,17 +156,16 @@ public class InventoryIntegrationTest { var player = connection.connect(instance, new Pos(0, 42, 0)).join(); assertEquals(instance, player.getInstance()); - Inventory inventory = new Inventory(InventoryType.CHEST_6_ROW, Component.empty()); + Inventory inventory = new ContainerInventory(InventoryType.CHEST_6_ROW, Component.empty()); player.openInventory(inventory); assertEquals(inventory, player.getOpenInventory()); // Ensure that slots not in the inner inventory are sent separately var packetTracker = connection.trackIncoming(SetSlotPacket.class); - player.getInventory().setItemStack(PlayerInventoryUtils.OFFHAND_SLOT, MAGIC_STACK); + player.getInventory().setItemStack(PlayerInventoryUtils.OFF_HAND_SLOT, MAGIC_STACK); packetTracker.assertSingle(slot -> { - System.out.println(slot); assertEquals((byte) 0, slot.windowId()); - assertEquals(PlayerInventoryUtils.OFFHAND_SLOT, slot.slot()); + assertEquals(PlayerInventoryUtils.OFF_HAND_SLOT, slot.slot()); assertEquals(MAGIC_STACK, slot.itemStack()); }); @@ -175,8 +174,7 @@ public class InventoryIntegrationTest { player.getInventory().setItemStack(0, MAGIC_STACK); // Test with first inner inventory slot packetTracker.assertSingle(slot -> { assertEquals(inventory.getWindowId(), slot.windowId()); - System.out.println(slot.slot()); - assertEquals(PlayerInventoryUtils.convertToPacketSlot(0) - PlayerInventoryUtils.OFFSET + inventory.getSize(), slot.slot()); + assertEquals(PlayerInventoryUtils.minestomToProtocol(0, inventory.getSize()), slot.slot()); assertEquals(MAGIC_STACK, slot.itemStack()); }); @@ -184,7 +182,7 @@ public class InventoryIntegrationTest { player.getInventory().setItemStack(35, MAGIC_STACK); // Test with last inner inventory slot packetTracker.assertSingle(slot -> { assertEquals(inventory.getWindowId(), slot.windowId()); - assertEquals(PlayerInventoryUtils.convertToPacketSlot(35) - PlayerInventoryUtils.OFFSET + inventory.getSize(), slot.slot()); + assertEquals(PlayerInventoryUtils.minestomToProtocol(35, inventory.getSize()), slot.slot()); assertEquals(MAGIC_STACK, slot.itemStack()); }); } diff --git a/src/test/java/net/minestom/server/inventory/InventoryTest.java b/src/test/java/net/minestom/server/inventory/InventoryTest.java index 612e3b162..0a83a7a9b 100644 --- a/src/test/java/net/minestom/server/inventory/InventoryTest.java +++ b/src/test/java/net/minestom/server/inventory/InventoryTest.java @@ -17,7 +17,7 @@ public class InventoryTest { @Test public void testCreation() { - Inventory inventory = new Inventory(InventoryType.CHEST_1_ROW, "title"); + ContainerInventory inventory = new ContainerInventory(InventoryType.CHEST_1_ROW, "title"); assertEquals(InventoryType.CHEST_1_ROW, inventory.getInventoryType()); assertEquals(Component.text("title"), inventory.getTitle()); @@ -30,7 +30,7 @@ public class InventoryTest { var item1 = ItemStack.of(Material.DIAMOND); var item2 = ItemStack.of(Material.GOLD_INGOT); - Inventory inventory = new Inventory(InventoryType.CHEST_1_ROW, "title"); + ContainerInventory inventory = new ContainerInventory(InventoryType.CHEST_1_ROW, "title"); assertSame(ItemStack.AIR, inventory.getItemStack(0)); inventory.setItemStack(0, item1); assertSame(item1, inventory.getItemStack(0)); @@ -54,7 +54,7 @@ public class InventoryTest { @Test public void testTake() { ItemStack item = ItemStack.of(Material.DIAMOND, 32); - Inventory inventory = new Inventory(InventoryType.CHEST_1_ROW, "title"); + ContainerInventory inventory = new ContainerInventory(InventoryType.CHEST_1_ROW, "title"); inventory.setItemStack(0, item); assertTrue(inventory.takeItemStack(item, TransactionOption.DRY_RUN)); assertTrue(inventory.takeItemStack(item.withAmount(31), TransactionOption.DRY_RUN)); @@ -67,7 +67,7 @@ public class InventoryTest { @Test public void testAdd() { - Inventory inventory = new Inventory(InventoryType.HOPPER, "title"); + ContainerInventory inventory = new ContainerInventory(InventoryType.HOPPER, "title"); assertTrue(inventory.addItemStack(ItemStack.of(Material.DIAMOND, 32), TransactionOption.ALL_OR_NOTHING)); assertTrue(inventory.addItemStack(ItemStack.of(Material.GOLD_BLOCK, 32), TransactionOption.ALL_OR_NOTHING)); assertTrue(inventory.addItemStack(ItemStack.of(Material.MAP, 32), TransactionOption.ALL_OR_NOTHING)); @@ -79,7 +79,7 @@ public class InventoryTest { @Test public void testIds() { for (int i = 0; i <= 1000; ++i) { - final byte windowId = new Inventory(InventoryType.CHEST_1_ROW, "title").getWindowId(); + final byte windowId = new ContainerInventory(InventoryType.CHEST_1_ROW, "title").getWindowId(); assertTrue(windowId > 0); } } diff --git a/src/test/java/net/minestom/server/inventory/PlayerCreativeSlotTest.java b/src/test/java/net/minestom/server/inventory/PlayerCreativeSlotTest.java index a18215deb..e02755f34 100644 --- a/src/test/java/net/minestom/server/inventory/PlayerCreativeSlotTest.java +++ b/src/test/java/net/minestom/server/inventory/PlayerCreativeSlotTest.java @@ -25,9 +25,9 @@ public class PlayerCreativeSlotTest { assertEquals(instance, player.getInstance()); player.setGameMode(GameMode.CREATIVE); - player.addPacketToQueue(new ClientCreativeInventoryActionPacket((short) PlayerInventoryUtils.OFFHAND_SLOT, ItemStack.of(Material.STICK))); + player.addPacketToQueue(new ClientCreativeInventoryActionPacket((short) PlayerInventoryUtils.OFF_HAND_SLOT, ItemStack.of(Material.STICK))); player.interpretPacketQueue(); - assertEquals(Material.STICK, player.getInventory().getItemInOffHand().material()); + assertEquals(Material.STICK, player.getItemInOffHand().material()); } @Test diff --git a/src/test/java/net/minestom/server/inventory/PlayerInventoryIntegrationTest.java b/src/test/java/net/minestom/server/inventory/PlayerInventoryIntegrationTest.java index 268a34a27..03b838ab1 100644 --- a/src/test/java/net/minestom/server/inventory/PlayerInventoryIntegrationTest.java +++ b/src/test/java/net/minestom/server/inventory/PlayerInventoryIntegrationTest.java @@ -118,19 +118,19 @@ public class PlayerInventoryIntegrationTest { var equipmentTracker = connectionViewer.trackIncoming(EntityEquipmentPacket.class); // Setting to an item should send EntityEquipmentPacket to viewer - playerArmored.getInventory().setEquipment(EquipmentSlot.HELMET, MAGIC_STACK); + playerArmored.setEquipment(EquipmentSlot.HELMET, MAGIC_STACK); equipmentTracker.assertSingle(entityEquipmentPacket -> { assertEquals(MAGIC_STACK, entityEquipmentPacket.equipments().get(EquipmentSlot.HELMET)); }); // Setting to the same item shouldn't send packet equipmentTracker = connectionViewer.trackIncoming(EntityEquipmentPacket.class); - playerArmored.getInventory().setEquipment(EquipmentSlot.HELMET, MAGIC_STACK); + playerArmored.setEquipment(EquipmentSlot.HELMET, MAGIC_STACK); equipmentTracker.assertEmpty(); // Setting to air should send packet equipmentTracker = connectionViewer.trackIncoming(EntityEquipmentPacket.class); - playerArmored.getInventory().setEquipment(EquipmentSlot.HELMET, ItemStack.AIR); + playerArmored.setEquipment(EquipmentSlot.HELMET, ItemStack.AIR); equipmentTracker.assertSingle(entityEquipmentPacket -> { assertEquals(ItemStack.AIR, entityEquipmentPacket.equipments().get(EquipmentSlot.HELMET)); }); diff --git a/src/test/java/net/minestom/server/inventory/PlayerSlotConversionTest.java b/src/test/java/net/minestom/server/inventory/PlayerSlotConversionTest.java deleted file mode 100644 index 992051039..000000000 --- a/src/test/java/net/minestom/server/inventory/PlayerSlotConversionTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package net.minestom.server.inventory; - -import org.junit.jupiter.api.Test; - -import static net.minestom.server.utils.inventory.PlayerInventoryUtils.*; -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** - * Test conversion from packet slots to internal ones (used in events and inventory methods) - */ -public class PlayerSlotConversionTest { - - @Test - public void hotbar() { - // Convert 36-44 into 0-8 - for (int i = 0; i < 9; i++) { - assertEquals(i, convertPlayerInventorySlot(i + 36, OFFSET)); - } - } - - @Test - public void mainInventory() { - // No conversion, slots should stay 9-35 - for (int i = 9; i < 9 * 4; i++) { - assertEquals(i, convertPlayerInventorySlot(i, OFFSET)); - } - } - - @Test - public void armor() { - assertEquals(HELMET_SLOT, 41); - assertEquals(CHESTPLATE_SLOT, 42); - assertEquals(LEGGINGS_SLOT, 43); - assertEquals(BOOTS_SLOT, 44); - assertEquals(OFFHAND_SLOT, 45); - - // Convert 5-8 & 45 into 41-45 - assertEquals(HELMET_SLOT, convertPlayerInventorySlot(5, OFFSET)); - assertEquals(CHESTPLATE_SLOT, convertPlayerInventorySlot(6, OFFSET)); - assertEquals(LEGGINGS_SLOT, convertPlayerInventorySlot(7, OFFSET)); - assertEquals(BOOTS_SLOT, convertPlayerInventorySlot(8, OFFSET)); - assertEquals(OFFHAND_SLOT, convertPlayerInventorySlot(45, OFFSET)); - } - - @Test - public void craft() { - assertEquals(CRAFT_RESULT, 36); - assertEquals(CRAFT_SLOT_1, 37); - assertEquals(CRAFT_SLOT_2, 38); - assertEquals(CRAFT_SLOT_3, 39); - assertEquals(CRAFT_SLOT_4, 40); - - // Convert 0-4 into 36-40 - assertEquals(CRAFT_RESULT, convertPlayerInventorySlot(0, OFFSET)); - assertEquals(CRAFT_SLOT_1, convertPlayerInventorySlot(1, OFFSET)); - assertEquals(CRAFT_SLOT_2, convertPlayerInventorySlot(2, OFFSET)); - assertEquals(CRAFT_SLOT_3, convertPlayerInventorySlot(3, OFFSET)); - assertEquals(CRAFT_SLOT_4, convertPlayerInventorySlot(4, OFFSET)); - } -} diff --git a/src/test/java/net/minestom/server/inventory/click/ClickPreprocessorTest.java b/src/test/java/net/minestom/server/inventory/click/ClickPreprocessorTest.java new file mode 100644 index 000000000..d90d2c3e8 --- /dev/null +++ b/src/test/java/net/minestom/server/inventory/click/ClickPreprocessorTest.java @@ -0,0 +1,96 @@ +package net.minestom.server.inventory.click; + +import it.unimi.dsi.fastutil.ints.IntList; +import net.minestom.server.entity.GameMode; +import org.junit.jupiter.api.Test; + +import static net.minestom.server.inventory.click.ClickUtils.*; +import static net.minestom.server.network.packet.client.play.ClientClickWindowPacket.ClickType.*; + +public class ClickPreprocessorTest { + + @Test + public void testPickupType() { + assertProcessed(new Click.Info.LeftDropCursor(), clickPacket(PICKUP, 1, 0, -999)); + assertProcessed(new Click.Info.RightDropCursor(), clickPacket(PICKUP, 1, 1, -999)); + assertProcessed(new Click.Info.MiddleDropCursor(), clickPacket(PICKUP, 1, 2, -999)); + + assertProcessed(new Click.Info.Left(0), clickPacket(PICKUP, 1, 0, 0)); + assertProcessed(new Click.Info.Left(SIZE), clickPacket(PICKUP, 1, 0, 5)); + assertProcessed(null, clickPacket(PICKUP, 1, 0, 99)); + + assertProcessed(new Click.Info.Right(0), clickPacket(PICKUP, 1, 1, 0)); + assertProcessed(new Click.Info.Right(SIZE), clickPacket(PICKUP, 1, 1, 5)); + assertProcessed(null, clickPacket(PICKUP, 1, 1, 99)); + + assertProcessed(null, clickPacket(PICKUP, 1, -1, 0)); + assertProcessed(null, clickPacket(PICKUP, 1, 2, 0)); + } + + @Test + public void testQuickMoveType() { + assertProcessed(new Click.Info.LeftShift(0), clickPacket(QUICK_MOVE, 1, 0, 0)); + assertProcessed(new Click.Info.LeftShift(SIZE), clickPacket(QUICK_MOVE, 1, 0, 5)); + assertProcessed(null, clickPacket(QUICK_MOVE, 1, 0, -1)); + } + + @Test + public void testSwapType() { + assertProcessed(null, clickPacket(SWAP, 1, 0, -1)); + assertProcessed(new Click.Info.HotbarSwap(0, 2), clickPacket(SWAP, 1, 0, 2)); + assertProcessed(new Click.Info.HotbarSwap(8, 2), clickPacket(SWAP, 1, 8, 2)); + assertProcessed(new Click.Info.OffhandSwap(2), clickPacket(SWAP, 1, 40, 2)); + + assertProcessed(null, clickPacket(SWAP, 1, 9, 2)); + assertProcessed(null, clickPacket(SWAP, 1, 39, 2)); + } + + @Test + public void testCloneType() { + var player = createPlayer(); + player.setGameMode(GameMode.CREATIVE); + + assertProcessed(null, clickPacket(CLONE, 1, 0, 0)); + assertProcessed(player, new Click.Info.Middle(0), clickPacket(CLONE, 1, 0, 0)); + assertProcessed(player, null, clickPacket(CLONE, 1, 0, -1)); + } + + @Test + public void testThrowType() { + assertProcessed(new Click.Info.DropSlot(0, true), clickPacket(THROW, 1, 1, 0)); + + assertProcessed(new Click.Info.DropSlot(0, false), clickPacket(THROW, 1, 0, 0)); + assertProcessed(new Click.Info.DropSlot(0, true), clickPacket(THROW, 1, 1, 0)); + + assertProcessed(new Click.Info.DropSlot(1, false), clickPacket(THROW, 1, 0, 1)); + assertProcessed(new Click.Info.DropSlot(1, true), clickPacket(THROW, 1, 1, 1)); + } + + @Test + public void testQuickCraft() { + var processor = createPreprocessor(); + var player = createPlayer(); + + assertProcessed(player, null, clickPacket(QUICK_CRAFT, 1, 8, 0)); + assertProcessed(player, null, clickPacket(QUICK_CRAFT, 1, 9, 0)); + assertProcessed(player, null, clickPacket(QUICK_CRAFT, 1, 10, 0)); + + player.setGameMode(GameMode.CREATIVE); + + assertProcessed(processor, player, null, clickPacket(QUICK_CRAFT, 1, 0, 0)); + assertProcessed(processor, player, null, clickPacket(QUICK_CRAFT, 1, 1, 0)); + assertProcessed(processor, player, null, clickPacket(QUICK_CRAFT, 1, 1, 1)); + assertProcessed(processor, player, new Click.Info.LeftDrag(IntList.of(0, 1)), clickPacket(QUICK_CRAFT, 1, 2, 0)); + + assertProcessed(processor, player, null, clickPacket(QUICK_CRAFT, 1, 4, 0)); + assertProcessed(processor, player, null, clickPacket(QUICK_CRAFT, 1, 5, 0)); + assertProcessed(processor, player, null, clickPacket(QUICK_CRAFT, 1, 5, 1)); + assertProcessed(processor, player, new Click.Info.RightDrag(IntList.of(0, 1)), clickPacket(QUICK_CRAFT, 1, 6, 0)); + + assertProcessed(processor, player, null, clickPacket(QUICK_CRAFT, 1, 8, 0)); + assertProcessed(processor, player, null, clickPacket(QUICK_CRAFT, 1, 9, 0)); + assertProcessed(processor, player, null, clickPacket(QUICK_CRAFT, 1, 9, 1)); + assertProcessed(processor, player, new Click.Info.MiddleDrag(IntList.of(0, 1)), clickPacket(QUICK_CRAFT, 1, 10, 0)); + } + +} diff --git a/src/test/java/net/minestom/server/inventory/click/ClickUtils.java b/src/test/java/net/minestom/server/inventory/click/ClickUtils.java new file mode 100644 index 000000000..ef2797cdc --- /dev/null +++ b/src/test/java/net/minestom/server/inventory/click/ClickUtils.java @@ -0,0 +1,84 @@ +package net.minestom.server.inventory.click; + +import net.minestom.server.entity.Player; +import net.minestom.server.inventory.ContainerInventory; +import net.minestom.server.inventory.Inventory; +import net.minestom.server.inventory.InventoryType; +import net.minestom.server.item.ItemStack; +import net.minestom.server.network.packet.client.play.ClientClickWindowPacket; +import net.minestom.server.network.packet.server.SendablePacket; +import net.minestom.server.network.player.PlayerConnection; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.net.SocketAddress; +import java.util.List; +import java.util.UUID; +import java.util.function.UnaryOperator; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ClickUtils { + + public static final @NotNull InventoryType TYPE = InventoryType.HOPPER; + + public static final int SIZE = TYPE.getSize(); // Default hopper size + + public static @NotNull Inventory createInventory() { + return new ContainerInventory(TYPE, "TestInventory"); + } + + public static @NotNull Click.Preprocessor createPreprocessor() { + return new Click.Preprocessor(); + } + + public static @NotNull Player createPlayer() { + return new Player(UUID.randomUUID(), "TestPlayer", new PlayerConnection() { + @Override + public void sendPacket(@NotNull SendablePacket packet) {} + + @Override + public @NotNull SocketAddress getRemoteAddress() { + return null; + } + + @Override + public void disconnect() {} + }); + } + + public static void assertClick(@NotNull UnaryOperator initialChanges, @NotNull Click.Info info, @NotNull UnaryOperator expectedChanges) { + var player = createPlayer(); + var inventory = createInventory(); + + ContainerInventory.apply(initialChanges.apply(new Click.Setter(inventory.getSize())).build(), player, inventory); + var changes = inventory.handleClick(player, info); + assertEquals(expectedChanges.apply(new Click.Setter(inventory.getSize())).build(), changes); + } + + public static void assertPlayerClick(@NotNull UnaryOperator initialChanges, @NotNull Click.Info info, @NotNull UnaryOperator expectedChanges) { + var player = createPlayer(); + var inventory = player.getInventory(); + + ContainerInventory.apply(initialChanges.apply(new Click.Setter(inventory.getSize())).build(), player, inventory); + var changes = inventory.handleClick(player, info); + assertEquals(expectedChanges.apply(new Click.Setter(inventory.getSize())).build(), changes); + } + + public static void assertProcessed(@NotNull Click.Preprocessor preprocessor, @NotNull Player player, @Nullable Click.Info info, @NotNull ClientClickWindowPacket packet) { + assertEquals(info, preprocessor.process(packet, createInventory(), player.isCreative())); + } + + public static void assertProcessed(@NotNull Player player, @Nullable Click.Info info, @NotNull ClientClickWindowPacket packet) { + assertProcessed(new Click.Preprocessor(), player, info, packet); + } + + public static void assertProcessed(@Nullable Click.Info info, @NotNull ClientClickWindowPacket packet) { + assertProcessed(createPlayer(), info, packet); + } + + public static @NotNull ClientClickWindowPacket clickPacket(@NotNull ClientClickWindowPacket.ClickType type, int windowId, int button, int slot) { + return new ClientClickWindowPacket((byte) windowId, 0, (short) slot, (byte) button, type, List.of(), ItemStack.AIR); + } + +} diff --git a/src/test/java/net/minestom/server/inventory/click/integration/HeldClickIntegrationTest.java b/src/test/java/net/minestom/server/inventory/click/integration/HeldClickIntegrationTest.java deleted file mode 100644 index 18ac2c326..000000000 --- a/src/test/java/net/minestom/server/inventory/click/integration/HeldClickIntegrationTest.java +++ /dev/null @@ -1,190 +0,0 @@ -package net.minestom.server.inventory.click.integration; - -import net.minestom.server.coordinate.Pos; -import net.minestom.server.entity.Player; -import net.minestom.server.event.inventory.InventoryPreClickEvent; -import net.minestom.server.inventory.Inventory; -import net.minestom.server.inventory.InventoryType; -import net.minestom.server.inventory.click.ClickType; -import net.minestom.server.item.ItemStack; -import net.minestom.server.item.Material; -import net.minestom.server.network.packet.client.play.ClientClickWindowPacket; -import net.minestom.server.utils.inventory.PlayerInventoryUtils; -import net.minestom.testing.Env; -import net.minestom.testing.EnvTest; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -@EnvTest -public class HeldClickIntegrationTest { - - @Test - public void heldSelf(Env env) { - var instance = env.createFlatInstance(); - var player = env.createPlayer(instance, new Pos(0, 40, 0)); - var inventory = player.getInventory(); - var listener = env.listen(InventoryPreClickEvent.class); - inventory.setItemStack(1, ItemStack.of(Material.DIAMOND)); - inventory.setItemStack(2, ItemStack.of(Material.GOLD_INGOT)); - inventory.setItemStack(3, ItemStack.of(Material.EGG)); - inventory.setItemStack(6, ItemStack.of(Material.DIAMOND)); - // Empty - { - listener.followup(event -> { - assertNull(event.getInventory()); // Player inventory - assertTrue(event.getSlot() == 4 || event.getSlot() == 5); - assertEquals(ClickType.CHANGE_HELD, event.getClickType()); - - assertEquals(ItemStack.AIR, inventory.getCursorItem()); - assertEquals(ItemStack.AIR, event.getCursorItem()); - - assertEquals(ItemStack.AIR, event.getClickedItem()); - }); - heldClick(player, 4, 5); - } - // Swap air - { - listener.followup(event -> { - assertNull(event.getInventory()); // Player inventory - assertTrue(event.getSlot() == 1 || event.getSlot() == 0); - assertEquals(ClickType.CHANGE_HELD, event.getClickType()); - - assertEquals(ItemStack.AIR, inventory.getCursorItem()); - assertEquals(ItemStack.AIR, event.getCursorItem()); - - assertEquals(ItemStack.of(Material.DIAMOND), event.getClickedItem()); - }); - heldClick(player, 1, 0); - assertEquals(ItemStack.AIR, inventory.getCursorItem()); - assertEquals(ItemStack.AIR, inventory.getItemStack(1)); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getItemStack(0)); - } - // Swap items - { - listener.followup(event -> { - assertTrue(event.getSlot() == 0 || event.getSlot() == 2); - assertEquals(ItemStack.AIR, inventory.getCursorItem()); - assertEquals(ItemStack.AIR, inventory.getItemStack(1)); - }); - heldClick(player, 0, 2); - assertEquals(ItemStack.AIR, inventory.getCursorItem()); - assertEquals(ItemStack.AIR, inventory.getItemStack(1)); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getItemStack(2)); - assertEquals(ItemStack.of(Material.GOLD_INGOT), inventory.getItemStack(0)); - } - // Swap offhand - { - listener.followup(event -> { - assertTrue(event.getSlot() == 3 || event.getSlot() == 45 /* Vanilla offhand slot is 40, Minestom is 45 */); - }); - heldClick(player, 3, 40); - assertEquals(ItemStack.AIR, inventory.getItemStack(3)); - assertEquals(ItemStack.of(Material.EGG), inventory.getItemInOffHand()); - } - // Cancel event - { - listener.followup(event -> event.setCancelled(true)); - heldClick(player, 2, 0); - assertEquals(ItemStack.AIR, inventory.getCursorItem()); - assertEquals(ItemStack.AIR, inventory.getItemStack(1)); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getItemStack(2)); - assertEquals(ItemStack.of(Material.GOLD_INGOT), inventory.getItemStack(0)); - } - } - - @Test - public void heldExternal(Env env) { - var instance = env.createFlatInstance(); - var player = env.createPlayer(instance, new Pos(0, 40, 0)); - var inventory = new Inventory(InventoryType.HOPPER, "test"); - var playerInv = player.getInventory(); - player.openInventory(inventory); - var listener = env.listen(InventoryPreClickEvent.class); - inventory.setItemStack(1, ItemStack.of(Material.DIAMOND)); - inventory.setItemStack(2, ItemStack.of(Material.GOLD_INGOT)); - inventory.setItemStack(3, ItemStack.of(Material.EGG)); - inventory.setItemStack(4, ItemStack.of(Material.DIAMOND)); - // Empty - { - listener.followup(event -> { - if (event.getInventory() != null) assertEquals(inventory, event.getInventory()); - assertEquals(0, event.getSlot()); - assertEquals(ClickType.CHANGE_HELD, event.getClickType()); - assertEquals(ItemStack.AIR, inventory.getCursorItem(player)); - }); - heldClickOpenInventory(player, 0, 0); - assertEquals(ItemStack.AIR, inventory.getCursorItem(player)); - assertEquals(ItemStack.AIR, inventory.getItemStack(0)); - } - // Swap empty - { - listener.followup(event -> { - if (event.getInventory() != null) assertEquals(inventory, event.getInventory()); - assertTrue(event.getSlot() == 1 || event.getSlot() == 0); - assertEquals(ItemStack.AIR, inventory.getCursorItem(player)); - }); - heldClickOpenInventory(player, 1, 0); - assertEquals(ItemStack.AIR, inventory.getCursorItem(player)); - assertEquals(ItemStack.AIR, inventory.getItemStack(1)); - assertEquals(ItemStack.of(Material.DIAMOND), playerInv.getItemStack(0)); - } - // Swap items - { - listener.followup(event -> { - if (event.getInventory() != null) assertEquals(inventory, event.getInventory()); - assertTrue(event.getSlot() == 2 || event.getSlot() == 0); - assertEquals(ItemStack.AIR, inventory.getCursorItem(player)); - }); - heldClickOpenInventory(player, 2, 0); - assertEquals(ItemStack.AIR, inventory.getCursorItem(player)); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getItemStack(2)); - assertEquals(ItemStack.of(Material.GOLD_INGOT), playerInv.getItemStack(0)); - } - // Swap offhand - { - listener.followup(event -> { - if (event.getInventory() != null) assertEquals(inventory, event.getInventory()); - assertTrue(event.getSlot() == 3 || event.getSlot() == 45); - }); - heldClickOpenInventory(player, 3, 40); - assertEquals(ItemStack.AIR, inventory.getItemStack(3)); - assertEquals(ItemStack.of(Material.EGG), playerInv.getItemInOffHand()); - } - // Cancel event - { - listener.followup(event -> event.setCancelled(true)); - heldClickOpenInventory(player, 2, 0); - assertEquals(ItemStack.AIR, inventory.getCursorItem(player)); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getItemStack(2)); - assertEquals(ItemStack.of(Material.GOLD_INGOT), playerInv.getItemStack(0)); - } - } - - private void heldClickOpenInventory(Player player, int slot, int target) { - _heldClick(player.getOpenInventory(), true, player, slot, target); - } - - private void heldClick(Player player, int slot, int target) { - _heldClick(player.getOpenInventory(), false, player, slot, target); - } - - private void _heldClick(Inventory openInventory, boolean clickOpenInventory, Player player, int slot, int target) { - final byte windowId = openInventory != null ? openInventory.getWindowId() : 0; - if (clickOpenInventory) { - assert openInventory != null; - // Do not touch slot - } else { - int offset = openInventory != null ? openInventory.getInnerSize() : 0; - slot = PlayerInventoryUtils.convertToPacketSlot(slot); - if (openInventory != null) { - slot = slot - 9 + offset; - } - } - player.addPacketToQueue(new ClientClickWindowPacket(windowId, 0, (short) slot, (byte) target, - ClientClickWindowPacket.ClickType.SWAP, List.of(), ItemStack.AIR)); - player.interpretPacketQueue(); - } -} diff --git a/src/test/java/net/minestom/server/inventory/click/integration/LeftClickIntegrationTest.java b/src/test/java/net/minestom/server/inventory/click/integration/LeftClickIntegrationTest.java deleted file mode 100644 index af9d31a48..000000000 --- a/src/test/java/net/minestom/server/inventory/click/integration/LeftClickIntegrationTest.java +++ /dev/null @@ -1,173 +0,0 @@ -package net.minestom.server.inventory.click.integration; - - -import net.minestom.server.coordinate.Pos; -import net.minestom.server.entity.Player; -import net.minestom.server.event.inventory.InventoryPreClickEvent; -import net.minestom.server.inventory.Inventory; -import net.minestom.server.inventory.InventoryType; -import net.minestom.server.inventory.click.ClickType; -import net.minestom.server.item.ItemStack; -import net.minestom.server.item.Material; -import net.minestom.server.network.packet.client.play.ClientClickWindowPacket; -import net.minestom.server.utils.inventory.PlayerInventoryUtils; -import net.minestom.testing.Env; -import net.minestom.testing.EnvTest; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -@EnvTest -public class LeftClickIntegrationTest { - - @Test - public void leftSelf(Env env) { - var instance = env.createFlatInstance(); - var player = env.createPlayer(instance, new Pos(0, 40, 0)); - var inventory = player.getInventory(); - var listener = env.listen(InventoryPreClickEvent.class); - inventory.setItemStack(1, ItemStack.of(Material.DIAMOND)); - // Empty click - { - listener.followup(event -> { - assertNull(event.getInventory()); // Player inventory - assertEquals(0, event.getSlot()); - assertEquals(ClickType.LEFT_CLICK, event.getClickType()); - assertEquals(ItemStack.AIR, inventory.getCursorItem()); - }); - leftClick(player, 0); - } - // Pickup diamond - { - listener.followup(event -> { - assertEquals(1, event.getSlot()); - assertEquals(ItemStack.AIR, inventory.getCursorItem()); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getItemStack(1)); - }); - leftClick(player, 1); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getCursorItem()); - assertEquals(ItemStack.AIR, inventory.getItemStack(1)); - } - // Place it back - { - listener.followup(event -> { - assertEquals(1, event.getSlot()); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getCursorItem()); - assertEquals(ItemStack.AIR, inventory.getItemStack(1)); - }); - leftClick(player, 1); - assertEquals(ItemStack.AIR, inventory.getCursorItem()); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getItemStack(1)); - } - // Cancel event - { - listener.followup(event -> event.setCancelled(true)); - leftClick(player, 1); - assertEquals(ItemStack.AIR, inventory.getCursorItem(), "Left click cancellation did not work"); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getItemStack(1)); - } - // Change items - { - listener.followup(event -> { - event.setClickedItem(ItemStack.of(Material.DIAMOND, 5)); - event.setCursorItem(ItemStack.of(Material.DIAMOND)); - }); - leftClick(player, 1); - assertEquals(ItemStack.AIR, inventory.getCursorItem()); - assertEquals(ItemStack.of(Material.DIAMOND, 6), inventory.getItemStack(1)); - } - } - - @Test - public void leftExternal(Env env) { - var instance = env.createFlatInstance(); - var player = env.createPlayer(instance, new Pos(0, 40, 0)); - var inventory = new Inventory(InventoryType.HOPPER, "test"); - player.openInventory(inventory); - var listener = env.listen(InventoryPreClickEvent.class); - inventory.setItemStack(1, ItemStack.of(Material.DIAMOND)); - // Empty click in player inv - { - listener.followup(event -> { - assertNull(event.getInventory()); // Player inventory - assertEquals(0, event.getSlot()); - assertEquals(ClickType.LEFT_CLICK, event.getClickType()); - assertEquals(ItemStack.AIR, inventory.getCursorItem(player)); - }); - leftClick(player, 0); - } - // Pickup diamond - { - listener.followup(event -> { - assertEquals(inventory, event.getInventory()); - assertEquals(1, event.getSlot()); - // Ensure that the inventory didn't change yet - assertEquals(ItemStack.AIR, inventory.getCursorItem(player)); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getItemStack(1)); - }); - leftClickOpenInventory(player, 1); - // Verify inventory changes - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getCursorItem(player)); - assertEquals(ItemStack.AIR, inventory.getItemStack(1)); - } - // Place it back - { - listener.followup(event -> { - assertEquals(inventory, event.getInventory()); - assertEquals(1, event.getSlot()); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getCursorItem(player)); - assertEquals(ItemStack.AIR, inventory.getItemStack(1)); - }); - leftClickOpenInventory(player, 1); - assertEquals(ItemStack.AIR, inventory.getCursorItem(player)); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getItemStack(1)); - } - // Cancel event - { - listener.followup(event -> event.setCancelled(true)); - leftClickOpenInventory(player, 1); - assertEquals(ItemStack.AIR, inventory.getCursorItem(player), "Left click cancellation did not work"); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getItemStack(1)); - } - // Change items - { - listener.followup(event -> { - assertNull(event.getInventory()); - assertEquals(9, event.getSlot()); - event.setClickedItem(ItemStack.of(Material.DIAMOND, 5)); - event.setCursorItem(ItemStack.of(Material.DIAMOND)); - }); - leftClick(player, 9); - assertEquals(ItemStack.AIR, inventory.getCursorItem(player)); - assertEquals(ItemStack.of(Material.DIAMOND, 6), player.getInventory().getItemStack(9)); - } - } - - private void leftClickOpenInventory(Player player, int slot) { - _leftClick(player.getOpenInventory(), true, player, slot); - } - - private void leftClick(Player player, int slot) { - _leftClick(player.getOpenInventory(), false, player, slot); - } - - private void _leftClick(Inventory openInventory, boolean clickOpenInventory, Player player, int slot) { - final byte windowId = openInventory != null ? openInventory.getWindowId() : 0; - if (clickOpenInventory) { - assert openInventory != null; - // Do not touch slot - } else { - int offset = openInventory != null ? openInventory.getInnerSize() : 0; - slot = PlayerInventoryUtils.convertToPacketSlot(slot); - if (openInventory != null) { - slot = slot - 9 + offset; - } - } - player.addPacketToQueue(new ClientClickWindowPacket(windowId, 0, (short) slot, (byte) 0, - ClientClickWindowPacket.ClickType.PICKUP, List.of(), ItemStack.AIR)); - player.interpretPacketQueue(); - } -} diff --git a/src/test/java/net/minestom/server/inventory/click/integration/RightClickIntegrationTest.java b/src/test/java/net/minestom/server/inventory/click/integration/RightClickIntegrationTest.java deleted file mode 100644 index 98d2be504..000000000 --- a/src/test/java/net/minestom/server/inventory/click/integration/RightClickIntegrationTest.java +++ /dev/null @@ -1,194 +0,0 @@ -package net.minestom.server.inventory.click.integration; - -import net.minestom.server.coordinate.Pos; -import net.minestom.server.entity.Player; -import net.minestom.server.event.inventory.InventoryPreClickEvent; -import net.minestom.server.inventory.Inventory; -import net.minestom.server.inventory.InventoryType; -import net.minestom.server.inventory.click.ClickType; -import net.minestom.server.item.ItemStack; -import net.minestom.server.item.Material; -import net.minestom.server.network.packet.client.play.ClientClickWindowPacket; -import net.minestom.server.utils.inventory.PlayerInventoryUtils; -import net.minestom.testing.Env; -import net.minestom.testing.EnvTest; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -@EnvTest -public class RightClickIntegrationTest { - - @Test - public void rightSelf(Env env) { - var instance = env.createFlatInstance(); - var player = env.createPlayer(instance, new Pos(0, 40, 0)); - var inventory = player.getInventory(); - var listener = env.listen(InventoryPreClickEvent.class); - inventory.setItemStack(1, ItemStack.of(Material.DIAMOND)); - inventory.setItemStack(2, ItemStack.of(Material.DIAMOND)); - // Empty click - { - listener.followup(event -> { - assertNull(event.getInventory()); // Player inventory - assertEquals(0, event.getSlot()); - assertEquals(ClickType.RIGHT_CLICK, event.getClickType()); - assertEquals(ItemStack.AIR, inventory.getCursorItem()); - }); - rightClick(player, 0); - } - // Pickup diamond - { - listener.followup(event -> { - assertEquals(1, event.getSlot()); - assertEquals(ItemStack.AIR, inventory.getCursorItem()); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getItemStack(1)); - }); - rightClick(player, 1); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getCursorItem()); - assertEquals(ItemStack.AIR, inventory.getItemStack(1)); - } - // Place it back - { - listener.followup(event -> { - assertEquals(1, event.getSlot()); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getCursorItem()); - assertEquals(ItemStack.AIR, inventory.getItemStack(1)); - }); - rightClick(player, 1); - assertEquals(ItemStack.AIR, inventory.getCursorItem()); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getItemStack(1)); - } - // Pickup diamond - { - listener.followup(event -> { - assertEquals(1, event.getSlot()); - assertEquals(ItemStack.AIR, inventory.getCursorItem()); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getItemStack(1)); - }); - rightClick(player, 1); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getCursorItem()); - assertEquals(ItemStack.AIR, inventory.getItemStack(1)); - } - // Stack diamond - { - listener.followup(event -> { - assertEquals(2, event.getSlot()); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getCursorItem()); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getItemStack(2)); - }); - rightClick(player, 2); - assertEquals(ItemStack.AIR, inventory.getCursorItem()); - assertEquals(ItemStack.of(Material.DIAMOND, 2), inventory.getItemStack(2)); - } - // Cancel event - { - listener.followup(event -> event.setCancelled(true)); - rightClick(player, 2); - assertEquals(ItemStack.AIR, inventory.getCursorItem(), "Left click cancellation did not work"); - assertEquals(ItemStack.of(Material.DIAMOND, 2), inventory.getItemStack(2)); - } - // Change items - { - listener.followup(event -> { - event.setClickedItem(ItemStack.of(Material.DIAMOND, 5)); - event.setCursorItem(ItemStack.of(Material.DIAMOND)); - }); - rightClick(player, 1); - assertEquals(ItemStack.AIR, inventory.getCursorItem()); - assertEquals(ItemStack.of(Material.DIAMOND, 6), inventory.getItemStack(1)); - } - } - - @Test - public void rightExternal(Env env) { - var instance = env.createFlatInstance(); - var player = env.createPlayer(instance, new Pos(0, 40, 0)); - var inventory = new Inventory(InventoryType.HOPPER, "test"); - player.openInventory(inventory); - var listener = env.listen(InventoryPreClickEvent.class); - inventory.setItemStack(1, ItemStack.of(Material.DIAMOND)); - // Empty click in player inv - { - listener.followup(event -> { - assertNull(event.getInventory()); // Player inventory - assertEquals(0, event.getSlot()); - assertEquals(ClickType.RIGHT_CLICK, event.getClickType()); - assertEquals(ItemStack.AIR, inventory.getCursorItem(player)); - }); - rightClick(player, 0); - } - // Pickup diamond - { - listener.followup(event -> { - assertEquals(inventory, event.getInventory()); - assertEquals(1, event.getSlot()); - assertEquals(ItemStack.AIR, inventory.getCursorItem(player)); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getItemStack(1)); - }); - rightClickOpenInventory(player, 1); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getCursorItem(player)); - assertEquals(ItemStack.AIR, inventory.getItemStack(1)); - } - // Place back to player inv - { - listener.followup(event -> { - assertNull(event.getInventory()); - assertEquals(1, event.getSlot()); - assertEquals(ItemStack.of(Material.DIAMOND), inventory.getCursorItem(player)); - assertEquals(ItemStack.AIR, inventory.getItemStack(1)); - assertEquals(ItemStack.AIR, player.getInventory().getItemStack(1)); - }); - rightClick(player, 1); - assertEquals(ItemStack.AIR, inventory.getCursorItem(player)); - assertEquals(ItemStack.of(Material.DIAMOND), player.getInventory().getItemStack(1)); - } - // Cancel event - { - listener.followup(event -> event.setCancelled(true)); - rightClick(player, 1); - assertEquals(ItemStack.of(Material.DIAMOND), player.getInventory().getItemStack(1), "Left click cancellation did not work"); - assertEquals(ItemStack.AIR, inventory.getCursorItem(player)); - } - // Change items - { - listener.followup(event -> { - assertNull(event.getInventory()); - assertEquals(9, event.getSlot()); - event.setClickedItem(ItemStack.of(Material.DIAMOND, 5)); - event.setCursorItem(ItemStack.of(Material.DIAMOND)); - }); - rightClick(player, 9); - assertEquals(ItemStack.AIR, inventory.getCursorItem(player)); - assertEquals(ItemStack.of(Material.DIAMOND, 6), player.getInventory().getItemStack(9)); - } - } - - private void rightClickOpenInventory(Player player, int slot) { - _rightClick(player.getOpenInventory(), true, player, slot); - } - - private void rightClick(Player player, int slot) { - _rightClick(player.getOpenInventory(), false, player, slot); - } - - private void _rightClick(Inventory openInventory, boolean clickOpenInventory, Player player, int slot) { - final byte windowId = openInventory != null ? openInventory.getWindowId() : 0; - if (clickOpenInventory) { - assert openInventory != null; - // Do not touch slot - } else { - int offset = openInventory != null ? openInventory.getInnerSize() : 0; - slot = PlayerInventoryUtils.convertToPacketSlot(slot); - if (openInventory != null) { - slot = slot - 9 + offset; - } - } - player.addPacketToQueue(new ClientClickWindowPacket(windowId, 0, (short) slot, (byte) 1, - ClientClickWindowPacket.ClickType.PICKUP, List.of(), ItemStack.AIR)); - player.interpretPacketQueue(); - } -} diff --git a/src/test/java/net/minestom/server/inventory/click/type/InventoryCreativeDropItemTest.java b/src/test/java/net/minestom/server/inventory/click/type/InventoryCreativeDropItemTest.java new file mode 100644 index 000000000..92c398241 --- /dev/null +++ b/src/test/java/net/minestom/server/inventory/click/type/InventoryCreativeDropItemTest.java @@ -0,0 +1,33 @@ +package net.minestom.server.inventory.click.type; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.junit.jupiter.api.Test; + +import static net.minestom.server.inventory.click.ClickUtils.assertClick; + +public class InventoryCreativeDropItemTest { + + static { + MinecraftServer.init(); + } + + @Test + public void testDropItem() { + assertClick( + builder -> builder, + new Click.Info.CreativeDropItem(ItemStack.of(Material.DIRT, 64)), + builder -> builder.sideEffects(new Click.SideEffect.DropFromPlayer(ItemStack.of(Material.DIRT, 64))) + ); + + // Make sure it doesn't drop a full stack + assertClick( + builder -> builder, + new Click.Info.CreativeDropItem(ItemStack.of(Material.DIRT, 1)), + builder -> builder.sideEffects(new Click.SideEffect.DropFromPlayer(ItemStack.of(Material.DIRT, 1))) + ); + } + +} diff --git a/src/test/java/net/minestom/server/inventory/click/type/InventoryCreativeSetItemTest.java b/src/test/java/net/minestom/server/inventory/click/type/InventoryCreativeSetItemTest.java new file mode 100644 index 000000000..e67924f0e --- /dev/null +++ b/src/test/java/net/minestom/server/inventory/click/type/InventoryCreativeSetItemTest.java @@ -0,0 +1,33 @@ +package net.minestom.server.inventory.click.type; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.junit.jupiter.api.Test; + +import static net.minestom.server.inventory.click.ClickUtils.assertClick; + +public class InventoryCreativeSetItemTest { + + static { + MinecraftServer.init(); + } + + @Test + public void testSetItem() { + assertClick( + builder -> builder, + new Click.Info.CreativeSetItem(0, ItemStack.of(Material.DIRT, 64)), + builder -> builder.set(0, ItemStack.of(Material.DIRT, 64)) + ); + + // Make sure it doesn't set a full stack + assertClick( + builder -> builder, + new Click.Info.CreativeSetItem(0, ItemStack.of(Material.DIRT, 1)), + builder -> builder.set(0, ItemStack.of(Material.DIRT, 1)) + ); + } + +} diff --git a/src/test/java/net/minestom/server/inventory/click/type/InventoryDoubleClickTest.java b/src/test/java/net/minestom/server/inventory/click/type/InventoryDoubleClickTest.java new file mode 100644 index 000000000..5d83eb936 --- /dev/null +++ b/src/test/java/net/minestom/server/inventory/click/type/InventoryDoubleClickTest.java @@ -0,0 +1,91 @@ +package net.minestom.server.inventory.click.type; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.junit.jupiter.api.Test; + +import static net.minestom.server.inventory.click.ClickUtils.assertClick; + +public class InventoryDoubleClickTest { + + static { + MinecraftServer.init(); + } + + @Test + public void testNoChanges() { + assertClick(builder -> builder, new Click.Info.Double(0), builder -> builder); + } + + @Test + public void testCannotTakeAny() { + assertClick( + builder -> builder.cursor(ItemStack.of(Material.STONE, 32)), + new Click.Info.Double(0), + builder -> builder + ); + } + + @Test + public void testPartialTake() { + assertClick( + builder -> builder.set(1, ItemStack.of(Material.STONE, 48)).cursor(ItemStack.of(Material.STONE, 32)), + new Click.Info.Double(0), + builder -> builder.set(1, ItemStack.of(Material.STONE, 16)).cursor(ItemStack.of(Material.STONE, 64)) + ); + } + + @Test + public void testTakeAll() { + assertClick( + builder -> builder.set(1, ItemStack.of(Material.STONE, 32)).cursor(ItemStack.of(Material.STONE, 32)), + new Click.Info.Double(0), + builder -> builder.set(1, ItemStack.AIR).cursor(ItemStack.of(Material.STONE, 64)) + ); + + assertClick( + builder -> builder.set(1, ItemStack.of(Material.STONE, 16)).cursor(ItemStack.of(Material.STONE, 32)), + new Click.Info.Double(0), + builder -> builder.set(1, ItemStack.AIR).cursor(ItemStack.of(Material.STONE, 48)) + ); + } + + @Test + public void testTakeSeparated() { + assertClick( + builder -> builder + .set(1, ItemStack.of(Material.STONE, 16)) + .set(2, ItemStack.of(Material.STONE, 16)) + .cursor(ItemStack.of(Material.STONE, 32)), + new Click.Info.Double(0), + builder -> builder + .set(1, ItemStack.AIR) + .set(2, ItemStack.AIR) + .cursor(ItemStack.of(Material.STONE, 64)) + ); + + assertClick( + builder -> builder + .set(1, ItemStack.of(Material.STONE, 16)) + .set(2, ItemStack.of(Material.STONE, 32)) + .cursor(ItemStack.of(Material.STONE, 32)), + new Click.Info.Double(0), + builder -> builder + .set(1, ItemStack.AIR) + .set(2, ItemStack.of(Material.STONE, 16)) + .cursor(ItemStack.of(Material.STONE, 64)) + ); + } + + @Test + public void testCursorFull() { + assertClick( + builder -> builder.set(1, ItemStack.of(Material.STONE, 48)).cursor(ItemStack.of(Material.STONE, 64)), + new Click.Info.Double(0), + builder -> builder + ); + } + +} diff --git a/src/test/java/net/minestom/server/inventory/click/type/InventoryDropCursorTest.java b/src/test/java/net/minestom/server/inventory/click/type/InventoryDropCursorTest.java new file mode 100644 index 000000000..abfe71bb2 --- /dev/null +++ b/src/test/java/net/minestom/server/inventory/click/type/InventoryDropCursorTest.java @@ -0,0 +1,51 @@ +package net.minestom.server.inventory.click.type; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.junit.jupiter.api.Test; + +import static net.minestom.server.inventory.click.ClickUtils.assertClick; + +public class InventoryDropCursorTest { + + static { + MinecraftServer.init(); + } + + @Test + public void testNoChanges() { + assertClick(builder -> builder, new Click.Info.LeftDropCursor(), builder -> builder); + assertClick(builder -> builder, new Click.Info.MiddleDropCursor(), builder -> builder); + assertClick(builder -> builder, new Click.Info.RightDropCursor(), builder -> builder); + } + + @Test + public void testDropEntireStack() { + assertClick( + builder -> builder.cursor(ItemStack.of(Material.STONE, 32)), + new Click.Info.LeftDropCursor(), + builder -> builder.cursor(ItemStack.AIR).sideEffects(new Click.SideEffect.DropFromPlayer(ItemStack.of(Material.STONE, 32))) + ); + } + + @Test + public void testDropSingleItem() { + assertClick( + builder -> builder.cursor(ItemStack.of(Material.STONE, 32)), + new Click.Info.RightDropCursor(), + builder -> builder.cursor(ItemStack.of(Material.STONE, 31)).sideEffects(new Click.SideEffect.DropFromPlayer(ItemStack.of(Material.STONE, 1))) + ); + } + + @Test + public void testMiddleClickNoop() { + assertClick( + builder -> builder.cursor(ItemStack.of(Material.STONE, 32)), + new Click.Info.MiddleDropCursor(), + builder -> builder + ); + } + +} diff --git a/src/test/java/net/minestom/server/inventory/click/type/InventoryDropSlotTest.java b/src/test/java/net/minestom/server/inventory/click/type/InventoryDropSlotTest.java new file mode 100644 index 000000000..1bed2e19c --- /dev/null +++ b/src/test/java/net/minestom/server/inventory/click/type/InventoryDropSlotTest.java @@ -0,0 +1,41 @@ +package net.minestom.server.inventory.click.type; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.junit.jupiter.api.Test; + +import static net.minestom.server.inventory.click.ClickUtils.assertClick; + +public class InventoryDropSlotTest { + + static { + MinecraftServer.init(); + } + + @Test + public void testNoChanges() { + assertClick(builder -> builder, new Click.Info.DropSlot(0, false), builder -> builder); + assertClick(builder -> builder, new Click.Info.DropSlot(0, true), builder -> builder); + } + + @Test + public void testDropEntireStack() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.STONE, 32)), + new Click.Info.DropSlot(0, true), + builder -> builder.set(0, ItemStack.AIR).sideEffects(new Click.SideEffect.DropFromPlayer(ItemStack.of(Material.STONE, 32))) + ); + } + + @Test + public void testDropSingleItem() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.STONE, 32)), + new Click.Info.DropSlot(0, false), + builder -> builder.set(0, ItemStack.of(Material.STONE, 31)).sideEffects(new Click.SideEffect.DropFromPlayer(ItemStack.of(Material.STONE, 1))) + ); + } + +} diff --git a/src/test/java/net/minestom/server/inventory/click/type/InventoryHotbarSwapTest.java b/src/test/java/net/minestom/server/inventory/click/type/InventoryHotbarSwapTest.java new file mode 100644 index 000000000..6ba864350 --- /dev/null +++ b/src/test/java/net/minestom/server/inventory/click/type/InventoryHotbarSwapTest.java @@ -0,0 +1,33 @@ +package net.minestom.server.inventory.click.type; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.junit.jupiter.api.Test; + +import static net.minestom.server.inventory.click.ClickUtils.assertClick; + +public class InventoryHotbarSwapTest { + + static { + MinecraftServer.init(); + } + + @Test + public void testNoChanges() { + for (int i = 0; i < 9; i++) { + assertClick(builder -> builder, new Click.Info.HotbarSwap(i, 9), builder -> builder); + } + } + + @Test + public void testSwappedItems() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.DIRT)).setPlayer(0, ItemStack.of(Material.STONE)), + new Click.Info.HotbarSwap(0, 0), + builder -> builder.set(0, ItemStack.of(Material.STONE)).setPlayer(0, ItemStack.of(Material.DIRT)) + ); + } + +} diff --git a/src/test/java/net/minestom/server/inventory/click/type/InventoryLeftClickTest.java b/src/test/java/net/minestom/server/inventory/click/type/InventoryLeftClickTest.java new file mode 100644 index 000000000..1ff08b07a --- /dev/null +++ b/src/test/java/net/minestom/server/inventory/click/type/InventoryLeftClickTest.java @@ -0,0 +1,49 @@ +package net.minestom.server.inventory.click.type; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.junit.jupiter.api.Test; + +import static net.minestom.server.inventory.click.ClickUtils.assertClick; + +public class InventoryLeftClickTest { + + static { + MinecraftServer.init(); + } + + @Test + public void testNoChanges() { + assertClick(builder -> builder, new Click.Info.Left(0), builder -> builder); + } + + @Test + public void testInsertEntireStack() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.STONE, 32)).cursor(ItemStack.of(Material.STONE, 32)), + new Click.Info.Left(0), + builder -> builder.set(0, ItemStack.of(Material.STONE, 64)).cursor(ItemStack.AIR) + ); + } + + @Test + public void testInsertPartialStack() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.STONE, 32)).cursor(ItemStack.of(Material.STONE, 48)), + new Click.Info.Left(0), + builder -> builder.set(0, ItemStack.of(Material.STONE, 64)).cursor(ItemStack.of(Material.STONE, 16)) + ); + } + + @Test + public void testSwitchItems() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.STONE)).cursor(ItemStack.of(Material.DIRT)), + new Click.Info.Left(0), + builder -> builder.set(0, ItemStack.of(Material.DIRT)).cursor(ItemStack.of(Material.STONE)) + ); + } + +} diff --git a/src/test/java/net/minestom/server/inventory/click/type/InventoryLeftDragTest.java b/src/test/java/net/minestom/server/inventory/click/type/InventoryLeftDragTest.java new file mode 100644 index 000000000..a860af4a8 --- /dev/null +++ b/src/test/java/net/minestom/server/inventory/click/type/InventoryLeftDragTest.java @@ -0,0 +1,102 @@ +package net.minestom.server.inventory.click.type; + +import it.unimi.dsi.fastutil.ints.IntList; +import net.minestom.server.MinecraftServer; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.junit.jupiter.api.Test; + +import static net.minestom.server.inventory.click.ClickUtils.assertClick; + +public class InventoryLeftDragTest { + + static { + MinecraftServer.init(); + } + + @Test + public void testNoCursor() { + assertClick(builder -> builder, new Click.Info.LeftDrag(IntList.of(0)), builder -> builder); + } + + @Test + public void testDistributeNone() { + assertClick( + builder -> builder.cursor(ItemStack.of(Material.DIRT, 32)), + new Click.Info.LeftDrag(IntList.of()), + builder -> builder + ); + } + + @Test + public void testDistributeOne() { + assertClick( + builder -> builder.cursor(ItemStack.of(Material.DIRT, 32)), + new Click.Info.LeftDrag(IntList.of(0)), + builder -> builder.set(0, ItemStack.of(Material.DIRT, 32)).cursor(ItemStack.of(Material.AIR)) + ); + } + + @Test + public void testDistributeExactlyEnough() { + assertClick( + builder -> builder.cursor(ItemStack.of(Material.DIRT, 32)), + new Click.Info.LeftDrag(IntList.of(0, 1)), + builder -> builder.set(0, ItemStack.of(Material.DIRT, 16)).set(1, ItemStack.of(Material.DIRT, 16)).cursor(ItemStack.of(Material.AIR)) + ); + + assertClick( + builder -> builder.cursor(ItemStack.of(Material.DIRT, 30)), + new Click.Info.LeftDrag(IntList.of(0, 1, 2)), + builder -> builder + .set(0, ItemStack.of(Material.DIRT, 10)) + .set(1, ItemStack.of(Material.DIRT, 10)) + .set(2, ItemStack.of(Material.DIRT, 10)) + .cursor(ItemStack.of(Material.AIR)) + ); + } + + @Test + public void testRemainderItems() { + assertClick( + builder -> builder.cursor(ItemStack.of(Material.DIRT, 32)), + new Click.Info.LeftDrag(IntList.of(0, 1, 2)), + builder -> builder + .set(0, ItemStack.of(Material.DIRT, 10)) + .set(1, ItemStack.of(Material.DIRT, 10)) + .set(2, ItemStack.of(Material.DIRT, 10)) + .cursor(ItemStack.of(Material.DIRT, 2)) + ); + + assertClick( + builder -> builder.cursor(ItemStack.of(Material.DIRT, 25)), + new Click.Info.LeftDrag(IntList.of(0, 1, 2, 3)), + builder -> builder + .set(0, ItemStack.of(Material.DIRT, 6)) + .set(1, ItemStack.of(Material.DIRT, 6)) + .set(2, ItemStack.of(Material.DIRT, 6)) + .set(3, ItemStack.of(Material.DIRT, 6)) + .cursor(ItemStack.of(Material.DIRT, 1)) + ); + } + + @Test + public void testDistributeOverExisting() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.DIRT, 16)).cursor(ItemStack.of(Material.DIRT, 32)), + new Click.Info.LeftDrag(IntList.of(0)), + builder -> builder.set(0, ItemStack.of(Material.DIRT, 48)).cursor(ItemStack.of(Material.AIR)) + ); + } + + @Test + public void testDistributeOverFull() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.DIRT, 64)).cursor(ItemStack.of(Material.DIRT, 32)), + new Click.Info.LeftDrag(IntList.of(0)), + builder -> builder + ); + } + +} diff --git a/src/test/java/net/minestom/server/inventory/click/type/InventoryMiddleClickTest.java b/src/test/java/net/minestom/server/inventory/click/type/InventoryMiddleClickTest.java new file mode 100644 index 000000000..7142f0434 --- /dev/null +++ b/src/test/java/net/minestom/server/inventory/click/type/InventoryMiddleClickTest.java @@ -0,0 +1,40 @@ +package net.minestom.server.inventory.click.type; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.junit.jupiter.api.Test; + +import static net.minestom.server.inventory.click.ClickUtils.assertClick; + +public class InventoryMiddleClickTest { + + static { + MinecraftServer.init(); + } + + @Test + public void testNoChanges() { + assertClick(builder -> builder, new Click.Info.Middle(0), builder -> builder); + } + + @Test + public void testCopy() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.DIRT, 64)), + new Click.Info.Middle(0), + builder -> builder.cursor(ItemStack.of(Material.DIRT, 64)) + ); + } + + @Test + public void testCopyNotFull() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.DIRT, 32)), + new Click.Info.Middle(0), + builder -> builder.cursor(ItemStack.of(Material.DIRT, 64)) + ); + } + +} diff --git a/src/test/java/net/minestom/server/inventory/click/type/InventoryMiddleDragTest.java b/src/test/java/net/minestom/server/inventory/click/type/InventoryMiddleDragTest.java new file mode 100644 index 000000000..72890fb30 --- /dev/null +++ b/src/test/java/net/minestom/server/inventory/click/type/InventoryMiddleDragTest.java @@ -0,0 +1,50 @@ +package net.minestom.server.inventory.click.type; + +import it.unimi.dsi.fastutil.ints.IntList; +import net.minestom.server.MinecraftServer; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.junit.jupiter.api.Test; + +import static net.minestom.server.inventory.click.ClickUtils.assertClick; + +public class InventoryMiddleDragTest { + + static { + MinecraftServer.init(); + } + + @Test + public void testNoChanges() { + assertClick(builder -> builder, new Click.Info.MiddleDrag(IntList.of()), builder -> builder); + } + + @Test + public void testExistingSlots() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.STONE)).cursor(ItemStack.of(Material.DIRT)), + new Click.Info.MiddleDrag(IntList.of(0)), + builder -> builder + ); + } + + @Test + public void testPartialExistingSlots() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.STONE)).cursor(ItemStack.of(Material.DIRT)), + new Click.Info.MiddleDrag(IntList.of(0, 1)), + builder -> builder.set(1, ItemStack.of(Material.DIRT)) + ); + } + + @Test + public void testFullCopy() { + assertClick( + builder -> builder.cursor(ItemStack.of(Material.DIRT)), + new Click.Info.MiddleDrag(IntList.of(0, 1)), + builder -> builder.set(0, ItemStack.of(Material.DIRT)).set(1, ItemStack.of(Material.DIRT)) + ); + } + +} diff --git a/src/test/java/net/minestom/server/inventory/click/type/InventoryOffhandSwapTest.java b/src/test/java/net/minestom/server/inventory/click/type/InventoryOffhandSwapTest.java new file mode 100644 index 000000000..cb2c4b4ab --- /dev/null +++ b/src/test/java/net/minestom/server/inventory/click/type/InventoryOffhandSwapTest.java @@ -0,0 +1,32 @@ +package net.minestom.server.inventory.click.type; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.utils.inventory.PlayerInventoryUtils; +import org.junit.jupiter.api.Test; + +import static net.minestom.server.inventory.click.ClickUtils.assertClick; + +public class InventoryOffhandSwapTest { + + static { + MinecraftServer.init(); + } + + @Test + public void testNoChanges() { + assertClick(builder -> builder, new Click.Info.OffhandSwap(0), builder -> builder); + } + + @Test + public void testSwappedItems() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.DIRT)).setPlayer(PlayerInventoryUtils.OFF_HAND_SLOT, ItemStack.of(Material.STONE)), + new Click.Info.OffhandSwap(0), + builder -> builder.set(0, ItemStack.of(Material.STONE)).setPlayer(PlayerInventoryUtils.OFF_HAND_SLOT, ItemStack.of(Material.DIRT)) + ); + } + +} diff --git a/src/test/java/net/minestom/server/inventory/click/type/InventoryRightClickTest.java b/src/test/java/net/minestom/server/inventory/click/type/InventoryRightClickTest.java new file mode 100644 index 000000000..c8ad77ea5 --- /dev/null +++ b/src/test/java/net/minestom/server/inventory/click/type/InventoryRightClickTest.java @@ -0,0 +1,67 @@ +package net.minestom.server.inventory.click.type; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.junit.jupiter.api.Test; + +import static net.minestom.server.inventory.click.ClickUtils.assertClick; + +public class InventoryRightClickTest { + + static { + MinecraftServer.init(); + } + + @Test + public void testNoChanges() { + assertClick(builder -> builder, new Click.Info.Right(0), builder -> builder); + } + + @Test + public void testAddOne() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.STONE, 32)).cursor(ItemStack.of(Material.STONE, 32)), + new Click.Info.Right(0), + builder -> builder.set(0, ItemStack.of(Material.STONE, 33)).cursor(ItemStack.of(Material.STONE, 31)) + ); + } + + @Test + public void testClickedStackFull() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.STONE, 64)).cursor(ItemStack.of(Material.STONE, 32)), + new Click.Info.Right(0), + builder -> builder + ); + } + + @Test + public void testTakeHalf() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.STONE, 32)), + new Click.Info.Right(0), + builder -> builder.set(0, ItemStack.of(Material.STONE, 16)).cursor(ItemStack.of(Material.STONE, 16)) + ); + } + + @Test + public void testLeaveOne() { + assertClick( + builder -> builder.cursor(ItemStack.of(Material.STONE, 32)), + new Click.Info.Right(0), + builder -> builder.set(0, ItemStack.of(Material.STONE, 1)).cursor(ItemStack.of(Material.STONE, 31)) + ); + } + + @Test + public void testSwitchItems() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.STONE)).cursor(ItemStack.of(Material.DIRT)), + new Click.Info.Right(0), + builder -> builder.set(0, ItemStack.of(Material.DIRT)).cursor(ItemStack.of(Material.STONE)) + ); + } + +} diff --git a/src/test/java/net/minestom/server/inventory/click/type/InventoryRightDragTest.java b/src/test/java/net/minestom/server/inventory/click/type/InventoryRightDragTest.java new file mode 100644 index 000000000..c24cd7b4d --- /dev/null +++ b/src/test/java/net/minestom/server/inventory/click/type/InventoryRightDragTest.java @@ -0,0 +1,77 @@ +package net.minestom.server.inventory.click.type; + +import it.unimi.dsi.fastutil.ints.IntList; +import net.minestom.server.MinecraftServer; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.junit.jupiter.api.Test; + +import static net.minestom.server.inventory.click.ClickUtils.assertClick; + +public class InventoryRightDragTest { + + static { + MinecraftServer.init(); + } + + @Test + public void testNoCursor() { + assertClick(builder -> builder, new Click.Info.RightDrag(IntList.of(0)), builder -> builder); + } + + @Test + public void testDistributeNone() { + assertClick( + builder -> builder.cursor(ItemStack.of(Material.DIRT, 32)), + new Click.Info.RightDrag(IntList.of()), + builder -> builder + ); + } + + @Test + public void testDistributeOne() { + assertClick( + builder -> builder.cursor(ItemStack.of(Material.DIRT, 32)), + new Click.Info.RightDrag(IntList.of(0)), + builder -> builder.set(0, ItemStack.of(Material.DIRT)).cursor(ItemStack.of(Material.DIRT, 31)) + ); + } + + @Test + public void testDistributeExactlyEnough() { + assertClick( + builder -> builder.cursor(ItemStack.of(Material.DIRT, 2)), + new Click.Info.RightDrag(IntList.of(0, 1)), + builder -> builder.set(0, ItemStack.of(Material.DIRT)).set(1, ItemStack.of(Material.DIRT)).cursor(ItemStack.of(Material.AIR)) + ); + } + + @Test + public void testTooManySlots() { + assertClick( + builder -> builder.cursor(ItemStack.of(Material.DIRT, 2)), + new Click.Info.RightDrag(IntList.of(0, 1, 2)), + builder -> builder.set(0, ItemStack.of(Material.DIRT)).set(1, ItemStack.of(Material.DIRT)).cursor(ItemStack.of(Material.AIR)) + ); + } + + @Test + public void testDistributeOverExisting() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.DIRT, 16)).cursor(ItemStack.of(Material.DIRT, 32)), + new Click.Info.RightDrag(IntList.of(0)), + builder -> builder.set(0, ItemStack.of(Material.DIRT, 17)).cursor(ItemStack.of(Material.DIRT, 31)) + ); + } + + @Test + public void testDistributeOverFull() { + assertClick( + builder -> builder.set(0, ItemStack.of(Material.DIRT, 64)).cursor(ItemStack.of(Material.DIRT, 32)), + new Click.Info.RightDrag(IntList.of(0)), + builder -> builder + ); + } + +} diff --git a/src/test/java/net/minestom/server/inventory/click/type/InventoryShiftClickTest.java b/src/test/java/net/minestom/server/inventory/click/type/InventoryShiftClickTest.java new file mode 100644 index 000000000..24723325f --- /dev/null +++ b/src/test/java/net/minestom/server/inventory/click/type/InventoryShiftClickTest.java @@ -0,0 +1,149 @@ +package net.minestom.server.inventory.click.type; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.inventory.click.Click; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.utils.inventory.PlayerInventoryUtils; +import org.junit.jupiter.api.Test; + +import static net.minestom.server.inventory.click.ClickUtils.*; + +public class InventoryShiftClickTest { + + static { + MinecraftServer.init(); + } + + @Test + public void testNoChanges() { + assertClick(builder -> builder, new Click.Info.LeftShift(0), builder -> builder); + assertClick(builder -> builder, new Click.Info.RightShift(0), builder -> builder); + } + + @Test + public void testSimpleTransfer() { + assertClick( + builder -> builder.setPlayer(9, ItemStack.of(Material.STONE, 32)), + new Click.Info.LeftShift(SIZE), + builder -> builder.set(0, ItemStack.of(Material.STONE, 32)).setPlayer(9, ItemStack.AIR) + ); + } + + @Test + public void testSeparatedTransfer() { + assertClick( + builder -> builder + .setPlayer(9, ItemStack.of(Material.STONE, 64)) + .set(0, ItemStack.of(Material.STONE, 32)) + .set(1, ItemStack.of(Material.STONE, 32)) + , + new Click.Info.LeftShift(SIZE), + builder -> builder + .setPlayer(9, ItemStack.AIR) + .set(0, ItemStack.of(Material.STONE, 64)) + .set(1, ItemStack.of(Material.STONE, 64)) + + ); + } + + @Test + public void testSeparatedAndNewTransfer() { + assertClick( + builder -> builder + .setPlayer(9, ItemStack.of(Material.STONE, 64)) + .set(0, ItemStack.of(Material.STONE, 32)), + new Click.Info.LeftShift(SIZE), + builder -> builder + .setPlayer(9, ItemStack.AIR) + .set(0, ItemStack.of(Material.STONE, 64)) + .set(1, ItemStack.of(Material.STONE, 32)) + + ); + } + + @Test + public void testPartialTransfer() { + assertClick( + builder -> builder + .setPlayer(9, ItemStack.of(Material.STONE, 64)) + .set(0, ItemStack.of(Material.STONE, 32)) + .set(1, ItemStack.of(Material.DIRT)) + .set(2, ItemStack.of(Material.DIRT)) + .set(3, ItemStack.of(Material.DIRT)) + .set(4, ItemStack.of(Material.DIRT)), + new Click.Info.LeftShift(SIZE), + builder -> builder + .setPlayer(9, ItemStack.of(Material.STONE, 32)) + .set(0, ItemStack.of(Material.STONE, 64)) + + ); + } + + @Test + public void testCannotTransfer() { + assertClick( + builder -> builder + .setPlayer(9, ItemStack.of(Material.STONE, 64)) + .set(0, ItemStack.of(Material.STONE, 64)) + .set(1, ItemStack.of(Material.DIRT)) + .set(2, ItemStack.of(Material.DIRT)) + .set(3, ItemStack.of(Material.DIRT)) + .set(4, ItemStack.of(Material.DIRT)), + new Click.Info.LeftShift(SIZE), // Equivalent to player slot 9 + builder -> builder + ); + + assertClick( + builder -> builder + .setPlayer(9, ItemStack.of(Material.STONE, 64)) + .set(0, ItemStack.of(Material.DIRT)) + .set(1, ItemStack.of(Material.DIRT)) + .set(2, ItemStack.of(Material.DIRT)) + .set(3, ItemStack.of(Material.DIRT)) + .set(4, ItemStack.of(Material.DIRT)), + new Click.Info.LeftShift(SIZE), // Equivalent to player slot 9 + builder -> builder + ); + } + + @Test + public void testPlayerInteraction() { + assertPlayerClick( + builder -> builder.set(9, ItemStack.of(Material.STONE, 32)), + new Click.Info.LeftShift(9), + builder -> builder.set(9, ItemStack.AIR).set(0, ItemStack.of(Material.STONE, 32)) + ); + + assertPlayerClick( + builder -> builder.set(8, ItemStack.of(Material.STONE, 32)), + new Click.Info.LeftShift(8), + builder -> builder.set(8, ItemStack.AIR).set(9, ItemStack.of(Material.STONE, 32)) + ); + + assertPlayerClick( + builder -> builder.set(9, ItemStack.of(Material.IRON_CHESTPLATE)), + new Click.Info.LeftShift(9), + builder -> builder.set(9, ItemStack.AIR).set(PlayerInventoryUtils.CHESTPLATE_SLOT, ItemStack.of(Material.IRON_CHESTPLATE)) + ); + + assertPlayerClick( + builder -> builder.set(PlayerInventoryUtils.CHESTPLATE_SLOT, ItemStack.of(Material.IRON_CHESTPLATE)), + new Click.Info.LeftShift(PlayerInventoryUtils.CHESTPLATE_SLOT), + builder -> builder.set(PlayerInventoryUtils.CHESTPLATE_SLOT, ItemStack.AIR).set(9, ItemStack.of(Material.IRON_CHESTPLATE)) + ); + + assertPlayerClick( + builder -> builder.set(9, ItemStack.of(Material.SHIELD)), + new Click.Info.LeftShift(9), + builder -> builder.set(9, ItemStack.AIR).set(PlayerInventoryUtils.OFF_HAND_SLOT, ItemStack.of(Material.SHIELD)) + ); + + assertPlayerClick( + builder -> builder.set(PlayerInventoryUtils.OFF_HAND_SLOT, ItemStack.of(Material.SHIELD)), + new Click.Info.LeftShift(PlayerInventoryUtils.OFF_HAND_SLOT), + builder -> builder.set(PlayerInventoryUtils.OFF_HAND_SLOT, ItemStack.AIR).set(9, ItemStack.of(Material.SHIELD)) + ); + } + +}