From 95f37317601193466feaffc4f7f27640afe6217a Mon Sep 17 00:00:00 2001 From: Teyir Date: Tue, 27 Oct 2026 14:50:11 +0100 Subject: [PATCH] :bug: Keep player-head updates in the player inventory for player-inventory buttons Co-Authored-By: Claude Opus 5.5 (1M context) --- .../maxlego08/menu/api/InventoryListener.java | 10 ++++++ .../maxlego08/menu/api/engine/ItemButton.java | 10 ++++++ .../maxlego08/menu/inventory/VInventory.java | 36 +++++++++++++++++++ .../inventories/InventoryDefault.java | 14 +++++--- .../PacketEventPlayerInventoryManager.java | 14 ++++++++ 5 files changed, 79 insertions(+), 5 deletions(-) diff --git a/API/src/main/java/fr/maxlego08/menu/api/InventoryListener.java b/API/src/main/java/fr/maxlego08/menu/api/InventoryListener.java index c1effa370..47c71ebb2 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/InventoryListener.java +++ b/API/src/main/java/fr/maxlego08/menu/api/InventoryListener.java @@ -11,6 +11,16 @@ default boolean addItem(BaseInventory baseInventory, boolean inPlayerInventory, return false; } + /** + * Called when the item of a button already placed is replaced after the inventory has been opened, + * for example once a player head texture has been loaded. + * + * @return true if the listener displayed the item itself and the default placement must be skipped + */ + default boolean updateItem(BaseInventory baseInventory, boolean inPlayerInventory, ItemButton itemButton) { + return false; + } + default void onInventoryPreOpen(Player player, BaseInventory baseInventory, int page, Object... objects){ } diff --git a/API/src/main/java/fr/maxlego08/menu/api/engine/ItemButton.java b/API/src/main/java/fr/maxlego08/menu/api/engine/ItemButton.java index 12541cfc6..0c0d2c027 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/engine/ItemButton.java +++ b/API/src/main/java/fr/maxlego08/menu/api/engine/ItemButton.java @@ -84,6 +84,16 @@ public ItemButton setLeftClick(@NotNull Consumer onLeftClic return this; } + /** + * Changes the item stored by this button without placing it anywhere. + * The caller is responsible for displaying it. + * + * @param displayItem the new display item + */ + public void setDisplayItem(@NotNull ItemStack displayItem) { + this.displayItem = displayItem; + } + public void updateDisplayItem(@NotNull ItemStack displayItem) { this.displayItem = displayItem; if (this.inPlayerInventory) { diff --git a/Common/src/main/java/fr/maxlego08/menu/inventory/VInventory.java b/Common/src/main/java/fr/maxlego08/menu/inventory/VInventory.java index ec6c4241e..c08b994db 100644 --- a/Common/src/main/java/fr/maxlego08/menu/inventory/VInventory.java +++ b/Common/src/main/java/fr/maxlego08/menu/inventory/VInventory.java @@ -146,6 +146,42 @@ public ItemButton addItem(boolean inPlayerInventory, int slot, ItemStack itemSta return button; } + /** + * Replaces the item of a button already placed, keeping its click handlers. + * Goes through the same anti-dupe and inventory listeners as {@link #addItem(boolean, int, ItemStack, boolean)}, + * so a button placed in the player inventory is updated in the player inventory and not in the menu. + */ + public void updateItem(boolean inPlayerInventory, int slot, @NotNull ItemStack itemStack, boolean enableAntiDupe) { + + if (this.isClose) return; + + ItemButton button = (inPlayerInventory ? this.playerInventoryItems : this.items).get(slot); + if (button == null) return; + + ItemStack displayStack = itemStack.clone(); + + if (Configuration.enableAntiDupe && enableAntiDupe) { + displayStack = this.plugin.getDupeManager().protectItem(displayStack); + } + + button.setDisplayItem(itemStack); + + boolean needCancel = false; + for (InventoryListener inventoryListener : this.plugin.getInventoryManager().getInventoryListeners()) { + if (inventoryListener.updateItem(this, inPlayerInventory, button)) { + needCancel = true; + } + } + + if (needCancel) return; + + if (inPlayerInventory) { + this.player.getInventory().setItem(slot, displayStack); + } else { + this.inventory.setItem(slot, displayStack); + } + } + @Override public void removeItem(int slot) { this.items.remove(slot); diff --git a/Common/src/main/java/fr/maxlego08/menu/inventory/inventories/InventoryDefault.java b/Common/src/main/java/fr/maxlego08/menu/inventory/inventories/InventoryDefault.java index 0ddeb0cc3..158af47af 100644 --- a/Common/src/main/java/fr/maxlego08/menu/inventory/inventories/InventoryDefault.java +++ b/Common/src/main/java/fr/maxlego08/menu/inventory/inventories/InventoryDefault.java @@ -331,13 +331,17 @@ public void displayFinalButton(@NotNull Button button, @NotNull Placeholders pla int maxSlotSize = button.isPlayerInventory() ? 36 : this.inventory.size(); if (button.getPlayerHead() != null && itemStack.getItemMeta() instanceof SkullMeta) { - this.plugin.getScheduler().runAsync(w -> { + var scheduler = this.plugin.getScheduler(); + scheduler.runAsync(w -> { var updatedItemStack = this.plugin.getInventoryManager().postProcessSkullItemStack(itemStack, button, this.player, placeholders); - for (int slot : slots) { - if (slot < 0 || slot >= maxSlotSize) continue; + // Back on the player's thread, and in the inventory the button was placed in (menu or player inventory) + scheduler.runAtEntity(this.player, w2 -> { + for (int slot : slots) { + if (slot < 0 || slot >= maxSlotSize) continue; - this.getSpigotInventory().setItem(slot, updatedItemStack); - } + this.updateItem(button.isPlayerInventory(), slot, updatedItemStack, true); + } + }); }); } diff --git a/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/PacketEventPlayerInventoryManager.java b/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/PacketEventPlayerInventoryManager.java index f551dba29..552bb1f93 100644 --- a/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/PacketEventPlayerInventoryManager.java +++ b/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/PacketEventPlayerInventoryManager.java @@ -52,6 +52,20 @@ public boolean addItem(BaseInventory baseInventory, boolean inPlayerInventory, I return false; } + public boolean updateItem(BaseInventory baseInventory, boolean inPlayerInventory, ItemButton itemButton) { + Player player = baseInventory.getPlayer(); + if (baseInventory.getClearInvType() == ClearInvType.PACKET_EVENT && inPlayerInventory && player != null) { + // Still waiting for the post open flush: replace the pending packet, otherwise send it now + if (this.pendingInventoryUpdates.containsKey(player.getUniqueId())) { + this.addItemLater(player, itemButton.getSlot(), itemButton.getDisplayItem()); + } else { + this.addItemInstantly(player, itemButton.getSlot(), itemButton.getDisplayItem()); + } + return true; + } + return false; + } + public void onInventoryPreOpen(Player player, BaseInventory baseInventory, int page, Object... objects){ this.pendingInventoryUpdates.remove(player.getUniqueId()); }