diff --git a/src/main/java/net/minestom/server/inventory/Inventory.java b/src/main/java/net/minestom/server/inventory/Inventory.java index 51ba15ca9..849943c78 100644 --- a/src/main/java/net/minestom/server/inventory/Inventory.java +++ b/src/main/java/net/minestom/server/inventory/Inventory.java @@ -250,16 +250,22 @@ public non-sealed class Inventory extends AbstractInventory implements Viewable public boolean shiftClick(@NotNull Player player, int slot) { final boolean isInWindow = isClickInWindow(slot); final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset); + final AbstractInventory inventory = isInWindow ? this : player.getInventory(); + final ItemStack item = getItemStack(clickSlot); PlayerInventory playerInv = player.getInventory(); - if (isInWindow) { - final ItemStack item = getItemStack(clickSlot); - return handleResult(ClickProcessor.shiftToPlayer(playerInv, item), - itemStack -> setItemStack(clickSlot, itemStack), player, playerInv, ClickType.SHIFT_CLICK); - } else { - final ItemStack item = playerInv.getItemStack(clickSlot); - return handleResult(ClickProcessor.shiftToInventory(this, item), - itemStack -> playerInv.setItemStack(clickSlot, itemStack), player, this, ClickType.SHIFT_CLICK); - } + final var tmp = handlePreClick(inventory, player, clickSlot, ClickType.START_SHIFT_CLICK, + getCursorItem(player), inventory.getItemStack(clickSlot)); + if (tmp.cancelled()) { + update(); + return false; + } + + var result = isInWindow ? ClickProcessor.shiftToPlayer(playerInv, item) : ClickProcessor.shiftToInventory(this, item); + var inverseInv = isInWindow ? player.getInventory() : this; + // TODO call pre-click for each changed slot + return handleResult(result, + itemStack -> inventory.setItemStack(clickSlot, itemStack), player, inverseInv, ClickType.SHIFT_CLICK); + } @Override diff --git a/src/main/java/net/minestom/server/inventory/PlayerInventory.java b/src/main/java/net/minestom/server/inventory/PlayerInventory.java index a27d9b8b7..046ee62a3 100644 --- a/src/main/java/net/minestom/server/inventory/PlayerInventory.java +++ b/src/main/java/net/minestom/server/inventory/PlayerInventory.java @@ -14,6 +14,7 @@ import net.minestom.server.network.packet.server.play.WindowItemsPacket; import org.jetbrains.annotations.NotNull; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import static net.minestom.server.utils.inventory.PlayerInventoryUtils.*; @@ -239,8 +240,27 @@ public non-sealed class PlayerInventory extends AbstractInventory implements Equ public boolean shiftClick(@NotNull Player player, int slot) { final int convertedSlot = convertPlayerInventorySlot(slot, OFFSET); final ItemStack shifted = getItemStack(convertedSlot); - return handleResult(ClickProcessor.shiftWithinPlayer(this, convertedSlot, shifted), - itemStack -> setItemStack(convertedSlot, itemStack), ClickType.SHIFT_CLICK); + final var tmp = handlePreClick(this, player, convertedSlot, ClickType.START_SHIFT_CLICK, + getCursorItem(), shifted); + if (tmp.cancelled()) { + update(); + return false; + } + var result = ClickProcessor.shiftWithinPlayer(this, convertedSlot, shifted); + AtomicBoolean modified = new AtomicBoolean(); + result.changedSlots().forEach((updateSlot, itemStack) -> { + final var tmp2 = handlePreClick(this, player, updateSlot, ClickType.SHIFT_CLICK, + getCursorItem(), itemStack); + if (tmp2.cancelled()) { + modified.setPlain(true); + return; + } + setItemStack(updateSlot, itemStack); + callClickEvent(player, null, updateSlot, ClickType.SHIFT_CLICK, itemStack, getCursorItem()); + }); + setItemStack(convertedSlot, result.remaining()); + if (modified.getPlain()) update(); + return true; } @Override diff --git a/src/test/java/net/minestom/server/inventory/click/integration/ShiftClickIntegrationTest.java b/src/test/java/net/minestom/server/inventory/click/integration/ShiftClickIntegrationTest.java new file mode 100644 index 000000000..dcc99d054 --- /dev/null +++ b/src/test/java/net/minestom/server/inventory/click/integration/ShiftClickIntegrationTest.java @@ -0,0 +1,133 @@ +package net.minestom.server.inventory.click.integration; + +import net.minestom.server.api.Env; +import net.minestom.server.api.EnvTest; +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 org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +@EnvTest +public class ShiftClickIntegrationTest { + + @Test + public void shiftSelf(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); + // Drag to air + { + inventory.setItemStack(0, ItemStack.of(Material.STONE, 64)); + List events = new ArrayList<>(); + listener.followup(events::add); + shift(player, 0); + // start->slot + assertEquals(2, events.size()); + { + // Start + var event = events.get(0); + assertNull(event.getInventory()); + assertEquals(ClickType.START_SHIFT_CLICK, event.getClickType()); + assertEquals(0, event.getSlot()); + assertEquals(ItemStack.AIR, event.getCursorItem()); + assertEquals(ItemStack.of(Material.STONE, 64), event.getClickedItem()); + } + { + // Slot + var event = events.get(1); + assertNull(event.getInventory()); + assertEquals(ClickType.SHIFT_CLICK, event.getClickType()); + assertEquals(9, event.getSlot()); + assertEquals(ItemStack.AIR, event.getCursorItem()); + assertEquals(ItemStack.of(Material.STONE, 64), event.getClickedItem()); + } + + assertEquals(ItemStack.AIR, inventory.getCursorItem()); + assertEquals(ItemStack.AIR, inventory.getItemStack(0)); + assertEquals(ItemStack.of(Material.STONE, 64), inventory.getItemStack(9)); + } + } + + @Test + public void shiftExternal(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); + // Drag to air + { + inventory.setItemStack(0, ItemStack.of(Material.STONE, 64)); + List events = new ArrayList<>(); + listener.followup(events::add); + shiftOpenInventory(player, 0); + // start->slot + assertEquals(2, events.size()); + { + // Start + var event = events.get(0); + assertEquals(inventory, event.getInventory()); + assertEquals(ClickType.START_SHIFT_CLICK, event.getClickType()); + assertEquals(0, event.getSlot()); + assertEquals(ItemStack.AIR, event.getCursorItem()); + assertEquals(ItemStack.of(Material.STONE, 64), event.getClickedItem()); + } + { + // Slot + var event = events.get(1); + assertNull(event.getInventory()); + assertEquals(ClickType.SHIFT_CLICK, event.getClickType()); + assertEquals(9, event.getSlot()); + assertEquals(ItemStack.AIR, event.getCursorItem()); + assertEquals(ItemStack.of(Material.STONE, 64), event.getClickedItem()); + } + + assertEquals(ItemStack.AIR, inventory.getCursorItem(player)); + assertEquals(ItemStack.AIR, inventory.getItemStack(0)); + assertEquals(ItemStack.of(Material.STONE, 64), player.getInventory().getItemStack(8)); + } + } + + private void shiftOpenInventory(Player player, int slot) { + _shift(player.getOpenInventory(), true, player, slot); + } + + private void shift(Player player, int slot) { + _shift(player.getOpenInventory(), false, player, slot); + } + + private void _shift(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; + } + } + + // button 0 = left + // button 1 = right + // They should have identical behavior + player.addPacketToQueue(new ClientClickWindowPacket(windowId, 0, (short) slot, (byte) 0, + ClientClickWindowPacket.ClickType.QUICK_MOVE, List.of(), ItemStack.AIR)); + player.interpretPacketQueue(); + } +}