From bccfba4eb4bf84e801df2793190f1c32fd07ac12 Mon Sep 17 00:00:00 2001 From: Alexteens24 Date: Fri, 17 Jul 2026 14:18:10 +0700 Subject: [PATCH] Track storage inventories across Folia regions --- .../gui/storage/SpawnerStorageAction.java | 42 +++++++++------ .../spawner/gui/storage/SpawnerStorageUI.java | 36 +++++++++++-- .../SpawnerGuiViewManager.java | 54 ++++++++++--------- .../services/StorageUpdateService.java | 30 +++++------ 4 files changed, 97 insertions(+), 65 deletions(-) diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageAction.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageAction.java index 64567717..89e3dec0 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageAction.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageAction.java @@ -43,6 +43,7 @@ public class SpawnerStorageAction implements Listener { private final MessageService messageService; private final FilterConfigUI filterConfigUI; private final SpawnerManager spawnerManager; + private final SpawnerStorageUI spawnerStorageUI; private static final int INVENTORY_SIZE = 54; private static final int STORAGE_SLOTS = 45; @@ -59,6 +60,7 @@ public SpawnerStorageAction(SmartSpawner plugin) { this.messageService = plugin.getMessageService(); this.filterConfigUI = plugin.getFilterConfigUI(); this.spawnerManager = plugin.getSpawnerManager(); + this.spawnerStorageUI = plugin.getSpawnerStorageUI(); loadConfig(); } @@ -69,8 +71,12 @@ public void loadConfig() { @EventHandler(priority = EventPriority.LOWEST) public void onInventoryClick(InventoryClickEvent event) { - if (!(event.getWhoClicked() instanceof Player player) || - !(event.getInventory().getHolder(false) instanceof StoragePageHolder holder)) { + if (!(event.getWhoClicked() instanceof Player player)) { + return; + } + + StoragePageHolder holder = spawnerStorageUI.getStorageHolder(event.getInventory()); + if (holder == null) { return; } @@ -252,7 +258,7 @@ private boolean handleCollectExpAction(Player player, SpawnerData spawner, Inven boolean collected = plugin.getSpawnerMenuAction().tryCollectExpForPlayer(player, spawner); if (collected) { // Refresh button display so the XP counter updates to 0 - StoragePageHolder holder = (StoragePageHolder) inventory.getHolder(false); + StoragePageHolder holder = spawnerStorageUI.getStorageHolder(inventory); if (holder != null) { plugin.getSpawnerStorageUI().updateDisplay(inventory, spawner, holder.getCurrentPage(), holder.getTotalPages()); } @@ -434,7 +440,7 @@ private void updatePageAfterRemoval(Player player, Inventory inventory, } private boolean handleDropPageItems(Player player, SpawnerData spawner, Inventory inventory) { - StoragePageHolder holder = (StoragePageHolder) inventory.getHolder(false); + StoragePageHolder holder = spawnerStorageUI.getStorageHolder(inventory); if (holder == null) { return false; } @@ -546,11 +552,13 @@ private void openFilterConfig(Player player, SpawnerData spawner) { private void updatePageContent(Player player, SpawnerData spawner, int newPage, Inventory inventory) { SpawnerStorageUI spawnerStorageUI = plugin.getSpawnerStorageUI(); - StoragePageHolder holder = (StoragePageHolder) inventory.getHolder(false); + StoragePageHolder holder = spawnerStorageUI.getStorageHolder(inventory); + if (holder == null) { + return; + } int totalPages = calculateTotalPages(spawner); - assert holder != null; holder.setTotalPages(totalPages); holder.setCurrentPage(newPage); holder.updateOldUsedSlots(); @@ -680,7 +688,7 @@ private boolean handleSortItemsClick(Player player, SpawnerData spawner, Invento spawner.getVirtualInventory().sortItems(nextSort); // Update GUI display to reflect VirtualInventory state - StoragePageHolder holder = (StoragePageHolder) inventory.getHolder(false); + StoragePageHolder holder = spawnerStorageUI.getStorageHolder(inventory); if (holder != null) { updatePageContent(player, spawner, holder.getCurrentPage(), inventory); } @@ -719,7 +727,10 @@ private void openLootPage(Player player, SpawnerData spawner, int page) { } public boolean handleTakeAllItems(Player player, Inventory sourceInventory) { - StoragePageHolder holder = (StoragePageHolder) sourceInventory.getHolder(false); + StoragePageHolder holder = spawnerStorageUI.getStorageHolder(sourceInventory); + if (holder == null) { + return false; + } SpawnerData spawner = holder.getSpawnerData(); VirtualInventory virtualInv = spawner.getVirtualInventory(); @@ -745,7 +756,7 @@ public boolean handleTakeAllItems(Player player, Inventory sourceInventory) { } // Transfer items and update VirtualInventory - TransferResult result = transferItems(player, sourceInventory, sourceItems, virtualInv); + TransferResult result = transferItems(player, sourceInventory, sourceItems, virtualInv, holder); sendTransferMessage(player, result); player.updateInventory(); @@ -803,7 +814,8 @@ private void playActionResult( } private TransferResult transferItems(Player player, Inventory sourceInventory, - Map sourceItems, VirtualInventory virtualInv) { + Map sourceItems, VirtualInventory virtualInv, + StoragePageHolder holder) { boolean anyItemMoved = false; boolean inventoryFull = false; PlayerInventory playerInv = player.getInventory(); @@ -864,7 +876,6 @@ else if (targetItem.isSimilar(itemToMove)) { // Update VirtualInventory if (!itemsToRemove.isEmpty()) { - StoragePageHolder holder = (StoragePageHolder) sourceInventory.getHolder(false); SpawnerData spawnerData = holder.getSpawnerData(); spawnerData.removeItemsAndUpdateSellValue(itemsToRemove); @@ -886,7 +897,7 @@ private void sendTransferMessage(Player player, TransferResult result) { @EventHandler public void onInventoryDrag(InventoryDragEvent event) { - if (!(event.getInventory().getHolder(false) instanceof StoragePageHolder)) { + if (spawnerStorageUI.getStorageHolder(event.getInventory()) == null) { return; } event.setCancelled(true); @@ -895,14 +906,11 @@ public void onInventoryDrag(InventoryDragEvent event) { @EventHandler public void onInventoryClose(InventoryCloseEvent event) { Inventory inventory = event.getInventory(); - if (!(inventory.getHolder(false) instanceof StoragePageHolder holder)) { + StoragePageHolder holder = spawnerStorageUI.unregisterStorageInventory(inventory); + if (holder == null) { return; } - // Inventory close events already execute on the owning player's region thread. - // Do not defer this work to the player's scheduler: after closing, a block-backed - // inventory could belong to a different region and resolving its holder there - // violates Folia's thread ownership rules. handleInventoryClose(holder); } diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageUI.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageUI.java index 4c6e427a..1feb00e4 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageUI.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageUI.java @@ -38,6 +38,7 @@ public class SpawnerStorageUI { // Precomputed buttons to avoid repeated creation private final Map staticButtons; + private final Map storageInventories; // Lightweight caches with better eviction strategies private final Map navigationButtonCache; @@ -59,6 +60,7 @@ public SpawnerStorageUI(SmartSpawner plugin) { // Initialize caches with appropriate initial capacity this.staticButtons = new ConcurrentHashMap<>(16); + this.storageInventories = Collections.synchronizedMap(new IdentityHashMap<>()); this.navigationButtonCache = new ConcurrentHashMap<>(16); this.pageIndicatorCache = new ConcurrentHashMap<>(16); @@ -209,17 +211,38 @@ public Inventory createStorageInventory(Player player, SpawnerData spawner, int GuiLayout layout = layoutConfig.getStorageLayout(spawner, player); // Create inventory with title including page info using placeholder-based format + StoragePageHolder holder = new StoragePageHolder(spawner, page, totalPages, layout); Inventory pageInv = Bukkit.createInventory( - new StoragePageHolder(spawner, page, totalPages, layout), + holder, INVENTORY_SIZE, getStorageTitle(spawner, page, totalPages) ); + storageInventories.put(pageInv, holder); - // Populate the inventory - updateDisplay(pageInv, spawner, page, totalPages); + // Populate the inventory. If construction fails, do not retain a + // registry entry for an inventory that will never be opened. + try { + updateDisplay(pageInv, spawner, page, totalPages); + } catch (RuntimeException | Error failure) { + storageInventories.remove(pageInv); + throw failure; + } return pageInv; } + /** + * Resolves holders only for virtual storage inventories created by this UI. + * This deliberately avoids Inventory#getHolder(), which may access a block + * inventory in a region not owned by the current Folia thread. + */ + public StoragePageHolder getStorageHolder(Inventory inventory) { + return storageInventories.get(inventory); + } + + public StoragePageHolder unregisterStorageInventory(Inventory inventory) { + return storageInventories.remove(inventory); + } + public void updateDisplay(Inventory inventory, SpawnerData spawner, int page, int totalPages) { if (!spawner.getInventoryLock().tryLock()) { if (plugin.isDebugMode()) { @@ -242,8 +265,11 @@ public void updateDisplay(Inventory inventory, SpawnerData spawner, int page, in } // Also mark all button slots for potential clearing (fixes visual bug where buttons remain when they shouldn't) - StoragePageHolder holder = (StoragePageHolder) inventory.getHolder(false); - assert holder != null; + StoragePageHolder holder = getStorageHolder(inventory); + if (holder == null) { + spawner.getInventoryLock().unlock(); + return; + } GuiLayout layout = holder.getLayout(); if (layout != null) { slotsToEmpty.addAll(layout.getUsedSlots()); diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java index 5617b57a..48efd5f9 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java @@ -13,7 +13,6 @@ import github.nighter.smartspawner.spawner.gui.synchronization.services.TimerUpdateService; import github.nighter.smartspawner.spawner.properties.SpawnerData; import org.bukkit.Bukkit; -import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; @@ -204,36 +203,39 @@ public void updateSpawnerMenuViewers(SpawnerData spawner) { continue; } - // Schedule batched GUI update for main menu viewers - guiUpdateService.scheduleUpdate(viewerId, GuiUpdateService.UPDATE_ALL); + ViewerTrackingManager.ViewerInfo viewerInfo = viewerTrackingManager.getViewerInfo(viewerId); + if (viewerInfo == null) { + continue; + } - // Handle storage page updates - calculate pages on the correct thread - Inventory openInv = viewer.getOpenInventory().getTopInventory(); - if (openInv.getHolder(false) instanceof StoragePageHolder holder) { - // Schedule storage update - page calculation happens inside - Location loc = viewer.getLocation(); + if (viewerInfo.getViewerType() == ViewerTrackingManager.ViewerType.MAIN_MENU) { + guiUpdateService.scheduleUpdate(viewerId, GuiUpdateService.UPDATE_ALL); + continue; + } + if (viewerInfo.getViewerType() != ViewerTrackingManager.ViewerType.STORAGE) { + continue; + } - if (!holder.getSpawnerData().getSpawnerId().equals(spawner.getSpawnerId())) { - continue; + // Entity schedulers follow players across region changes. Resolve the + // inventory only after the task owns the player, then use our registry + // rather than querying a potentially block-backed inventory holder. + Scheduler.runEntityTask(viewer, () -> { + if (!viewer.isOnline()) { + return; } - Scheduler.runLocationTask(loc, () -> { - if (!viewer.isOnline()) { - return; - } - Inventory inv = viewer.getOpenInventory().getTopInventory(); - if (inv.getHolder(false) instanceof StoragePageHolder spHolder) { - - if (!spHolder.getSpawnerData().getSpawnerId().equals(spawner.getSpawnerId())) { - return; - } - int oldPages = storageUpdateService.calculateTotalPages(holder.getOldUsedSlots()); - int newPages = storageUpdateService.calculateTotalPages(spawner.getVirtualInventory().getUsedSlots()); + Inventory inv = viewer.getOpenInventory().getTopInventory(); + StoragePageHolder holder = plugin.getSpawnerStorageUI().getStorageHolder(inv); + if (holder == null || + !holder.getSpawnerData().getSpawnerId().equals(spawner.getSpawnerId())) { + return; + } - storageUpdateService.processStorageUpdateDirect(viewer, inv, spawner, spHolder, oldPages, newPages); - } - }); - } + int oldPages = storageUpdateService.calculateTotalPages(holder.getOldUsedSlots()); + int newPages = storageUpdateService.calculateTotalPages(spawner.getVirtualInventory().getUsedSlots()); + storageUpdateService.processStorageUpdateDirect( + viewer, inv, spawner, holder, oldPages, newPages); + }); } } diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/services/StorageUpdateService.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/services/StorageUpdateService.java index 9179d697..2d15b8a2 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/services/StorageUpdateService.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/services/StorageUpdateService.java @@ -6,7 +6,6 @@ import github.nighter.smartspawner.spawner.gui.storage.SpawnerStorageUI; import github.nighter.smartspawner.spawner.gui.storage.StoragePageHolder; import github.nighter.smartspawner.spawner.properties.SpawnerData; -import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; @@ -58,22 +57,19 @@ public void reloadCache() { * @param newTotalPages New total pages */ public void processStorageUpdate(Player viewer, SpawnerData spawner, int oldTotalPages, int newTotalPages) { - Location loc = viewer.getLocation(); - if (loc != null) { - Scheduler.runLocationTask(loc, () -> { - if (!viewer.isOnline()) { - return; - } - - Inventory openInv = viewer.getOpenInventory().getTopInventory(); - if (openInv == null || !(openInv.getHolder(false) instanceof StoragePageHolder)) { - return; - } - - StoragePageHolder holder = (StoragePageHolder) openInv.getHolder(false); - processStorageUpdateDirect(viewer, openInv, spawner, holder, oldTotalPages, newTotalPages); - }); - } + Scheduler.runEntityTask(viewer, () -> { + if (!viewer.isOnline()) { + return; + } + + Inventory openInv = viewer.getOpenInventory().getTopInventory(); + StoragePageHolder holder = spawnerStorageUI.getStorageHolder(openInv); + if (holder == null) { + return; + } + + processStorageUpdateDirect(viewer, openInv, spawner, holder, oldTotalPages, newTotalPages); + }); } /**