diff --git a/build.gradle.kts b/build.gradle.kts index 1db5f4b0..55778bb9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ allprojects { apply(plugin = "maven-publish") group = "github.nighter" - version = "1.7.0.1" + version = "1.7.0.1-DEV" repositories { mavenCentral() diff --git a/core/src/main/java/github/nighter/smartspawner/extras/HopperTransfer.java b/core/src/main/java/github/nighter/smartspawner/extras/HopperTransfer.java index 5d0ef16b..663beb4c 100644 --- a/core/src/main/java/github/nighter/smartspawner/extras/HopperTransfer.java +++ b/core/src/main/java/github/nighter/smartspawner/extras/HopperTransfer.java @@ -6,6 +6,7 @@ import github.nighter.smartspawner.spawner.properties.SpawnerData; import github.nighter.smartspawner.spawner.properties.VirtualInventory; import github.nighter.smartspawner.utils.BlockPos; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; @@ -64,36 +65,47 @@ private void transferItems(Location hopperLoc, Location spawnerLoc) { var state = hopperLoc.getBlock().getState(false); if (!(state instanceof Hopper hopper)) return; - Map displayItems = virtualInv.getDisplayInventory(); - if (displayItems == null || displayItems.isEmpty()) return; - Inventory hopperInv = hopper.getInventory(); int transferred = 0; - + int rangeStart = 0; + int rangeSize = Math.max(plugin.getHopperConfig().getStackPerTransfer(), 9); List removed = new ArrayList<>(); - for (ItemStack item : displayItems.values()) { - if (transferred >= plugin.getHopperConfig().getStackPerTransfer()) break; - if (item == null || item.getType() == Material.AIR) continue; + while (transferred < plugin.getHopperConfig().getStackPerTransfer()) { + Int2ObjectMap displayItems = virtualInv.getDisplayRange(rangeStart, rangeSize); + if (displayItems.isEmpty()) { + break; + } + + for (ItemStack item : displayItems.values()) { + if (transferred >= plugin.getHopperConfig().getStackPerTransfer()) { + break; + } + if (item == null || item.getType() == Material.AIR) { + continue; + } - ItemStack clone = item.clone(); - int originalAmount = clone.getAmount(); + ItemStack clone = item.clone(); + int originalAmount = clone.getAmount(); - HashMap leftovers = hopperInv.addItem(clone); + HashMap leftovers = hopperInv.addItem(clone); - int insertedAmount = originalAmount; + int insertedAmount = originalAmount; - if (!leftovers.isEmpty()) { - insertedAmount -= leftovers.values().iterator().next().getAmount(); - } + if (!leftovers.isEmpty()) { + insertedAmount -= leftovers.values().iterator().next().getAmount(); + } - if (insertedAmount > 0) { - ItemStack toRemove = item.clone(); - toRemove.setAmount(insertedAmount); - removed.add(toRemove); - transferred++; + if (insertedAmount > 0) { + ItemStack toRemove = item.clone(); + toRemove.setAmount(insertedAmount); + removed.add(toRemove); + transferred++; + } } + + rangeStart += rangeSize; } if (!removed.isEmpty()) { diff --git a/core/src/main/java/github/nighter/smartspawner/language/cache/LanguageCache.java b/core/src/main/java/github/nighter/smartspawner/language/cache/LanguageCache.java index b3a0a953..7488caf2 100644 --- a/core/src/main/java/github/nighter/smartspawner/language/cache/LanguageCache.java +++ b/core/src/main/java/github/nighter/smartspawner/language/cache/LanguageCache.java @@ -1,5 +1,7 @@ package github.nighter.smartspawner.language.cache; +import github.nighter.smartspawner.utils.LRUCache; + import java.util.List; import java.util.concurrent.atomic.AtomicInteger; diff --git a/core/src/main/java/github/nighter/smartspawner/language/section/GuiLanguageSection.java b/core/src/main/java/github/nighter/smartspawner/language/section/GuiLanguageSection.java index 9b9d6c4c..ed749c42 100644 --- a/core/src/main/java/github/nighter/smartspawner/language/section/GuiLanguageSection.java +++ b/core/src/main/java/github/nighter/smartspawner/language/section/GuiLanguageSection.java @@ -1,6 +1,6 @@ package github.nighter.smartspawner.language.section; -import github.nighter.smartspawner.language.cache.LRUCache; +import github.nighter.smartspawner.utils.LRUCache; import github.nighter.smartspawner.language.format.ColorUtil; import github.nighter.smartspawner.language.format.LanguageComponentFormatter; diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/data/SpawnerFileHandler.java b/core/src/main/java/github/nighter/smartspawner/spawner/data/SpawnerFileHandler.java index bbf104e8..2fae264a 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/data/SpawnerFileHandler.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/data/SpawnerFileHandler.java @@ -438,13 +438,7 @@ private SpawnerData loadSpawnerFromConfig(String spawnerId, boolean logErrors, b int amount = entry.getValue(); if (item != null && amount > 0) { - while (amount > 0) { - int batchSize = Math.min(amount, item.getMaxStackSize()); - ItemStack batch = item.clone(); - batch.setAmount(batchSize); - virtualInv.addItems(Collections.singletonList(batch)); - amount -= batchSize; - } + virtualInv.addItem(item, amount); } } } catch (Exception e) { diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/data/database/SpawnerDatabaseHandler.java b/core/src/main/java/github/nighter/smartspawner/spawner/data/database/SpawnerDatabaseHandler.java index 22ee73dd..4eb923b0 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/data/database/SpawnerDatabaseHandler.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/data/database/SpawnerDatabaseHandler.java @@ -680,13 +680,7 @@ private void loadInventoryFromJson(String jsonData, VirtualInventory virtualInv) int amount = entry.getValue(); if (item != null && amount > 0) { - while (amount > 0) { - int batchSize = Math.min(amount, item.getMaxStackSize()); - ItemStack batch = item.clone(); - batch.setAmount(batchSize); - virtualInv.addItems(Collections.singletonList(batch)); - amount -= batchSize; - } + virtualInv.addItem(item, amount); } } } catch (Exception e) { 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..2de08278 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 @@ -14,6 +14,7 @@ import github.nighter.smartspawner.spawner.properties.SpawnerData; import github.nighter.smartspawner.Scheduler; import github.nighter.smartspawner.Scheduler.Task; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import lombok.Getter; import net.kyori.adventure.text.Component; import org.bukkit.inventory.Inventory; @@ -283,30 +284,20 @@ public void updateDisplay(Inventory inventory, SpawnerData spawner, int page, in } } - private void addPageItems(Map updates, Set slotsToEmpty, - SpawnerData spawner, int page) { + private void addPageItems(Map updates, Set slotsToEmpty, SpawnerData spawner, int page) { try { - // Get display items directly from VirtualInventory (source of truth) + // Read only the requested page instead of materializing the full logical inventory. VirtualInventory virtualInv = spawner.getVirtualInventory(); - Map displayItems = virtualInv.getDisplayInventory(); + Int2ObjectMap displayItems = virtualInv.getDisplayPage(page, StoragePageHolder.MAX_ITEMS_PER_PAGE); if (displayItems.isEmpty()) { return; } - // Calculate start index for current page - int startIndex = (page - 1) * StoragePageHolder.MAX_ITEMS_PER_PAGE; - - // Add items for this page - for (Map.Entry entry : displayItems.entrySet()) { - int globalIndex = entry.getKey(); - - // Check if item belongs on this page - if (globalIndex >= startIndex && globalIndex < startIndex + StoragePageHolder.MAX_ITEMS_PER_PAGE) { - int displaySlot = globalIndex - startIndex; - updates.put(displaySlot, entry.getValue()); - slotsToEmpty.remove(displaySlot); - } + for (Int2ObjectMap.Entry entry : displayItems.int2ObjectEntrySet()) { + int displaySlot = entry.getIntKey(); + updates.put(displaySlot, entry.getValue()); + slotsToEmpty.remove(displaySlot); } } finally { spawner.getInventoryLock().unlock(); diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/button/NavigationButtonCache.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/button/NavigationButtonCache.java new file mode 100644 index 00000000..05641c33 --- /dev/null +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/button/NavigationButtonCache.java @@ -0,0 +1,129 @@ +package github.nighter.smartspawner.spawner.gui.storage.button; + +import github.nighter.smartspawner.language.LanguageManager; +import github.nighter.smartspawner.spawner.gui.layout.GuiButton; +import github.nighter.smartspawner.spawner.gui.layout.GuiLayout; +import github.nighter.smartspawner.utils.LRUCache; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +public final class NavigationButtonCache { + private static final int CACHE_SIZE = 512; + + private final LRUCache previousButtons = new LRUCache<>(CACHE_SIZE); + private final LRUCache nextButtons = new LRUCache<>(CACHE_SIZE); + private final Function buttonFactory; + + private String previousButtonName; + private String nextButtonName; + private List previousButtonLore = Collections.emptyList(); + private List nextButtonLore = Collections.emptyList(); + private Material previousButtonMaterial; + private Material nextButtonMaterial; + + public NavigationButtonCache(Function buttonFactory) { + this.buttonFactory = buttonFactory; + } + + public void reload(GuiLayout layout, LanguageManager languageManager) { + clear(); + previousButtonName = languageManager.getGuiItemName("navigation_button_previous.name"); + nextButtonName = languageManager.getGuiItemName("navigation_button_next.name"); + previousButtonLore = languageManager.getGuiItemLoreAsList("navigation_button_previous.lore"); + nextButtonLore = languageManager.getGuiItemLoreAsList("navigation_button_next.lore"); + previousButtonMaterial = null; + nextButtonMaterial = null; + + for (GuiButton button : layout.getAllButtons().values()) { + String action = getAnyActionFromButton(button); + if (action == null) { + continue; + } + + switch (action) { + case "previous_page" -> previousButtonMaterial = button.getMaterial(); + case "next_page" -> nextButtonMaterial = button.getMaterial(); + } + } + } + + public ItemStack getPreviousButton(int targetPage) { + return previousButtons.get(targetPage, this::createPreviousButton); + } + + public ItemStack getNextButton(int targetPage) { + return nextButtons.get(targetPage, this::createNextButton); + } + + public void clear() { + previousButtons.clear(); + nextButtons.clear(); + } + + private ItemStack createPreviousButton(int targetPage) { + return createButton(previousButtonMaterial, previousButtonName, previousButtonLore, targetPage); + } + + private ItemStack createNextButton(int targetPage) { + return createButton(nextButtonMaterial, nextButtonName, nextButtonLore, targetPage); + } + + private ItemStack createButton(Material material, String name, List lore, int targetPage) { + String targetPageText = String.valueOf(targetPage); + return buttonFactory.apply( + new ButtonData( + material, + replaceTargetPage(name, targetPageText), + replaceTargetPage(lore, targetPageText) + ) + ); + } + + private String replaceTargetPage(String text, String targetPage) { + return text != null ? text.replace("{target_page}", targetPage) : null; + } + + private List replaceTargetPage(List lore, String targetPage) { + if (lore.isEmpty()) { + return Collections.emptyList(); + } + + List replacedLore = new ArrayList<>(lore.size()); + for (String line : lore) { + replacedLore.add(line.replace("{target_page}", targetPage)); + } + return replacedLore; + } + + private String getAnyActionFromButton(GuiButton button) { + Map actions = button.getActions(); + if (actions == null || actions.isEmpty()) { + return null; + } + + String action = actions.get("click"); + if (action != null && !action.isEmpty()) { + return action; + } + + action = actions.get("left_click"); + if (action != null && !action.isEmpty()) { + return action; + } + + action = actions.get("right_click"); + if (action != null && !action.isEmpty()) { + return action; + } + + return null; + } + + public record ButtonData(Material material, String name, List lore) {} +} diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/button/SortButton.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/button/SortButton.java new file mode 100644 index 00000000..bed4523c --- /dev/null +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/button/SortButton.java @@ -0,0 +1,135 @@ +package github.nighter.smartspawner.spawner.gui.storage.button; + +import github.nighter.smartspawner.language.LanguageManager; +import github.nighter.smartspawner.utils.LRUCache; +import github.nighter.smartspawner.spawner.lootgen.loot.EntityLootConfig; +import github.nighter.smartspawner.spawner.lootgen.loot.LootItem; +import github.nighter.smartspawner.spawner.properties.SpawnerData; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.*; +import java.util.function.Function; + +public final class SortButton { + + private static final int SORT_BUTTON_CACHE_SIZE = 256; + private static final LRUCache SORT_BUTTON_CACHE = new LRUCache<>(SORT_BUTTON_CACHE_SIZE); + + private static final EnumMap MATERIAL_NAME_CACHE = new EnumMap<>(Material.class); + + private SortButton() {} + + public static ItemStack getOrBuildSortButton(SpawnerData spawner, Material buttonMaterial, + LanguageManager languageManager, Function buttonFactory) { + + EntityLootConfig lootConfig = spawner.getLootConfig(); + + return SORT_BUTTON_CACHE.get( + new SortButtonCacheKey( + lootConfig, + spawner.getPreferredSortItem(), + buttonMaterial + ), + key -> buildSortButton( + lootConfig, + key.selectedMaterial, + key.buttonMaterial, + languageManager, + buttonFactory + ) + ); + } + + private static ItemStack buildSortButton(EntityLootConfig lootConfig, Material currentSort, Material buttonMaterial, + LanguageManager languageManager, Function buttonFactory) { + + String selectedItemFormat = languageManager.getGuiItemName("sort_items_button.selected_item"); + String unselectedItemFormat = languageManager.getGuiItemName("sort_items_button.unselected_item"); + String noneText = languageManager.getGuiItemName("sort_items_button.no_item"); + + String availableItemsString; + + if (lootConfig != null && lootConfig.getAllItems() != null && !lootConfig.getAllItems().isEmpty()) { + + List sortedLoot = new ArrayList<>(lootConfig.getAllItems()); + + sortedLoot.sort(Comparator.comparing(item -> item.material().name())); + + StringBuilder availableItems = new StringBuilder(sortedLoot.size() * 32); + + boolean first = true; + + for (LootItem lootItem : sortedLoot) { + Material lootMaterial = lootItem.material(); + + if (!first) { + availableItems.append('\n'); + } + + String itemName = MATERIAL_NAME_CACHE.computeIfAbsent( + lootMaterial, + languageManager::getVanillaItemName + ); + + String format = currentSort == lootMaterial + ? selectedItemFormat + : unselectedItemFormat; + + availableItems.append(format.replace("{item_name}", itemName)); + + first = false; + } + + availableItemsString = availableItems.toString(); + } else { + availableItemsString = noneText; + } + + Map placeholders = new HashMap<>(1); + placeholders.put("available_items", availableItemsString); + + return buttonFactory.apply( + new ButtonData(buttonMaterial, + languageManager.getGuiItemName("sort_items_button.name", placeholders), + languageManager.getGuiItemLoreWithMultilinePlaceholders("sort_items_button.lore", placeholders) + ) + ); + } + + public record ButtonData(Material material, String name, List lore) {} + + private static final class SortButtonCacheKey { + private final EntityLootConfig lootConfig; + private final Material selectedMaterial; + private final Material buttonMaterial; + private final int hashCode; + + private SortButtonCacheKey(EntityLootConfig lootConfig, Material selectedMaterial, Material buttonMaterial) { + this.lootConfig = lootConfig; + this.selectedMaterial = selectedMaterial; + this.buttonMaterial = buttonMaterial; + + int hash = System.identityHashCode(lootConfig); + hash = 31 * hash + (selectedMaterial != null ? selectedMaterial.ordinal() : -1); + hash = 31 * hash + buttonMaterial.ordinal(); + + this.hashCode = hash; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SortButtonCacheKey other)) return false; + + return lootConfig == other.lootConfig + && selectedMaterial == other.selectedMaterial + && buttonMaterial == other.buttonMaterial; + } + + @Override + public int hashCode() { + return hashCode; + } + } +} diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/utils/LootPreGenerationHelper.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/utils/LootPreGenerationHelper.java index e9fc095f..b44526e7 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/utils/LootPreGenerationHelper.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/utils/LootPreGenerationHelper.java @@ -2,11 +2,11 @@ import github.nighter.smartspawner.SmartSpawner; import github.nighter.smartspawner.Scheduler; +import github.nighter.smartspawner.spawner.properties.ItemSignature; import github.nighter.smartspawner.spawner.properties.SpawnerData; import org.bukkit.Location; -import org.bukkit.inventory.ItemStack; -import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; /** @@ -116,7 +116,7 @@ public void addPreGeneratedLootEarly(SpawnerData spawner, long cachedDelay) { } if (spawner.hasPreGeneratedLoot()) { - List items = spawner.getAndClearPreGeneratedItems(); + Map items = spawner.getAndClearPreGeneratedItems(); long exp = spawner.getAndClearPreGeneratedExperience(); // Add the loot with scheduled spawn time for accurate timer reset diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/LootResult.java b/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/LootResult.java index 38e7ff3e..55bc6212 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/LootResult.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/LootResult.java @@ -1,8 +1,7 @@ package github.nighter.smartspawner.spawner.lootgen; -import org.bukkit.inventory.ItemStack; +import github.nighter.smartspawner.spawner.properties.ItemSignature; -import java.util.List; +import java.util.Map; -public record LootResult(List items, long experience) { -} \ No newline at end of file +public record LootResult(Map items, long experience) {} diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/SpawnerLootGenerator.java b/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/SpawnerLootGenerator.java index 0d645a86..0cc7d026 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/SpawnerLootGenerator.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/SpawnerLootGenerator.java @@ -15,7 +15,6 @@ import java.util.*; import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.atomic.AtomicInteger; public class SpawnerLootGenerator { private final SmartSpawner plugin; @@ -61,19 +60,17 @@ public void spawnLootToSpawner(SpawnerData spawner) { final long spawnTime; final int minMobs; final int maxMobs; - final AtomicInteger usedSlots; - final AtomicInteger maxSlots; try { // Timing is now managed by SpawnerRangeChecker (timer) and SpawnerGuiViewManager (spawn trigger) // No need for time check here since spawn is only called when timer expires // Get exact inventory slot usage - usedSlots = new AtomicInteger(spawner.getVirtualInventory().getUsedSlots()); - maxSlots = new AtomicInteger(spawner.getMaxSpawnerLootSlots()); + int usedSlots = spawner.getVirtualInventory().getUsedSlots(); + int maxSlots = spawner.getMaxSpawnerLootSlots(); // Check if both inventory and exp are full, only then skip loot generation - if (usedSlots.get() >= maxSlots.get() && spawner.getSpawnerExp() >= spawner.getMaxStoredExp()) { + if (usedSlots >= maxSlots && spawner.getSpawnerExp() >= spawner.getMaxStoredExp()) { if (!spawner.getIsAtCapacity()) { spawner.setIsAtCapacity(true); } @@ -127,24 +124,17 @@ public void spawnLootToSpawner(SpawnerData spawner) { } } - // Re-check max slots as it could have changed - maxSlots.set(spawner.getMaxSpawnerLootSlots()); - usedSlots.set(spawner.getVirtualInventory().getUsedSlots()); - - // Process items if there are any to add and inventory isn't completely full - if (!loot.items().isEmpty() && usedSlots.get() < maxSlots.get()) { - List itemsToAdd = new ArrayList<>(loot.items()); - - // Get exact calculation of slots with the new items - int totalRequiredSlots = calculateRequiredSlots(itemsToAdd, spawner.getVirtualInventory()); + if (!loot.items().isEmpty()) { + Map lootToAdd = loot.items(); + int maxSlots = spawner.getMaxSpawnerLootSlots(); - // If we'll exceed the limit, limit the items we're adding - if (totalRequiredSlots > maxSlots.get()) { - itemsToAdd = limitItemsToAvailableSlots(itemsToAdd, spawner); + int totalRequiredSlots = calculateRequiredSlots(lootToAdd, spawner.getVirtualInventory()); + if (totalRequiredSlots > maxSlots) { + lootToAdd = limitLootToAvailableSlots(lootToAdd, spawner); } - if (!itemsToAdd.isEmpty()) { - spawner.addItemsAndUpdateSellValue(itemsToAdd); + if (!lootToAdd.isEmpty()) { + spawner.addItemsAndUpdateSellValue(lootToAdd); changed = true; } } @@ -184,67 +174,53 @@ public void spawnLootToSpawner(SpawnerData spawner) { } public LootResult generateLoot(int minMobs, int maxMobs, SpawnerData spawner) { - int mobCount = ThreadLocalRandom.current().nextInt(maxMobs - minMobs + 1) + minMobs; long totalExperience = (long) spawner.getEntityExperienceValue() * mobCount; // Get valid items from the spawner's EntityLootConfig - List validItems = spawner.getValidLootItems(); + List validItems = spawner.getValidLootItems(); if (validItems.isEmpty()) { - return new LootResult(Collections.emptyList(), totalExperience); + return new LootResult(Collections.emptyMap(), totalExperience); } // Use a Map to consolidate identical drops instead of List - Map consolidatedLoot = new HashMap<>(); + Map consolidatedLoot = new HashMap<>(); + + boolean shouldApproximateLoot = Config.get().isApproximateLoot(); + int approximationThreshold = Config.get().getApproximationThreshold(); // Process mobs in batch rather than individually for (LootItem lootItem : validItems) { - // Calculate the probability for the entire mob batch at once - int totalAmount; + long totalAmount; - if (Config.get().isApproximateLoot() && shouldApproximate(lootItem.chance(), mobCount)) { - // O(1) binomial approximation + if (shouldApproximateLoot && shouldApproximate(lootItem.chance(), mobCount, approximationThreshold)) { totalAmount = generateApproximatedLoot(lootItem, mobCount); } else { - // O(n) binomial distribution totalAmount = generateExactLoot(lootItem, mobCount); } - if (totalAmount > 0) { - // Create item just once per loot type - ItemStack prototype = lootItem.createItemStack(); - if (prototype != null) { - consolidatedLoot.merge(prototype, totalAmount, Integer::sum); - } + if (totalAmount <= 0) { + continue; } - } - // Convert consolidated map to item stacks - List finalLoot = new ArrayList<>(consolidatedLoot.size()); - for (Map.Entry entry : consolidatedLoot.entrySet()) { - ItemStack item = entry.getKey().clone(); - item.setAmount(Math.min(entry.getValue(), item.getMaxStackSize())); - finalLoot.add(item); - - // Handle amounts exceeding max stack size - int remaining = entry.getValue() - item.getMaxStackSize(); - while (remaining > 0) { - ItemStack extraStack = item.clone(); - extraStack.setAmount(Math.min(remaining, item.getMaxStackSize())); - finalLoot.add(extraStack); - remaining -= extraStack.getAmount(); + ItemStack prototype = lootItem.createItemStack(); + if (prototype == null || prototype.getType() == Material.AIR) { + continue; } + + ItemSignature signature = VirtualInventory.getSignature(prototype); + consolidatedLoot.merge(signature, totalAmount, Long::sum); } - return new LootResult(finalLoot, totalExperience); + return new LootResult(consolidatedLoot, totalExperience); } // Determines whether to use expected-value approximation - private boolean shouldApproximate(double chance, int mobCount) { + private boolean shouldApproximate(double chance, int mobCount, int approximationThreshold) { // simple heuristic: use expected if at least threshold items can be generated if (chance <= 0D) return false; - return mobCount > (97.5D / chance) * Config.get().getApproximationThreshold(); + return mobCount > (97.5D / chance) * approximationThreshold; } // O(n) simulation: exact per-mob drop calculation @@ -275,99 +251,111 @@ private int generateApproximatedLoot(LootItem lootItem, int mobCount) { return (int) Math.round(expectedDrops * avgAmount * jitter); } - private List limitItemsToAvailableSlots(List items, SpawnerData spawner) { - VirtualInventory currentInventory = spawner.getVirtualInventory(); + private Map limitLootToAvailableSlots(Map loot, SpawnerData spawner) { + VirtualInventory inventory = spawner.getVirtualInventory(); + int maxSlots = spawner.getMaxSpawnerLootSlots(); - // If already full, return empty list - if (currentInventory.getUsedSlots() >= maxSlots) { - return Collections.emptyList(); + if (maxSlots <= 0) { + return Collections.emptyMap(); } - // Create a simulation inventory - Map simulatedInventory = new HashMap<>(currentInventory.getConsolidatedItems()); - List acceptedItems = new ArrayList<>(); + Map simulatedInventory = new HashMap<>(inventory.getConsolidatedItems()); + Map acceptedLoot = new HashMap<>(loot.size()); - // Sort items by priority (you can change this sorting strategy) - items.sort(Comparator.comparing(item -> item.getType().name())); + int usedSlots = calculateSlots(simulatedInventory); - for (ItemStack item : items) { - if (item == null || item.getAmount() <= 0) continue; + List> entries = new ArrayList<>(loot.entrySet()); - // Add to simulation and check slot count - Map tempSimulation = new HashMap<>(simulatedInventory); - // Use cached signature to avoid excessive cloning - ItemSignature sig = VirtualInventory.getSignature(item); - tempSimulation.merge(sig, (long) item.getAmount(), (a, b) -> a + b); + entries.sort(Comparator.comparing(entry -> entry.getKey().getMaterial().name())); - // Calculate slots needed - int slotsNeeded = calculateSlots(tempSimulation); + for (Map.Entry entry : entries) { + ItemSignature signature = entry.getKey(); - // If we still have room, accept this item - if (slotsNeeded <= maxSlots) { - acceptedItems.add(item); - simulatedInventory = tempSimulation; // Update simulation - } else { - // Try to accept a partial amount of this item - int maxStackSize = item.getMaxStackSize(); - long currentAmount = simulatedInventory.getOrDefault(sig, 0L); - - // Calculate how many we can add without exceeding slot limit - int remainingSlots = maxSlots - calculateSlots(simulatedInventory); - if (remainingSlots > 0) { - // Maximum items we can add in the remaining slots - long maxAddAmount = (long) remainingSlots * maxStackSize - (currentAmount % maxStackSize); - if (maxAddAmount > 0) { - // Create a partial item - ItemStack partialItem = item.clone(); - partialItem.setAmount((int) Math.min(maxAddAmount, item.getAmount())); - acceptedItems.add(partialItem); - - // Update simulation - simulatedInventory.merge(sig, (long) partialItem.getAmount(), (a, b) -> a + b); - } - } + long amount = entry.getValue(); + + int maxStackSize = signature.getMaxStackSize(); + + long currentAmount = simulatedInventory.getOrDefault(signature, 0L); + + int oldSlots = slotsFor(currentAmount, maxStackSize); + int newSlots = slotsFor(currentAmount + amount, maxStackSize); + + int slotDelta = newSlots - oldSlots; + + if (usedSlots + slotDelta <= maxSlots) { + acceptedLoot.put(signature, amount); + + simulatedInventory.put(signature, currentAmount + amount); + + usedSlots += slotDelta; + + continue; + } + + int remainingSlots = Math.max(0, maxSlots - usedSlots); + long maxAddAmount = ((long) (oldSlots + remainingSlots) * maxStackSize) - currentAmount; + + if (maxAddAmount <= 0) { + continue; + } + + long acceptedAmount = (int) Math.min(maxAddAmount, amount); - // We've filled all slots, stop processing - break; + if (acceptedAmount > 0) { + acceptedLoot.put(signature, acceptedAmount); + simulatedInventory.put(signature, currentAmount + acceptedAmount); + usedSlots = calculateSlots(simulatedInventory); } } - return acceptedItems; + return acceptedLoot; } - private int calculateSlots(Map items) { - // Use a more efficient calculation approach - return items.entrySet().stream() - .mapToInt(entry -> { - long amount = entry.getValue(); - int maxStackSize = entry.getKey().getMaxStackSize(); - // Use integer division with ceiling function - return (int) ((amount + maxStackSize - 1) / maxStackSize); - }) - .sum(); + private int calculateRequiredSlots(Map loot, VirtualInventory inventory) { + Map simulatedItems = new HashMap<>(inventory.getConsolidatedItems()); + + for (Map.Entry entry : loot.entrySet()) { + simulatedItems.merge(entry.getKey(), (long) entry.getValue(), Long::sum); + } + + return calculateSlots(simulatedItems); } - private int calculateRequiredSlots(List items, VirtualInventory inventory) { - // Create a temporary map to simulate how items would stack - Map simulatedItems = new HashMap<>(); + private int calculateSlots(Map items) { + int total = 0; - // First, get existing items if we need to account for them - if (inventory != null) { - simulatedItems.putAll(inventory.getConsolidatedItems()); + for (Map.Entry entry : items.entrySet()) { + total += slotsFor(entry.getValue(), entry.getKey().getMaxStackSize()); } - // Add the new items to our simulation - for (ItemStack item : items) { - if (item == null || item.getAmount() <= 0) continue; + return total; + } - // Use cached signature to avoid excessive cloning - ItemSignature sig = VirtualInventory.getSignature(item); - simulatedItems.merge(sig, (long) item.getAmount(), (a, b) -> a + b); + private int slotsFor(long amount, int maxStackSize) { + if (amount <= 0) { + return 0; } - // Calculate exact slots needed - return calculateSlots(simulatedItems); + return (int) ((amount + maxStackSize - 1) / maxStackSize); + } + + private Map copyLoot(Map loot) { + if (loot == null || loot.isEmpty()) { + return Collections.emptyMap(); + } + + Map copy = new HashMap<>(loot.size()); + for (Map.Entry entry : loot.entrySet()) { + ItemSignature signature = entry.getKey(); + Long amount = entry.getValue(); + if (amount <= 0) { + continue; + } + copy.merge(signature, amount, Long::sum); + } + + return copy; } /** @@ -418,34 +406,31 @@ private void handleGuiUpdates(SpawnerData spawner) { */ public void preGenerateLoot(SpawnerData spawner, LootGenerationCallback callback) { if (!spawner.getLootGenerationLock().tryLock()) { - callback.onLootGenerated(Collections.emptyList(), 0); + callback.onLootGenerated(Collections.emptyMap(), 0); return; } try { try { if (!spawner.getDataLock().tryLock(50, java.util.concurrent.TimeUnit.MILLISECONDS)) { - callback.onLootGenerated(Collections.emptyList(), 0); + callback.onLootGenerated(Collections.emptyMap(), 0); return; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); - callback.onLootGenerated(Collections.emptyList(), 0); + callback.onLootGenerated(Collections.emptyMap(), 0); return; } final int minMobs; final int maxMobs; - final boolean itemStorageFull; - try { int usedSlots = spawner.getVirtualInventory().getUsedSlots(); int maxSlots = spawner.getMaxSpawnerLootSlots(); - itemStorageFull = usedSlots >= maxSlots; - boolean atCapacity = itemStorageFull && spawner.getSpawnerExp() >= spawner.getMaxStoredExp(); + boolean atCapacity = usedSlots >= maxSlots && spawner.getSpawnerExp() >= spawner.getMaxStoredExp(); if (atCapacity) { - callback.onLootGenerated(Collections.emptyList(), 0); + callback.onLootGenerated(Collections.emptyMap(), 0); return; } @@ -456,15 +441,10 @@ public void preGenerateLoot(SpawnerData spawner, LootGenerationCallback callback } Scheduler.runTaskAsync(() -> { - LootResult loot; - if (itemStorageFull) { - loot = generateExperienceOnlyLoot(minMobs, maxMobs, spawner); - } else { - loot = generateLoot(minMobs, maxMobs, spawner); - } + LootResult loot = generateLoot(minMobs, maxMobs, spawner); callback.onLootGenerated( - loot.items() != null ? new ArrayList<>(loot.items()) : Collections.emptyList(), + copyLoot(loot.items()), loot.experience() ); }); @@ -473,13 +453,6 @@ public void preGenerateLoot(SpawnerData spawner, LootGenerationCallback callback } } - private LootResult generateExperienceOnlyLoot(int minMobs, int maxMobs, SpawnerData spawner) { - int mobCount = ThreadLocalRandom.current().nextInt(maxMobs - minMobs + 1) + minMobs; - long totalExperienceLong = (long) spawner.getEntityExperienceValue() * mobCount; - long totalExperience = Math.min(totalExperienceLong, Long.MAX_VALUE); - return new LootResult(Collections.emptyList(), totalExperience); - } - /** * Adds pre-generated loot to spawner instantly when timer expires. * @@ -495,10 +468,10 @@ private LootResult generateExperienceOnlyLoot(int minMobs, int maxMobs, SpawnerD *

Thread Safety: All Bukkit API calls are scheduled on main thread via Scheduler.runLocationTask * * @param spawner The spawner to add loot to - * @param items Pre-generated items list + * @param items Pre-generated items map * @param experience Pre-generated experience amount */ - public void addPreGeneratedLoot(SpawnerData spawner, List items, long experience) { + public void addPreGeneratedLoot(SpawnerData spawner, Map items, long experience) { addPreGeneratedLoot(spawner, items, experience, System.currentTimeMillis()); } @@ -507,11 +480,11 @@ public void addPreGeneratedLoot(SpawnerData spawner, List items, long * Used for early loot addition to prevent timer stutter. * * @param spawner The spawner to add loot to - * @param items Pre-generated items list + * @param items Pre-generated items map * @param experience Pre-generated experience amount * @param spawnTime The spawn time to set (for timer accuracy) */ - public void addPreGeneratedLoot(SpawnerData spawner, List items, long experience, long spawnTime) { + public void addPreGeneratedLoot(SpawnerData spawner, Map items, long experience, long spawnTime) { if ((items == null || items.isEmpty()) && experience == 0) { return; } @@ -564,29 +537,19 @@ public void addPreGeneratedLoot(SpawnerData spawner, List items, long } if (items != null && !items.isEmpty()) { - List validItems = new ArrayList<>(); - for (ItemStack item : items) { - if (item != null && item.getType() != Material.AIR) { - validItems.add(item.clone()); - } - } + Map lootToAdd = copyLoot(items); - if (!validItems.isEmpty()) { - int usedSlots = spawner.getVirtualInventory().getUsedSlots(); + if (!lootToAdd.isEmpty()) { int maxSlots = spawner.getMaxSpawnerLootSlots(); - if (usedSlots < maxSlots) { - List itemsToAdd = validItems; - - int totalRequiredSlots = calculateRequiredSlots(itemsToAdd, spawner.getVirtualInventory()); - if (totalRequiredSlots > maxSlots) { - itemsToAdd = limitItemsToAvailableSlots(itemsToAdd, spawner); - } + int totalRequiredSlots = calculateRequiredSlots(lootToAdd, spawner.getVirtualInventory()); + if (totalRequiredSlots > maxSlots) { + lootToAdd = limitLootToAvailableSlots(lootToAdd, spawner); + } - if (!itemsToAdd.isEmpty()) { - spawner.addItemsAndUpdateSellValue(itemsToAdd); - changed = true; - } + if (!lootToAdd.isEmpty()) { + spawner.addItemsAndUpdateSellValue(lootToAdd); + changed = true; } } } @@ -622,9 +585,9 @@ public interface LootGenerationCallback { /** * Called when loot generation completes. * - * @param items Generated items list (never null, may be empty) + * @param items Generated items map (never null, may be empty) * @param experience Generated experience amount */ - void onLootGenerated(List items, long experience); + void onLootGenerated(Map items, long experience); } } diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/SpawnerRangeChecker.java b/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/SpawnerRangeChecker.java index 8e3ce3e5..bb7e29f9 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/SpawnerRangeChecker.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/SpawnerRangeChecker.java @@ -2,13 +2,13 @@ import github.nighter.smartspawner.SmartSpawner; import github.nighter.smartspawner.spawner.data.SpawnerManager; +import github.nighter.smartspawner.spawner.properties.ItemSignature; import github.nighter.smartspawner.spawner.properties.SpawnerData; import github.nighter.smartspawner.Scheduler; import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; import java.util.*; import java.util.concurrent.ExecutorService; @@ -207,7 +207,7 @@ private void checkAndSpawnLoot(SpawnerData spawner) { // Spawn loot (pre-generated if available, otherwise generate new) if (spawner.hasPreGeneratedLoot()) { - List items = spawner.getAndClearPreGeneratedItems(); + Map items = spawner.getAndClearPreGeneratedItems(); long exp = spawner.getAndClearPreGeneratedExperience(); plugin.getSpawnerLootGenerator().addPreGeneratedLoot(spawner, items, exp); } else { @@ -245,4 +245,3 @@ public void cleanup() { } } } - diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/properties/ItemSignature.java b/core/src/main/java/github/nighter/smartspawner/spawner/properties/ItemSignature.java index 9c20e3a3..3bf7edf4 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/properties/ItemSignature.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/properties/ItemSignature.java @@ -28,7 +28,6 @@ public ItemSignature(ItemStack item) { this.hashCode = calculateHashCode(meta); } - // Replace the current calculateHashCode() method with: private int calculateHashCode(ItemMeta meta) { // Use a faster hash algorithm and cache more item properties int result = 31 * this.material.ordinal(); // Using ordinal() instead of name() hashing diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/properties/SpawnerData.java b/core/src/main/java/github/nighter/smartspawner/spawner/properties/SpawnerData.java index efd6a6c6..dcf9171b 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/properties/SpawnerData.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/properties/SpawnerData.java @@ -1,5 +1,6 @@ package github.nighter.smartspawner.spawner.properties; +import com.google.common.util.concurrent.AtomicDouble; import github.nighter.smartspawner.SmartSpawner; import github.nighter.smartspawner.commands.hologram.SpawnerHologram; import github.nighter.smartspawner.spawner.lootgen.loot.EntityLootConfig; @@ -14,6 +15,7 @@ import org.bukkit.inventory.meta.ItemMeta; import java.util.*; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; @@ -80,7 +82,7 @@ public class SpawnerData { // Calculated values based on stackSize @Getter private int maxStoragePages; - @Getter @Setter + @Getter private int maxSpawnerLootSlots; @Getter @Setter private long maxStoredExp; @@ -94,7 +96,7 @@ public class SpawnerData { @Getter @Setter private int maxStackSize; - @Getter @Setter + @Getter private VirtualInventory virtualInventory; @Getter private final Set filteredItems = new HashSet<>(); @@ -108,8 +110,7 @@ public class SpawnerData { private boolean lastSellProcessed; // Accumulated sell value for optimization - @Getter - private volatile double accumulatedSellValue; + private AtomicDouble accumulatedSellValue; @Getter private volatile boolean sellValueDirty; @@ -123,7 +124,7 @@ public class SpawnerData { private Material preferredSortItem; // CRITICAL: Pre-generated loot storage for better UX - access must be synchronized via lootGenerationLock - private volatile List preGeneratedItems; + private volatile Map preGeneratedItems; private volatile long preGeneratedExperience; private volatile boolean isPreGenerating; @@ -167,7 +168,7 @@ private void initializeDefaults() { this.stackSize = 1; this.lastSpawnTime = System.currentTimeMillis(); this.preferredSortItem = null; // Initialize sort preference as null - this.accumulatedSellValue = 0.0; + this.accumulatedSellValue = new AtomicDouble(0); this.sellValueDirty = true; } @@ -191,9 +192,7 @@ public void loadConfigurationValues() { public void recalculateAfterConfigReload() { calculateStackBasedValues(); - if (virtualInventory != null && virtualInventory.getMaxSlots() != maxSpawnerLootSlots) { - recreateVirtualInventory(); - } + // Mark sell value as dirty after config reload since prices may have changed this.sellValueDirty = true; updateHologramData(); @@ -213,9 +212,7 @@ public void recalculateAfterConfigReload() { */ public void recalculateAfterAPIModification() { calculateStackBasedValues(); - if (virtualInventory != null && virtualInventory.getMaxSlots() != maxSpawnerLootSlots) { - recreateVirtualInventory(); - } + updateHologramData(); // Invalidate GUI cache after API modifications @@ -230,12 +227,26 @@ public void recalculateAfterAPIModification() { private void calculateStackBasedValues() { this.maxStoredExp = clampToLong(baseMaxStoredExp * stackSize, 0L, Long.MAX_VALUE); this.maxStoragePages = clampToInt((long) baseMaxStoragePages * stackSize, 0, Integer.MAX_VALUE); - this.maxSpawnerLootSlots = clampToInt((long) maxStoragePages * 45L, 0, Integer.MAX_VALUE); + setMaxSpawnerLootSlots(clampToInt((long) maxStoragePages * 45L, 0, Integer.MAX_VALUE)); this.minMobs = clampToInt((long) baseMinMobs * stackSize, 0, Integer.MAX_VALUE); this.maxMobs = clampToInt((long) baseMaxMobs * stackSize, 0, Integer.MAX_VALUE); this.spawnerExp = clampToLong(this.spawnerExp, 0L, this.maxStoredExp); } + public void setMaxSpawnerLootSlots(int maxSpawnerLootSlots) { + this.maxSpawnerLootSlots = Math.max(0, maxSpawnerLootSlots); + if (virtualInventory != null) { + virtualInventory.setMaxSlots(this.maxSpawnerLootSlots); + } + } + + public void setVirtualInventory(VirtualInventory virtualInventory) { + this.virtualInventory = virtualInventory; + if (this.virtualInventory != null) { + this.virtualInventory.setMaxSlots(this.maxSpawnerLootSlots); + } + } + public void setSpawnDelay(long baseSpawnerDelay) { this.spawnDelay = baseSpawnerDelay > 0 ? baseSpawnerDelay : 500; long ticksWithBuffer = this.spawnDelay > Long.MAX_VALUE - 20L ? Long.MAX_VALUE : this.spawnDelay + 20L; @@ -322,9 +333,6 @@ private void updateStackSize(int newStackSize, boolean restartHopper) { this.stackSize = newStackSize; calculateStackBasedValues(); - // Resize the existing virtual inventory instead of creating a new one - virtualInventory.resize(this.maxSpawnerLootSlots); - // Reset lastSpawnTime to prevent exploit where players break spawners to trigger immediate loot this.lastSpawnTime = System.currentTimeMillis(); updateHologramData(); @@ -338,11 +346,6 @@ private void updateStackSize(int newStackSize, boolean restartHopper) { } } - private void recreateVirtualInventory() { - if (virtualInventory == null) return; - virtualInventory.resize(maxSpawnerLootSlots); - } - public void setSpawnerExp(long exp) { this.spawnerExp = Math.clamp(exp, 0L, maxStoredExp); updateHologramData(); @@ -374,6 +377,7 @@ private int clampToInt(long value, int min, int max) { return (int) value; } + // TODO: this does NOT work :cryo: private long clampToLong(long value, long min, long max) { if (value < min) { return min; @@ -551,13 +555,16 @@ public void markSellValueDirty() { this.sellValueDirty = true; } + public double getAccumulatedSellValue() { + return accumulatedSellValue.get(); + } + /** * Updates the accumulated sell value for specific items being added * @param itemsAdded Map of item signatures to quantities added * @param priceCache Price cache from loot config */ - public void incrementSellValue(Map itemsAdded, - Map priceCache) { + public void incrementSellValue(Map itemsAdded, Map priceCache) { if (itemsAdded == null || itemsAdded.isEmpty()) { return; } @@ -570,7 +577,9 @@ public void incrementSellValue(Map itemsAdded, } } - this.accumulatedSellValue += addedValue; + if (addedValue > 0.0) { + this.accumulatedSellValue.addAndGet(addedValue); + } this.sellValueDirty = false; } @@ -584,24 +593,39 @@ public void decrementSellValue(List itemsRemoved, Map return; } - // Consolidate removed items Map consolidated = new java.util.HashMap<>(); for (ItemStack item : itemsRemoved) { if (item == null || item.getAmount() <= 0) continue; - // Use cached signature to avoid excessive cloning ItemSignature sig = VirtualInventory.getSignature(item); - consolidated.merge(sig, (long) item.getAmount(), (a, b) -> a + b); + consolidated.merge(sig, (long) item.getAmount(), Long::sum); + } + + decrementSellValue(consolidated, priceCache); + } + + /** + * Decrements the accumulated sell value when already-consolidated items are removed. + * @param itemsRemoved Map of item signatures to quantities removed + * @param priceCache Price cache from loot config + */ + public void decrementSellValue(Map itemsRemoved, Map priceCache) { + if (itemsRemoved == null || itemsRemoved.isEmpty()) { + return; } double removedValue = 0.0; - for (Map.Entry entry : consolidated.entrySet()) { + for (Map.Entry entry : itemsRemoved.entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) { + continue; + } + double itemPrice = findItemPrice(entry.getKey(), priceCache); if (itemPrice > 0.0) { - removedValue += itemPrice * entry.getValue(); + removedValue += itemPrice * entry.getValue().longValue(); } } - this.accumulatedSellValue = Math.max(0.0, this.accumulatedSellValue - removedValue); + subtractAccumulatedSellValue(removedValue); } /** @@ -610,7 +634,7 @@ public void decrementSellValue(List itemsRemoved, Map */ public void recalculateSellValue() { if (lootConfig == null) { - this.accumulatedSellValue = 0.0; + this.accumulatedSellValue.set(0.0); this.sellValueDirty = false; return; } @@ -625,14 +649,27 @@ public void recalculateSellValue() { for (Map.Entry entry : items.entrySet()) { double itemPrice = findItemPrice(entry.getKey(), priceCache); if (itemPrice > 0.0) { - totalValue += itemPrice * entry.getValue(); + totalValue += itemPrice * entry.getValue().longValue(); } } - this.accumulatedSellValue = totalValue; + this.accumulatedSellValue.set(totalValue); this.sellValueDirty = false; } + private void subtractAccumulatedSellValue(double removedValue) { + if (removedValue <= 0.0) { + return; + } + + double current; + double updated; + do { + current = accumulatedSellValue.get(); + updated = Math.max(0.0, current - removedValue); + } while (!accumulatedSellValue.compareAndSet(current, updated)); + } + /** * Gets the price cache from loot config. * Prefers live prices from ItemPriceManager to avoid startup timing issues where @@ -712,12 +749,11 @@ private String createItemKey(ItemSignature itemSignature) { } /** - * Adds items to virtual inventory and updates accumulated sell value - * This is the preferred method to add items to maintain accurate sell value cache - * THREAD-SAFE: Uses inventoryLock to ensure atomicity - * @param items Items to add + * Adds already-consolidated items to virtual inventory and updates accumulated sell value. + * THREAD-SAFE: Uses inventoryLock to ensure atomicity. + * @param items Items to add, keyed by the same signature used by VirtualInventory */ - public void addItemsAndUpdateSellValue(List items) { + public void addItemsAndUpdateSellValue(Map items) { if (items == null || items.isEmpty()) { return; } @@ -725,22 +761,12 @@ public void addItemsAndUpdateSellValue(List items) { // CRITICAL: Acquire inventoryLock to ensure VirtualInventory remains source of truth inventoryLock.lock(); try { - // Consolidate items being added for efficient price lookup - Map itemsToAdd = new java.util.HashMap<>(); - for (ItemStack item : items) { - if (item == null || item.getAmount() <= 0) continue; - // Use cached signature to avoid excessive cloning - ItemSignature sig = VirtualInventory.getSignature(item); - itemsToAdd.merge(sig, (long) item.getAmount(), (a, b) -> a + b); - } - - // Add to VirtualInventory (source of truth) - this operation is atomic within the lock virtualInventory.addItems(items); // Update sell value atomically if (!sellValueDirty) { Map priceCache = createPriceCache(); - incrementSellValue(itemsToAdd, priceCache); + incrementSellValue(items, priceCache); } } finally { inventoryLock.unlock(); @@ -758,7 +784,27 @@ public boolean removeItemsAndUpdateSellValue(List items) { return true; } - // CRITICAL: Acquire inventoryLock to ensure VirtualInventory remains source of truth + Map itemsToRemove = new java.util.HashMap<>(); + for (ItemStack item : items) { + if (item == null || item.getAmount() <= 0) continue; + ItemSignature sig = VirtualInventory.getSignature(item); + itemsToRemove.merge(sig, (long) item.getAmount(), Long::sum); + } + + return removeItemsAndUpdateSellValue(itemsToRemove); + } + + /** + * Removes already-consolidated items from virtual inventory and updates accumulated sell value. + * THREAD-SAFE: Uses inventoryLock to ensure atomicity. + * @param items Items to remove, keyed by the same signature used by VirtualInventory + * @return true if items were removed successfully + */ + public boolean removeItemsAndUpdateSellValue(Map items) { + if (items == null || items.isEmpty()) { + return true; + } + inventoryLock.lock(); try { // Remove from VirtualInventory (source of truth) - atomic operation within lock @@ -776,13 +822,13 @@ public boolean removeItemsAndUpdateSellValue(List items) { } } - public synchronized void storePreGeneratedLoot(List items, long experience) { + public synchronized void storePreGeneratedLoot(Map items, long experience) { this.preGeneratedItems = items; this.preGeneratedExperience = experience; } - public synchronized List getAndClearPreGeneratedItems() { - List items = preGeneratedItems; + public synchronized Map getAndClearPreGeneratedItems() { + Map items = preGeneratedItems; preGeneratedItems = null; return items; } diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/properties/VirtualInventory.java b/core/src/main/java/github/nighter/smartspawner/spawner/properties/VirtualInventory.java index 27c29719..f56cd42a 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/properties/VirtualInventory.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/properties/VirtualInventory.java @@ -1,6 +1,10 @@ package github.nighter.smartspawner.spawner.properties; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import lombok.Getter; +import org.bukkit.Material; import org.bukkit.inventory.ItemStack; import java.util.*; @@ -8,25 +12,14 @@ public class VirtualInventory { private final Map consolidatedItems; - @Getter - private int maxSlots; - private final Map displayInventoryCache; - private boolean displayCacheDirty; - private int usedSlotsCache; - private long totalItemsCache; - private boolean metricsCacheDirty; + @Getter private int maxSlots; // Cache sorted entries to avoid resorting when display isn't changing private List> sortedEntriesCache; - private org.bukkit.Material preferredSortMaterial; + private Material preferredSortMaterial; public VirtualInventory(int maxSlots) { this.maxSlots = maxSlots; this.consolidatedItems = new ConcurrentHashMap<>(); - this.displayInventoryCache = new HashMap<>(maxSlots); // Pre-size the map - this.displayCacheDirty = true; - this.metricsCacheDirty = true; - this.usedSlotsCache = 0; - this.totalItemsCache = 0; this.sortedEntriesCache = null; this.preferredSortMaterial = null; } @@ -35,155 +28,110 @@ public static ItemSignature getSignature(ItemStack item) { return new ItemSignature(item); } - // Add items in bulk with minimal operations - public void addItems(List items) { - if (items.isEmpty()) return; - - // Pre-allocate space for batch processing - Map itemBatch = new HashMap<>(items.size()); + public void setMaxSlots(int maxSlots) { + this.maxSlots = Math.max(0, maxSlots); + } - // Consolidate all items first - for (ItemStack item : items) { - if (item == null || item.getAmount() <= 0) continue; - ItemSignature sig = getSignature(item); // Use cached signature - itemBatch.merge(sig, (long) item.getAmount(), (a, b) -> a + b); + /* + * FAST PATH + * Used for loading already-consolidated storage data. + */ + public void addItem(ItemStack item, long amount) { + if (item == null || amount <= 0) { + return; } - // Apply all changes in one operation - if (!itemBatch.isEmpty()) { - for (Map.Entry entry : itemBatch.entrySet()) { - consolidatedItems.merge(entry.getKey(), entry.getValue(), (a, b) -> a + b); - } - displayCacheDirty = true; - metricsCacheDirty = true; - sortedEntriesCache = null; - } - } - // Remove items in bulk with minimal operations - public boolean removeItems(List items) { - if (items.isEmpty()) return true; - - Map toRemove = new HashMap<>(); - - // Calculate total amounts to remove in a single pass - for (ItemStack item : items) { - if (item == null || item.getAmount() <= 0) continue; - // Use cached signature to avoid excessive cloning - ItemSignature sig = getSignature(item); - toRemove.merge(sig, (long) item.getAmount(), (a, b) -> a + b); - } + ItemSignature signature = getSignature(item); - if (toRemove.isEmpty()) return true; + consolidatedItems.merge(signature, amount, Long::sum); - // Verify we have enough of each item - for (Map.Entry entry : toRemove.entrySet()) { - Long currentAmount = consolidatedItems.getOrDefault(entry.getKey(), 0L); - if (currentAmount < entry.getValue()) { - return false; - } + sortedEntriesCache = null; + } + + /* + * Bulk insert for already-consolidated storage data. + */ + public void addItems(Map items) { + if (items == null || items.isEmpty()) { + return; } - // Perform removals all at once - boolean updated = false; - for (Map.Entry entry : toRemove.entrySet()) { - ItemSignature sig = entry.getKey(); - long amountToRemove = entry.getValue(); + boolean changed = false; - consolidatedItems.computeIfPresent(sig, (key, current) -> { - long newAmount = current - amountToRemove; - return newAmount <= 0 ? null : newAmount; - }); + for (Map.Entry entry : items.entrySet()) { + ItemSignature signature = entry.getKey(); + Long amountValue = entry.getValue(); - updated = true; - } + if (amountValue <= 0) { + continue; + } - if (updated) { - displayCacheDirty = true; - metricsCacheDirty = true; - sortedEntriesCache = null; // Invalidate sorted entries cache + consolidatedItems.merge(signature, amountValue, Long::sum); + changed = true; } - return true; + if (changed) { + sortedEntriesCache = null; + } } - // Optimized getDisplayInventory method - public Map getDisplayInventory() { - // Return cached result if available - if (!displayCacheDirty) { - // Return a shallow copy to prevent modification of the cache - return Collections.unmodifiableMap(displayInventoryCache); + public boolean removeItems(Map items) { + if (items == null || items.isEmpty()) { + return true; } - // Clear the cache for a fresh rebuild but reuse the existing map - displayInventoryCache.clear(); + Map toRemove = new HashMap<>(items.size()); - if (consolidatedItems.isEmpty()) { - displayCacheDirty = false; - usedSlotsCache = 0; - return Collections.emptyMap(); - } + for (Map.Entry entry : items.entrySet()) { + ItemSignature signature = entry.getKey(); + Number amountValue = entry.getValue(); - // Get and sort the items - only use cached sort result if available - if (sortedEntriesCache == null) { - sortedEntriesCache = new ArrayList<>(consolidatedItems.entrySet()); - // Apply preferred sort if set, otherwise sort alphabetically - if (preferredSortMaterial != null) { - sortedEntriesCache.sort((e1, e2) -> { - // Use getTemplateRef() to avoid cloning - we only need to read the type - boolean e1Preferred = e1.getKey().getMaterial() == preferredSortMaterial; - boolean e2Preferred = e2.getKey().getMaterial() == preferredSortMaterial; + if (signature == null || amountValue == null) { + continue; + } - if (e1Preferred && !e2Preferred) return -1; - if (!e1Preferred && e2Preferred) return 1; - - // Both preferred or both not preferred, sort by material name - return e1.getKey().getMaterialName().compareTo(e2.getKey().getMaterialName()); - }); - } else { - // Use optimized comparator based on cached material name - sortedEntriesCache.sort(Comparator.comparing(e -> e.getKey().getMaterialName())); + long amount = amountValue.longValue(); + if (amount <= 0) { + continue; } - } - // Process items directly to the display inventory - int currentSlot = 0; + toRemove.merge(signature, amount, Long::sum); + } - for (Map.Entry entry : sortedEntriesCache) { - if (currentSlot >= maxSlots) break; + if (toRemove.isEmpty()) { + return true; + } - ItemSignature sig = entry.getKey(); - long totalAmount = entry.getValue(); - int maxStackSize = sig.getMaxStackSize(); + for (Map.Entry entry : toRemove.entrySet()) { + if (consolidatedItems.getOrDefault(entry.getKey(), 0L) < entry.getValue()) { + return false; + } + } - // Create as many stacks as needed for this item type - while (totalAmount > 0 && currentSlot < maxSlots) { - int stackSize = (int) Math.min(totalAmount, maxStackSize); + for (Map.Entry entry : toRemove.entrySet()) { + consolidatedItems.computeIfPresent(entry.getKey(), (key, current) -> { + long remaining = current - entry.getValue(); + return remaining <= 0 ? null : remaining; + }); + } - // Create the display item only once per slot - ItemStack displayItem = sig.getTemplate(); - displayItem.setAmount(stackSize); + sortedEntriesCache = null; - // Store in cache - displayInventoryCache.put(currentSlot, displayItem); + return true; + } - totalAmount -= stackSize; - currentSlot++; - } + public Int2ObjectMap getDisplayPage(int page, int pageSize) { + if (pageSize <= 0) { + return Int2ObjectMaps.emptyMap(); } - // Update cache state - displayCacheDirty = false; - usedSlotsCache = displayInventoryCache.size(); - - // Return unmodifiable map to prevent external changes - return Collections.unmodifiableMap(displayInventoryCache); + int safePage = Math.max(1, page); + int startSlot = (safePage - 1) * pageSize; + return buildDisplaySection(startSlot, pageSize); } - public long getTotalItems() { - if (metricsCacheDirty) { - updateMetricsCache(); - } - return totalItemsCache; + public Int2ObjectMap getDisplayRange(int startSlot, int maxResults) { + return buildDisplaySection(startSlot, maxResults); } public Map getConsolidatedItems() { @@ -191,38 +139,21 @@ public Map getConsolidatedItems() { } public int getUsedSlots() { - // If cache is dirty but we haven't regenerated the display inventory yet, - // calculate a quick estimate instead of rebuilding the whole display - if (displayCacheDirty) { - if (consolidatedItems.isEmpty()) { - return 0; - } + if (consolidatedItems.isEmpty()) { + return 0; + } - // Quick estimate - not perfectly accurate but avoids full rebuilds - int estimatedSlots = 0; - for (Map.Entry entry : consolidatedItems.entrySet()) { - long amount = entry.getValue(); - int maxStackSize = entry.getKey().getMaxStackSize(); - estimatedSlots += (int) Math.ceil((double) amount / maxStackSize); - if (estimatedSlots >= maxSlots) { - return maxSlots; // Cap at max slots - } + // Quick estimate - not perfectly accurate but avoids full rebuilds + int estimatedSlots = 0; + for (Map.Entry entry : consolidatedItems.entrySet()) { + long amount = entry.getValue(); + int maxStackSize = entry.getKey().getMaxStackSize(); + estimatedSlots += (int) Math.ceil((double) amount / maxStackSize); + if (estimatedSlots >= maxSlots) { + return maxSlots; // Cap at max slots } - return estimatedSlots; } - - return usedSlotsCache; - } - - private void updateMetricsCache() { - totalItemsCache = consolidatedItems.values().stream() - .mapToLong(Long::longValue) - .sum(); - metricsCacheDirty = false; - } - - public boolean isDirty() { - return displayCacheDirty; + return estimatedSlots; } /** @@ -240,7 +171,6 @@ public void sortItems(org.bukkit.Material preferredMaterial) { // Only proceed if we have items to sort if (consolidatedItems.isEmpty()) { - this.displayCacheDirty = true; return; } @@ -265,34 +195,90 @@ public void sortItems(org.bukkit.Material preferredMaterial) { .sorted(Comparator.comparing(e -> e.getKey().getMaterialName())) .collect(java.util.stream.Collectors.toList()); } - - // Mark display cache as dirty to force regeneration - this.displayCacheDirty = true; } - /** - * Resizes the virtual inventory to a new maximum slot count. - * If the new size is smaller and items exceed the new capacity, - * items will be truncated based on the current sort order. - * - * @param newMaxSlots The new maximum number of slots - */ - public void resize(int newMaxSlots) { - if (newMaxSlots == this.maxSlots) { - return; // No change needed + private Int2ObjectMap buildDisplaySection(int startSlot, int maxResults) { + if (maxResults <= 0 || startSlot >= maxSlots) { + return Int2ObjectMaps.emptyMap(); + } + + if (consolidatedItems.isEmpty()) { + return Int2ObjectMaps.emptyMap(); + } + + int safeStart = Math.max(0, startSlot); + int sectionLimit = Math.min(maxResults, maxSlots - safeStart); + if (sectionLimit <= 0) { + return Int2ObjectMaps.emptyMap(); + } + + Int2ObjectOpenHashMap section = new Int2ObjectOpenHashMap<>(Math.min(sectionLimit, 45)); + List> sortedEntries = getSortedEntries(); + + int currentGlobalSlot = 0; + int relativeSlot = 0; + + for (Map.Entry entry : sortedEntries) { + if (relativeSlot >= sectionLimit || currentGlobalSlot >= maxSlots) { + break; + } + + ItemSignature sig = entry.getKey(); + int maxStackSize = sig.getMaxStackSize(); + if (maxStackSize <= 0) { + continue; + } + + long totalAmount = entry.getValue(); + int stacksForEntry = (int) Math.min( + Integer.MAX_VALUE, + (totalAmount + maxStackSize - 1L) / maxStackSize + ); + + if (currentGlobalSlot + stacksForEntry <= safeStart) { + currentGlobalSlot += stacksForEntry; + continue; + } + + int stacksToSkip = Math.max(0, safeStart - currentGlobalSlot); + long remainingAmount = totalAmount - ((long) stacksToSkip * maxStackSize); + currentGlobalSlot += stacksToSkip; + + while (remainingAmount > 0 && relativeSlot < sectionLimit && currentGlobalSlot < maxSlots) { + ItemStack displayItem = sig.getTemplate(); + displayItem.setAmount((int) Math.min(remainingAmount, maxStackSize)); + section.put(relativeSlot++, displayItem); + + remainingAmount -= maxStackSize; + currentGlobalSlot++; + } + } + + return Int2ObjectMaps.unmodifiable(section); + } + + private List> getSortedEntries() { + if (sortedEntriesCache == null) { + sortedEntriesCache = new ArrayList<>(consolidatedItems.entrySet()); + sortEntries(sortedEntriesCache); } + return sortedEntriesCache; + } - this.maxSlots = newMaxSlots; + private void sortEntries(List> entries) { + if (preferredSortMaterial != null) { + entries.sort((e1, e2) -> { + boolean e1Preferred = e1.getKey().getMaterial() == preferredSortMaterial; + boolean e2Preferred = e2.getKey().getMaterial() == preferredSortMaterial; - // Mark caches as dirty since slot count changed - this.displayCacheDirty = true; + if (e1Preferred && !e2Preferred) return -1; + if (!e1Preferred && e2Preferred) return 1; - // If downsizing, we may need to remove items that exceed capacity - if (newMaxSlots < usedSlotsCache) { - // Let the display inventory rebuild handle the truncation naturally - // Items beyond maxSlots will simply not be displayed - // Note: This doesn't remove items from consolidatedItems, - // but they won't be accessible in the display + return e1.getKey().getMaterialName().compareTo(e2.getKey().getMaterialName()); + }); + return; } + + entries.sort(Comparator.comparing(e -> e.getKey().getMaterialName())); } } diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/sell/SellResult.java b/core/src/main/java/github/nighter/smartspawner/spawner/sell/SellResult.java index 3f0310e9..5714f631 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/sell/SellResult.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/sell/SellResult.java @@ -1,11 +1,11 @@ package github.nighter.smartspawner.spawner.sell; +import github.nighter.smartspawner.spawner.properties.ItemSignature; import lombok.Getter; -import org.bukkit.inventory.ItemStack; -import java.util.ArrayList; import java.util.Collections; -import java.util.List; +import java.util.HashMap; +import java.util.Map; public class SellResult { @Getter @@ -13,25 +13,25 @@ public class SellResult { @Getter private final long itemsSold; @Getter - private final List itemsToRemove; + private final Map itemsToRemove; @Getter private final long timestamp; @Getter private final boolean successful; - public SellResult(double totalValue, long itemsSold, List itemsToRemove) { + public SellResult(double totalValue, long itemsSold, Map itemsToRemove) { this.totalValue = totalValue; this.itemsSold = itemsSold; - this.itemsToRemove = new ArrayList<>(itemsToRemove); + this.itemsToRemove = new HashMap<>(itemsToRemove); this.timestamp = System.currentTimeMillis(); this.successful = totalValue > 0.0 && !itemsToRemove.isEmpty(); } public static SellResult empty() { - return new SellResult(0.0, 0, Collections.emptyList()); + return new SellResult(0.0, 0, Collections.emptyMap()); } public boolean hasItems() { return !itemsToRemove.isEmpty(); } -} \ No newline at end of file +} diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/sell/SpawnerSellManager.java b/core/src/main/java/github/nighter/smartspawner/spawner/sell/SpawnerSellManager.java index 14675191..6ad0a72a 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/sell/SpawnerSellManager.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/sell/SpawnerSellManager.java @@ -99,40 +99,36 @@ public void sellAllItems(Player player, SpawnerData spawner, Runnable onComplete final double accumulatedValue = spawner.getAccumulatedSellValue(); final Location spawnerLocation = spawner.getSpawnerLocation(); - // Async: pure CPU computation, no Bukkit API - Scheduler.runTaskAsync(() -> { - SellResult result; - try { - result = calculateSellValue(itemSnapshot, accumulatedValue); - } catch (Exception e) { - plugin.getLogger().warning("Sell calculation error for " + player.getName() + ": " + e.getMessage()); - Scheduler.runLocationTask(spawnerLocation, () -> { - try { - if (onComplete != null) onComplete.run(); - } finally { - spawner.stopSelling(); - } - messageService.sendMessage(player, "action_failed"); - }); - return; - } - - // Apply on the location's region thread (Folia) or the main thread (Paper) + SellResult result; + try { + result = calculateSellValue(itemSnapshot, accumulatedValue); + } catch (Exception e) { + plugin.getLogger().warning("Sell calculation error for " + player.getName() + ": " + e.getMessage()); Scheduler.runLocationTask(spawnerLocation, () -> { try { - applySellResult(player, spawner, result, expCollected, expMending); + if (onComplete != null) onComplete.run(); } finally { - // onComplete MUST run in finally so activeSells is always cleared, - // even when applySellResult throws (e.g. economy plugin error). - try { - if (onComplete != null) onComplete.run(); - } finally { - spawner.stopSelling(); - } + spawner.stopSelling(); } + messageService.sendMessage(player, "action_failed"); }); + return; + } + + // Apply on the location's region thread (Folia) or the main thread (Paper) + Scheduler.runLocationTask(spawnerLocation, () -> { + try { + applySellResult(player, spawner, result, expCollected, expMending); + } finally { + // onComplete MUST run in finally so activeSells is always cleared, + // even when applySellResult throws (e.g. economy plugin error). + try { + if (onComplete != null) onComplete.run(); + } finally { + spawner.stopSelling(); + } + } }); - // stopSelling() ownership is transferred to the async chain above } /** @@ -154,7 +150,11 @@ private void applySellResult(Player player, SpawnerData spawner, SellResult sell // Fire the cancellable API event if (SpawnerSellEvent.getHandlerList().getRegisteredListeners().length != 0) { SpawnerSellEvent event = new SpawnerSellEvent( - player, spawner.getSpawnerLocation(), sellResult.getItemsToRemove(), amount, spawner.getEntityType()); + player, + spawner.getSpawnerLocation(), + toApiItemStacks(sellResult.getItemsToRemove()), + amount, + spawner.getEntityType()); Bukkit.getPluginManager().callEvent(event); if (event.isCancelled()) return; if (event.getMoneyAmount() >= 0) amount = event.getMoneyAmount(); @@ -203,33 +203,35 @@ private void applySellResult(Player player, SpawnerData spawner, SellResult sell } /** - * Calculates the total sell value and constructs the list of {@link ItemStack}s to remove. + * Calculates the total sell value and records the consolidated item signatures to remove. * Pure computation – no Bukkit API calls, safe to run on an async thread. */ - private SellResult calculateSellValue(Map consolidatedItems, - double totalValue) { + private SellResult calculateSellValue(Map consolidatedItems, double totalValue) { long totalItemsSold = 0; - ArrayList itemsToRemove = new ArrayList<>(); for (Map.Entry entry : consolidatedItems.entrySet()) { - ItemSignature signature = entry.getKey(); - long amount = entry.getValue(); - int maxStackSize = signature.getMaxStackSize(); + totalItemsSold += entry.getValue(); + } - totalItemsSold += amount; + return new SellResult(totalValue, totalItemsSold, consolidatedItems); + } - int stacksNeeded = (int) Math.ceil((double) amount / maxStackSize); - itemsToRemove.ensureCapacity(itemsToRemove.size() + stacksNeeded); + private List toApiItemStacks(Map items) { + if (items == null || items.isEmpty()) { + return Collections.emptyList(); + } - long remaining = amount; - while (remaining > 0) { - ItemStack stack = signature.getTemplate(); - stack.setAmount((int) Math.min(remaining, maxStackSize)); - itemsToRemove.add(stack); - remaining -= stack.getAmount(); - } + List apiItems = new ArrayList<>(items.size()); + + for (Map.Entry entry : items.entrySet()) { + ItemSignature signature = entry.getKey(); + Long amount = entry.getValue(); + + ItemStack stack = signature.getTemplate(); + stack.setAmount((int) Math.min(amount, Integer.MAX_VALUE)); + apiItems.add(stack); } - return new SellResult(totalValue, totalItemsSold, itemsToRemove); + return apiItems; } } diff --git a/core/src/main/java/github/nighter/smartspawner/language/cache/LRUCache.java b/core/src/main/java/github/nighter/smartspawner/utils/LRUCache.java similarity index 72% rename from core/src/main/java/github/nighter/smartspawner/language/cache/LRUCache.java rename to core/src/main/java/github/nighter/smartspawner/utils/LRUCache.java index 88b92ac1..fe96fea9 100644 --- a/core/src/main/java/github/nighter/smartspawner/language/cache/LRUCache.java +++ b/core/src/main/java/github/nighter/smartspawner/utils/LRUCache.java @@ -1,7 +1,10 @@ -package github.nighter.smartspawner.language.cache; +package github.nighter.smartspawner.utils; + +import com.google.common.base.Preconditions; import java.util.LinkedHashMap; import java.util.Map; +import java.util.function.Function; /** * A simple LRU (Least Recently Used) cache implementation @@ -52,6 +55,25 @@ public synchronized V put(K key, V value) { return cache.put(key, value); } + /** + * Returns the value associated with the specified key, computing and + * caching it with the supplied mapping function when no mapping exists. + * + *

Accessing an existing entry updates its recency, and adding a new + * entry may evict the least recently used entry if the cache exceeds its + * configured capacity.

+ * + * @param key The key whose associated value is to be returned or computed + * @param mappingFunction The function used to create a value when the key is absent + * @return The existing or newly computed value associated with the key + * @throws NullPointerException if {@code key} is null + */ + public synchronized V get(K key, Function mappingFunction) { + Preconditions.checkNotNull(key); + + return cache.computeIfAbsent(key, mappingFunction); + } + /** * Removes all entries from the cache */