Compare commits

...

18 Commits

Author SHA1 Message Date
GoldenStack 2de92f5152
Merge b728031ca5 into 5c23713c03 2024-04-27 17:54:46 +00:00
GoldenStack b728031ca5 Fix double clicking and fix tests 2024-04-27 12:54:36 -05:00
GoldenStack ed225efb11 Rename Change.Main and document Change 2024-04-20 21:01:53 -05:00
GoldenStack 95efe11ba4 Fix middle click drop handling 2024-04-20 20:48:54 -05:00
GoldenStack 93421e1740 Fix inventory close regression 2024-04-20 20:21:04 -05:00
themode 67e6686acc Remove fastutil pair 2024-04-15 00:03:37 +02:00
themode 0e91a565b4 Inline requireCreative 2024-04-14 22:57:21 +02:00
themode da3523f559 Little simplification 2024-04-14 05:17:19 +02:00
GoldenStack d4b44c7137 Switch Click.Result to List<Click.Change> 2024-04-13 21:35:40 -05:00
GoldenStack 3a8bab554a Fix player inventory size usage 2024-04-13 19:36:18 -05:00
GoldenStack e9d957814e Add Click.Change 2024-04-12 13:21:04 -05:00
themode 1dc9a20541 Move some constants out of PlayerInventory 2024-04-11 22:47:47 +02:00
themode 3e3182660c Store all processors in ClickProcessors 2024-04-11 22:42:02 +02:00
themode 1573b34970 More style 2024-04-11 22:27:51 +02:00
themode 74eb1ac794 Remove Click's Inventory dependency 2024-04-11 22:27:51 +02:00
themode 18406cb504 Style change 2024-04-11 22:27:51 +02:00
GoldenStack 8c79660681 Remove fastutil references in API 2024-04-11 22:27:51 +02:00
GoldenStack 1a497115b0 Inventory rework (88 squashed commits) 2024-04-11 22:27:51 +02:00
72 changed files with 3007 additions and 2884 deletions

View File

@ -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<Event> 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));
}

View File

@ -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<ItemStack> itemStacks;

View File

@ -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<EquipmentSlot> ARMORS = List.of(BOOTS, LEGGINGS, CHESTPLATE, HELMET);

View File

@ -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,10 +179,11 @@ 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
private boolean didCloseInventory;
private boolean skipClosePacket;
private byte heldSlot;
@ -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 skipClosePacket) {
var closedInventory = getOpenInventory();
if (closedInventory == null) return;
this.skipClosePacket = skipClosePacket;
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(true);
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,61 +1777,28 @@ 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();
}
/**
* Used internally to prevent an inventory click to be processed
* when the inventory listeners closed the inventory.
* <p>
* Should only be used within an inventory listener (event or condition).
*
* @return true if the inventory has been closed, false otherwise
* Used internally to determine when sending the close inventory packet should be skipped.
*/
public boolean didCloseInventory() {
return didCloseInventory;
public boolean skipClosePacket() {
return skipClosePacket;
}
/**
* Used internally to reset the didCloseInventory field.
* Used internally to reset the skipClosePacket field, which determines when sending the close inventory packet
* should be skipped.
* <p>
* Shouldn't be used externally without proper understanding of its consequence.
*
* @param didCloseInventory the new didCloseInventory field
* @param skipClosePacket the new skipClosePacket field
*/
@ApiStatus.Internal
public void UNSAFE_changeDidCloseInventory(boolean didCloseInventory) {
this.didCloseInventory = didCloseInventory;
public void UNSAFE_changeSkipClosePacket(boolean skipClosePacket) {
this.skipClosePacket = skipClosePacket;
}
public int getNextTeleportId() {
@ -2304,62 +2283,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

View File

@ -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 <a href="https://wiki.vg/Protocol#Click_Container_Button">wiki.vg</a> 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;
}
}

View File

@ -1,89 +1,95 @@
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;
import java.util.List;
/**
* 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 List<Click.Change> 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 List<Click.Change> 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 List<Click.Change> 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 List<Click.Change> 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -0,0 +1,62 @@
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;
import java.util.List;
/**
* 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 List<Click.Change> changes;
public InventoryPostClickEvent(@NotNull Player player, @NotNull Inventory inventory, @NotNull Click.Info info, @NotNull List<Click.Change> 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 List<Click.Change> getChanges() {
return changes;
}
@Override
public @NotNull Inventory getInventory() {
return inventory;
}
}

View File

@ -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;
}
}

View File

@ -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.
* <p>
* 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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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<InventoryCondition> 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.
* <p>
* 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 <T> @NotNull T processItemStack(@NotNull ItemStack itemStack,
@NotNull TransactionType type,
@NotNull TransactionOption<T> option) {
return option.fill(type, this, itemStack);
}
public synchronized <T> @NotNull List<@NotNull T> processItemStacks(@NotNull List<@NotNull ItemStack> itemStacks,
@NotNull TransactionType type,
@NotNull TransactionOption<T> option) {
List<T> 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 <T> @NotNull T addItemStack(@NotNull ItemStack itemStack, @NotNull TransactionOption<T> 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 <T> @NotNull List<@NotNull T> addItemStacks(@NotNull List<@NotNull ItemStack> itemStacks,
@NotNull TransactionOption<T> 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 <T> @NotNull T takeItemStack(@NotNull ItemStack itemStack, @NotNull TransactionOption<T> 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 <T> @NotNull List<@NotNull T> takeItemStacks(@NotNull List<@NotNull ItemStack> itemStacks,
@NotNull TransactionOption<T> 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.
* <p>
* 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;
}
}

View File

@ -0,0 +1,178 @@
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.inventory.click.ClickProcessors;
import net.minestom.server.item.ItemStack;
import net.minestom.server.network.packet.server.play.OpenWindowPacket;
import net.minestom.server.network.packet.server.play.WindowPropertyPacket;
import net.minestom.server.utils.inventory.PlayerInventoryUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Represents an inventory which can be viewed by a collection of {@link Player}.
* <p>
* 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 List<Click.Change> handleClick(@NotNull Inventory inventory, @NotNull Player player, @NotNull Click.Info info,
@NotNull ClickProcessors.InventoryProcessor processor) {
PlayerInventory playerInventory = player.getInventory();
InventoryPreClickEvent preClickEvent = new InventoryPreClickEvent(playerInventory, inventory, player, info);
EventDispatcher.call(preClickEvent);
if (!preClickEvent.isCancelled()) {
final Click.Info newInfo = preClickEvent.getClickInfo();
Click.Getter getter = new Click.Getter(inventory::getItemStack, playerInventory::getItemStack, playerInventory.getCursorItem(), inventory.getSize());
final List<Click.Change> changes = processor.apply(newInfo, getter);
InventoryClickEvent clickEvent = new InventoryClickEvent(playerInventory, inventory, player, newInfo, changes);
EventDispatcher.call(clickEvent);
if (!clickEvent.isCancelled()) {
final List<Click.Change> newChanges = clickEvent.getChanges();
apply(newChanges, player, inventory);
EventDispatcher.call(new InventoryPostClickEvent(player, inventory, newInfo, newChanges));
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 List<Click.Change> changes, @NotNull Player player, @NotNull Inventory inventory) {
PlayerInventory playerInventory = player.getInventory();
for (var change : changes) {
switch (change) {
case Click.Change.Container(int slot, ItemStack item) -> {
if (slot < inventory.getSize()) {
inventory.setItemStack(slot, item);
} else {
int converted = PlayerInventoryUtils.protocolToMinestom(slot, inventory.getSize());
playerInventory.setItemStack(converted, item);
}
}
case Click.Change.Player(int slot, ItemStack item) -> playerInventory.setItemStack(slot, item);
case Click.Change.Cursor(ItemStack item) -> playerInventory.setCursorItem(item);
case Click.Change.DropFromPlayer(ItemStack item) -> 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 @Nullable List<Click.Change> handleClick(@NotNull Player player, Click.@NotNull Info info) {
return ContainerInventory.handleClick(this, player, info,
ClickProcessors.PROCESSORS_MAP.getOrDefault(inventoryType, ClickProcessors.GENERIC_PROCESSOR));
}
@Override
public byte getWindowId() {
return id;
}
@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 <a href="https://wiki.vg/Protocol#Set_Container_Property">https://wiki.vg/Protocol#Set_Container_Property</a>
*/
protected void sendProperty(@NotNull InventoryProperty property, short value) {
sendPacketToViewers(new WindowPropertyPacket(getWindowId(), property.getProperty(), value));
}
}

View File

@ -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)));
}

View File

@ -1,394 +1,145 @@
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.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}.
* <p>
* 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<Player> viewers = new CopyOnWriteArraySet<>();
private final Set<Player> unmodifiableViewers = Collections.unmodifiableSet(viewers);
// (player -> cursor item) map, used by the click listeners
private final ConcurrentHashMap<Player, ItemStack> 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 List<Click.Change> handleClick(@NotNull Player player, @NotNull Click.Info info);
/**
* Gets all the {@link ItemStack} in the inventory.
* <p>
* 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.
* <p>
* 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<Player> 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);
<T> @NotNull T processItemStack(@NotNull ItemStack itemStack,
@NotNull TransactionType type,
@NotNull TransactionOption<T> option);
<T> @NotNull List<@NotNull T> processItemStacks(@NotNull List<@NotNull ItemStack> itemStacks,
@NotNull TransactionType type, @NotNull TransactionOption<T> option);
/**
* Changes the cursor item of a viewer,
* does nothing if <code>player</code> 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);
}
}
<T> @NotNull T addItemStack(@NotNull ItemStack itemStack, @NotNull TransactionOption<T> 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 <a href="https://wiki.vg/Protocol#Window_Property">https://wiki.vg/Protocol#Window_Property</a>
* @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));
}
<T> @NotNull List<@NotNull T> addItemStacks(@NotNull List<@NotNull ItemStack> itemStacks, @NotNull TransactionOption<T> 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
*/
<T> @NotNull T takeItemStack(@NotNull ItemStack itemStack, @NotNull TransactionOption<T> 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
*/
<T> @NotNull List<@NotNull T> takeItemStacks(@NotNull List<@NotNull ItemStack> itemStacks, @NotNull TransactionOption<T> 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);
}
}

View File

@ -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.
* <p>
* 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));
}
}

View File

@ -0,0 +1,277 @@
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.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 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<Player> viewers = new CopyOnWriteArraySet<>();
protected final Set<Player> 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<Player> 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.skipClosePacket()) {
player.UNSAFE_changeSkipClosePacket(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 @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.
* <p>
* 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 <T> @NotNull T processItemStack(@NotNull ItemStack itemStack,
@NotNull TransactionType type,
@NotNull TransactionOption<T> option) {
lock.lock();
try {
return option.fill(type, this, itemStack);
} finally {
lock.unlock();
}
}
@Override
public <T> @NotNull List<@NotNull T> processItemStacks(@NotNull List<@NotNull ItemStack> itemStacks,
@NotNull TransactionType type,
@NotNull TransactionOption<T> option) {
List<T> 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 <T> @NotNull T addItemStack(@NotNull ItemStack itemStack, @NotNull TransactionOption<T> option) {
List<Integer> 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 <T> @NotNull List<@NotNull T> addItemStacks(@NotNull List<@NotNull ItemStack> itemStacks,
@NotNull TransactionOption<T> option) {
List<T> result = new ArrayList<>(itemStacks.size());
lock.lock();
try {
for (ItemStack item : itemStacks) {
result.add(addItemStack(item, option));
}
} finally {
lock.unlock();
}
return result;
}
@Override
public <T> @NotNull T takeItemStack(@NotNull ItemStack itemStack, @NotNull TransactionOption<T> option) {
return processItemStack(itemStack, TransactionType.take(IntStream.range(0, getSize()).boxed().toList()), option);
}
@Override
public <T> @NotNull List<@NotNull T> takeItemStacks(@NotNull List<@NotNull ItemStack> itemStacks,
@NotNull TransactionOption<T> option) {
List<T> result = new ArrayList<>(itemStacks.size());
lock.lock();
try {
for (ItemStack item : itemStacks) {
result.add(takeItemStack(item, option));
}
} finally {
lock.unlock();
}
return result;
}
}

View File

@ -1,7 +1,7 @@
package net.minestom.server.inventory;
/**
* Represents a type of {@link Inventory}
* Represents a type of {@link ContainerInventory}
*/
public enum InventoryType {

View File

@ -4,327 +4,179 @@ 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.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.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 {
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<Integer> FILL_ADD_SLOTS = IntStream.concat(
IntStream.of(OFF_HAND_SLOT),
IntStream.range(0, 36)
).boxed().toList();
private static final List<Integer> AIR_ADD_SLOTS = IntStream.range(0, 36).boxed().toList();
private static final List<Integer> 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.
* Gets the cursor item of this inventory
*
* @return the cursor item
* @return the cursor item that is shared between all viewers
*/
public @NotNull ItemStack getCursorItem() {
return cursorItem;
}
/**
* Changes the player cursor item.
* Sets the cursor item for all viewers of this inventory.
*
* @param cursorItem the new cursor item
* @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 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) 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 (Player 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 List<Click.Change> handleClick(@NotNull Player player, @NotNull Click.Info info) {
return ContainerInventory.handleClick(this, player, info, ClickProcessors.PLAYER_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 <T> @NotNull T addItemStack(@NotNull ItemStack itemStack, @NotNull TransactionOption<T> 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;
}
@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];
public <T> @NotNull T takeItemStack(@NotNull ItemStack itemStack, @NotNull TransactionOption<T> option) {
return processItemStack(itemStack, TransactionType.take(TAKE_SLOTS), option);
}
}

View File

@ -0,0 +1,121 @@
package net.minestom.server.inventory;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.StackingRule;
import org.jetbrains.annotations.NotNull;
import java.util.function.BiPredicate;
import java.util.function.UnaryOperator;
/**
* A transaction operator is a simpler operation that takes two items and returns two items.
* <br>
* This allows a significant amount of logic reuse, since many operations are just the {@link #flip(TransactionOperator) flipped}
* version of others.
*/
public interface TransactionOperator extends UnaryOperator<TransactionOperator.Entry> {
/**
* Creates a new operator that filters the given one using the provided predicate
*/
static @NotNull TransactionOperator filter(@NotNull TransactionOperator operator, @NotNull BiPredicate<ItemStack, ItemStack> predicate) {
return (entry) -> {
final ItemStack left = entry.left();
final ItemStack right = entry.right();
return predicate.test(left, right) ? operator.apply(entry) : 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 (entry) -> {
final Entry pair = operator.apply(entry.reverse());
return pair != null ? new Entry(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 (entry) -> {
final ItemStack left = entry.left();
final ItemStack right = entry.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 new Entry(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.<br>
* This will not swap the items if they are of different types.
*/
TransactionOperator STACK_LEFT = (entry) -> {
final ItemStack left = entry.left();
final ItemStack right = entry.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 new Entry(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 = (entry) -> {
final ItemStack left = entry.left();
final ItemStack right = entry.right();
final StackingRule rule = StackingRule.get();
if (right.isAir() || !rule.canBeStacked(left, right)) {
return null;
}
final int leftAmount = rule.getAmount(left);
final int rightAmount = rule.getAmount(right);
final int subtracted = Math.min(leftAmount, rightAmount);
return new Entry(rule.apply(left, leftAmount - subtracted), rule.apply(right, rightAmount - subtracted));
};
default Entry apply(@NotNull ItemStack left, @NotNull ItemStack right) {
return apply(new Entry(left, right));
}
record Entry(@NotNull ItemStack left, @NotNull ItemStack right) {
public Entry reverse() {
return new Entry(right, left);
}
}
}

View File

@ -9,24 +9,22 @@ import java.util.Map;
public interface TransactionOption<T> {
/**
* Place as much as the item as possible.
* <p>
* 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<ItemStack> ALL = (inventory, result, itemChangesMap) -> {
itemChangesMap.forEach(inventory::safeItemInsert);
itemChangesMap.forEach(inventory::setItemStack);
return result;
};
/**
* Only place the item if can be fully added.
* <p>
* 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 the operation
* was performed.
*/
TransactionOption<Boolean> 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,15 @@ public interface TransactionOption<T> {
};
/**
* Loop through the inventory items without changing anything.
* <p>
* Returns true if the item can be fully added, false otherwise.
* Discards the result of the operation, returning whether the operation could have finished.
*/
TransactionOption<Boolean> 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 Map<Integer, ItemStack> 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) {
final TransactionType.Entry result = type.apply(itemStack, inventory::getItemStack);
return fill(inventory, result.remaining(), result.changes());
}
}

View File

@ -1,124 +1,101 @@
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.List;
import java.util.Map;
import java.util.function.BiFunction;
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 {
public interface TransactionType extends BiFunction<@NotNull ItemStack, @NotNull IntFunction<ItemStack>, TransactionType.@NotNull Entry> {
/**
* Applies a transaction operator to a given list of slots, turning it into a TransactionType.
*/
static @NotNull TransactionType general(@NotNull TransactionOperator operator, @NotNull List<Integer> slots) {
return (item, getter) -> {
Int2ObjectMap<ItemStack> map = new Int2ObjectArrayMap<>();
for (int slot : slots) {
final ItemStack slotItem = getter.apply(slot);
final TransactionOperator.Entry result = operator.apply(slotItem, item);
if (result == null) continue;
map.put(slot, result.left());
item = result.right();
}
return new Entry(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
final Entry f = first.apply(item, getter);
final Entry s = second.apply(f.remaining(), getter);
// Join results
Map<Integer, ItemStack> map = new Int2ObjectArrayMap<>();
map.putAll(f.changes());
map.putAll(s.changes());
return new Entry(s.remaining(), 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<ItemStack> 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<Integer> fill, @NotNull List<Integer> air) {
final TransactionType first = general(entry -> {
final ItemStack slotItem = entry.left();
final ItemStack extra = entry.right();
return !slotItem.isAir() ? TransactionOperator.STACK_LEFT.apply(slotItem, extra) : null;
}, fill);
final TransactionType second = general(entry -> {
final ItemStack slotItem = entry.left();
final ItemStack extra = entry.right();
return 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<ItemStack> 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;
}
static @NotNull TransactionType take(@NotNull List<Integer> takeSlots) {
return general(TransactionOperator.TAKE, takeSlots);
}
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;
}
}
/**
* Processes the provided item into the given inventory.
*
* @param itemStack the item to process
* @param inventory the inventory function
* @return the remaining portion of the processed item, as well as the changes
*/
@Override
@NotNull
Entry apply(@NotNull ItemStack itemStack, @NotNull IntFunction<ItemStack> inventory);
record Entry(@NotNull ItemStack remaining, @NotNull Map<@NotNull Integer, @NotNull ItemStack> changes) {
public Entry {
changes = Map.copyOf(changes);
}
return Pair.of(itemStack, itemChangesMap);
};
@NotNull Pair<ItemStack, Map<Integer, ItemStack>> process(@NotNull AbstractInventory inventory,
@NotNull ItemStack itemStack,
@NotNull SlotPredicate slotPredicate,
int start, int end, int step);
default @NotNull Pair<ItemStack, Map<Integer, ItemStack>> process(@NotNull AbstractInventory inventory,
@NotNull ItemStack itemStack,
@NotNull SlotPredicate slotPredicate) {
return process(inventory, itemStack, slotPredicate, 0, inventory.getInnerSize(), 1);
}
default @NotNull Pair<ItemStack, Map<Integer, ItemStack>> process(@NotNull AbstractInventory inventory,
@NotNull ItemStack itemStack) {
return process(inventory, itemStack, (slot, itemStack1) -> true);
}
@FunctionalInterface
interface SlotPredicate {
boolean test(int slot, @NotNull ItemStack itemStack);
}
}

View File

@ -0,0 +1,252 @@
package net.minestom.server.inventory.click;
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.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.IntFunction;
public final class Click {
/**
* Contains information about a click. These are equal to the packet slot IDs from <a href="https://wiki.vg/Inventory">the Minecraft protocol.</a>.
* 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<Integer> slots) implements Info {
public LeftDrag {
slots = List.copyOf(slots);
}
}
record RightDrag(List<Integer> slots) implements Info {
public RightDrag {
slots = List.copyOf(slots);
}
}
record MiddleDrag(List<Integer> slots) implements Info {
// Creative only
public MiddleDrag {
slots = List.copyOf(slots);
}
}
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 Set<Integer> leftDrag = new LinkedHashSet<>();
private final Set<Integer> rightDrag = new LinkedHashSet<>();
private final Set<Integer> middleDrag = new LinkedHashSet<>();
public void clearCache() {
this.leftDrag.clear();
this.rightDrag.clear();
this.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)}.
*
* @param packet the raw click packet
* @param isCreative whether the player is in creative mode (used for ignoring some actions)
* @param containerSize the size of the open container, or null if the player inventory is open
* @return the information about the click, or nothing if there was no immediately usable information
*/
public @Nullable Click.Info processClick(@NotNull ClientClickWindowPacket packet, boolean isCreative, @Nullable Integer containerSize) {
final byte button = packet.button();
final boolean requireCreative = switch (packet.clickType()) {
case CLONE -> packet.slot() != -999; // Permit middle click dropping
case QUICK_CRAFT -> button == 8 || button == 9 || button == 10;
default -> false;
};
if (requireCreative && !isCreative) return null;
final int slot = packet.slot() == -999 ? -999 :
containerSize == null ? PlayerInventoryUtils.protocolToMinestom(packet.slot()) : packet.slot();
final int maxSize = containerSize != null ? containerSize + PlayerInventoryUtils.INNER_SIZE : PlayerInventoryUtils.INVENTORY_SIZE;
final boolean valid = slot >= 0 && slot < maxSize;
if (!valid) {
return slot == -999 ? processInvalidSlot(packet.clickType(), button) : null;
}
return process(packet.clickType(), slot, button);
}
private @Nullable Click.Info processInvalidSlot(@NotNull ClientClickWindowPacket.ClickType type, byte button) {
return switch (type) {
case PICKUP -> {
if (button == 0) yield new Info.LeftDropCursor();
if (button == 1) yield new Info.RightDropCursor();
yield null;
}
case CLONE -> {
if (button == 2) yield new Info.MiddleDropCursor(); // Why Mojang, why?
yield null;
}
case QUICK_CRAFT -> {
// Trust me, a switch would not make this cleaner
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);
}
if (button == 0) leftDrag.clear();
if (button == 4) rightDrag.clear();
if (button == 8) middleDrag.clear();
yield null;
}
default -> null;
};
}
/**
* Processes a packet into click info.
*
* @param type the type of the click
* @param slot the clicked slot
* @param button the sent button
* @return the information about the click, or nothing if there was no immediately usable information
*/
private @Nullable Click.Info process(@NotNull ClientClickWindowPacket.ClickType type, int slot, byte button) {
return switch (type) {
case PICKUP -> switch (button) {
case 0 -> new Info.Left(slot);
case 1 -> new Info.Right(slot);
default -> null;
};
case QUICK_MOVE -> button == 0 ? new Info.LeftShift(slot) : new Info.RightShift(slot);
case SWAP -> {
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 -> new Info.Middle(slot);
case THROW -> new Info.DropSlot(slot, button == 1);
case QUICK_CRAFT -> {
switch (button) {
case 1 -> leftDrag.add(slot);
case 5 -> rightDrag.add(slot);
case 9 -> middleDrag.add(slot);
}
yield null;
}
case PICKUP_ALL -> new Info.Double(slot);
};
}
}
public record Getter(@NotNull IntFunction<ItemStack> main, @NotNull IntFunction<ItemStack> 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()));
}
}
}
/**
* Represents an individual change that occurred to an inventory.
*/
public sealed interface Change {
/**
* A change to the open container. If the click was in the player inventory, this could indicate a change there,
* despite the existence of {@link Player}
* @param slot the changed slot
* @param item the new item in the slot
*/
record Container(int slot, @NotNull ItemStack item) implements Change {
}
/**
* A change that must have occurred in the player's inventory, even if they have another one open.
* @param slot the changed player slot
* @param item the new item in the slot
*/
record Player(int slot, @NotNull ItemStack item) implements Change {
}
/**
* A new cursor item. The cursor item is stored in the player's {@link net.minestom.server.inventory.PlayerInventory}.
* @param item the new cursor item
*/
record Cursor(@NotNull ItemStack item) implements Change {
}
/**
* An item that was dropped from the player during the click.
* @param item the item to drop
*/
record DropFromPlayer(@NotNull ItemStack item) implements Change {
}
}
}

View File

@ -0,0 +1,317 @@
package net.minestom.server.inventory.click;
import net.minestom.server.entity.EquipmentSlot;
import net.minestom.server.inventory.InventoryType;
import net.minestom.server.inventory.TransactionOperator;
import net.minestom.server.inventory.TransactionType;
import net.minestom.server.inventory.click.Click.Change.Cursor;
import net.minestom.server.inventory.click.Click.Change.DropFromPlayer;
import net.minestom.server.inventory.click.Click.Change.Container;
import net.minestom.server.inventory.click.Click.Change.Player;
import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.item.StackingRule;
import net.minestom.server.utils.inventory.PlayerInventoryUtils;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static java.util.Map.entry;
import static net.minestom.server.utils.inventory.PlayerInventoryUtils.*;
/**
* Provides standard implementations of most click functions.
*/
public final class ClickProcessors {
private static final @NotNull StackingRule RULE = StackingRule.get();
public static @NotNull List<Click.Change> leftClick(int slot, @NotNull Click.Getter getter) {
final ItemStack cursor = getter.cursor();
final ItemStack clickedItem = getter.get(slot);
final TransactionOperator.Entry pair = TransactionOperator.STACK_LEFT.apply(clickedItem, cursor);
if (pair != null) { // Stackable items, combine their counts
return List.of(new Container(slot, pair.left()), new Cursor(pair.right()));
} else if (!RULE.canBeStacked(cursor, clickedItem)) { // If they're unstackable, switch them
return List.of(new Container(slot, cursor), new Cursor(clickedItem));
} else {
return List.of();
}
}
public static @NotNull List<Click.Change> rightClick(int slot, @NotNull Click.Getter getter) {
final ItemStack cursor = getter.cursor();
final ItemStack clickedItem = getter.get(slot);
if (cursor.isAir() && clickedItem.isAir()) return List.of(); // 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);
final TransactionOperator.Entry cursorSlot = TransactionOperator.stackLeftN(newAmount).apply(cursor, clickedItem);
if (cursorSlot == null) return List.of();
return List.of(new Container(slot, cursorSlot.right()), new Cursor(cursorSlot.left()));
} else if (clickedItem.isAir() || RULE.canBeStacked(clickedItem, cursor)) { // Can add, transfer one over
final TransactionOperator.Entry slotCursor = TransactionOperator.stackLeftN(1).apply(clickedItem, cursor);
if (slotCursor == null) return List.of();
return List.of(new Container(slot, slotCursor.left()), new Cursor(slotCursor.right()));
} else { // Two existing of items of different types, so switch
return List.of(new Cursor(clickedItem), new Container(slot, cursor));
}
}
public static @NotNull List<Click.Change> middleClick(int slot, @NotNull Click.Getter getter) {
final ItemStack item = getter.get(slot);
if (!getter.cursor().isAir() || item.isAir()) return List.of();
return List.of(new Cursor(RULE.apply(item, RULE.getMaxSize(item))));
}
public static @NotNull List<Click.Change> shiftClick(int slot, @NotNull List<Integer> slots, @NotNull Click.Getter getter) {
final ItemStack clicked = getter.get(slot);
slots = new ArrayList<>(slots);
slots.removeIf(i -> i == slot);
final TransactionType.Entry result = TransactionType.add(slots, slots).apply(clicked, getter::get);
List<Click.Change> changes = new ArrayList<>();
result.changes().forEach((slotId, item) -> changes.add(new Container(slotId, item)));
if (!result.remaining().equals(clicked)) {
changes.add(new Container(slot, result.remaining()));
}
return changes;
}
public static @NotNull List<Click.Change> doubleClick(@NotNull List<Integer> slots, @NotNull Click.Getter getter) {
final ItemStack cursor = getter.cursor();
if (cursor.isAir()) return List.of();
final TransactionType unstacked = TransactionType.general(TransactionOperator.filter(TransactionOperator.STACK_RIGHT,
(left, right) -> RULE.getAmount(left) < RULE.getMaxSize(left)), slots);
final TransactionType stacked = TransactionType.general(TransactionOperator.filter(TransactionOperator.STACK_RIGHT,
(left, right) -> RULE.getAmount(left) == RULE.getMaxSize(left)), slots);
final TransactionType.Entry result = TransactionType.join(unstacked, stacked).apply(cursor, getter::get);
List<Click.Change> changes = new ArrayList<>();
result.changes().forEach((slotId, item) -> changes.add(new Container(slotId, item)));
if (!result.remaining().equals(cursor)) {
changes.add(new Cursor(result.remaining()));
}
return changes;
}
public static @NotNull List<Click.Change> dragClick(int countPerSlot, @NotNull List<Integer> slots, @NotNull Click.Getter getter) {
final ItemStack cursor = getter.cursor();
if (cursor.isAir()) return List.of();
final TransactionType.Entry result = TransactionType.general(TransactionOperator.stackLeftN(countPerSlot), slots).apply(cursor, getter::get);
List<Click.Change> changes = new ArrayList<>();
result.changes().forEach((slotId, item) -> changes.add(new Container(slotId, item)));
if (!result.remaining().equals(cursor)) {
changes.add(new Cursor(result.remaining()));
}
return changes;
}
public static @NotNull List<Click.Change> middleDragClick(@NotNull List<Integer> slots, @NotNull Click.Getter getter) {
final ItemStack cursor = getter.cursor();
return slots.stream()
.filter(slot -> getter.get(slot).isAir())
.map(slot -> (Click.Change) new Container(slot, cursor))
.toList();
}
public static @NotNull List<Click.Change> dropFromCursor(int amount, @NotNull Click.Getter getter) {
final ItemStack cursor = getter.cursor();
if (cursor.isAir()) return List.of(); // Do nothing
final TransactionOperator.Entry pair = TransactionOperator.stackLeftN(amount).apply(ItemStack.AIR, cursor);
if (pair == null) return List.of();
return List.of(new Cursor(pair.right()), new DropFromPlayer(pair.left()));
}
public static @NotNull List<Click.Change> dropFromSlot(int slot, int amount, @NotNull Click.Getter getter) {
final ItemStack item = getter.get(slot);
if (item.isAir()) return List.of(); // Do nothing
final TransactionOperator.Entry pair = TransactionOperator.stackLeftN(amount).apply(ItemStack.AIR, item);
if (pair == null) return List.of();
return List.of(new Container(slot, pair.right()), new DropFromPlayer(pair.left()));
}
/**
* Handles clicks, given a shift click provider and a double click provider.<br>
* When shift clicks or double clicks need to be handled, the slots provided from the relevant handler will be
* checked in their given order.<br>
* 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 ClickProcessors.@NotNull InventoryProcessor standard(@NotNull SlotSuggestor shiftClickSlots, @NotNull SlotSuggestor doubleClickSlots) {
return (info, getter) -> switch (info) {
case Click.Info.Left(int slot) -> leftClick(slot, getter);
case Click.Info.Right(int slot) -> rightClick(slot, getter);
case Click.Info.Middle(int slot) -> middleClick(slot, getter);
case Click.Info.LeftShift(int slot) ->
shiftClick(slot, shiftClickSlots.getList(getter, getter.get(slot), slot), getter);
case Click.Info.RightShift(int slot) ->
shiftClick(slot, shiftClickSlots.getList(getter, getter.get(slot), slot), getter);
case Click.Info.Double(int slot) ->
doubleClick(doubleClickSlots.getList(getter, getter.get(slot), slot), getter);
case Click.Info.LeftDrag(List<Integer> 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<Integer> slots) -> dragClick(1, slots, getter);
case Click.Info.MiddleDrag(List<Integer> 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() -> List.of();
case Click.Info.HotbarSwap(int hotbarSlot, int clickedSlot) -> {
var hotbarItem = getter.player().apply(hotbarSlot);
var selectedItem = getter.get(clickedSlot);
if (hotbarItem.equals(selectedItem)) yield List.of();
yield List.of(new Container(clickedSlot, hotbarItem), new Player(hotbarSlot, selectedItem));
}
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 List.of();
yield List.of(new Container(slot, offhandItem), new Player(OFF_HAND_SLOT, selectedItem));
}
case Click.Info.CreativeSetItem(int slot, ItemStack item) -> List.of(new Container(slot, item));
case Click.Info.CreativeDropItem(ItemStack item) -> List.of(new DropFromPlayer(item));
};
}
public interface InventoryProcessor extends BiFunction<Click.Info, Click.Getter, List<Click.Change>> {
}
/**
* A generic interface for providing options for clicks like shift clicks and double clicks.<br>
* 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.<br>
*/
@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<Integer> getList(@NotNull Click.Getter builder, @NotNull ItemStack item, int slot) {
return get(builder, item, slot).boxed().toList();
}
}
/**
* Handle player inventory (without any container open).
*/
public static final InventoryProcessor PLAYER_PROCESSOR = ClickProcessors.standard(
(getter, item, slot) -> {
List<Integer> slots = new ArrayList<>();
final EquipmentSlot equipmentSlot = item.material().registry().equipmentSlot();
if (equipmentSlot != null && slot != equipmentSlot.armorSlot()) {
slots.add(equipmentSlot.armorSlot());
}
if (item.material() == Material.SHIELD && slot != OFF_HAND_SLOT) {
slots.add(OFF_HAND_SLOT);
}
if (slot < 9 || slot > 35) IntStream.range(9, 36).forEach(slots::add);
if (slot < 0 || slot > 8) IntStream.range(0, 9).forEach(slots::add);
if (slot == CRAFT_RESULT) {
Collections.reverse(slots);
}
return slots.stream().mapToInt(i -> i);
},
(getter, item, slot) -> Stream.of(
IntStream.range(CRAFT_SLOT_1, CRAFT_SLOT_4 + 1), // 1-4
IntStream.range(HELMET_SLOT, BOOTS_SLOT + 1), // 5-8
IntStream.range(9, 36), // 9-35
IntStream.range(0, 9), // 36-44
IntStream.of(OFF_HAND_SLOT) // 45
).flatMapToInt(i -> i)
);
/**
* Assumes all the container's slots to be accessible.
*/
public static final InventoryProcessor GENERIC_PROCESSOR = ClickProcessors.standard(
(builder, item, slot) -> {
final int size = builder.mainSize();
return slot >= size ?
IntStream.range(0, size) :
PlayerInventoryUtils.getInnerShiftClickSlots(size);
},
(builder, item, slot) -> {
final int size = builder.mainSize();
return IntStream.concat(
IntStream.range(0, size),
PlayerInventoryUtils.getInnerDoubleClickSlots(size)
);
});
// SPECIALIZED PROCESSORS DEFINITIONS
/**
* Client prediction appears to disallow shift clicking into furnace inventories.<br>
* Instead:
* - Shift clicks in the inventory go to the player inventory like normal
* - Shift clicks in the hotbar go to the storage
* - Shift clicks in the storage go to the hotbar
*/
public static final InventoryProcessor FURNACE_PROCESSOR = ClickProcessors.standard(
(builder, item, slot) -> {
final int size = builder.mainSize();
if (slot < size) {
return PlayerInventoryUtils.getInnerShiftClickSlots(size);
} else if (slot < size + 27) {
return IntStream.range(27, 36).map(i -> i + size);
} else {
return IntStream.range(0, 27).map(i -> i + size);
}
},
(builder, item, slot) -> {
final int size = builder.mainSize();
return IntStream.concat(
IntStream.range(0, size),
PlayerInventoryUtils.getInnerDoubleClickSlots(size)
);
});
public static final Map<InventoryType, InventoryProcessor> PROCESSORS_MAP = Map.ofEntries(
entry(InventoryType.FURNACE, FURNACE_PROCESSOR)
);
}

View File

@ -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
}

View File

@ -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<Player, List<DragData>> leftDraggingMap = new ConcurrentHashMap<>();
private final Map<Player, List<DragData>> 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<Integer, ItemStack> 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<DragData> left = leftDraggingMap.get(player);
if (left == null) return null;
left.add(new DragData(slot, inventory));
} else if (button == 5) {
// Add right
List<DragData> 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<DragData> 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<DragData> 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<AbstractInventory, ItemStack, ItemStack> 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<InventoryCondition> 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) {
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 FurnaceInventory extends Inventory {
public class FurnaceInventory extends ContainerInventory {
private short remainingFuelTick;
private short maximumFuelBurnTime;

View File

@ -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<TradeListPacket.Trade> 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() {

View File

@ -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}.
* <p>
* An item stack cannot be null, {@link ItemStack#AIR} should be used instead.
*/

View File

@ -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();

View File

@ -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()));
}

View File

@ -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));
}
}

View File

@ -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) {
}
}

View File

@ -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);
}
}

View File

@ -2,78 +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.inventory.click.Click;
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 boolean playerInventory = windowId == 0;
final Inventory inventory = playerInventory ? player.getInventory() : player.getOpenInventory();
final short slot = packet.slot();
final byte button = packet.button();
final ClientClickWindowPacket.ClickType clickType = packet.clickType();
// Prevent some invalid packets
if (inventory == null || packet.slot() == -1) return;
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);
Click.Preprocessor preprocessor = player.clickPreprocessor();
final Click.Info info = preprocessor.processClick(packet, player.isCreative(), playerInventory ? null : inventory.getSize());
if (info != null) inventory.handleClick(player, info);
// (Why is the ping packet necessary?)
player.sendPacket(new PingPacket((1 << 30) | (windowId << 16)));
@ -85,7 +36,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 +49,11 @@ 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();
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());
}
InventoryButtonClickEvent event = new InventoryButtonClickEvent(openInventory, player, packet.buttonId());
EventDispatcher.call(event);
}
}

View File

@ -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);

View File

@ -1,8 +1,22 @@
package net.minestom.server.utils.inventory;
public final class PlayerInventoryUtils {
public static final int OFFSET = 9;
import org.jetbrains.annotations.NotNull;
import java.util.stream.IntStream;
/**
* 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).<br>
* These can be mapped 1:1 to and from protocol slots using {@link #minestomToProtocol(int)} and {@link #protocolToMinestom(int)}.<br>
* <p>
* Read about protocol slot IDs <a href="https://wiki.vg/Inventory">here</a>.
*/
public final class PlayerInventoryUtils {
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 +28,58 @@ 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;
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 PlayerInventoryUtils() {
}
/**
* Converts a packet slot to an internal one.
* Converts a Minestom slot ID to a Minecraft protocol slot ID.<br>
* This is the inverse of {@link #protocolToMinestom(int)}.
*
* @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
* @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.<br>
* 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 +90,45 @@ 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.<br>
* 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.<br>
* 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);
}
}

View File

@ -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);

View File

@ -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());
});
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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));
});

View File

@ -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));
}
}

View File

@ -0,0 +1,101 @@
package net.minestom.server.inventory.click;
import it.unimi.dsi.fastutil.ints.IntList;
import net.minestom.server.MinecraftServer;
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 {
static {
MinecraftServer.init();
}
@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(CLONE, 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, -999));
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, -999));
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, -999));
}
}

View File

@ -0,0 +1,125 @@
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.item.Material;
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 net.minestom.server.utils.inventory.PlayerInventoryUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.SocketAddress;
import java.util.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
public final 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 ItemStack magic(int amount) {
return ItemStack.of(Material.STONE, amount);
}
public static @NotNull ItemStack magic2(int amount) {
return ItemStack.of(Material.DIRT, amount);
}
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 List<Click.Change> initial, @NotNull Click.Info info, @NotNull List<Click.Change> expected) {
var player = createPlayer();
var inventory = createInventory();
ContainerInventory.apply(initial, player, inventory);
var actual = inventory.handleClick(player, info);
assertChanges(expected, actual, inventory.getSize());
}
public static void assertPlayerClick(@NotNull List<Click.Change> initial, @NotNull Click.Info info, @NotNull List<Click.Change> expected) {
var player = createPlayer();
var inventory = player.getInventory();
ContainerInventory.apply(initial, player, inventory);
var actual = inventory.handleClick(player, info);
assertChanges(expected, actual, inventory.getSize());
}
public static void assertChanges(List<Click.Change> expected, List<Click.Change> actual, int size) {
assertEquals(ChangeResult.make(expected, size), ChangeResult.make(actual, size));
}
private record ChangeResult(Map<Integer, ItemStack> main, Map<Integer, ItemStack> player,
@Nullable ItemStack cursor, List<ItemStack> drops) {
private static ChangeResult make(@NotNull List<Click.Change> changes, int size) {
Map<Integer, ItemStack> main = new HashMap<>();
Map<Integer, ItemStack> player = new HashMap<>();
@Nullable ItemStack cursor = null;
List<ItemStack> drops = new ArrayList<>();
for (var change : changes) {
switch (change) {
case Click.Change.Container(int slot, ItemStack item) -> {
if (slot < size) {
main.put(slot, item);
} else {
player.put(PlayerInventoryUtils.protocolToMinestom(slot, size), item);
}
}
case Click.Change.Player(int slot, ItemStack item) -> player.put(slot, item);
case Click.Change.Cursor(ItemStack item) -> cursor = item;
case Click.Change.DropFromPlayer(ItemStack item) -> drops.add(item);
}
}
return new ChangeResult(main, player, cursor, drops);
}
}
public static void assertProcessed(@NotNull Click.Preprocessor preprocessor, @NotNull Player player, @Nullable Click.Info info, @NotNull ClientClickWindowPacket packet) {
assertEquals(info, preprocessor.processClick(packet, player.isCreative(), createInventory().getSize()));
}
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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,35 @@
package net.minestom.server.inventory.click.type;
import net.minestom.server.MinecraftServer;
import net.minestom.server.inventory.click.Click;
import net.minestom.server.inventory.click.Click.Change.DropFromPlayer;
import org.junit.jupiter.api.Test;
import java.util.List;
import static net.minestom.server.inventory.click.ClickUtils.assertClick;
import static net.minestom.server.inventory.click.ClickUtils.magic;
public class InventoryCreativeDropItemTest {
static {
MinecraftServer.init();
}
@Test
public void testDropItem() {
assertClick(
List.of(),
new Click.Info.CreativeDropItem(magic(64)),
List.of(new DropFromPlayer(magic(64)))
);
// Make sure it doesn't drop a full stack
assertClick(
List.of(),
new Click.Info.CreativeDropItem(magic(1)),
List.of(new DropFromPlayer(magic(1)))
);
}
}

View File

@ -0,0 +1,35 @@
package net.minestom.server.inventory.click.type;
import net.minestom.server.MinecraftServer;
import net.minestom.server.inventory.click.Click;
import net.minestom.server.inventory.click.Click.Change.Container;
import org.junit.jupiter.api.Test;
import java.util.List;
import static net.minestom.server.inventory.click.ClickUtils.assertClick;
import static net.minestom.server.inventory.click.ClickUtils.magic;
public class InventoryCreativeSetItemTest {
static {
MinecraftServer.init();
}
@Test
public void testSetItem() {
assertClick(
List.of(),
new Click.Info.CreativeSetItem(0, magic(64)),
List.of(new Container(0, magic(64)))
);
// Make sure it doesn't set a full stack
assertClick(
List.of(),
new Click.Info.CreativeSetItem(0, magic(1)),
List.of(new Container(0, magic(1)))
);
}
}

View File

@ -0,0 +1,83 @@
package net.minestom.server.inventory.click.type;
import net.minestom.server.MinecraftServer;
import net.minestom.server.inventory.click.Click;
import net.minestom.server.inventory.click.Click.Change.Cursor;
import net.minestom.server.inventory.click.Click.Change.Container;
import net.minestom.server.item.ItemStack;
import org.junit.jupiter.api.Test;
import java.util.List;
import static net.minestom.server.inventory.click.ClickUtils.assertClick;
import static net.minestom.server.inventory.click.ClickUtils.magic;
public class InventoryDoubleClickTest {
static {
MinecraftServer.init();
}
@Test
public void testNoChanges() {
assertClick(List.of(), new Click.Info.Double(0), List.of());
}
@Test
public void testCannotTakeAny() {
assertClick(
List.of(new Cursor(magic(32))),
new Click.Info.Double(0),
List.of()
);
}
@Test
public void testPartialTake() {
assertClick(
List.of(new Container(1, magic(48)), new Cursor(magic(32))),
new Click.Info.Double(0),
List.of(new Container(1, magic(16)), new Cursor(magic(64)))
);
}
@Test
public void testTakeAll() {
assertClick(
List.of(new Container(1, magic(32)), new Cursor(magic(32))),
new Click.Info.Double(0),
List.of(new Container(1, ItemStack.AIR), new Cursor(magic(64)))
);
assertClick(
List.of(new Container(1, magic(16)), new Cursor(magic(32))),
new Click.Info.Double(0),
List.of(new Container(1, ItemStack.AIR), new Cursor(magic(48)))
);
}
@Test
public void testTakeSeparated() {
assertClick(
List.of(new Container(1, magic(16)), new Container(2, magic(16)), new Cursor(magic(32))),
new Click.Info.Double(0),
List.of(new Container(1, ItemStack.AIR), new Container(2, ItemStack.AIR), new Cursor(magic(64)))
);
assertClick(
List.of(new Container(1, magic(16)), new Container(2, magic(32)), new Cursor(magic(32))),
new Click.Info.Double(0),
List.of(new Container(1, ItemStack.AIR), new Container(2, magic(16)), new Cursor(magic(64)))
);
}
@Test
public void testCursorFull() {
assertClick(
List.of(new Container(1, magic(48)), new Cursor(magic(64))),
new Click.Info.Double(0),
List.of()
);
}
}

View File

@ -0,0 +1,55 @@
package net.minestom.server.inventory.click.type;
import net.minestom.server.MinecraftServer;
import net.minestom.server.inventory.click.Click;
import net.minestom.server.inventory.click.Click.Change.Cursor;
import net.minestom.server.inventory.click.Click.Change.DropFromPlayer;
import net.minestom.server.item.ItemStack;
import org.junit.jupiter.api.Test;
import java.util.List;
import static net.minestom.server.inventory.click.ClickUtils.assertClick;
import static net.minestom.server.inventory.click.ClickUtils.magic;
public class InventoryDropCursorTest {
static {
MinecraftServer.init();
}
@Test
public void testNoChanges() {
assertClick(List.of(), new Click.Info.LeftDropCursor(), List.of());
assertClick(List.of(), new Click.Info.MiddleDropCursor(), List.of());
assertClick(List.of(), new Click.Info.RightDropCursor(), List.of());
}
@Test
public void testDropEntireStack() {
assertClick(
List.of(new Cursor(magic(32))),
new Click.Info.LeftDropCursor(),
List.of(new Cursor(ItemStack.AIR), new DropFromPlayer(magic(32)))
);
}
@Test
public void testDropSingleItem() {
assertClick(
List.of(new Cursor(magic(32))),
new Click.Info.RightDropCursor(),
List.of(new Cursor(magic(31)), new DropFromPlayer(magic(1)))
);
}
@Test
public void testMiddleClickNoop() {
assertClick(
List.of(new Cursor(magic(32))),
new Click.Info.MiddleDropCursor(),
List.of()
);
}
}

View File

@ -0,0 +1,45 @@
package net.minestom.server.inventory.click.type;
import net.minestom.server.MinecraftServer;
import net.minestom.server.inventory.click.Click;
import net.minestom.server.inventory.click.Click.Change.DropFromPlayer;
import net.minestom.server.inventory.click.Click.Change.Container;
import net.minestom.server.item.ItemStack;
import org.junit.jupiter.api.Test;
import java.util.List;
import static net.minestom.server.inventory.click.ClickUtils.assertClick;
import static net.minestom.server.inventory.click.ClickUtils.magic;
public class InventoryDropSlotTest {
static {
MinecraftServer.init();
}
@Test
public void testNoChanges() {
assertClick(List.of(), new Click.Info.DropSlot(0, false), List.of());
assertClick(List.of(), new Click.Info.DropSlot(0, true), List.of());
}
@Test
public void testDropEntireStack() {
assertClick(
List.of(new Container(0, magic(32))),
new Click.Info.DropSlot(0, true),
List.of(new Container(0, ItemStack.AIR), new DropFromPlayer(magic(32)))
);
}
@Test
public void testDropSingleItem() {
assertClick(
List.of(new Container(0, magic(32))),
new Click.Info.DropSlot(0, false),
List.of(new Container(0, magic(31)), new DropFromPlayer(magic(1)))
);
}
}

View File

@ -0,0 +1,35 @@
package net.minestom.server.inventory.click.type;
import net.minestom.server.MinecraftServer;
import net.minestom.server.inventory.click.Click;
import net.minestom.server.inventory.click.Click.Change.Container;
import net.minestom.server.inventory.click.Click.Change.Player;
import org.junit.jupiter.api.Test;
import java.util.List;
import static net.minestom.server.inventory.click.ClickUtils.*;
public class InventoryHotbarSwapTest {
static {
MinecraftServer.init();
}
@Test
public void testNoChanges() {
for (int i = 0; i < 9; i++) {
assertClick(List.of(), new Click.Info.HotbarSwap(i, 9), List.of());
}
}
@Test
public void testSwappedItems() {
assertClick(
List.of(new Container(0, magic2(1)), new Player(0, magic(1))),
new Click.Info.HotbarSwap(0, 0),
List.of(new Container(0, magic(1)), new Player(0, magic2(1)))
);
}
}

View File

@ -0,0 +1,52 @@
package net.minestom.server.inventory.click.type;
import net.minestom.server.MinecraftServer;
import net.minestom.server.inventory.click.Click;
import net.minestom.server.inventory.click.Click.Change.Cursor;
import net.minestom.server.inventory.click.Click.Change.Container;
import net.minestom.server.item.ItemStack;
import org.junit.jupiter.api.Test;
import java.util.List;
import static net.minestom.server.inventory.click.ClickUtils.*;
public class InventoryLeftClickTest {
static {
MinecraftServer.init();
}
@Test
public void testNoChanges() {
assertClick(List.of(), new Click.Info.Left(0), List.of());
}
@Test
public void testInsertEntireStack() {
assertClick(
List.of(new Container(0, magic(32)), new Click.Change.Cursor(magic(32))),
new Click.Info.Left(0),
List.of(new Container(0, magic(64)), new Cursor(ItemStack.AIR))
);
}
@Test
public void testInsertPartialStack() {
assertClick(
List.of(new Container(0, magic(32)), new Cursor(magic(48))),
new Click.Info.Left(0),
List.of(new Container(0, magic(64)), new Cursor(magic(16)))
);
}
@Test
public void testSwitchItems() {
assertClick(
List.of(new Container(0, magic(1)), new Cursor(magic2(1))),
new Click.Info.Left(0),
List.of(new Container(0, magic2(1)), new Cursor(magic(1)))
);
}
}

View File

@ -0,0 +1,109 @@
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.inventory.click.Click.Change.Cursor;
import net.minestom.server.inventory.click.Click.Change.Container;
import net.minestom.server.item.ItemStack;
import org.junit.jupiter.api.Test;
import java.util.List;
import static net.minestom.server.inventory.click.ClickUtils.assertClick;
import static net.minestom.server.inventory.click.ClickUtils.magic;
public class InventoryLeftDragTest {
static {
MinecraftServer.init();
}
@Test
public void testNoCursor() {
assertClick(List.of(), new Click.Info.LeftDrag(IntList.of(0)), List.of());
}
@Test
public void testDistributeNone() {
assertClick(
List.of(new Cursor(magic(32))),
new Click.Info.LeftDrag(IntList.of()),
List.of()
);
}
@Test
public void testDistributeOne() {
assertClick(
List.of(new Cursor(magic(32))),
new Click.Info.LeftDrag(IntList.of(0)),
List.of(new Container(0, magic(32)), new Cursor(ItemStack.AIR))
);
}
@Test
public void testDistributeExactlyEnough() {
assertClick(
List.of(new Cursor(magic(32))),
new Click.Info.LeftDrag(IntList.of(0, 1)),
List.of(new Container(0, magic(16)), new Container(1, magic(16)), new Cursor(ItemStack.AIR))
);
assertClick(
List.of(new Cursor(magic(30))),
new Click.Info.LeftDrag(IntList.of(0, 1, 2)),
List.of(
new Container(0, magic(10)),
new Container(1, magic(10)),
new Container(2, magic(10)),
new Cursor(ItemStack.AIR)
)
);
}
@Test
public void testRemainderItems() {
assertClick(
List.of(new Cursor(magic(32))),
new Click.Info.LeftDrag(IntList.of(0, 1, 2)),
List.of(
new Container(0, magic(10)),
new Container(1, magic(10)),
new Container(2, magic(10)),
new Cursor(magic(2))
)
);
assertClick(
List.of(new Cursor(magic(25))),
new Click.Info.LeftDrag(IntList.of(0, 1, 2, 3)),
List.of(
new Container(0, magic(6)),
new Container(1, magic(6)),
new Container(2, magic(6)),
new Container(3, magic(6)),
new Cursor(magic(1))
)
);
}
@Test
public void testDistributeOverExisting() {
assertClick(
List.of(new Container(0, magic(16)), new Cursor(magic(32))),
new Click.Info.LeftDrag(IntList.of(0)),
List.of(new Container(0, magic(48)), new Cursor(ItemStack.AIR))
);
}
@Test
public void testDistributeOverFull() {
assertClick(
List.of(new Container(0, magic(64)), new Cursor(magic(32))),
new Click.Info.LeftDrag(IntList.of(0)),
List.of()
);
}
}

View File

@ -0,0 +1,43 @@
package net.minestom.server.inventory.click.type;
import net.minestom.server.MinecraftServer;
import net.minestom.server.inventory.click.Click;
import net.minestom.server.inventory.click.Click.Change.Cursor;
import net.minestom.server.inventory.click.Click.Change.Container;
import org.junit.jupiter.api.Test;
import java.util.List;
import static net.minestom.server.inventory.click.ClickUtils.assertClick;
import static net.minestom.server.inventory.click.ClickUtils.magic;
public class InventoryMiddleClickTest {
static {
MinecraftServer.init();
}
@Test
public void testNoChanges() {
assertClick(List.of(), new Click.Info.Middle(0), List.of());
}
@Test
public void testCopy() {
assertClick(
List.of(new Container(0, magic(64))),
new Click.Info.Middle(0),
List.of(new Cursor(magic(64)))
);
}
@Test
public void testCopyNotFull() {
assertClick(
List.of(new Container(0, magic(32))),
new Click.Info.Middle(0),
List.of(new Cursor(magic(64)))
);
}
}

View File

@ -0,0 +1,52 @@
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.inventory.click.Click.Change.Cursor;
import net.minestom.server.inventory.click.Click.Change.Container;
import org.junit.jupiter.api.Test;
import java.util.List;
import static net.minestom.server.inventory.click.ClickUtils.*;
public class InventoryMiddleDragTest {
static {
MinecraftServer.init();
}
@Test
public void testNoChanges() {
assertClick(List.of(), new Click.Info.MiddleDrag(IntList.of()), List.of());
}
@Test
public void testExistingSlots() {
assertClick(
List.of(new Container(0, magic(1)), new Cursor(magic2(1))),
new Click.Info.MiddleDrag(IntList.of(0)),
List.of()
);
}
@Test
public void testPartialExistingSlots() {
assertClick(
List.of(new Container(0, magic(1)), new Cursor(magic2(1))),
new Click.Info.MiddleDrag(IntList.of(0, 1)),
List.of(new Container(1, magic2(1)))
);
}
@Test
public void testFullCopy() {
assertClick(
List.of(new Cursor(magic(1))),
new Click.Info.MiddleDrag(IntList.of(0, 1)),
List.of(new Container(0, magic(1)), new Container(1, magic(1)))
);
}
}

View File

@ -0,0 +1,34 @@
package net.minestom.server.inventory.click.type;
import net.minestom.server.MinecraftServer;
import net.minestom.server.inventory.click.Click;
import net.minestom.server.inventory.click.Click.Change.Container;
import net.minestom.server.inventory.click.Click.Change.Player;
import org.junit.jupiter.api.Test;
import java.util.List;
import static net.minestom.server.inventory.click.ClickUtils.*;
import static net.minestom.server.utils.inventory.PlayerInventoryUtils.OFF_HAND_SLOT;
public class InventoryOffhandSwapTest {
static {
MinecraftServer.init();
}
@Test
public void testNoChanges() {
assertClick(List.of(), new Click.Info.OffhandSwap(0), List.of());
}
@Test
public void testSwappedItems() {
assertClick(
List.of(new Container(0, magic2(1)), new Player(OFF_HAND_SLOT, magic(1))),
new Click.Info.OffhandSwap(0),
List.of(new Container(0, magic(1)), new Player(OFF_HAND_SLOT, magic2(1)))
);
}
}

View File

@ -0,0 +1,69 @@
package net.minestom.server.inventory.click.type;
import net.minestom.server.MinecraftServer;
import net.minestom.server.inventory.click.Click;
import net.minestom.server.inventory.click.Click.Change.Cursor;
import net.minestom.server.inventory.click.Click.Change.Container;
import org.junit.jupiter.api.Test;
import java.util.List;
import static net.minestom.server.inventory.click.ClickUtils.*;
public class InventoryRightClickTest {
static {
MinecraftServer.init();
}
@Test
public void testNoChanges() {
assertClick(List.of(), new Click.Info.Right(0), List.of());
}
@Test
public void testAddOne() {
assertClick(
List.of(new Container(0, magic(32)), new Cursor(magic(32))),
new Click.Info.Right(0),
List.of(new Container(0, magic(33)), new Cursor(magic(31)))
);
}
@Test
public void testClickedStackFull() {
assertClick(
List.of(new Container(0, magic(64)), new Cursor(magic(32))),
new Click.Info.Right(0),
List.of()
);
}
@Test
public void testTakeHalf() {
assertClick(
List.of(new Container(0, magic(32))),
new Click.Info.Right(0),
List.of(new Container(0, magic(16)), new Cursor(magic(16)))
);
}
@Test
public void testLeaveOne() {
assertClick(
List.of(new Cursor(magic(32))),
new Click.Info.Right(0),
List.of(new Container(0, magic(1)), new Cursor(magic(31)))
);
}
@Test
public void testSwitchItems() {
assertClick(
List.of(new Container(0, magic(1)), new Cursor(magic2(1))),
new Click.Info.Right(0),
List.of(new Container(0, magic2(1)), new Cursor(magic(1)))
);
}
}

View File

@ -0,0 +1,80 @@
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.inventory.click.Click.Change.*;
import net.minestom.server.item.ItemStack;
import org.junit.jupiter.api.Test;
import java.util.List;
import static net.minestom.server.inventory.click.ClickUtils.assertClick;
import static net.minestom.server.inventory.click.ClickUtils.magic;
public class InventoryRightDragTest {
static {
MinecraftServer.init();
}
@Test
public void testNoCursor() {
assertClick(List.of(), new Click.Info.RightDrag(IntList.of(0)), List.of());
}
@Test
public void testDistributeNone() {
assertClick(
List.of(new Cursor(magic(32))),
new Click.Info.RightDrag(IntList.of()),
List.of()
);
}
@Test
public void testDistributeOne() {
assertClick(
List.of(new Cursor(magic(32))),
new Click.Info.RightDrag(IntList.of(0)),
List.of(new Container(0, magic(1)), new Cursor(magic(31)))
);
}
@Test
public void testDistributeExactlyEnough() {
assertClick(
List.of(new Cursor(magic(2))),
new Click.Info.RightDrag(IntList.of(0, 1)),
List.of(new Container(0, magic(1)), new Container(1, magic(1)), new Cursor(ItemStack.AIR))
);
}
@Test
public void testTooManySlots() {
assertClick(
List.of(new Cursor(magic(2))),
new Click.Info.RightDrag(IntList.of(0, 1, 2)),
List.of(new Container(0, magic(1)), new Container(1, magic(1)), new Cursor(ItemStack.AIR))
);
}
@Test
public void testDistributeOverExisting() {
assertClick(
List.of(new Container(0, magic(16)), new Cursor(magic(32))),
new Click.Info.RightDrag(IntList.of(0)),
List.of(new Container(0, magic(17)), new Cursor(magic(31)))
);
}
@Test
public void testDistributeOverFull() {
assertClick(
List.of(new Container(0, magic(64)), new Cursor(magic(32))),
new Click.Info.RightDrag(IntList.of(0)),
List.of()
);
}
}

View File

@ -0,0 +1,153 @@
package net.minestom.server.inventory.click.type;
import net.minestom.server.MinecraftServer;
import net.minestom.server.inventory.click.Click;
import net.minestom.server.inventory.click.Click.Change.*;
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 java.util.List;
import static net.minestom.server.inventory.click.ClickUtils.*;
public class InventoryShiftClickTest {
static {
MinecraftServer.init();
}
@Test
public void testNoChanges() {
assertClick(List.of(), new Click.Info.LeftShift(0), List.of());
assertClick(List.of(), new Click.Info.RightShift(0), List.of());
}
@Test
public void testSimpleTransfer() {
assertClick(
List.of(new Player(9, magic(32))),
new Click.Info.LeftShift(SIZE),
List.of(new Container(0, magic(32)), new Player(9, ItemStack.AIR))
);
}
@Test
public void testSeparatedTransfer() {
assertClick(
List.of(
new Player(9, magic(64)),
new Container(0, magic(32)),
new Container(1, magic(32))
),
new Click.Info.LeftShift(SIZE),
List.of(
new Player(9, ItemStack.AIR),
new Container(0, magic(64)),
new Container(1, magic(64))
)
);
}
@Test
public void testSeparatedAndNewTransfer() {
assertClick(
List.of(
new Player(9, magic(64)),
new Container(0, magic(32))
),
new Click.Info.LeftShift(SIZE),
List.of(
new Player(9, ItemStack.AIR),
new Container(0, magic(64)),
new Container(1, magic(32))
)
);
}
@Test
public void testPartialTransfer() {
assertClick(
List.of(
new Player(9, magic(64)),
new Container(0, magic(32)),
new Container(1, magic2(1)),
new Container(2, magic2(1)),
new Container(3, magic2(1)),
new Container(4, magic2(1))
),
new Click.Info.LeftShift(SIZE),
List.of(new Player(9, magic(32)), new Container(0, magic(64)))
);
}
@Test
public void testCannotTransfer() {
assertClick(
List.of(
new Player(9, magic(64)),
new Container(0, magic(64)),
new Container(1, magic2(1)),
new Container(2, magic2(1)),
new Container(3, magic2(1)),
new Container(4, magic2(1))
),
new Click.Info.LeftShift(SIZE), // Equivalent to player slot 9
List.of()
);
assertClick(
List.of(
new Player(9, magic(64)),
new Container(0, magic2(1)),
new Container(1, magic2(1)),
new Container(2, magic2(1)),
new Container(3, magic2(1)),
new Container(4, magic2(1))
),
new Click.Info.LeftShift(SIZE), // Equivalent to player slot 9
List.of()
);
}
@Test
public void testPlayerInteraction() {
assertPlayerClick(
List.of(new Container(9, magic(32))),
new Click.Info.LeftShift(9),
List.of(new Container(9, ItemStack.AIR), new Container(0, magic(32)))
);
assertPlayerClick(
List.of(new Container(8, magic(32))),
new Click.Info.LeftShift(8),
List.of(new Container(8, ItemStack.AIR), new Container(9, magic(32)))
);
assertPlayerClick(
List.of(new Container(9, ItemStack.of(Material.IRON_CHESTPLATE))),
new Click.Info.LeftShift(9),
List.of(new Container(9, ItemStack.AIR), new Container(PlayerInventoryUtils.CHESTPLATE_SLOT, ItemStack.of(Material.IRON_CHESTPLATE)))
);
assertPlayerClick(
List.of(new Container(PlayerInventoryUtils.CHESTPLATE_SLOT, ItemStack.of(Material.IRON_CHESTPLATE))),
new Click.Info.LeftShift(PlayerInventoryUtils.CHESTPLATE_SLOT),
List.of(new Container(PlayerInventoryUtils.CHESTPLATE_SLOT, ItemStack.AIR), new Container(9, ItemStack.of(Material.IRON_CHESTPLATE)))
);
assertPlayerClick(
List.of(new Container(9, ItemStack.of(Material.SHIELD))),
new Click.Info.LeftShift(9),
List.of(new Container(9, ItemStack.AIR), new Container(PlayerInventoryUtils.OFF_HAND_SLOT, ItemStack.of(Material.SHIELD)))
);
assertPlayerClick(
List.of(new Container(PlayerInventoryUtils.OFF_HAND_SLOT, ItemStack.of(Material.SHIELD))),
new Click.Info.LeftShift(PlayerInventoryUtils.OFF_HAND_SLOT),
List.of(new Container(PlayerInventoryUtils.OFF_HAND_SLOT, ItemStack.AIR), new Container(9, ItemStack.of(Material.SHIELD)))
);
}
}