Minestom/src/main/java/net/minestom/server/inventory/Inventory.java

424 lines
17 KiB
Java
Raw Normal View History

2020-04-24 03:25:58 +02:00
package net.minestom.server.inventory;
2022-01-28 07:36:35 +01:00
import it.unimi.dsi.fastutil.Pair;
import net.kyori.adventure.text.Component;
2020-04-24 03:25:58 +02:00
import net.minestom.server.Viewable;
import net.minestom.server.entity.Player;
2022-01-28 07:36:35 +01:00
import net.minestom.server.inventory.click.ClickProcessor;
import net.minestom.server.inventory.click.ClickResult;
import net.minestom.server.inventory.click.ClickType;
2022-01-28 07:36:35 +01:00
import net.minestom.server.inventory.click.DragHelper;
2020-04-24 03:25:58 +02:00
import net.minestom.server.item.ItemStack;
2020-07-28 18:40:10 +02:00
import net.minestom.server.network.packet.server.play.OpenWindowPacket;
2020-04-24 03:25:58 +02:00
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;
2020-05-22 23:19:04 +02:00
import net.minestom.server.utils.inventory.PlayerInventoryUtils;
2020-10-24 10:46:23 +02:00
import org.jetbrains.annotations.NotNull;
2019-08-13 17:52:09 +02:00
import java.util.Collections;
2021-11-30 17:49:41 +01:00
import java.util.List;
2022-01-28 07:36:35 +01:00
import java.util.Map;
2019-08-13 17:52:09 +02:00
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
2022-01-28 07:36:35 +01:00
import java.util.function.Consumer;
import static net.minestom.server.utils.inventory.PlayerInventoryUtils.OFFHAND_SLOT;
2019-08-13 17:52:09 +02:00
2020-10-14 16:41:36 +02:00
/**
* 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)}.
*/
2021-10-22 01:55:55 +02:00
public non-sealed class Inventory extends AbstractInventory implements Viewable {
private static final AtomicInteger ID_COUNTER = new AtomicInteger();
2019-08-13 17:52:09 +02:00
// the id of this inventory
2020-08-19 01:24:51 +02:00
private final byte id;
// the type of this inventory
2020-07-23 07:36:49 +02:00
private final InventoryType inventoryType;
// the title of this inventory
private Component title;
2019-08-13 17:52:09 +02:00
2020-08-19 01:24:51 +02:00
private final int offset;
2019-08-13 17:52:09 +02:00
// the players currently viewing this inventory
2020-08-19 01:24:51 +02:00
private final Set<Player> viewers = new CopyOnWriteArraySet<>();
private final Set<Player> unmodifiableViewers = Collections.unmodifiableSet(viewers);
// (player -> cursor item) map, used by the click listeners
2020-08-19 01:24:51 +02:00
private final ConcurrentHashMap<Player, ItemStack> cursorPlayersItem = new ConcurrentHashMap<>();
2019-08-13 17:52:09 +02:00
public Inventory(@NotNull InventoryType inventoryType, @NotNull Component title) {
super(inventoryType.getSize());
2019-08-13 17:52:09 +02:00
this.id = generateId();
this.inventoryType = inventoryType;
this.title = title;
this.offset = getSize();
2019-08-13 17:52:09 +02:00
}
public Inventory(@NotNull InventoryType inventoryType, @NotNull String title) {
this(inventoryType, Component.text(title));
}
private static byte generateId() {
return (byte) Math.abs((byte) ID_COUNTER.incrementAndGet());
2019-08-13 17:52:09 +02:00
}
/**
2020-10-15 21:16:31 +02:00
* Gets the inventory type.
*
* @return the inventory type
*/
public @NotNull InventoryType getInventoryType() {
2019-08-13 17:52:09 +02:00
return inventoryType;
}
/**
2020-10-15 21:16:31 +02:00
* Gets the inventory title.
*
* @return the inventory title
*/
public @NotNull Component getTitle() {
2019-08-13 17:52:09 +02:00
return title;
}
/**
2020-10-15 21:16:31 +02:00
* Changes the inventory title.
*
* @param title the new inventory title
*/
public void setTitle(@NotNull Component title) {
2020-07-28 18:28:45 +02:00
this.title = title;
2020-10-02 03:54:59 +02:00
// Re-open the inventory
2021-11-30 17:49:41 +01:00
sendPacketToViewers(new OpenWindowPacket(getWindowId(), getInventoryType().getWindowType(), title));
2020-10-02 03:54:59 +02:00
// Send inventory items
2020-07-28 18:28:45 +02:00
update();
}
/**
2020-10-15 21:16:31 +02:00
* Gets this window id.
* <p>
2020-10-15 21:16:31 +02:00
* This is the id that the client will send to identify the affected inventory, mostly used by packets.
*
* @return the window id
*/
2020-04-28 19:22:47 +02:00
public byte getWindowId() {
2019-08-13 17:52:09 +02:00
return id;
}
@Override
public synchronized void clear() {
this.cursorPlayersItem.clear();
super.clear();
}
2020-05-22 23:19:04 +02:00
/**
* Refreshes the inventory for all viewers.
2020-05-22 23:19:04 +02:00
*/
@Override
2019-08-27 05:23:25 +02:00
public void update() {
this.viewers.forEach(p -> p.sendPacket(createNewWindowItemsPacket(p)));
2019-08-13 17:52:09 +02:00
}
2020-05-22 23:19:04 +02:00
/**
* Refreshes the inventory for a specific viewer.
2020-10-26 19:14:50 +01:00
* <p>
* The player needs to be a viewer, otherwise nothing is sent.
2020-05-22 23:19:04 +02:00
*
* @param player the player to update the inventory
*/
2020-10-24 11:19:54 +02:00
public void update(@NotNull Player player) {
if (!isViewer(player)) return;
player.sendPacket(createNewWindowItemsPacket(player));
2020-05-22 23:19:04 +02:00
}
2019-08-19 17:04:19 +02:00
@Override
public @NotNull Set<Player> getViewers() {
return unmodifiableViewers;
2019-08-13 17:52:09 +02:00
}
/**
* 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
*/
2019-08-19 17:04:19 +02:00
@Override
2020-10-24 10:46:23 +02:00
public boolean addViewer(@NotNull Player player) {
2020-07-23 07:36:49 +02:00
final boolean result = this.viewers.add(player);
update(player);
return result;
2019-08-13 17:52:09 +02:00
}
/**
* This will not close the inventory for {@code player}, use {@link Player#closeInventory()}.
*
* @param player the viewer to remove
* @return true if the player has successfully been removed
*/
2019-08-19 17:04:19 +02:00
@Override
2020-10-24 10:46:23 +02:00
public boolean removeViewer(@NotNull Player player) {
2020-07-23 07:36:49 +02:00
final boolean result = this.viewers.remove(player);
setCursorItem(player, ItemStack.AIR);
2022-01-28 07:36:35 +01:00
this.dragHelper.clearCache(player);
return result;
2019-08-13 17:52:09 +02:00
}
/**
2020-10-15 21:16:31 +02:00
* Gets the cursor item of a viewer.
*
* @param player the player to get the cursor item from
* @return the player cursor item, air item if the player is not a viewer
*/
public @NotNull ItemStack getCursorItem(@NotNull Player player) {
2021-04-02 18:13:02 +02:00
return cursorPlayersItem.getOrDefault(player, ItemStack.AIR);
2019-08-13 17:52:09 +02:00
}
/**
2020-10-15 21:16:31 +02:00
* Changes the cursor item of a viewer,
* does nothing if <code>player</code> is not a viewer.
*
* @param player the player to change the cursor item
* @param cursorItem the new player cursor item
*/
2020-10-24 11:19:54 +02:00
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));
2020-12-15 05:39:28 +01:00
}
if (!cursorItem.isAir()) {
this.cursorPlayersItem.put(player, cursorItem);
} else {
this.cursorPlayersItem.remove(player);
}
}
@Override
protected void UNSAFE_itemInsert(int slot, @NotNull ItemStack itemStack, boolean sendPacket) {
2021-09-06 18:59:55 +02:00
itemStacks[slot] = itemStack;
if (sendPacket) sendPacketToViewers(new SetSlotPacket(getWindowId(), 0, (short) slot, itemStack));
2019-08-13 17:52:09 +02:00
}
private @NotNull WindowItemsPacket createNewWindowItemsPacket(Player player) {
2021-11-30 17:49:41 +01:00
return new WindowItemsPacket(getWindowId(), 0, List.of(getItemStacks()), cursorPlayersItem.getOrDefault(player, ItemStack.AIR));
2019-08-13 17:52:09 +02:00
}
2020-08-03 00:37:03 +02:00
/**
2020-10-15 21:16:31 +02:00
* Sends a window property to all viewers.
2020-08-03 00:37:03 +02:00
*
* @param property the property to send
* @param value the value of the property
2020-09-20 15:04:07 +02:00
* @see <a href="https://wiki.vg/Protocol#Window_Property">https://wiki.vg/Protocol#Window_Property</a>
2020-08-03 00:37:03 +02:00
*/
2020-10-24 11:19:54 +02:00
protected void sendProperty(@NotNull InventoryProperty property, short value) {
2021-07-22 09:54:34 +02:00
sendPacketToViewers(new WindowPropertyPacket(getWindowId(), property.getProperty(), value));
}
2019-08-13 17:52:09 +02:00
@Override
2020-10-24 11:19:54 +02:00
public boolean leftClick(@NotNull Player player, int slot) {
2020-07-23 07:36:49 +02:00
final boolean isInWindow = isClickInWindow(slot);
final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset);
2022-01-28 07:36:35 +01:00
var inventory = isInWindow ? this : player.getInventory();
final var tmp = handlePreClick(inventory, player, clickSlot, ClickType.LEFT_CLICK, getCursorItem(player), inventory.getItemStack(clickSlot));
if (tmp.cancelled()) {
update();
return false;
2020-05-22 23:19:04 +02:00
}
2022-01-28 07:36:35 +01:00
return handleResult(ClickProcessor.left(clickSlot, tmp.clicked(), tmp.cursor()),
itemStack -> setCursorItem(player, itemStack), player, inventory, ClickType.LEFT_CLICK);
2019-08-13 17:52:09 +02:00
}
@Override
2020-10-24 11:19:54 +02:00
public boolean rightClick(@NotNull Player player, int slot) {
2020-07-23 07:36:49 +02:00
final boolean isInWindow = isClickInWindow(slot);
final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset);
2022-01-28 07:36:35 +01:00
var inventory = isInWindow ? this : player.getInventory();
final var tmp = handlePreClick(inventory, player, clickSlot, ClickType.RIGHT_CLICK, getCursorItem(player), inventory.getItemStack(clickSlot));
if (tmp.cancelled()) {
update();
return false;
2020-05-22 23:19:04 +02:00
}
2022-01-28 07:36:35 +01:00
return handleResult(ClickProcessor.right(clickSlot, tmp.clicked(), tmp.cursor()),
itemStack -> setCursorItem(player, itemStack), player, inventory, ClickType.RIGHT_CLICK);
2019-08-13 17:52:09 +02:00
}
@Override
2020-10-24 11:19:54 +02:00
public boolean shiftClick(@NotNull Player player, int slot) {
2020-07-23 07:36:49 +02:00
final boolean isInWindow = isClickInWindow(slot);
final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset);
final AbstractInventory inventory = isInWindow ? this : player.getInventory();
final ItemStack item = getItemStack(clickSlot);
2022-01-28 07:36:35 +01:00
PlayerInventory playerInv = player.getInventory();
final var tmp = handlePreClick(inventory, player, clickSlot, ClickType.START_SHIFT_CLICK,
getCursorItem(player), inventory.getItemStack(clickSlot));
if (tmp.cancelled()) {
update();
return false;
}
var result = isInWindow ? ClickProcessor.shiftToPlayer(playerInv, item) : ClickProcessor.shiftToInventory(this, item);
var inverseInv = isInWindow ? player.getInventory() : this;
// TODO call pre-click for each changed slot
return handleResult(result,
itemStack -> inventory.setItemStack(clickSlot, itemStack), player, inverseInv, ClickType.SHIFT_CLICK);
2019-08-13 17:52:09 +02:00
}
@Override
2020-10-24 11:19:54 +02:00
public boolean changeHeld(@NotNull Player player, int slot, int key) {
2020-07-23 07:36:49 +02:00
final PlayerInventory playerInventory = player.getInventory();
final boolean isInWindow = isClickInWindow(slot);
final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset);
2022-01-28 07:36:35 +01:00
final int convertedKey = key == 40 ? OFFHAND_SLOT : key;
final var clickInv = isInWindow ? this : playerInventory;
final var tmp = handlePreClick(clickInv, player, clickSlot, ClickType.CHANGE_HELD,
getCursorItem(player), clickInv.getItemStack(clickSlot));
if (tmp.cancelled()) {
update();
return false;
2020-05-22 23:19:04 +02:00
}
2022-01-28 07:36:35 +01:00
return handleResult(ClickProcessor.held(playerInventory, clickInv, clickSlot, tmp.clicked(), convertedKey, playerInventory.getItemStack(convertedKey)),
itemStack -> clickInv.setItemStack(clickSlot, itemStack), player, playerInventory, ClickType.SHIFT_CLICK);
2019-08-13 17:52:09 +02:00
}
@Override
2020-10-24 11:19:54 +02:00
public boolean middleClick(@NotNull Player player, int slot) {
2020-05-22 23:19:04 +02:00
// TODO
update(player);
2020-05-22 23:19:04 +02:00
return false;
2019-08-13 17:52:09 +02:00
}
@Override
2021-06-06 01:50:28 +02:00
public boolean drop(@NotNull Player player, boolean all, int slot, int button) {
2020-07-23 07:36:49 +02:00
final boolean isInWindow = isClickInWindow(slot);
final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset);
2022-01-28 07:36:35 +01:00
var inv = isInWindow ? this : player.getInventory();
2020-07-23 07:36:49 +02:00
final ItemStack cursor = getCursorItem(player);
2022-01-28 07:36:35 +01:00
final boolean outsideDrop = slot == -999;
final ItemStack clicked = outsideDrop ? ItemStack.AIR : inv.getItemStack(clickSlot);
var drop = ClickProcessor.drop(all, slot, button, clicked, cursor);
player.dropItem(drop.drop());
if (outsideDrop) {
setCursorItem(player, drop.remaining());
} else {
inv.setItemStack(clickSlot, drop.remaining());
2020-04-21 15:31:41 +02:00
}
2022-01-28 07:36:35 +01:00
// TODO events
return true;
2019-08-13 17:52:09 +02:00
}
2022-01-28 07:36:35 +01:00
private final DragHelper dragHelper = new DragHelper();
2020-03-20 19:50:22 +01:00
@Override
2020-10-24 11:19:54 +02:00
public boolean dragging(@NotNull Player player, int slot, int button) {
2022-01-28 07:36:35 +01:00
var playerInv = player.getInventory();
2020-07-23 07:36:49 +02:00
final boolean isInWindow = isClickInWindow(slot);
final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset);
2022-01-28 07:36:35 +01:00
final var clickInv = isInWindow ? this : playerInv;
return dragHelper.test(player, slot, button, clickSlot, clickInv,
// Start
(clickType) -> {
final var tmp = handlePreClick(null, player, -999, clickType,
2022-01-28 07:36:35 +01:00
getCursorItem(player), ItemStack.AIR);
if (tmp.cancelled()) {
update();
return false;
}
return true;
},
// Step
(clickType) -> {
final var tmp = handlePreClick(clickInv, player, clickSlot, clickType,
getCursorItem(player), getItemStack(clickSlot));
return !tmp.cancelled();
},
// End
(clickType, entries) -> {
var slots = entries.stream().map(dragData -> Pair.of(dragData.inventory(), dragData.slot())).toList();
// Handle last drag
final var tmp = handlePreClick(null, player, -999, clickType,
getCursorItem(player), ItemStack.AIR);
if (tmp.cancelled()) {
update();
return false;
2022-01-28 07:36:35 +01:00
}
return switch (clickType) {
case END_LEFT_DRAGGING ->
handleResult(ClickProcessor.leftDrag(playerInv, this, getCursorItem(player), slots),
itemStack -> setCursorItem(player, itemStack), player, clickType);
case END_RIGHT_DRAGGING ->
handleResult(ClickProcessor.rightDrag(playerInv, this, getCursorItem(player), slots),
itemStack -> setCursorItem(player, itemStack), player, clickType);
default -> throw new IllegalStateException("Invalid click type: " + clickType);
};
2022-01-28 07:36:35 +01:00
});
2020-03-20 19:50:22 +01:00
}
@Override
2020-10-24 11:19:54 +02:00
public boolean doubleClick(@NotNull Player player, int slot) {
2022-01-28 07:36:35 +01:00
return handleResult(ClickProcessor.doubleClick(player.getInventory(), this, getCursorItem(player)),
itemStack -> setCursorItem(player, itemStack), player, ClickType.DOUBLE_CLICK);
}
private boolean handleResult(ClickResult.Double result, Consumer<ItemStack> remainingSetter,
Player player, ClickType clickType) {
// Player changes
{
var inv = player.getInventory();
Map<Integer, ItemStack> playerChanges = result.playerChanges();
playerChanges.forEach((slot, itemStack) -> {
// TODO call events (conditions/pre-click)
});
playerChanges.forEach((slot, itemStack) -> {
inv.setItemStack(slot, itemStack);
callClickEvent(player, null, slot, clickType, itemStack, getCursorItem(player));
});
}
// This inventory changes
{
Map<Integer, ItemStack> playerChanges = result.inventoryChanges();
playerChanges.forEach((slot, itemStack) -> {
// TODO call events (conditions/pre-click)
});
playerChanges.forEach((slot, itemStack) -> {
setItemStack(slot, itemStack);
callClickEvent(player, this, slot, clickType, itemStack, getCursorItem(player));
});
}
2022-01-28 07:36:35 +01:00
remainingSetter.accept(result.remaining());
return true;
}
private boolean handleResult(ClickResult.Single result, Consumer<ItemStack> remainingSetter,
Player player, AbstractInventory inventory, ClickType clickType) {
Inventory eventInv = inventory instanceof Inventory ? ((Inventory) inventory) : null;
Map<Integer, ItemStack> changes = result.changedSlots();
changes.forEach((slot, itemStack) -> {
// TODO call events (conditions/pre-click)
});
changes.forEach((slot, itemStack) -> {
inventory.setItemStack(slot, itemStack);
callClickEvent(player, eventInv, slot, clickType, itemStack, getCursorItem(player));
});
remainingSetter.accept(result.remaining());
return true;
2020-05-22 23:19:04 +02:00
}
private boolean isClickInWindow(int slot) {
return slot < getSize();
}
private void updateAll(Player player) {
player.getInventory().update();
update(player);
}
2019-08-13 17:52:09 +02:00
}