mirror of https://github.com/Minestom/Minestom.git
424 lines
17 KiB
Java
424 lines
17 KiB
Java
package net.minestom.server.inventory;
|
|
|
|
import it.unimi.dsi.fastutil.Pair;
|
|
import net.kyori.adventure.text.Component;
|
|
import net.minestom.server.Viewable;
|
|
import net.minestom.server.entity.Player;
|
|
import net.minestom.server.inventory.click.ClickProcessor;
|
|
import net.minestom.server.inventory.click.ClickResult;
|
|
import net.minestom.server.inventory.click.ClickType;
|
|
import net.minestom.server.inventory.click.DragHelper;
|
|
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 org.jetbrains.annotations.NotNull;
|
|
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.concurrent.CopyOnWriteArraySet;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
import java.util.function.Consumer;
|
|
|
|
import static net.minestom.server.utils.inventory.PlayerInventoryUtils.OFFHAND_SLOT;
|
|
|
|
/**
|
|
* 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)}.
|
|
*/
|
|
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) Math.abs((byte) ID_COUNTER.incrementAndGet());
|
|
}
|
|
|
|
/**
|
|
* Gets the inventory type.
|
|
*
|
|
* @return the inventory type
|
|
*/
|
|
public @NotNull InventoryType getInventoryType() {
|
|
return inventoryType;
|
|
}
|
|
|
|
/**
|
|
* Gets the inventory title.
|
|
*
|
|
* @return the inventory title
|
|
*/
|
|
public @NotNull Component getTitle() {
|
|
return title;
|
|
}
|
|
|
|
/**
|
|
* Changes the inventory title.
|
|
*
|
|
* @param title the new inventory title
|
|
*/
|
|
public void setTitle(@NotNull Component title) {
|
|
this.title = title;
|
|
// Re-open the inventory
|
|
sendPacketToViewers(new OpenWindowPacket(getWindowId(), getInventoryType().getWindowType(), title));
|
|
// Send inventory items
|
|
update();
|
|
}
|
|
|
|
/**
|
|
* Gets this window id.
|
|
* <p>
|
|
* This is the id that the client will send to identify the affected inventory, mostly used by packets.
|
|
*
|
|
* @return the window id
|
|
*/
|
|
public byte getWindowId() {
|
|
return id;
|
|
}
|
|
|
|
@Override
|
|
public synchronized void clear() {
|
|
this.cursorPlayersItem.clear();
|
|
super.clear();
|
|
}
|
|
|
|
/**
|
|
* Refreshes the inventory for all viewers.
|
|
*/
|
|
@Override
|
|
public void update() {
|
|
this.viewers.forEach(p -> p.sendPacket(createNewWindowItemsPacket(p)));
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
public void update(@NotNull Player player) {
|
|
if (!isViewer(player)) return;
|
|
player.sendPacket(createNewWindowItemsPacket(player));
|
|
}
|
|
|
|
@Override
|
|
public @NotNull Set<Player> getViewers() {
|
|
return unmodifiableViewers;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
@Override
|
|
public boolean addViewer(@NotNull Player player) {
|
|
final boolean result = this.viewers.add(player);
|
|
update(player);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
@Override
|
|
public boolean removeViewer(@NotNull Player player) {
|
|
final boolean result = this.viewers.remove(player);
|
|
setCursorItem(player, ItemStack.AIR);
|
|
this.dragHelper.clearCache(player);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
return cursorPlayersItem.getOrDefault(player, ItemStack.AIR);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
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);
|
|
}
|
|
}
|
|
|
|
@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));
|
|
}
|
|
|
|
/**
|
|
* 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#Window_Property">https://wiki.vg/Protocol#Window_Property</a>
|
|
*/
|
|
protected void sendProperty(@NotNull InventoryProperty property, short value) {
|
|
sendPacketToViewers(new WindowPropertyPacket(getWindowId(), property.getProperty(), value));
|
|
}
|
|
|
|
@Override
|
|
public boolean leftClick(@NotNull Player player, int slot) {
|
|
final boolean isInWindow = isClickInWindow(slot);
|
|
final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset);
|
|
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;
|
|
}
|
|
return handleResult(ClickProcessor.left(clickSlot, tmp.clicked(), tmp.cursor()),
|
|
itemStack -> setCursorItem(player, itemStack), player, inventory, ClickType.LEFT_CLICK);
|
|
}
|
|
|
|
@Override
|
|
public boolean rightClick(@NotNull Player player, int slot) {
|
|
final boolean isInWindow = isClickInWindow(slot);
|
|
final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset);
|
|
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;
|
|
}
|
|
return handleResult(ClickProcessor.right(clickSlot, tmp.clicked(), tmp.cursor()),
|
|
itemStack -> setCursorItem(player, itemStack), player, inventory, ClickType.RIGHT_CLICK);
|
|
}
|
|
|
|
@Override
|
|
public boolean shiftClick(@NotNull Player player, int slot) {
|
|
final boolean isInWindow = isClickInWindow(slot);
|
|
final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset);
|
|
final AbstractInventory inventory = isInWindow ? this : player.getInventory();
|
|
final ItemStack item = getItemStack(clickSlot);
|
|
PlayerInventory playerInv = player.getInventory();
|
|
final var tmp = handlePreClick(inventory, player, clickSlot, ClickType.START_SHIFT_CLICK,
|
|
getCursorItem(player), inventory.getItemStack(clickSlot));
|
|
if (tmp.cancelled()) {
|
|
update();
|
|
return false;
|
|
}
|
|
|
|
var result = isInWindow ? ClickProcessor.shiftToPlayer(playerInv, item) : ClickProcessor.shiftToInventory(this, item);
|
|
var inverseInv = isInWindow ? player.getInventory() : this;
|
|
// TODO call pre-click for each changed slot
|
|
return handleResult(result,
|
|
itemStack -> inventory.setItemStack(clickSlot, itemStack), player, inverseInv, ClickType.SHIFT_CLICK);
|
|
|
|
}
|
|
|
|
@Override
|
|
public boolean changeHeld(@NotNull Player player, int slot, int key) {
|
|
final PlayerInventory playerInventory = player.getInventory();
|
|
final boolean isInWindow = isClickInWindow(slot);
|
|
final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset);
|
|
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;
|
|
}
|
|
return handleResult(ClickProcessor.held(playerInventory, clickInv, clickSlot, tmp.clicked(), convertedKey, playerInventory.getItemStack(convertedKey)),
|
|
itemStack -> clickInv.setItemStack(clickSlot, itemStack), player, playerInventory, ClickType.SHIFT_CLICK);
|
|
}
|
|
|
|
@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 boolean isInWindow = isClickInWindow(slot);
|
|
final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset);
|
|
var inv = isInWindow ? this : player.getInventory();
|
|
final ItemStack cursor = getCursorItem(player);
|
|
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());
|
|
}
|
|
// TODO events
|
|
return true;
|
|
}
|
|
|
|
private final DragHelper dragHelper = new DragHelper();
|
|
|
|
@Override
|
|
public boolean dragging(@NotNull Player player, int slot, int button) {
|
|
var playerInv = player.getInventory();
|
|
final boolean isInWindow = isClickInWindow(slot);
|
|
final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset);
|
|
final var clickInv = isInWindow ? this : playerInv;
|
|
return dragHelper.test(player, slot, button, clickSlot, clickInv,
|
|
// Start
|
|
(clickType) -> {
|
|
final var tmp = handlePreClick(null, player, -999, clickType,
|
|
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;
|
|
}
|
|
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);
|
|
};
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public boolean doubleClick(@NotNull Player player, int slot) {
|
|
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));
|
|
});
|
|
}
|
|
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;
|
|
}
|
|
|
|
private boolean isClickInWindow(int slot) {
|
|
return slot < getSize();
|
|
}
|
|
|
|
private void updateAll(Player player) {
|
|
player.getInventory().update();
|
|
update(player);
|
|
}
|
|
}
|