Integrate PlayerInventory (#2332)

* Integrate player inventory and normal inventory

* Fix errors and broken tests (drop before adding, wrong order, etc)

* Refactor slot refreshing

* Fix viewer logic error

* Fix incorrect merge

* Move inventory update

* Minor simplifications

* More minor simplifications
This commit is contained in:
GoldenStack 2024-10-28 23:58:24 +00:00 committed by Matt Worzala
parent 41738bb62a
commit e0939f089b
23 changed files with 245 additions and 293 deletions

View File

@ -48,6 +48,7 @@ import net.minestom.server.instance.EntityTracker;
import net.minestom.server.instance.Instance; import net.minestom.server.instance.Instance;
import net.minestom.server.instance.SharedInstance; import net.minestom.server.instance.SharedInstance;
import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.Block;
import net.minestom.server.inventory.AbstractInventory;
import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.Inventory;
import net.minestom.server.inventory.PlayerInventory; import net.minestom.server.inventory.PlayerInventory;
import net.minestom.server.item.ItemComponent; import net.minestom.server.item.ItemComponent;
@ -175,7 +176,7 @@ public class Player extends LivingEntity implements CommandSender, HoverEventSou
private int portalCooldown = 0; private int portalCooldown = 0;
protected PlayerInventory inventory; protected PlayerInventory inventory;
private Inventory openInventory; private AbstractInventory openInventory;
// Used internally to allow the closing of inventory within the inventory listener // Used internally to allow the closing of inventory within the inventory listener
private boolean didCloseInventory; private boolean didCloseInventory;
@ -236,7 +237,7 @@ public class Player extends LivingEntity implements CommandSender, HoverEventSou
setRespawnPoint(Pos.ZERO); setRespawnPoint(Pos.ZERO);
this.inventory = new PlayerInventory(this); this.inventory = new PlayerInventory();
setCanPickupItem(true); // By default setCanPickupItem(true); // By default
@ -295,6 +296,9 @@ public class Player extends LivingEntity implements CommandSender, HoverEventSou
true); true);
sendPacket(joinGamePacket); sendPacket(joinGamePacket);
// Start sending inventory updates
inventory.addViewer(this);
// Difficulty // Difficulty
sendPacket(new ServerDifficultyPacket(MinecraftServer.getDifficulty(), true)); sendPacket(new ServerDifficultyPacket(MinecraftServer.getDifficulty(), true));
@ -573,8 +577,9 @@ public class Player extends LivingEntity implements CommandSender, HoverEventSou
super.remove(permanent); super.remove(permanent);
final Inventory currentInventory = getOpenInventory(); final AbstractInventory currentInventory = getOpenInventory();
if (currentInventory != null) currentInventory.removeViewer(this); if (currentInventory != null) currentInventory.removeViewer(this);
MinecraftServer.getBossBarManager().removeAllBossBars(this); MinecraftServer.getBossBarManager().removeAllBossBars(this);
// Advancement tabs cache // Advancement tabs cache
{ {
@ -1724,7 +1729,7 @@ public class Player extends LivingEntity implements CommandSender, HoverEventSou
* *
* @return the currently open inventory, null if there is not (player inventory is not detected) * @return the currently open inventory, null if there is not (player inventory is not detected)
*/ */
public @Nullable Inventory getOpenInventory() { public @Nullable AbstractInventory getOpenInventory() {
return openInventory; return openInventory;
} }
@ -1738,19 +1743,13 @@ public class Player extends LivingEntity implements CommandSender, HoverEventSou
InventoryOpenEvent inventoryOpenEvent = new InventoryOpenEvent(inventory, this); InventoryOpenEvent inventoryOpenEvent = new InventoryOpenEvent(inventory, this);
EventDispatcher.callCancellable(inventoryOpenEvent, () -> { EventDispatcher.callCancellable(inventoryOpenEvent, () -> {
Inventory openInventory = getOpenInventory(); AbstractInventory openInventory = getOpenInventory();
if (openInventory != null) { if (openInventory != null) {
openInventory.removeViewer(this); openInventory.removeViewer(this);
} }
Inventory newInventory = inventoryOpenEvent.getInventory(); AbstractInventory newInventory = inventoryOpenEvent.getInventory();
if (newInventory == null) {
// just close the inventory
return;
}
sendPacket(new OpenWindowPacket(newInventory.getWindowId(),
newInventory.getInventoryType().getWindowType(), newInventory.getTitle()));
newInventory.addViewer(this); newInventory.addViewer(this);
this.openInventory = newInventory; this.openInventory = newInventory;
}); });
@ -1767,31 +1766,15 @@ public class Player extends LivingEntity implements CommandSender, HoverEventSou
@ApiStatus.Internal @ApiStatus.Internal
public void closeInventory(boolean fromClient) { public void closeInventory(boolean fromClient) {
Inventory openInventory = getOpenInventory(); AbstractInventory openInventory = getOpenInventory();
if (openInventory == null) return;
// Drop cursor item when closing inventory this.openInventory = null;
ItemStack cursorItem = getInventory().getCursorItem(); openInventory.removeViewer(this);
getInventory().setCursorItem(ItemStack.AIR); inventory.update();
if (!cursorItem.isAir()) { if (!fromClient) {
// Add item to inventory if he hasn't been able to drop it didCloseInventory = true;
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;
} }
} }
@ -2276,12 +2259,12 @@ public class Player extends LivingEntity implements CommandSender, HoverEventSou
@Override @Override
public @NotNull ItemStack getEquipment(@NotNull EquipmentSlot slot) { public @NotNull ItemStack getEquipment(@NotNull EquipmentSlot slot) {
return inventory.getEquipment(slot); return inventory.getEquipment(slot, heldSlot);
} }
@Override @Override
public void setEquipment(@NotNull EquipmentSlot slot, @NotNull ItemStack itemStack) { public void setEquipment(@NotNull EquipmentSlot slot, @NotNull ItemStack itemStack) {
inventory.setEquipment(slot, itemStack); inventory.setEquipment(slot, heldSlot, itemStack);
} }
@Override @Override

View File

@ -5,7 +5,7 @@ import net.minestom.server.entity.Player;
import net.minestom.server.event.trait.*; import net.minestom.server.event.trait.*;
import net.minestom.server.instance.Instance; import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.Block;
import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.AbstractInventory;
import net.minestom.server.item.ItemStack; import net.minestom.server.item.ItemStack;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -34,7 +34,7 @@ public interface EventFilter<E extends Event, H> {
EventFilter<PlayerEvent, Player> PLAYER = from(PlayerEvent.class, Player.class, PlayerEvent::getPlayer); EventFilter<PlayerEvent, Player> PLAYER = from(PlayerEvent.class, Player.class, PlayerEvent::getPlayer);
EventFilter<ItemEvent, ItemStack> ITEM = from(ItemEvent.class, ItemStack.class, ItemEvent::getItemStack); EventFilter<ItemEvent, ItemStack> ITEM = from(ItemEvent.class, ItemStack.class, ItemEvent::getItemStack);
EventFilter<InstanceEvent, Instance> INSTANCE = from(InstanceEvent.class, Instance.class, InstanceEvent::getInstance); EventFilter<InstanceEvent, Instance> INSTANCE = from(InstanceEvent.class, Instance.class, InstanceEvent::getInstance);
EventFilter<InventoryEvent, Inventory> INVENTORY = from(InventoryEvent.class, Inventory.class, InventoryEvent::getInventory); EventFilter<InventoryEvent, AbstractInventory> INVENTORY = from(InventoryEvent.class, AbstractInventory.class, InventoryEvent::getInventory);
EventFilter<BlockEvent, Block> BLOCK = from(BlockEvent.class, Block.class, BlockEvent::getBlock); EventFilter<BlockEvent, Block> BLOCK = from(BlockEvent.class, Block.class, BlockEvent::getBlock);
static <E extends Event, H> EventFilter<E, H> from(@NotNull Class<E> eventType, static <E extends Event, H> EventFilter<E, H> from(@NotNull Class<E> eventType,

View File

@ -3,11 +3,10 @@ package net.minestom.server.event.inventory;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.event.trait.InventoryEvent; import net.minestom.server.event.trait.InventoryEvent;
import net.minestom.server.event.trait.PlayerInstanceEvent; import net.minestom.server.event.trait.PlayerInstanceEvent;
import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.AbstractInventory;
import net.minestom.server.inventory.click.ClickType; import net.minestom.server.inventory.click.ClickType;
import net.minestom.server.item.ItemStack; import net.minestom.server.item.ItemStack;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/** /**
* Called after {@link InventoryPreClickEvent}, this event cannot be cancelled and items related to the click * Called after {@link InventoryPreClickEvent}, this event cannot be cancelled and items related to the click
@ -15,14 +14,14 @@ import org.jetbrains.annotations.Nullable;
*/ */
public class InventoryClickEvent implements InventoryEvent, PlayerInstanceEvent { public class InventoryClickEvent implements InventoryEvent, PlayerInstanceEvent {
private final Inventory inventory; private final AbstractInventory inventory;
private final Player player; private final Player player;
private final int slot; private final int slot;
private final ClickType clickType; private final ClickType clickType;
private final ItemStack clickedItem; private final ItemStack clickedItem;
private final ItemStack cursorItem; private final ItemStack cursorItem;
public InventoryClickEvent(@Nullable Inventory inventory, @NotNull Player player, public InventoryClickEvent(@NotNull AbstractInventory inventory, @NotNull Player player,
int slot, @NotNull ClickType clickType, int slot, @NotNull ClickType clickType,
@NotNull ItemStack clicked, @NotNull ItemStack cursor) { @NotNull ItemStack clicked, @NotNull ItemStack cursor) {
this.inventory = inventory; this.inventory = inventory;
@ -83,7 +82,7 @@ public class InventoryClickEvent implements InventoryEvent, PlayerInstanceEvent
} }
@Override @Override
public @Nullable Inventory getInventory() { public @NotNull AbstractInventory getInventory() {
return inventory; return inventory;
} }
} }

View File

@ -3,20 +3,21 @@ package net.minestom.server.event.inventory;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.event.trait.InventoryEvent; import net.minestom.server.event.trait.InventoryEvent;
import net.minestom.server.event.trait.PlayerInstanceEvent; import net.minestom.server.event.trait.PlayerInstanceEvent;
import net.minestom.server.inventory.AbstractInventory;
import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.Inventory;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
/** /**
* Called when an {@link Inventory} is closed by a player. * Called when an {@link AbstractInventory} is closed by a player.
*/ */
public class InventoryCloseEvent implements InventoryEvent, PlayerInstanceEvent { public class InventoryCloseEvent implements InventoryEvent, PlayerInstanceEvent {
private final Inventory inventory; private final AbstractInventory inventory;
private final Player player; private final Player player;
private Inventory newInventory; private Inventory newInventory;
public InventoryCloseEvent(@Nullable Inventory inventory, @NotNull Player player) { public InventoryCloseEvent(@NotNull AbstractInventory inventory, @NotNull Player player) {
this.inventory = inventory; this.inventory = inventory;
this.player = player; this.player = player;
} }
@ -51,7 +52,7 @@ public class InventoryCloseEvent implements InventoryEvent, PlayerInstanceEvent
} }
@Override @Override
public @Nullable Inventory getInventory() { public @NotNull AbstractInventory getInventory() {
return inventory; return inventory;
} }
} }

View File

@ -3,10 +3,8 @@ package net.minestom.server.event.inventory;
import net.minestom.server.event.trait.InventoryEvent; import net.minestom.server.event.trait.InventoryEvent;
import net.minestom.server.event.trait.RecursiveEvent; import net.minestom.server.event.trait.RecursiveEvent;
import net.minestom.server.inventory.AbstractInventory; import net.minestom.server.inventory.AbstractInventory;
import net.minestom.server.inventory.Inventory;
import net.minestom.server.item.ItemStack; import net.minestom.server.item.ItemStack;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/** /**
* Called when {@link AbstractInventory#safeItemInsert(int, ItemStack)} is being invoked. * Called when {@link AbstractInventory#safeItemInsert(int, ItemStack)} is being invoked.
@ -17,12 +15,12 @@ import org.jetbrains.annotations.Nullable;
@SuppressWarnings("JavadocReference") @SuppressWarnings("JavadocReference")
public class InventoryItemChangeEvent implements InventoryEvent, RecursiveEvent { public class InventoryItemChangeEvent implements InventoryEvent, RecursiveEvent {
private final Inventory inventory; private final AbstractInventory inventory;
private final int slot; private final int slot;
private final ItemStack previousItem; private final ItemStack previousItem;
private final ItemStack newItem; private final ItemStack newItem;
public InventoryItemChangeEvent(@Nullable Inventory inventory, int slot, public InventoryItemChangeEvent(@NotNull AbstractInventory inventory, int slot,
@NotNull ItemStack previousItem, @NotNull ItemStack newItem) { @NotNull ItemStack previousItem, @NotNull ItemStack newItem) {
this.inventory = inventory; this.inventory = inventory;
this.slot = slot; this.slot = slot;
@ -58,7 +56,7 @@ public class InventoryItemChangeEvent implements InventoryEvent, RecursiveEvent
} }
@Override @Override
public @Nullable Inventory getInventory() { public @NotNull AbstractInventory getInventory() {
return inventory; return inventory;
} }
} }

View File

@ -4,23 +4,23 @@ import net.minestom.server.entity.Player;
import net.minestom.server.event.trait.CancellableEvent; import net.minestom.server.event.trait.CancellableEvent;
import net.minestom.server.event.trait.InventoryEvent; import net.minestom.server.event.trait.InventoryEvent;
import net.minestom.server.event.trait.PlayerInstanceEvent; import net.minestom.server.event.trait.PlayerInstanceEvent;
import net.minestom.server.inventory.AbstractInventory;
import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.Inventory;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/** /**
* Called when a player open an {@link Inventory}. * Called when a player open an {@link AbstractInventory}.
* <p> * <p>
* Executed by {@link Player#openInventory(Inventory)}. * Executed by {@link Player#openInventory(Inventory)}.
*/ */
public class InventoryOpenEvent implements InventoryEvent, PlayerInstanceEvent, CancellableEvent { public class InventoryOpenEvent implements InventoryEvent, PlayerInstanceEvent, CancellableEvent {
private Inventory inventory; private AbstractInventory inventory;
private final Player player; private final Player player;
private boolean cancelled; private boolean cancelled;
public InventoryOpenEvent(@Nullable Inventory inventory, @NotNull Player player) { public InventoryOpenEvent(@NotNull AbstractInventory inventory, @NotNull Player player) {
this.inventory = inventory; this.inventory = inventory;
this.player = player; this.player = player;
} }
@ -36,13 +36,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, this could have been change by the {@link #setInventory(AbstractInventory)}.
* *
* @return the inventory to open, null to just close the current inventory if any * @return the inventory to open, null to just close the current inventory if any
*/ */
@Nullable
@Override @Override
public Inventory getInventory() { public @NotNull AbstractInventory getInventory() {
return inventory; return inventory;
} }
@ -53,7 +52,7 @@ public class InventoryOpenEvent implements InventoryEvent, PlayerInstanceEvent,
* *
* @param inventory the inventory to open * @param inventory the inventory to open
*/ */
public void setInventory(@Nullable Inventory inventory) { public void setInventory(@NotNull AbstractInventory inventory) {
this.inventory = inventory; this.inventory = inventory;
} }

View File

@ -4,18 +4,17 @@ import net.minestom.server.entity.Player;
import net.minestom.server.event.trait.CancellableEvent; import net.minestom.server.event.trait.CancellableEvent;
import net.minestom.server.event.trait.InventoryEvent; import net.minestom.server.event.trait.InventoryEvent;
import net.minestom.server.event.trait.PlayerInstanceEvent; import net.minestom.server.event.trait.PlayerInstanceEvent;
import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.AbstractInventory;
import net.minestom.server.inventory.click.ClickType; import net.minestom.server.inventory.click.ClickType;
import net.minestom.server.item.ItemStack; import net.minestom.server.item.ItemStack;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/** /**
* Called before {@link InventoryClickEvent}, used to potentially cancel the click. * Called before {@link InventoryClickEvent}, used to potentially cancel the click.
*/ */
public class InventoryPreClickEvent implements InventoryEvent, PlayerInstanceEvent, CancellableEvent { public class InventoryPreClickEvent implements InventoryEvent, PlayerInstanceEvent, CancellableEvent {
private final Inventory inventory; private final AbstractInventory inventory;
private final Player player; private final Player player;
private final int slot; private final int slot;
private final ClickType clickType; private final ClickType clickType;
@ -24,7 +23,7 @@ public class InventoryPreClickEvent implements InventoryEvent, PlayerInstanceEve
private boolean cancelled; private boolean cancelled;
public InventoryPreClickEvent(@Nullable Inventory inventory, public InventoryPreClickEvent(@NotNull AbstractInventory inventory,
@NotNull Player player, @NotNull Player player,
int slot, @NotNull ClickType clickType, int slot, @NotNull ClickType clickType,
@NotNull ItemStack clicked, @NotNull ItemStack cursor) { @NotNull ItemStack clicked, @NotNull ItemStack cursor) {
@ -114,7 +113,7 @@ public class InventoryPreClickEvent implements InventoryEvent, PlayerInstanceEve
} }
@Override @Override
public @Nullable Inventory getInventory() { public @NotNull AbstractInventory getInventory() {
return inventory; 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,18 +1,16 @@
package net.minestom.server.event.trait; package net.minestom.server.event.trait;
import net.minestom.server.event.Event; import net.minestom.server.event.Event;
import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.AbstractInventory;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.NotNull;
/** /**
* Represents any event inside an {@link Inventory}. * Represents any event inside an {@link AbstractInventory}.
*/ */
public interface InventoryEvent extends Event { public interface InventoryEvent extends Event {
/** /**
* Gets the inventory. * Gets the inventory that was clicked.
*
* @return the inventory, null if this is a player's inventory
*/ */
@Nullable Inventory getInventory(); @NotNull AbstractInventory getInventory();
} }

View File

@ -1,11 +1,15 @@
package net.minestom.server.inventory; package net.minestom.server.inventory;
import net.minestom.server.Viewable;
import net.minestom.server.entity.Player;
import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.inventory.InventoryItemChangeEvent; 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.click.InventoryClickProcessor;
import net.minestom.server.inventory.condition.InventoryCondition; import net.minestom.server.inventory.condition.InventoryCondition;
import net.minestom.server.item.ItemStack; 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.tag.TagHandler;
import net.minestom.server.tag.Taggable; import net.minestom.server.tag.Taggable;
import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.MathUtils;
@ -14,16 +18,15 @@ import org.jetbrains.annotations.NotNull;
import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle; import java.lang.invoke.VarHandle;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;
/** /**
* Represents an inventory where items can be modified/retrieved. * Represents an inventory where items can be modified/retrieved.
*/ */
public sealed abstract class AbstractInventory implements InventoryClickHandler, Taggable public sealed abstract class AbstractInventory implements InventoryClickHandler, Taggable, Viewable
permits Inventory, PlayerInventory { permits Inventory, PlayerInventory {
private static final VarHandle ITEM_UPDATER = MethodHandles.arrayElementVarHandle(ItemStack[].class); private static final VarHandle ITEM_UPDATER = MethodHandles.arrayElementVarHandle(ItemStack[].class);
@ -38,58 +41,97 @@ public sealed abstract class AbstractInventory implements InventoryClickHandler,
private final TagHandler tagHandler = TagHandler.newHandler(); private final TagHandler tagHandler = TagHandler.newHandler();
// the players currently viewing this inventory
protected final Set<Player> viewers = new CopyOnWriteArraySet<>();
protected final Set<Player> unmodifiableViewers = Collections.unmodifiableSet(viewers);
protected AbstractInventory(int size) { protected AbstractInventory(int size) {
this.size = size; this.size = size;
this.itemStacks = new ItemStack[getSize()]; this.itemStacks = new ItemStack[getSize()];
Arrays.fill(itemStacks, ItemStack.AIR); Arrays.fill(itemStacks, ItemStack.AIR);
} }
/**
* 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 abstract byte getWindowId();
@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;
// Drop cursor item when closing inventory
ItemStack cursorItem = player.getInventory().getCursorItem();
player.getInventory().setCursorItem(ItemStack.AIR);
if (!cursorItem.isAir()) {
if (!player.dropItem(cursorItem)) {
player.getInventory().addItemStack(cursorItem);
}
}
if (player.didCloseInventory()) {
player.sendPacket(new CloseWindowPacket(getWindowId()));
}
return true;
}
/** /**
* Sets an {@link ItemStack} at the specified slot and send relevant update to the viewer(s). * 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 slot the slot to set the item
* @param itemStack the item to set * @param itemStack the item to set
*/ */
public synchronized void setItemStack(int slot, @NotNull ItemStack itemStack) { public void setItemStack(int slot, @NotNull ItemStack itemStack) {
Check.argCondition(!MathUtils.isBetween(slot, 0, getSize()), setItemStack(slot, itemStack, true);
"Inventory does not have the slot " + slot);
safeItemInsert(slot, itemStack);
} }
/** /**
* Inserts safely an item into the inventory. * Sets an {@link ItemStack} at the specified slot and send relevant update to the viewer(s).
* <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 slot the slot to set the item
* @param itemStack the item to insert (use air instead of null) * @param itemStack the item to set
* @throws IllegalArgumentException if the slot {@code slot} does not exist * @param sendPacket whether or not to send packets
*/ */
protected final void safeItemInsert(int slot, @NotNull ItemStack itemStack, boolean sendPacket) { public void setItemStack(int slot, @NotNull ItemStack itemStack, boolean sendPacket) {
Check.argCondition(!MathUtils.isBetween(slot, 0, getSize()),
"Inventory does not have the slot " + slot);
ItemStack previous; ItemStack previous;
synchronized (this) { synchronized (this) {
Check.argCondition(
!MathUtils.isBetween(slot, 0, getSize()),
"The slot {0} does not exist in this inventory",
slot
);
previous = itemStacks[slot]; previous = itemStacks[slot];
if (itemStack.equals(previous)) return; // Avoid sending updates if the item has not changed if (itemStack.equals(previous)) return; // Avoid sending updates if the item has not changed
UNSAFE_itemInsert(slot, itemStack, sendPacket); UNSAFE_itemInsert(slot, itemStack, previous, 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));
} }
EventDispatcher.call(new InventoryItemChangeEvent(this, slot, previous, itemStack));
} }
protected final void safeItemInsert(int slot, @NotNull ItemStack itemStack) { protected void UNSAFE_itemInsert(int slot, @NotNull ItemStack item, @NotNull ItemStack previous, boolean sendPacket) {
safeItemInsert(slot, itemStack, true); itemStacks[slot] = item;
if (sendPacket) sendSlotRefresh(slot, item, previous);
} }
protected abstract void UNSAFE_itemInsert(int slot, @NotNull ItemStack itemStack, boolean sendPacket); public void sendSlotRefresh(int slot, @NotNull ItemStack item, @NotNull ItemStack previous) {
sendPacketToViewers(new SetSlotPacket(getWindowId(), 0, (short) slot, item));
}
public synchronized <T> @NotNull T processItemStack(@NotNull ItemStack itemStack, public synchronized <T> @NotNull T processItemStack(@NotNull ItemStack itemStack,
@NotNull TransactionType type, @NotNull TransactionType type,
@ -167,13 +209,27 @@ public sealed abstract class AbstractInventory implements InventoryClickHandler,
public synchronized void clear() { public synchronized void clear() {
// Clear the item array // Clear the item array
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
safeItemInsert(i, ItemStack.AIR, false); setItemStack(i, ItemStack.AIR, false);
} }
// Send the cleared inventory to viewers // Send the cleared inventory to viewers
update(); update();
} }
public abstract void update(); /**
* Refreshes the inventory for all viewers.
*/
public void update() {
this.viewers.forEach(this::update);
}
/**
* Refreshes the inventory for a specific viewer.
*
* @param player the player to update the inventory for
*/
public void update(@NotNull Player player) {
player.sendPacket(new WindowItemsPacket(getWindowId(), 0, List.of(itemStacks), player.getInventory().getCursorItem()));
}
/** /**
* Gets the {@link ItemStack} at the specified slot. * Gets the {@link ItemStack} at the specified slot.

View File

@ -1,22 +1,16 @@
package net.minestom.server.inventory; package net.minestom.server.inventory;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.minestom.server.Viewable;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.inventory.click.ClickType; import net.minestom.server.inventory.click.ClickType;
import net.minestom.server.inventory.click.InventoryClickResult; import net.minestom.server.inventory.click.InventoryClickResult;
import net.minestom.server.item.ItemStack; import net.minestom.server.item.ItemStack;
import net.minestom.server.network.packet.server.play.OpenWindowPacket; 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.SetSlotPacket;
import net.minestom.server.network.packet.server.play.WindowItemsPacket;
import net.minestom.server.network.packet.server.play.WindowPropertyPacket; import net.minestom.server.network.packet.server.play.WindowPropertyPacket;
import net.minestom.server.utils.inventory.PlayerInventoryUtils; import net.minestom.server.utils.inventory.PlayerInventoryUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
/** /**
@ -25,22 +19,15 @@ import java.util.concurrent.atomic.AtomicInteger;
* You can create one with {@link Inventory#Inventory(InventoryType, String)} or by making your own subclass. * 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)}. * It can then be opened using {@link Player#openInventory(Inventory)}.
*/ */
public non-sealed class Inventory extends AbstractInventory implements Viewable { public non-sealed class Inventory extends AbstractInventory {
private static final AtomicInteger ID_COUNTER = new AtomicInteger(); private static final AtomicInteger ID_COUNTER = new AtomicInteger();
// the id of this inventory
private final byte id; private final byte id;
// the type of this inventory
private final InventoryType inventoryType; private final InventoryType inventoryType;
// the title of this inventory
private Component title; private Component title;
private final int offset; 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);
public Inventory(@NotNull InventoryType inventoryType, @NotNull Component title) { public Inventory(@NotNull InventoryType inventoryType, @NotNull Component title) {
super(inventoryType.getSize()); super(inventoryType.getSize());
this.id = generateId(); this.id = generateId();
@ -89,42 +76,11 @@ public non-sealed class Inventory extends AbstractInventory implements Viewable
update(); update();
} }
/** @Override
* 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() { public byte getWindowId() {
return id; return id;
} }
/**
* 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)}. * This will not open the inventory for {@code player}, use {@link Player#openInventory(Inventory)}.
* *
@ -133,9 +89,12 @@ public non-sealed class Inventory extends AbstractInventory implements Viewable
*/ */
@Override @Override
public boolean addViewer(@NotNull Player player) { public boolean addViewer(@NotNull Player player) {
final boolean result = this.viewers.add(player); if (!this.viewers.add(player)) return false;
// Also send the open window packet
player.sendPacket(new OpenWindowPacket(id, inventoryType.getWindowType(), title));
update(player); update(player);
return result; return true;
} }
/** /**
@ -146,9 +105,10 @@ public non-sealed class Inventory extends AbstractInventory implements Viewable
*/ */
@Override @Override
public boolean removeViewer(@NotNull Player player) { public boolean removeViewer(@NotNull Player player) {
final boolean result = this.viewers.remove(player); if (!super.removeViewer(player)) return false;
this.clickProcessor.clearCache(player); this.clickProcessor.clearCache(player);
return result; return true;
} }
/** /**
@ -173,16 +133,6 @@ public non-sealed class Inventory extends AbstractInventory implements Viewable
player.getInventory().setCursorItem(cursorItem); player.getInventory().setCursorItem(cursorItem);
} }
@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()), player.getInventory().getCursorItem());
}
/** /**
* Sends a window property to all viewers. * Sends a window property to all viewers.
* *
@ -200,9 +150,9 @@ public non-sealed class Inventory extends AbstractInventory implements Viewable
final ItemStack cursor = playerInventory.getCursorItem(); final ItemStack cursor = playerInventory.getCursorItem();
final boolean isInWindow = isClickInWindow(slot); final boolean isInWindow = isClickInWindow(slot);
final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset); final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset);
final AbstractInventory clickedInventory = isInWindow ? this : playerInventory;
final ItemStack clicked = isInWindow ? getItemStack(slot) : playerInventory.getItemStack(clickSlot); final ItemStack clicked = isInWindow ? getItemStack(slot) : playerInventory.getItemStack(clickSlot);
final InventoryClickResult clickResult = clickProcessor.leftClick(player, final InventoryClickResult clickResult = clickProcessor.leftClick(player, clickedInventory, clickSlot, clicked, cursor);
isInWindow ? this : playerInventory, clickSlot, clicked, cursor);
if (clickResult.isCancel()) { if (clickResult.isCancel()) {
updateAll(player); updateAll(player);
return false; return false;
@ -213,7 +163,7 @@ public non-sealed class Inventory extends AbstractInventory implements Viewable
playerInventory.setItemStack(clickSlot, clickResult.getClicked()); playerInventory.setItemStack(clickSlot, clickResult.getClicked());
} }
playerInventory.setCursorItem(clickResult.getCursor()); playerInventory.setCursorItem(clickResult.getCursor());
callClickEvent(player, isInWindow ? this : null, slot, ClickType.LEFT_CLICK, clicked, cursor); callClickEvent(player, clickedInventory, slot, ClickType.LEFT_CLICK, clicked, cursor);
return true; return true;
} }
@ -224,8 +174,8 @@ public non-sealed class Inventory extends AbstractInventory implements Viewable
final boolean isInWindow = isClickInWindow(slot); final boolean isInWindow = isClickInWindow(slot);
final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset); final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset);
final ItemStack clicked = isInWindow ? getItemStack(slot) : playerInventory.getItemStack(clickSlot); final ItemStack clicked = isInWindow ? getItemStack(slot) : playerInventory.getItemStack(clickSlot);
final InventoryClickResult clickResult = clickProcessor.rightClick(player, final AbstractInventory clickedInventory = isInWindow ? this : playerInventory;
isInWindow ? this : playerInventory, clickSlot, clicked, cursor); final InventoryClickResult clickResult = clickProcessor.rightClick(player, clickedInventory, clickSlot, clicked, cursor);
if (clickResult.isCancel()) { if (clickResult.isCancel()) {
updateAll(player); updateAll(player);
return false; return false;
@ -236,7 +186,7 @@ public non-sealed class Inventory extends AbstractInventory implements Viewable
playerInventory.setItemStack(clickSlot, clickResult.getClicked()); playerInventory.setItemStack(clickSlot, clickResult.getClicked());
} }
playerInventory.setCursorItem(clickResult.getCursor()); playerInventory.setCursorItem(clickResult.getCursor());
callClickEvent(player, isInWindow ? this : null, slot, ClickType.RIGHT_CLICK, clicked, cursor); callClickEvent(player, clickedInventory, slot, ClickType.RIGHT_CLICK, clicked, cursor);
return true; return true;
} }
@ -274,8 +224,8 @@ public non-sealed class Inventory extends AbstractInventory implements Viewable
final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset); final int clickSlot = isInWindow ? slot : PlayerInventoryUtils.convertSlot(slot, offset);
final ItemStack clicked = isInWindow ? getItemStack(slot) : playerInventory.getItemStack(clickSlot); final ItemStack clicked = isInWindow ? getItemStack(slot) : playerInventory.getItemStack(clickSlot);
final ItemStack heldItem = playerInventory.getItemStack(convertedKey); final ItemStack heldItem = playerInventory.getItemStack(convertedKey);
final InventoryClickResult clickResult = clickProcessor.changeHeld(player, final AbstractInventory clickedInventory = isInWindow ? this : playerInventory;
isInWindow ? this : playerInventory, clickSlot, convertedKey, clicked, heldItem); final InventoryClickResult clickResult = clickProcessor.changeHeld(player, clickedInventory, clickSlot, convertedKey, clicked, heldItem);
if (clickResult.isCancel()) { if (clickResult.isCancel()) {
updateAll(player); updateAll(player);
return false; return false;
@ -286,7 +236,7 @@ public non-sealed class Inventory extends AbstractInventory implements Viewable
playerInventory.setItemStack(clickSlot, clickResult.getClicked()); playerInventory.setItemStack(clickSlot, clickResult.getClicked());
} }
playerInventory.setItemStack(convertedKey, clickResult.getCursor()); playerInventory.setItemStack(convertedKey, clickResult.getCursor());
callClickEvent(player, isInWindow ? this : null, slot, ClickType.CHANGE_HELD, clicked, playerInventory.getCursorItem()); callClickEvent(player, clickedInventory, slot, ClickType.CHANGE_HELD, clicked, playerInventory.getCursorItem());
return true; return true;
} }

View File

@ -76,7 +76,7 @@ public sealed interface InventoryClickHandler permits AbstractInventory {
*/ */
boolean doubleClick(@NotNull Player player, int slot); boolean doubleClick(@NotNull Player player, int slot);
default void callClickEvent(@NotNull Player player, Inventory inventory, int slot, default void callClickEvent(@NotNull Player player, @NotNull AbstractInventory inventory, int slot,
@NotNull ClickType clickType, @NotNull ItemStack clicked, @NotNull ItemStack cursor) { @NotNull ClickType clickType, @NotNull ItemStack clicked, @NotNull ItemStack cursor) {
EventDispatcher.call(new InventoryClickEvent(inventory, player, slot, clickType, clicked, cursor)); EventDispatcher.call(new InventoryClickEvent(inventory, player, slot, clickType, clicked, cursor));
} }

View File

@ -1,7 +1,6 @@
package net.minestom.server.inventory; package net.minestom.server.inventory;
import net.minestom.server.entity.EquipmentSlot; import net.minestom.server.entity.EquipmentSlot;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.item.EntityEquipEvent; import net.minestom.server.event.item.EntityEquipEvent;
@ -12,6 +11,7 @@ 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.WindowItemsPacket;
import net.minestom.server.utils.validate.Check; import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
@ -20,24 +20,23 @@ import static net.minestom.server.utils.inventory.PlayerInventoryUtils.*;
/** /**
* Represents the inventory of a {@link Player}, retrieved with {@link Player#getInventory()}. * Represents the inventory of a {@link Player}, retrieved with {@link Player#getInventory()}.
*/ */
public non-sealed class PlayerInventory extends AbstractInventory implements EquipmentHandler { public non-sealed class PlayerInventory extends AbstractInventory {
public static final int INVENTORY_SIZE = 46; public static final int INVENTORY_SIZE = 46;
public static final int INNER_INVENTORY_SIZE = 36; public static final int INNER_INVENTORY_SIZE = 36;
protected final Player player;
private ItemStack cursorItem = ItemStack.AIR; private ItemStack cursorItem = ItemStack.AIR;
public PlayerInventory(@NotNull Player player) { public PlayerInventory() {
super(INVENTORY_SIZE); super(INVENTORY_SIZE);
this.player = player;
} }
@Override @Override
public synchronized void clear() { public synchronized void clear() {
cursorItem = ItemStack.AIR; cursorItem = ItemStack.AIR;
super.clear(); super.clear();
// Update equipments // Update equipments
this.player.sendPacketToViewersAndSelf(player.getEquipmentsPacket()); viewers.forEach(viewer -> viewer.sendPacketToViewersAndSelf(viewer.getEquipmentsPacket()));
} }
@Override @Override
@ -45,35 +44,45 @@ public non-sealed class PlayerInventory extends AbstractInventory implements Equ
return INNER_INVENTORY_SIZE; return INNER_INVENTORY_SIZE;
} }
private int getSlotId(@NotNull EquipmentSlot slot) { @Override
public byte getWindowId() {
return 0;
}
private int getSlotId(@NotNull EquipmentSlot slot, byte heldSlot) {
return switch (slot) { return switch (slot) {
case MAIN_HAND -> player.getHeldSlot(); case MAIN_HAND -> heldSlot;
case OFF_HAND -> OFFHAND_SLOT; case OFF_HAND -> OFFHAND_SLOT;
default -> slot.armorSlot(); default -> slot.armorSlot();
}; };
} }
@Override private @Nullable EquipmentSlot getEquipmentSlot(int slot, byte heldSlot) {
public @NotNull ItemStack getEquipment(@NotNull EquipmentSlot slot) { return switch (slot) {
if (slot == EquipmentSlot.BODY) return ItemStack.AIR; case OFFHAND_SLOT -> EquipmentSlot.OFF_HAND;
return getItemStack(getSlotId(slot)); case HELMET_SLOT -> EquipmentSlot.HELMET;
case CHESTPLATE_SLOT -> EquipmentSlot.CHESTPLATE;
case LEGGINGS_SLOT -> EquipmentSlot.LEGGINGS;
case BOOTS_SLOT -> EquipmentSlot.BOOTS;
default -> slot == heldSlot ? EquipmentSlot.MAIN_HAND : null;
};
} }
@Override public @NotNull ItemStack getEquipment(@NotNull EquipmentSlot slot, byte heldSlot) {
public void setEquipment(@NotNull EquipmentSlot slot, @NotNull ItemStack itemStack) { if (slot == EquipmentSlot.BODY) return ItemStack.AIR;
return getItemStack(getSlotId(slot, heldSlot));
}
public void setEquipment(@NotNull EquipmentSlot slot, byte heldSlot, @NotNull ItemStack itemStack) {
if (slot == EquipmentSlot.BODY) if (slot == EquipmentSlot.BODY)
Check.fail("PlayerInventory does not support body equipment"); Check.fail("PlayerInventory does not support body equipment");
safeItemInsert(getSlotId(slot), itemStack); setItemStack(getSlotId(slot, heldSlot), itemStack);
} }
/**
* Refreshes the player inventory by sending a {@link WindowItemsPacket} containing all.
* the inventory items
*/
@Override @Override
public void update() { public void update(@NotNull Player player) {
this.player.sendPacket(createWindowItemsPacket()); player.sendPacket(createWindowItemsPacket());
} }
/** /**
@ -93,49 +102,42 @@ public non-sealed class PlayerInventory extends AbstractInventory implements Equ
public void setCursorItem(@NotNull ItemStack cursorItem) { public void setCursorItem(@NotNull ItemStack cursorItem) {
if (this.cursorItem.equals(cursorItem)) return; if (this.cursorItem.equals(cursorItem)) return;
this.cursorItem = cursorItem; this.cursorItem = cursorItem;
final SetSlotPacket setSlotPacket = SetSlotPacket.createCursorPacket(cursorItem); sendPacketToViewers(SetSlotPacket.createCursorPacket(cursorItem));
this.player.sendPacket(setSlotPacket);
} }
@Override @Override
protected void UNSAFE_itemInsert(int slot, @NotNull ItemStack itemStack, boolean sendPacket) { protected void UNSAFE_itemInsert(int slot, @NotNull ItemStack item, @NotNull ItemStack previous, boolean sendPacket) {
final EquipmentSlot equipmentSlot = switch (slot) { for (Player player : getViewers()) {
case HELMET_SLOT -> EquipmentSlot.HELMET; final EquipmentSlot equipmentSlot = getEquipmentSlot(slot, player.getHeldSlot());
case CHESTPLATE_SLOT -> EquipmentSlot.CHESTPLATE; if (equipmentSlot == null) continue;
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) {
EntityEquipEvent entityEquipEvent = new EntityEquipEvent(player, itemStack, equipmentSlot);
EventDispatcher.call(entityEquipEvent);
itemStack = entityEquipEvent.getEquippedItem();
this.player.updateEquipmentAttributes(this.itemStacks[slot], itemStack, equipmentSlot);
}
this.itemStacks[slot] = itemStack;
if (sendPacket) { EntityEquipEvent entityEquipEvent = new EntityEquipEvent(player, item, equipmentSlot);
// Sync equipment EventDispatcher.call(entityEquipEvent);
if (equipmentSlot != null) this.player.syncEquipment(equipmentSlot); item = entityEquipEvent.getEquippedItem();
// Refresh slot
sendSlotRefresh((short) convertToPacketSlot(slot), itemStack);
} }
super.UNSAFE_itemInsert(slot, item, previous, sendPacket);
} }
/** @Override
* Refreshes an inventory slot. public void sendSlotRefresh(int slot, @NotNull ItemStack item, @NotNull ItemStack previous) {
* SetSlotPacket defaultPacket = new SetSlotPacket(getWindowId(), 0, (byte) slot, item);
* @param slot the packet slot,
* see {@link net.minestom.server.utils.inventory.PlayerInventoryUtils#convertToPacketSlot(int)} for (Player player : getViewers()) {
* @param itemStack the item stack in the slot // Equipment handling
*/ final EquipmentSlot equipmentSlot = getEquipmentSlot(slot, player.getHeldSlot());
protected void sendSlotRefresh(short slot, ItemStack itemStack) { if (equipmentSlot != null) {
var openInventory = player.getOpenInventory(); player.updateEquipmentAttributes(previous, item, equipmentSlot);
if (openInventory != null && slot >= OFFSET && slot < OFFSET + INNER_INVENTORY_SIZE) { player.syncEquipment(equipmentSlot);
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)); // Slot handling
AbstractInventory openInventory = player.getOpenInventory();
if (openInventory != null && slot >= OFFSET && slot < OFFSET + INNER_INVENTORY_SIZE) {
player.sendPacket(new SetSlotPacket(openInventory.getWindowId(), 0, (short) (slot + openInventory.getSize() - OFFSET), item));
} else if (openInventory == null || slot == OFFHAND_SLOT) {
player.sendPacket(defaultPacket);
}
} }
} }

View File

@ -14,7 +14,7 @@ public interface TransactionOption<T> {
* The remaining, can be air. * The remaining, can be air.
*/ */
TransactionOption<ItemStack> ALL = (inventory, result, itemChangesMap) -> { TransactionOption<ItemStack> ALL = (inventory, result, itemChangesMap) -> {
itemChangesMap.forEach(inventory::safeItemInsert); itemChangesMap.forEach(inventory::setItemStack);
return result; return result;
}; };
@ -26,7 +26,7 @@ public interface TransactionOption<T> {
TransactionOption<Boolean> ALL_OR_NOTHING = (inventory, result, itemChangesMap) -> { TransactionOption<Boolean> ALL_OR_NOTHING = (inventory, result, itemChangesMap) -> {
if (result.isAir()) { if (result.isAir()) {
// Item can be fully placed inside the inventory, do so // Item can be fully placed inside the inventory, do so
itemChangesMap.forEach(inventory::safeItemInsert); itemChangesMap.forEach(inventory::setItemStack);
return true; return true;
} else { } else {
// Inventory cannot accept the item fully // Inventory cannot accept the item fully

View File

@ -454,10 +454,9 @@ public final class InventoryClickProcessor {
return clickResult; return clickResult;
} }
private void callClickEvent(@NotNull Player player, @Nullable AbstractInventory inventory, int slot, private void callClickEvent(@NotNull Player player, @NotNull AbstractInventory inventory, int slot,
@NotNull ClickType clickType, @NotNull ItemStack clicked, @NotNull ItemStack cursor) { @NotNull ClickType clickType, @NotNull ItemStack clicked, @NotNull ItemStack cursor) {
final Inventory eventInventory = inventory instanceof Inventory ? (Inventory) inventory : null; EventDispatcher.call(new InventoryClickEvent(inventory, player, slot, clickType, clicked, cursor));
EventDispatcher.call(new InventoryClickEvent(eventInventory, player, slot, clickType, clicked, cursor));
} }
public void clearCache(@NotNull Player player) { public void clearCache(@NotNull Player player) {

View File

@ -37,7 +37,6 @@ public class BlockPlacementListener {
private static final BlockManager BLOCK_MANAGER = MinecraftServer.getBlockManager(); private static final BlockManager BLOCK_MANAGER = MinecraftServer.getBlockManager();
public static void listener(ClientPlayerBlockPlacementPacket packet, Player player) { public static void listener(ClientPlayerBlockPlacementPacket packet, Player player) {
final PlayerInventory playerInventory = player.getInventory();
final PlayerHand hand = packet.hand(); final PlayerHand hand = packet.hand();
final BlockFace blockFace = packet.blockFace(); final BlockFace blockFace = packet.blockFace();
Point blockPosition = packet.blockPosition(); Point blockPosition = packet.blockPosition();
@ -179,10 +178,10 @@ public class BlockPlacementListener {
if (playerBlockPlaceEvent.doesConsumeBlock()) { if (playerBlockPlaceEvent.doesConsumeBlock()) {
// Consume the block in the player's hand // Consume the block in the player's hand
final ItemStack newUsedItem = usedItem.consume(1); final ItemStack newUsedItem = usedItem.consume(1);
playerInventory.setItemInHand(hand, newUsedItem); player.setItemInHand(hand, newUsedItem);
} else { } else {
// Prevent invisible item on client // Prevent invisible item on client
playerInventory.update(); player.getInventory().update();
} }
} }

View File

@ -18,7 +18,6 @@ import net.minestom.server.event.player.PlayerSwapItemEvent;
import net.minestom.server.instance.Instance; import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockFace; import net.minestom.server.instance.block.BlockFace;
import net.minestom.server.inventory.PlayerInventory;
import net.minestom.server.item.ItemComponent; import net.minestom.server.item.ItemComponent;
import net.minestom.server.item.ItemStack; import net.minestom.server.item.ItemStack;
import net.minestom.server.item.component.BlockPredicates; import net.minestom.server.item.component.BlockPredicates;
@ -128,12 +127,12 @@ public final class PlayerDiggingListener {
} }
private static void dropStack(Player player) { private static void dropStack(Player player) {
final ItemStack droppedItemStack = player.getInventory().getItemInMainHand(); final ItemStack droppedItemStack = player.getItemInMainHand();
dropItem(player, droppedItemStack, ItemStack.AIR); dropItem(player, droppedItemStack, ItemStack.AIR);
} }
private static void dropSingle(Player player) { private static void dropSingle(Player player) {
final ItemStack handItem = player.getInventory().getItemInMainHand(); final ItemStack handItem = player.getItemInMainHand();
final int handAmount = handItem.amount(); final int handAmount = handItem.amount();
if (handAmount <= 1) { if (handAmount <= 1) {
// Drop the whole item without copy // Drop the whole item without copy
@ -162,13 +161,12 @@ public final class PlayerDiggingListener {
} }
private static void swapItemHand(Player player) { private static void swapItemHand(Player player) {
final PlayerInventory inventory = player.getInventory(); final ItemStack mainHand = player.getItemInMainHand();
final ItemStack mainHand = inventory.getItemInMainHand(); final ItemStack offHand = player.getItemInOffHand();
final ItemStack offHand = inventory.getItemInOffHand();
PlayerSwapItemEvent swapItemEvent = new PlayerSwapItemEvent(player, offHand, mainHand); PlayerSwapItemEvent swapItemEvent = new PlayerSwapItemEvent(player, offHand, mainHand);
EventDispatcher.callCancellable(swapItemEvent, () -> { EventDispatcher.callCancellable(swapItemEvent, () -> {
inventory.setItemInMainHand(swapItemEvent.getMainHandItem()); player.setItemInMainHand(swapItemEvent.getMainHandItem());
inventory.setItemInOffHand(swapItemEvent.getOffHandItem()); player.setItemInOffHand(swapItemEvent.getOffHandItem());
}); });
} }
@ -192,11 +190,10 @@ public final class PlayerDiggingListener {
private static void dropItem(@NotNull Player player, private static void dropItem(@NotNull Player player,
@NotNull ItemStack droppedItem, @NotNull ItemStack handItem) { @NotNull ItemStack droppedItem, @NotNull ItemStack handItem) {
final PlayerInventory playerInventory = player.getInventory();
if (player.dropItem(droppedItem)) { if (player.dropItem(droppedItem)) {
playerInventory.setItemInMainHand(handItem); player.setItemInMainHand(handItem);
} else { } else {
playerInventory.update(); player.getInventory().update();
} }
} }

View File

@ -20,7 +20,7 @@ public class UseItemListener {
public static void useItemListener(ClientUseItemPacket packet, Player player) { public static void useItemListener(ClientUseItemPacket packet, Player player) {
final PlayerHand hand = packet.hand(); final PlayerHand hand = packet.hand();
final ItemStack itemStack = player.getInventory().getItemInHand(hand); final ItemStack itemStack = player.getItemInHand(hand);
final Material material = itemStack.material(); final Material material = itemStack.material();
boolean usingMainHand = player.getItemUseHand() == Player.Hand.MAIN && hand == Player.Hand.OFF; boolean usingMainHand = player.getItemUseHand() == Player.Hand.MAIN && hand == Player.Hand.OFF;

View File

@ -27,7 +27,7 @@ public class PlayerCreativeSlotTest {
player.setGameMode(GameMode.CREATIVE); player.setGameMode(GameMode.CREATIVE);
player.addPacketToQueue(new ClientCreativeInventoryActionPacket((short) PlayerInventoryUtils.OFFHAND_SLOT, ItemStack.of(Material.STICK))); player.addPacketToQueue(new ClientCreativeInventoryActionPacket((short) PlayerInventoryUtils.OFFHAND_SLOT, ItemStack.of(Material.STICK)));
player.interpretPacketQueue(); player.interpretPacketQueue();
assertEquals(Material.STICK, player.getInventory().getItemInOffHand().material()); assertEquals(Material.STICK, player.getItemInOffHand().material());
} }
@Test @Test

View File

@ -118,19 +118,19 @@ public class PlayerInventoryIntegrationTest {
var equipmentTracker = connectionViewer.trackIncoming(EntityEquipmentPacket.class); var equipmentTracker = connectionViewer.trackIncoming(EntityEquipmentPacket.class);
// Setting to an item should send EntityEquipmentPacket to viewer // 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 -> { equipmentTracker.assertSingle(entityEquipmentPacket -> {
assertEquals(MAGIC_STACK, entityEquipmentPacket.equipments().get(EquipmentSlot.HELMET)); assertEquals(MAGIC_STACK, entityEquipmentPacket.equipments().get(EquipmentSlot.HELMET));
}); });
// Setting to the same item shouldn't send packet // Setting to the same item shouldn't send packet
equipmentTracker = connectionViewer.trackIncoming(EntityEquipmentPacket.class); equipmentTracker = connectionViewer.trackIncoming(EntityEquipmentPacket.class);
playerArmored.getInventory().setEquipment(EquipmentSlot.HELMET, MAGIC_STACK); playerArmored.setEquipment(EquipmentSlot.HELMET, MAGIC_STACK);
equipmentTracker.assertEmpty(); equipmentTracker.assertEmpty();
// Setting to air should send packet // Setting to air should send packet
equipmentTracker = connectionViewer.trackIncoming(EntityEquipmentPacket.class); equipmentTracker = connectionViewer.trackIncoming(EntityEquipmentPacket.class);
playerArmored.getInventory().setEquipment(EquipmentSlot.HELMET, ItemStack.AIR); playerArmored.setEquipment(EquipmentSlot.HELMET, ItemStack.AIR);
equipmentTracker.assertSingle(entityEquipmentPacket -> { equipmentTracker.assertSingle(entityEquipmentPacket -> {
assertEquals(ItemStack.AIR, entityEquipmentPacket.equipments().get(EquipmentSlot.HELMET)); assertEquals(ItemStack.AIR, entityEquipmentPacket.equipments().get(EquipmentSlot.HELMET));
}); });

View File

@ -3,6 +3,7 @@ package net.minestom.server.inventory.click.integration;
import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.event.inventory.InventoryPreClickEvent; import net.minestom.server.event.inventory.InventoryPreClickEvent;
import net.minestom.server.inventory.AbstractInventory;
import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.Inventory;
import net.minestom.server.inventory.InventoryType; import net.minestom.server.inventory.InventoryType;
import net.minestom.server.inventory.click.ClickType; import net.minestom.server.inventory.click.ClickType;
@ -82,7 +83,7 @@ public class HeldClickIntegrationTest {
}); });
heldClick(player, 3, 40); heldClick(player, 3, 40);
assertEquals(ItemStack.AIR, inventory.getItemStack(3)); assertEquals(ItemStack.AIR, inventory.getItemStack(3));
assertEquals(ItemStack.of(Material.EGG), inventory.getItemInOffHand()); assertEquals(ItemStack.of(Material.EGG), player.getItemInOffHand());
} }
// Cancel event // Cancel event
{ {
@ -151,7 +152,7 @@ public class HeldClickIntegrationTest {
}); });
heldClickOpenInventory(player, 3, 40); heldClickOpenInventory(player, 3, 40);
assertEquals(ItemStack.AIR, inventory.getItemStack(3)); assertEquals(ItemStack.AIR, inventory.getItemStack(3));
assertEquals(ItemStack.of(Material.EGG), playerInv.getItemInOffHand()); assertEquals(ItemStack.of(Material.EGG), player.getItemInOffHand());
} }
// Cancel event // Cancel event
{ {
@ -171,7 +172,7 @@ public class HeldClickIntegrationTest {
_heldClick(player.getOpenInventory(), false, player, slot, target); _heldClick(player.getOpenInventory(), false, player, slot, target);
} }
private void _heldClick(Inventory openInventory, boolean clickOpenInventory, Player player, int slot, int target) { private void _heldClick(AbstractInventory openInventory, boolean clickOpenInventory, Player player, int slot, int target) {
final byte windowId = openInventory != null ? openInventory.getWindowId() : 0; final byte windowId = openInventory != null ? openInventory.getWindowId() : 0;
if (clickOpenInventory) { if (clickOpenInventory) {
assert openInventory != null; assert openInventory != null;

View File

@ -4,6 +4,7 @@ package net.minestom.server.inventory.click.integration;
import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.event.inventory.InventoryPreClickEvent; import net.minestom.server.event.inventory.InventoryPreClickEvent;
import net.minestom.server.inventory.AbstractInventory;
import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.Inventory;
import net.minestom.server.inventory.InventoryType; import net.minestom.server.inventory.InventoryType;
import net.minestom.server.inventory.click.ClickType; import net.minestom.server.inventory.click.ClickType;
@ -154,7 +155,7 @@ public class LeftClickIntegrationTest {
_leftClick(player.getOpenInventory(), false, player, slot); _leftClick(player.getOpenInventory(), false, player, slot);
} }
private void _leftClick(Inventory openInventory, boolean clickOpenInventory, Player player, int slot) { private void _leftClick(AbstractInventory openInventory, boolean clickOpenInventory, Player player, int slot) {
final byte windowId = openInventory != null ? openInventory.getWindowId() : 0; final byte windowId = openInventory != null ? openInventory.getWindowId() : 0;
if (clickOpenInventory) { if (clickOpenInventory) {
assert openInventory != null; assert openInventory != null;

View File

@ -3,6 +3,7 @@ package net.minestom.server.inventory.click.integration;
import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.event.inventory.InventoryPreClickEvent; import net.minestom.server.event.inventory.InventoryPreClickEvent;
import net.minestom.server.inventory.AbstractInventory;
import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.Inventory;
import net.minestom.server.inventory.InventoryType; import net.minestom.server.inventory.InventoryType;
import net.minestom.server.inventory.click.ClickType; import net.minestom.server.inventory.click.ClickType;
@ -175,7 +176,7 @@ public class RightClickIntegrationTest {
_rightClick(player.getOpenInventory(), false, player, slot); _rightClick(player.getOpenInventory(), false, player, slot);
} }
private void _rightClick(Inventory openInventory, boolean clickOpenInventory, Player player, int slot) { private void _rightClick(AbstractInventory openInventory, boolean clickOpenInventory, Player player, int slot) {
final byte windowId = openInventory != null ? openInventory.getWindowId() : 0; final byte windowId = openInventory != null ? openInventory.getWindowId() : 0;
if (clickOpenInventory) { if (clickOpenInventory) {
assert openInventory != null; assert openInventory != null;