diff --git a/internal/paper1_21_11/src/main/java/com/lishid/openinv/internal/paper1_21_11/container/menu/OpenEnderChestMenu.java b/internal/paper1_21_11/src/main/java/com/lishid/openinv/internal/paper1_21_11/container/menu/OpenEnderChestMenu.java index dc45137e..163a24ea 100644 --- a/internal/paper1_21_11/src/main/java/com/lishid/openinv/internal/paper1_21_11/container/menu/OpenEnderChestMenu.java +++ b/internal/paper1_21_11/src/main/java/com/lishid/openinv/internal/paper1_21_11/container/menu/OpenEnderChestMenu.java @@ -59,14 +59,14 @@ public ItemStack quickMoveStack(Player player, int index) { } @Override - public void clicked(int i, int j, ClickType clickType, Player player) { + public void clicked(int slotIndex, int button, ClickType clickType, Player player) { if (viewOnly) { if (clickType == ClickType.QUICK_CRAFT) { sendAllDataToRemote(); } return; } - super.clicked(i, j, clickType, player); + super.clicked(slotIndex, button, clickType, player); } } diff --git a/internal/paper1_21_11/src/main/java/com/lishid/openinv/internal/paper1_21_11/container/menu/OpenInventoryMenu.java b/internal/paper1_21_11/src/main/java/com/lishid/openinv/internal/paper1_21_11/container/menu/OpenInventoryMenu.java index 3bc0f134..b2fc2d0e 100644 --- a/internal/paper1_21_11/src/main/java/com/lishid/openinv/internal/paper1_21_11/container/menu/OpenInventoryMenu.java +++ b/internal/paper1_21_11/src/main/java/com/lishid/openinv/internal/paper1_21_11/container/menu/OpenInventoryMenu.java @@ -26,14 +26,14 @@ public OpenInventoryMenu( } @Override - public void clicked(int i, int j, ClickType clickType, Player player) { + public void clicked(int slotIndex, int button, ClickType clickType, Player player) { if (viewOnly) { if (clickType == ClickType.QUICK_CRAFT) { sendAllDataToRemote(); } return; } - super.clicked(i, j, clickType, player); + super.clicked(slotIndex, button, clickType, player); } } diff --git a/internal/paper26_3/src/main/java/com/lishid/openinv/internal/paper26_3/container/BaseOpenInventory.java b/internal/paper26_3/src/main/java/com/lishid/openinv/internal/paper26_3/container/BaseOpenInventory.java index e7b11c94..c7451dba 100644 --- a/internal/paper26_3/src/main/java/com/lishid/openinv/internal/paper26_3/container/BaseOpenInventory.java +++ b/internal/paper26_3/src/main/java/com/lishid/openinv/internal/paper26_3/container/BaseOpenInventory.java @@ -187,7 +187,7 @@ public ServerPlayer getOwnerHandle() { @Override public org.bukkit.inventory.Inventory getBukkitInventory() { if (bukkitEntity == null) { - bukkitEntity = new OpenPlayerInventory(this); + bukkitEntity = new OpenPlayerInventory(factory, this); } return bukkitEntity; } diff --git a/internal/paper26_3/src/main/java/com/lishid/openinv/internal/paper26_3/container/bukkit/OpenPlayerInventory.java b/internal/paper26_3/src/main/java/com/lishid/openinv/internal/paper26_3/container/bukkit/OpenPlayerInventory.java index 9c957e92..16551fa4 100644 --- a/internal/paper26_3/src/main/java/com/lishid/openinv/internal/paper26_3/container/bukkit/OpenPlayerInventory.java +++ b/internal/paper26_3/src/main/java/com/lishid/openinv/internal/paper26_3/container/bukkit/OpenPlayerInventory.java @@ -1,12 +1,20 @@ package com.lishid.openinv.internal.paper26_3.container.bukkit; +import com.github.jikoo.openinv.internal.container.slot.InventoryFactory; import com.google.common.base.Preconditions; import com.lishid.openinv.internal.paper26_3.container.BaseOpenInventory; import net.minecraft.core.NonNullList; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import org.bukkit.Material; import org.bukkit.craftbukkit.inventory.CraftInventory; import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.craftbukkit.inventory.CraftItemType; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryType; import org.bukkit.inventory.ItemStack; @@ -14,13 +22,22 @@ import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.function.BiPredicate; @NullMarked public class OpenPlayerInventory extends CraftInventory implements PlayerInventory { - public OpenPlayerInventory(BaseOpenInventory inventory) { + protected final InventoryFactory factory; + + public OpenPlayerInventory( + InventoryFactory factory, + BaseOpenInventory inventory + ) { super(inventory); + this.factory = factory; } @Override @@ -29,16 +46,34 @@ public BaseOpenInventory getInventory() { } @Override - public ItemStack[] getContents() { - return asCraftMirror(getInventory().getOwnerHandle().getInventory().getContents()); + public @Nullable ItemStack[] getContents() { + return asCraftMirror(getInventory().getContents()); } @Override public void setContents(@Nullable ItemStack[] items) { - Inventory internal = getInventory().getOwnerHandle().getInventory(); + Container internal = getInventory(); int size = internal.getContainerSize(); Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size); + if (items.length < size) { + // If size matches normal player inventory size, redirect to it. + Container ownerInternal = getInventory().getOwnerHandle().getInventory(); + if (items.length == ownerInternal.getContainerSize()) { + internal = ownerInternal; + size = items.length; + } + } else { + // Otherwise, ensure that the display-only slots are not in use. + for (int index = 0; index < size; ++index) { + ItemStack itemStack = items[index]; + if (itemStack == null || itemStack.isEmpty()) { + continue; + } + validateSet(index); + } + } + for (int index = 0; index < size; ++index) { if (index < items.length) { internal.setItem(index, CraftItemStack.asNMSCopy(items[index])); @@ -48,8 +83,21 @@ public void setContents(@Nullable ItemStack[] items) { } } + protected void validateSet(int index) { + InventoryType.SlotType slotType = getInventory().getSlotType(index); + if (slotType == InventoryType.SlotType.OUTSIDE || slotType == InventoryType.SlotType.RESULT) { + throw new IllegalArgumentException("A plugin tried to put an item in a display slot. That would delete it!"); + } + } + + @Override + public void setItem(int index, @Nullable ItemStack item) { + validateSet(index); + super.setItem(index, item); + } + @Override - public ItemStack[] getStorageContents() { + public @Nullable ItemStack[] getStorageContents() { return asCraftMirror(getInventory().getOwnerHandle().getInventory().getNonEquipmentItems()); } @@ -63,6 +111,294 @@ public void setStorageContents(@Nullable ItemStack[] items) throws IllegalArgume } } + @SuppressWarnings("NonApiType") + @Override + public HashMap all(Material material) { + Preconditions.checkNotNull(material, "Material cannot be null"); + Item item = CraftItemType.bukkitToMinecraft(material); + Preconditions.checkNotNull(item, "Material must be an item"); + + BaseOpenInventory inventory = getInventory(); + int size = inventory.getOwnerHandle().getInventory().getNonEquipmentItems().size(); + + HashMap slots = new HashMap<>(); + for (int index = 0; index < size; ++index) { + net.minecraft.world.item.ItemStack itemStack = inventory.getItem(index); + if (!itemStack.isEmpty() && item.equals(itemStack.getItem())) { + slots.put(index, factory.asBukkitMirror(itemStack)); + } + } + + return slots; + } + + @SuppressWarnings("NonApiType") + @Override + public HashMap all(@Nullable ItemStack item) { + HashMap slots = new HashMap<>(); + + if (item == null) { + return slots; + } + + net.minecraft.world.item.ItemStack itemStack = CraftItemStack.unwrap(item); + BaseOpenInventory inventory = getInventory(); + int size = inventory.getOwnerHandle().getInventory().getNonEquipmentItems().size(); + + for (int index = 0; index < size; ++index) { + net.minecraft.world.item.ItemStack slotItemStack = inventory.getItem(index); + if (ItemMatcher.EXACT.predicate.test(itemStack, slotItemStack)) { + slots.put(index, factory.asBukkitMirror(slotItemStack)); + } + } + + return slots; + } + + @Override + public int first(Material material) { + Item item = CraftItemType.bukkitToMinecraft(material); + Preconditions.checkNotNull(item, "Material must be an item"); + net.minecraft.world.item.ItemStack itemStack = new net.minecraft.world.item.ItemStack(item); + int size = getInventory().getOwnerHandle().getInventory().getNonEquipmentItems().size(); + return first(itemStack, ItemMatcher.TYPE, 0, size); + } + + @Override + public int first(ItemStack item) { + net.minecraft.world.item.ItemStack itemStack = CraftItemStack.unwrap(item); + int size = getInventory().getOwnerHandle().getInventory().getNonEquipmentItems().size(); + return first(itemStack, ItemMatcher.EXACT, 0, size); + } + + @Override + public int firstEmpty() { + BaseOpenInventory inventory = getInventory(); + int size = inventory.getOwnerHandle().getInventory().getNonEquipmentItems().size(); + return first(net.minecraft.world.item.ItemStack.EMPTY, ItemMatcher.EMPTY, 0, size); + } + + private int first(net.minecraft.world.item.ItemStack itemStack, ItemMatcher matcher, int startIndex, int endIndex) { + for (int index = startIndex; index < endIndex; ++index) { + net.minecraft.world.item.ItemStack inventoryStack = inventory.getItem(index); + if (matcher.predicate.test(itemStack, inventoryStack)) { + return index; + } + } + return -1; + } + + @SuppressWarnings("ImmutableEnumChecker") // Ignore Google's style preference for predicate-like enums + private enum ItemMatcher { + EMPTY((ignored, b) -> b.isEmpty()), + TYPE(net.minecraft.world.item.ItemStack::isSameItem), + SIMILAR(net.minecraft.world.item.ItemStack::isSameItemSameComponents), + EXACT((a, b) -> net.minecraft.world.item.ItemStack.isSameItemSameComponents(a, b) && a.count() == b.count()); + + private final BiPredicate predicate; + + ItemMatcher(BiPredicate predicate) { + this.predicate = predicate; + } + } + + @SuppressWarnings("NonApiType") + @Override + public HashMap addItem(ItemStack... items) { + Preconditions.checkNotNull(items, "items cannot be null"); + HashMap leftover = new HashMap<>(); + + BaseOpenInventory inventory = getInventory(); + int storageSize = inventory.getOwnerHandle().getInventory().getNonEquipmentItems().size(); + + for (int index = 0; index < items.length; ++index) { + addItem(items, index, storageSize, leftover); + } + + return leftover; + } + + private void addItem( + ItemStack[] items, + int leftoverIndex, + int storageSize, + Map leftover + ) { + ItemStack bukkitStack = items[leftoverIndex]; + Preconditions.checkNotNull(bukkitStack, "ItemStack cannot be null"); + if (bukkitStack.isEmpty()) { + return; + } + net.minecraft.world.item.ItemStack nmsStack = CraftItemStack.unwrap(bukkitStack); + Preconditions.checkNotNull(nmsStack, "ItemStack cannot be empty"); + + int maxStackSize = inventory.getMaxStackSize(nmsStack); + int stackCount = nmsStack.count(); + + // Try to fill existing stacks first. Waste of time for items that don't stack. + if (maxStackSize > 1) { + stackCount = addPartial(storageSize, nmsStack, maxStackSize, stackCount); + if (nmsStack.count() < 1) { + // All done adding, next item. + return; + } + } + + // Add to empty slots. + stackCount = addEmpty(storageSize, stackCount, maxStackSize, nmsStack); + + if (stackCount > 0) { + bukkitStack.setAmount(stackCount); + leftover.put(leftoverIndex, bukkitStack); + } + } + + private int addPartial( + int storageSize, + net.minecraft.world.item.ItemStack nmsStack, + int maxStackSize, + int stackCount + ) { + for ( + int firstPartial = first(nmsStack, ItemMatcher.SIMILAR, 0, storageSize); + firstPartial < storageSize && firstPartial > -1; + firstPartial = first(nmsStack, ItemMatcher.SIMILAR, firstPartial + 1, storageSize) + ) { + net.minecraft.world.item.ItemStack existing = inventory.getItem(firstPartial); + int existingCount = existing.count(); + if (existingCount >= maxStackSize) { + continue; + } + + int fit = maxStackSize - existingCount; + + if (fit >= stackCount) { + nmsStack.setCount(0); + existing.setCount(existingCount + stackCount); + return 0; + } + + stackCount -= fit; + nmsStack.setCount(stackCount); + existing.setCount(maxStackSize); + } + return stackCount; + } + + private int addEmpty( + int storageSize, + int stackCount, + int maxStackSize, + net.minecraft.world.item.ItemStack nmsStack + ) { + for ( + int firstEmpty = firstEmpty(); + firstEmpty < storageSize && firstEmpty > -1; + firstEmpty = first(net.minecraft.world.item.ItemStack.EMPTY, ItemMatcher.EMPTY, firstEmpty, storageSize) + ) { + if (stackCount <= maxStackSize) { + inventory.setItem(firstEmpty, nmsStack.copy()); + return 0; + } + + var newStack = nmsStack.copy(); + stackCount -= maxStackSize; + nmsStack.setCount(stackCount); + newStack.setCount(maxStackSize); + inventory.setItem(firstEmpty, newStack); + } + + return stackCount; + } + + @SuppressWarnings("NonApiType") + @Override + public HashMap removeItem(ItemStack... items) { + Preconditions.checkNotNull(items, "items cannot be null"); + HashMap leftover = new HashMap<>(); + + BaseOpenInventory inventory = getInventory(); + int storageSize = inventory.getOwnerHandle().getInventory().getNonEquipmentItems().size(); + + for (int index = 0; index < items.length; ++index) { + removeItem(items[index], index, storageSize, leftover); + } + + return leftover; + } + + private void removeItem( + ItemStack bukkitStack, + int leftoverIndex, + int storageSize, + Map leftover + ) { + Preconditions.checkNotNull(bukkitStack, "ItemStack cannot be null"); + if (bukkitStack.isEmpty()) { + return; + } + net.minecraft.world.item.ItemStack nmsStack = CraftItemStack.unwrap(bukkitStack); + Preconditions.checkNotNull(nmsStack, "ItemStack cannot be empty"); + + int stackCount = nmsStack.count(); + + for ( + int first = first(nmsStack, ItemMatcher.SIMILAR, 0, storageSize); + first < storageSize && first > -1; + first = first(nmsStack, ItemMatcher.SIMILAR, first + 1, storageSize) + ) { + net.minecraft.world.item.ItemStack existing = inventory.getItem(first); + int existingCount = existing.count(); + if (existingCount > stackCount) { + existing.setCount(existingCount - stackCount); + bukkitStack.setAmount(0); + return; + } + + stackCount -= existingCount; + inventory.setItem(first, net.minecraft.world.item.ItemStack.EMPTY); + + if (stackCount <= 0) { + break; + } + } + + bukkitStack.setAmount(stackCount); + + if (stackCount > 0) { + leftover.put(leftoverIndex, bukkitStack); + } + } + + @Override + public void remove(Material material) { + Item item = CraftItemType.bukkitToMinecraft(material); + Preconditions.checkNotNull(item, "Material must be an item"); + + if (!item.equals(Items.AIR)) { + removeAll(new net.minecraft.world.item.ItemStack(item), ItemMatcher.SIMILAR); + } + } + + @Override + public void remove(ItemStack bukkitStack) { + net.minecraft.world.item.ItemStack nmsStack = CraftItemStack.unwrap(bukkitStack); + + if (!nmsStack.isEmpty()) { + removeAll(nmsStack, ItemMatcher.EXACT); + } + } + + private void removeAll(net.minecraft.world.item.ItemStack itemStack, ItemMatcher matcher) { + int storageSize = getInventory().getOwnerHandle().getInventory().getNonEquipmentItems().size(); + for (int index = 0; index < storageSize; ++index) { + net.minecraft.world.item.ItemStack content = inventory.getItem(index); + if (matcher.predicate.test(itemStack, content)) { + inventory.setItem(index, net.minecraft.world.item.ItemStack.EMPTY); + } + } + } + @Override public InventoryType getType() { return InventoryType.PLAYER; @@ -74,7 +410,7 @@ public Player getHolder() { } @Override - public ItemStack[] getArmorContents() { + public @Nullable ItemStack[] getArmorContents() { return asCraftMirror(getInventory().getOwnerHandle().getInventory().getArmorContents()); } @@ -91,7 +427,7 @@ public void setArmorContents(@Nullable ItemStack[] items) { } @Override - public ItemStack[] getExtraContents() { + public @Nullable ItemStack[] getExtraContents() { return asCraftMirror(List.of(getInventory().getOwnerHandle().getInventory().equipment.get(EquipmentSlot.OFFHAND))); } diff --git a/internal/paper26_3/src/main/java/com/lishid/openinv/internal/paper26_3/container/bukkit/OpenPlayerInventorySelf.java b/internal/paper26_3/src/main/java/com/lishid/openinv/internal/paper26_3/container/bukkit/OpenPlayerInventorySelf.java index 1d7eb3b4..70e05b95 100644 --- a/internal/paper26_3/src/main/java/com/lishid/openinv/internal/paper26_3/container/bukkit/OpenPlayerInventorySelf.java +++ b/internal/paper26_3/src/main/java/com/lishid/openinv/internal/paper26_3/container/bukkit/OpenPlayerInventorySelf.java @@ -1,28 +1,169 @@ package com.lishid.openinv.internal.paper26_3.container.bukkit; +import com.github.jikoo.openinv.internal.container.slot.InventoryFactory; +import com.google.common.base.Preconditions; import com.lishid.openinv.internal.paper26_3.container.BaseOpenInventory; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.inventory.Slot; +import org.bukkit.Material; +import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.inventory.ItemStack; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.HashMap; + @NullMarked public class OpenPlayerInventorySelf extends OpenPlayerInventory { private final int offset; - public OpenPlayerInventorySelf(BaseOpenInventory inventory, int offset) { - super(inventory); + public OpenPlayerInventorySelf( + InventoryFactory factory, + BaseOpenInventory inventory, + int offset + ) { + super(factory, inventory); this.offset = offset; } + @Override + public int getSize() { + return super.getSize() - offset; + } + @Override public @Nullable ItemStack getItem(int index) { return super.getItem(offset + index); } + @Override + public @Nullable ItemStack[] getStorageContents() { + return new ItemStack[0]; + } + + @Override + public void setStorageContents(@Nullable ItemStack[] items) throws IllegalArgumentException { + Preconditions.checkArgument(items.length == 0, "items.length must be <= 0"); + } + + @Override + public @Nullable ItemStack[] getContents() { + @Nullable ItemStack[] contents = new ItemStack[getSize()]; + for (int i = 0; i < contents.length; ++i) { + contents[i] = getItem(i); + } + return contents; + } + + @Override + public void setContents(@Nullable ItemStack[] items) { + int size = getSize(); + Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size); + + for (int index = 0; index < size; ++index) { + ItemStack itemStack = items[index]; + if (itemStack == null || itemStack.isEmpty()) { + continue; + } + validateSet(index); + } + + for (int index = 0; index < size; ++index) { + if (index < items.length) { + inventory.setItem(offset + index, CraftItemStack.asNMSCopy(items[index])); + } else { + inventory.setItem(offset + index, net.minecraft.world.item.ItemStack.EMPTY); + } + } + } + @Override public void setItem(int index, @Nullable ItemStack item) { super.setItem(offset + index, item); } + @Override + public boolean contains(Material material) { + // Only checks the storage contents. + return false; + } + + @Override + public boolean contains(ItemStack item) { + return false; + } + + @Override + public boolean contains(Material material, int amount) { + return false; + } + + @Override + public boolean contains(ItemStack item, int amount) { + return false; + } + + @Override + public boolean containsAtLeast(ItemStack item, int amount) { + return false; + } + + @SuppressWarnings("NonApiType") + @Override + public HashMap all(Material material) { + // No storage contents, no results. + return new HashMap<>(); + } + + @SuppressWarnings("NonApiType") + @Override + public HashMap all(@Nullable ItemStack item) { + return new HashMap<>(); + } + + @Override + public int first(Material material) { + return -1; + } + + @Override + public int first(ItemStack item) { + return -1; + } + + @Override + public int firstEmpty() { + return -1; + } + + @SuppressWarnings("NonApiType") + @Override + public HashMap addItem(ItemStack... items) { + HashMap map = new HashMap<>(); + for (int index = 0; index < items.length; ++index) { + map.put(index, items[index]); + } + return map; + } + + @SuppressWarnings("NonApiType") + @Override + public HashMap removeItem(ItemStack... items) { + return addItem(items); + } + + @Override + public void remove(Material material) { + Preconditions.checkNotNull(material, "Material cannot be null"); + Preconditions.checkArgument(material.isItem(), "Material must be an item"); + } + + @Override + public void remove(ItemStack itemStack) { + // Sure did. + } + } diff --git a/internal/paper26_3/src/main/java/com/lishid/openinv/internal/paper26_3/container/menu/BaseOpenInventoryMenu.java b/internal/paper26_3/src/main/java/com/lishid/openinv/internal/paper26_3/container/menu/BaseOpenInventoryMenu.java index d013f0c6..9c34d8cb 100644 --- a/internal/paper26_3/src/main/java/com/lishid/openinv/internal/paper26_3/container/menu/BaseOpenInventoryMenu.java +++ b/internal/paper26_3/src/main/java/com/lishid/openinv/internal/paper26_3/container/menu/BaseOpenInventoryMenu.java @@ -112,7 +112,7 @@ protected CraftInventoryView, Inventory> create if (viewOnly) { bukkitInventory = new OpenDummyPlayerInventory(container); } else if (ownContainer) { - bukkitInventory = new OpenPlayerInventorySelf(container, offset); + bukkitInventory = new OpenPlayerInventorySelf(factory, container, offset); } else { bukkitInventory = container.getBukkitInventory(); } diff --git a/internal/spigot26_3/build.gradle.kts b/internal/spigot26_3/build.gradle.kts index d7721ce4..763925d0 100644 --- a/internal/spigot26_3/build.gradle.kts +++ b/internal/spigot26_3/build.gradle.kts @@ -20,8 +20,10 @@ tasks.withType().configureEach { apply() dependencies { - compileOnly(libs.spigotapi) - extensions.getByType(SpigotDependencyExtension::class.java).version = "26.3-R0.1-SNAPSHOT" + val spigotVersion = "26.3-R0.1-SNAPSHOT" + extensions.getByType(SpigotDependencyExtension::class.java).version = spigotVersion + val spigotApi = libs.spigotapi.get() + compileOnly("${spigotApi.group}:${spigotApi.name}:$spigotVersion") compileOnly("com.mojang:logging:1.7.12") compileOnly("com.mojang:brigadier:1.3.11") compileOnly("com.mojang:datafixerupper:10.0.21") diff --git a/internal/spigot26_3/src/main/java/com/github/jikoo/openinv/internal/spigot26_3/container/bukkit/OpenPlayerInventory.java b/internal/spigot26_3/src/main/java/com/github/jikoo/openinv/internal/spigot26_3/container/bukkit/OpenPlayerInventory.java index 89f4cbfe..3f825fca 100644 --- a/internal/spigot26_3/src/main/java/com/github/jikoo/openinv/internal/spigot26_3/container/bukkit/OpenPlayerInventory.java +++ b/internal/spigot26_3/src/main/java/com/github/jikoo/openinv/internal/spigot26_3/container/bukkit/OpenPlayerInventory.java @@ -3,9 +3,14 @@ import com.github.jikoo.openinv.internal.spigot26_3.container.OpenInventory; import com.google.common.base.Preconditions; import net.minecraft.core.NonNullList; +import net.minecraft.world.Container; import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import org.bukkit.Material; import org.bukkit.craftbukkit.inventory.CraftInventory; import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.craftbukkit.inventory.CraftItemType; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryType; import org.bukkit.inventory.ItemStack; @@ -13,6 +18,10 @@ import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiPredicate; + @NullMarked public class OpenPlayerInventory extends CraftInventory implements PlayerInventory { @@ -26,16 +35,34 @@ public OpenInventory getInventory() { } @Override - public ItemStack[] getContents() { - return asCraftMirror(getInventory().getOwnerHandle().getInventory().getContents()); + public @Nullable ItemStack[] getContents() { + return asCraftMirror(getInventory().getContents()); } @Override - public void setContents(ItemStack[] items) { - Inventory internal = getInventory().getOwnerHandle().getInventory(); + public void setContents(@Nullable ItemStack[] items) { + Container internal = getInventory(); int size = internal.getContainerSize(); Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size); + if (items.length < size) { + // If size matches normal player inventory size, redirect to it. + Container ownerInternal = getInventory().getOwnerHandle().getInventory(); + if (items.length == ownerInternal.getContainerSize()) { + internal = ownerInternal; + size = items.length; + } + } else { + // Otherwise, ensure that the display-only slots are not in use. + for (int index = 0; index < size; ++index) { + ItemStack itemStack = items[index]; + if (itemStack == null || itemStack.getType() == Material.AIR || itemStack.getAmount() <= 0) { + continue; + } + validateSet(index); + } + } + for (int index = 0; index < size; ++index) { if (index < items.length) { internal.setItem(index, CraftItemStack.asNMSCopy(items[index])); @@ -45,13 +72,26 @@ public void setContents(ItemStack[] items) { } } + protected void validateSet(int index) { + InventoryType.SlotType slotType = getInventory().getSlotType(index); + if (slotType == InventoryType.SlotType.OUTSIDE || slotType == InventoryType.SlotType.RESULT) { + throw new IllegalArgumentException("A plugin tried to put an item in a display slot. That would delete it!"); + } + } + + @Override + public void setItem(int index, @Nullable ItemStack item) { + validateSet(index); + super.setItem(index, item); + } + @Override - public ItemStack[] getStorageContents() { + public @Nullable ItemStack[] getStorageContents() { return asCraftMirror(getInventory().getOwnerHandle().getInventory().getNonEquipmentItems()); } @Override - public void setStorageContents(ItemStack[] items) throws IllegalArgumentException { + public void setStorageContents(@Nullable ItemStack[] items) throws IllegalArgumentException { NonNullList list = getInventory().getOwnerHandle().getInventory().getNonEquipmentItems(); int size = list.size(); Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size); @@ -60,6 +100,294 @@ public void setStorageContents(ItemStack[] items) throws IllegalArgumentExceptio } } + @SuppressWarnings("NonApiType") + @Override + public HashMap all(Material material) { + Preconditions.checkNotNull(material, "Material cannot be null"); + Item item = CraftItemType.bukkitToMinecraft(material); + Preconditions.checkNotNull(item, "Material must be an item"); + + OpenInventory inventory = getInventory(); + int size = inventory.getOwnerHandle().getInventory().getNonEquipmentItems().size(); + + HashMap slots = new HashMap<>(); + for (int index = 0; index < size; ++index) { + net.minecraft.world.item.ItemStack itemStack = inventory.getItem(index); + if (!itemStack.isEmpty() && item.equals(itemStack.getItem())) { + slots.put(index, CraftItemStack.asCraftMirror(itemStack)); + } + } + + return slots; + } + + @SuppressWarnings("NonApiType") + @Override + public HashMap all(@Nullable ItemStack item) { + HashMap slots = new HashMap<>(); + + if (item == null) { + return slots; + } + + net.minecraft.world.item.ItemStack itemStack = CraftItemStack.asNMSCopy(item); + OpenInventory inventory = getInventory(); + int size = inventory.getOwnerHandle().getInventory().getNonEquipmentItems().size(); + + for (int index = 0; index < size; ++index) { + net.minecraft.world.item.ItemStack slotItemStack = inventory.getItem(index); + if (ItemMatcher.EXACT.predicate.test(itemStack, slotItemStack)) { + slots.put(index, CraftItemStack.asCraftMirror(slotItemStack)); + } + } + + return slots; + } + + @Override + public int first(Material material) { + Item item = CraftItemType.bukkitToMinecraft(material); + Preconditions.checkNotNull(item, "Material must be an item"); + net.minecraft.world.item.ItemStack itemStack = new net.minecraft.world.item.ItemStack(item); + int size = getInventory().getOwnerHandle().getInventory().getNonEquipmentItems().size(); + return first(itemStack, ItemMatcher.TYPE, 0, size); + } + + @Override + public int first(ItemStack item) { + net.minecraft.world.item.ItemStack itemStack = CraftItemStack.asNMSCopy(item); + int size = getInventory().getOwnerHandle().getInventory().getNonEquipmentItems().size(); + return first(itemStack, ItemMatcher.EXACT, 0, size); + } + + @Override + public int firstEmpty() { + OpenInventory inventory = getInventory(); + int size = inventory.getOwnerHandle().getInventory().getNonEquipmentItems().size(); + return first(net.minecraft.world.item.ItemStack.EMPTY, ItemMatcher.EMPTY, 0, size); + } + + private int first(net.minecraft.world.item.ItemStack itemStack, ItemMatcher matcher, int startIndex, int endIndex) { + for (int index = startIndex; index < endIndex; ++index) { + net.minecraft.world.item.ItemStack inventoryStack = inventory.getItem(index); + if (matcher.predicate.test(itemStack, inventoryStack)) { + return index; + } + } + return -1; + } + + @SuppressWarnings("ImmutableEnumChecker") // Ignore Google's style preference for predicate-like enums + private enum ItemMatcher { + EMPTY((ignored, b) -> b.isEmpty()), + TYPE(net.minecraft.world.item.ItemStack::isSameItem), + SIMILAR(net.minecraft.world.item.ItemStack::isSameItemSameComponents), + EXACT((a, b) -> net.minecraft.world.item.ItemStack.isSameItemSameComponents(a, b) && a.count() == b.count()); + + private final BiPredicate predicate; + + ItemMatcher(BiPredicate predicate) { + this.predicate = predicate; + } + } + + @SuppressWarnings("NonApiType") + @Override + public HashMap addItem(ItemStack... items) { + Preconditions.checkNotNull(items, "items cannot be null"); + HashMap leftover = new HashMap<>(); + + OpenInventory inventory = getInventory(); + int storageSize = inventory.getOwnerHandle().getInventory().getNonEquipmentItems().size(); + + for (int index = 0; index < items.length; ++index) { + addItem(items, index, storageSize, leftover); + } + + return leftover; + } + + private void addItem( + ItemStack[] items, + int leftoverIndex, + int storageSize, + Map leftover + ) { + ItemStack bukkitStack = items[leftoverIndex]; + Preconditions.checkNotNull(bukkitStack, "ItemStack cannot be null"); + if (bukkitStack.getType() == Material.AIR || bukkitStack.getAmount() <= 0) { + return; + } + net.minecraft.world.item.ItemStack nmsStack = CraftItemStack.asNMSCopy(bukkitStack); + Preconditions.checkNotNull(nmsStack, "ItemStack cannot be empty"); + + int maxStackSize = inventory.getMaxStackSize(nmsStack); + int stackCount = nmsStack.count(); + + // Try to fill existing stacks first. Waste of time for items that don't stack. + if (maxStackSize > 1) { + stackCount = addPartial(storageSize, nmsStack, maxStackSize, stackCount); + if (nmsStack.count() < 1) { + // All done adding, next item. + return; + } + } + + // Add to empty slots. + stackCount = addEmpty(storageSize, stackCount, maxStackSize, nmsStack); + + if (stackCount > 0) { + bukkitStack.setAmount(stackCount); + leftover.put(leftoverIndex, bukkitStack); + } + } + + private int addPartial( + int storageSize, + net.minecraft.world.item.ItemStack nmsStack, + int maxStackSize, + int stackCount + ) { + for ( + int firstPartial = first(nmsStack, ItemMatcher.SIMILAR, 0, storageSize); + firstPartial < storageSize && firstPartial > -1; + firstPartial = first(nmsStack, ItemMatcher.SIMILAR, firstPartial + 1, storageSize) + ) { + net.minecraft.world.item.ItemStack existing = inventory.getItem(firstPartial); + int existingCount = existing.count(); + if (existingCount >= maxStackSize) { + continue; + } + + int fit = maxStackSize - existingCount; + + if (fit >= stackCount) { + nmsStack.setCount(0); + existing.setCount(existingCount + stackCount); + return 0; + } + + stackCount -= fit; + nmsStack.setCount(stackCount); + existing.setCount(maxStackSize); + } + return stackCount; + } + + private int addEmpty( + int storageSize, + int stackCount, + int maxStackSize, + net.minecraft.world.item.ItemStack nmsStack + ) { + for ( + int firstEmpty = firstEmpty(); + firstEmpty < storageSize && firstEmpty > -1; + firstEmpty = first(net.minecraft.world.item.ItemStack.EMPTY, ItemMatcher.EMPTY, firstEmpty, storageSize) + ) { + if (stackCount <= maxStackSize) { + inventory.setItem(firstEmpty, nmsStack.copy()); + return 0; + } + + var newStack = nmsStack.copy(); + stackCount -= maxStackSize; + nmsStack.setCount(stackCount); + newStack.setCount(maxStackSize); + inventory.setItem(firstEmpty, newStack); + } + + return stackCount; + } + + @SuppressWarnings("NonApiType") + @Override + public HashMap removeItem(ItemStack... items) { + Preconditions.checkNotNull(items, "items cannot be null"); + HashMap leftover = new HashMap<>(); + + OpenInventory inventory = getInventory(); + int storageSize = inventory.getOwnerHandle().getInventory().getNonEquipmentItems().size(); + + for (int index = 0; index < items.length; ++index) { + removeItem(items[index], index, storageSize, leftover); + } + + return leftover; + } + + private void removeItem( + ItemStack bukkitStack, + int leftoverIndex, + int storageSize, + Map leftover + ) { + Preconditions.checkNotNull(bukkitStack, "ItemStack cannot be null"); + if (bukkitStack.getType() == Material.AIR || bukkitStack.getAmount() <= 0) { + return; + } + net.minecraft.world.item.ItemStack nmsStack = CraftItemStack.asNMSCopy(bukkitStack); + Preconditions.checkNotNull(nmsStack, "ItemStack cannot be empty"); + + int stackCount = nmsStack.count(); + + for ( + int first = first(nmsStack, ItemMatcher.SIMILAR, 0, storageSize); + first < storageSize && first > -1; + first = first(nmsStack, ItemMatcher.SIMILAR, first + 1, storageSize) + ) { + net.minecraft.world.item.ItemStack existing = inventory.getItem(first); + int existingCount = existing.count(); + if (existingCount > stackCount) { + existing.setCount(existingCount - stackCount); + bukkitStack.setAmount(0); + return; + } + + stackCount -= existingCount; + inventory.setItem(first, net.minecraft.world.item.ItemStack.EMPTY); + + if (stackCount <= 0) { + break; + } + } + + bukkitStack.setAmount(stackCount); + + if (stackCount > 0) { + leftover.put(leftoverIndex, bukkitStack); + } + } + + @Override + public void remove(Material material) { + Item item = CraftItemType.bukkitToMinecraft(material); + Preconditions.checkNotNull(item, "Material must be an item"); + + if (!item.equals(Items.AIR)) { + removeAll(new net.minecraft.world.item.ItemStack(item), ItemMatcher.SIMILAR); + } + } + + @Override + public void remove(ItemStack bukkitStack) { + net.minecraft.world.item.ItemStack nmsStack = CraftItemStack.asNMSCopy(bukkitStack); + + if (!nmsStack.isEmpty()) { + removeAll(nmsStack, ItemMatcher.EXACT); + } + } + + private void removeAll(net.minecraft.world.item.ItemStack itemStack, ItemMatcher matcher) { + int storageSize = getInventory().getOwnerHandle().getInventory().getNonEquipmentItems().size(); + for (int index = 0; index < storageSize; ++index) { + net.minecraft.world.item.ItemStack content = inventory.getItem(index); + if (matcher.predicate.test(itemStack, content)) { + inventory.setItem(index, net.minecraft.world.item.ItemStack.EMPTY); + } + } + } + @Override public InventoryType getType() { return InventoryType.PLAYER; @@ -76,7 +404,7 @@ public ItemStack[] getArmorContents() { } @Override - public void setArmorContents(ItemStack[] items) { + public void setArmorContents(@Nullable ItemStack[] items) { getInventory().getOwnerHandle().getBukkitEntity().getInventory().setArmorContents(items); } @@ -86,7 +414,7 @@ public ItemStack[] getExtraContents() { } @Override - public void setExtraContents(ItemStack[] items) { + public void setExtraContents(@Nullable ItemStack[] items) { getInventory().getOwnerHandle().getBukkitEntity().getInventory().setExtraContents(items); } @@ -117,39 +445,37 @@ public void setChestplate(@Nullable ItemStack chestplate) { @Override public void setLeggings(@Nullable ItemStack leggings) { - getInventory().getOwnerHandle().getBukkitEntity().getInventory().setLeggings(leggings); + getInventory().getOwner().getInventory().setLeggings(leggings); } @Override public @Nullable ItemStack getBoots() { - return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getBoots(); + return getInventory().getOwner().getInventory().getBoots(); } @Override public void setBoots(@Nullable ItemStack boots) { - getInventory().getOwnerHandle().getBukkitEntity().getInventory().setBoots(boots); + getInventory().getOwner().getInventory().setBoots(boots); } @Override public ItemStack getItemInMainHand() { - Inventory internal = getInventory().getOwnerHandle().getInventory(); - return CraftItemStack.asCraftMirror(internal.getSelectedItem()); + return getInventory().getOwner().getInventory().getItemInMainHand(); } @Override public void setItemInMainHand(@Nullable ItemStack item) { - Inventory internal = getInventory().getOwnerHandle().getInventory(); - internal.setSelectedItem(CraftItemStack.asNMSCopy(item)); + getInventory().getOwner().getInventory().setItemInMainHand(item); } @Override public ItemStack getItemInOffHand() { - return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getItemInOffHand(); + return getInventory().getOwner().getInventory().getItemInOffHand(); } @Override public void setItemInOffHand(@Nullable ItemStack item) { - getInventory().getOwnerHandle().getBukkitEntity().getInventory().setItemInOffHand(item); + getInventory().getOwner().getInventory().setItemInOffHand(item); } @SuppressWarnings("InlineMeSuggester") @@ -180,12 +506,28 @@ public void setHeldItemSlot(int slot) { @Override public @Nullable ItemStack getItem(org.bukkit.inventory.EquipmentSlot slot) { - return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getItem(slot); + return switch (slot) { + case HAND -> getItemInMainHand(); + case OFF_HAND -> getItemInOffHand(); + case FEET -> getBoots(); + case LEGS -> getLeggings(); + case CHEST -> getChestplate(); + case HEAD -> getHelmet(); + default -> throw new IllegalArgumentException("Unsupported EquipmentSlot " + slot); + }; } @Override public void setItem(org.bukkit.inventory.EquipmentSlot slot, @Nullable ItemStack item) { - getInventory().getOwnerHandle().getBukkitEntity().getInventory().setItem(slot, item); + switch (slot) { + case HAND -> setItemInMainHand(item); + case OFF_HAND -> setItemInOffHand(item); + case FEET -> setBoots(item); + case LEGS -> setLeggings(item); + case CHEST -> setChestplate(item); + case HEAD -> setHelmet(item); + default -> throw new IllegalArgumentException("Unsupported EquipmentSlot " + slot); + } } } diff --git a/internal/spigot26_3/src/main/java/com/github/jikoo/openinv/internal/spigot26_3/container/bukkit/OpenPlayerInventorySelf.java b/internal/spigot26_3/src/main/java/com/github/jikoo/openinv/internal/spigot26_3/container/bukkit/OpenPlayerInventorySelf.java index 02ef39d5..fb79acd3 100644 --- a/internal/spigot26_3/src/main/java/com/github/jikoo/openinv/internal/spigot26_3/container/bukkit/OpenPlayerInventorySelf.java +++ b/internal/spigot26_3/src/main/java/com/github/jikoo/openinv/internal/spigot26_3/container/bukkit/OpenPlayerInventorySelf.java @@ -1,10 +1,15 @@ package com.github.jikoo.openinv.internal.spigot26_3.container.bukkit; import com.github.jikoo.openinv.internal.spigot26_3.container.OpenInventory; +import com.google.common.base.Preconditions; +import org.bukkit.Material; +import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.inventory.ItemStack; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.HashMap; + @NullMarked public class OpenPlayerInventorySelf extends OpenPlayerInventory { @@ -15,14 +20,141 @@ public OpenPlayerInventorySelf(OpenInventory inventory, int offset) { this.offset = offset; } + @Override + public int getSize() { + return super.getSize() - offset; + } + @Override public @Nullable ItemStack getItem(int index) { return super.getItem(offset + index); } + @Override + public @Nullable ItemStack[] getStorageContents() { + return new ItemStack[0]; + } + + @Override + public void setStorageContents(@Nullable ItemStack[] items) throws IllegalArgumentException { + Preconditions.checkArgument(items.length == 0, "items.length must be <= 0"); + } + + @Override + public @Nullable ItemStack[] getContents() { + @Nullable ItemStack[] contents = new ItemStack[getSize()]; + for (int i = 0; i < contents.length; ++i) { + contents[i] = getItem(i); + } + return contents; + } + + @Override + public void setContents(@Nullable ItemStack[] items) { + int size = getSize(); + Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size); + + for (int index = 0; index < size; ++index) { + ItemStack itemStack = items[index]; + if (itemStack == null || itemStack.getType() == Material.AIR || itemStack.getAmount() <= 0) { + continue; + } + validateSet(index); + } + + for (int index = 0; index < size; ++index) { + if (index < items.length) { + inventory.setItem(offset + index, CraftItemStack.asNMSCopy(items[index])); + } else { + inventory.setItem(offset + index, net.minecraft.world.item.ItemStack.EMPTY); + } + } + } + @Override public void setItem(int index, @Nullable ItemStack item) { super.setItem(offset + index, item); } + @Override + public boolean contains(Material material) { + // Only checks the storage contents. + return false; + } + + @Override + public boolean contains(ItemStack item) { + return false; + } + + @Override + public boolean contains(Material material, int amount) { + return false; + } + + @Override + public boolean contains(ItemStack item, int amount) { + return false; + } + + @Override + public boolean containsAtLeast(ItemStack item, int amount) { + return false; + } + + @SuppressWarnings("NonApiType") + @Override + public HashMap all(Material material) { + // No storage contents, no results. + return new HashMap<>(); + } + + @SuppressWarnings("NonApiType") + @Override + public HashMap all(@Nullable ItemStack item) { + return new HashMap<>(); + } + + @Override + public int first(Material material) { + return -1; + } + + @Override + public int first(ItemStack item) { + return -1; + } + + @Override + public int firstEmpty() { + return -1; + } + + @SuppressWarnings("NonApiType") + @Override + public HashMap addItem(ItemStack... items) { + HashMap map = new HashMap<>(); + for (int index = 0; index < items.length; ++index) { + map.put(index, items[index]); + } + return map; + } + + @SuppressWarnings("NonApiType") + @Override + public HashMap removeItem(ItemStack... items) { + return addItem(items); + } + + @Override + public void remove(Material material) { + Preconditions.checkNotNull(material, "Material cannot be null"); + Preconditions.checkArgument(material.isItem(), "Material must be an item"); + } + + @Override + public void remove(ItemStack itemStack) { + // Sure did. + } + }