From 21b260300c784893e50885ff1c58158f3aaee809 Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Tue, 5 May 2026 19:19:51 +0200 Subject: [PATCH 01/19] Improve lootgen and add cached Config class --- .../nighter/smartspawner/SmartSpawner.java | 2 + .../commands/reload/ReloadSubCommand.java | 2 + .../nighter/smartspawner/config/Config.java | 30 +++++ .../spawner/lootgen/SpawnerLootGenerator.java | 104 +++++++++++------- .../spawner/lootgen/loot/LootItem.java | 6 +- core/src/main/resources/config.yml | 17 +++ 6 files changed, 121 insertions(+), 40 deletions(-) create mode 100644 core/src/main/java/github/nighter/smartspawner/config/Config.java diff --git a/core/src/main/java/github/nighter/smartspawner/SmartSpawner.java b/core/src/main/java/github/nighter/smartspawner/SmartSpawner.java index 6a5f04ae..32e66588 100644 --- a/core/src/main/java/github/nighter/smartspawner/SmartSpawner.java +++ b/core/src/main/java/github/nighter/smartspawner/SmartSpawner.java @@ -13,6 +13,7 @@ import github.nighter.smartspawner.commands.list.gui.adminstacker.AdminStackerHandler; import github.nighter.smartspawner.commands.list.gui.serverselection.ServerSelectionHandler; import github.nighter.smartspawner.commands.prices.PricesGUI; +import github.nighter.smartspawner.config.Config; import github.nighter.smartspawner.extras.HopperConfig; import github.nighter.smartspawner.spawner.config.SpawnerSettingsConfig; import github.nighter.smartspawner.spawner.config.ItemSpawnerSettingsConfig; @@ -167,6 +168,7 @@ public class SmartSpawner extends JavaPlugin implements SmartSpawnerPlugin { public void onEnable() { long startTime = System.currentTimeMillis(); instance = this; + Config.load(this); // Initialize version-specific components initializeVersionComponents(); diff --git a/core/src/main/java/github/nighter/smartspawner/commands/reload/ReloadSubCommand.java b/core/src/main/java/github/nighter/smartspawner/commands/reload/ReloadSubCommand.java index 82c0ad9e..9d58bfc1 100644 --- a/core/src/main/java/github/nighter/smartspawner/commands/reload/ReloadSubCommand.java +++ b/core/src/main/java/github/nighter/smartspawner/commands/reload/ReloadSubCommand.java @@ -3,6 +3,7 @@ import com.mojang.brigadier.context.CommandContext; import github.nighter.smartspawner.SmartSpawner; import github.nighter.smartspawner.commands.BaseSubCommand; +import github.nighter.smartspawner.config.Config; import io.papermc.paper.command.brigadier.CommandSourceStack; import org.bukkit.command.CommandSender; import org.jspecify.annotations.NullMarked; @@ -54,6 +55,7 @@ private void reloadAll(CommandSender sender) { // Reload all configurations plugin.reloadConfig(); + Config.reload(plugin); // Reload components in dependency order plugin.setUpHopperHandler(); diff --git a/core/src/main/java/github/nighter/smartspawner/config/Config.java b/core/src/main/java/github/nighter/smartspawner/config/Config.java new file mode 100644 index 00000000..27ad8616 --- /dev/null +++ b/core/src/main/java/github/nighter/smartspawner/config/Config.java @@ -0,0 +1,30 @@ +package github.nighter.smartspawner.config; + +import github.nighter.smartspawner.SmartSpawner; +import lombok.AccessLevel; +import lombok.Getter; +import org.bukkit.configuration.file.FileConfiguration; + +@Getter +public class Config { + @Getter(AccessLevel.NONE) + private static volatile Config instance; + + private final boolean optimizedLootgen; + + private Config(FileConfiguration config) { + this.optimizedLootgen = config.getBoolean("loot_generation.optimized_generation"); + } + + public static Config get() { + return instance; + } + + public static void reload(SmartSpawner plugin) { + load(plugin); + } + + public static void load(SmartSpawner plugin) { + instance = new Config(plugin.getConfig()); + } +} 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 7f6e13cf..ff8a21ff 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 @@ -1,6 +1,7 @@ package github.nighter.smartspawner.spawner.lootgen; import github.nighter.smartspawner.SmartSpawner; +import github.nighter.smartspawner.config.Config; import github.nighter.smartspawner.spawner.gui.synchronization.SpawnerGuiViewManager; import github.nighter.smartspawner.spawner.properties.SpawnerData; import github.nighter.smartspawner.spawner.data.SpawnerManager; @@ -12,19 +13,18 @@ import org.bukkit.inventory.ItemStack; import java.util.*; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicInteger; public class SpawnerLootGenerator { private final SmartSpawner plugin; private final SpawnerGuiViewManager spawnerGuiViewManager; private final SpawnerManager spawnerManager; - private final Random random; public SpawnerLootGenerator(SmartSpawner plugin) { this.plugin = plugin; this.spawnerGuiViewManager = plugin.getSpawnerGuiViewManager(); this.spawnerManager = plugin.getSpawnerManager(); - this.random = new Random(); } public void spawnLootToSpawner(SpawnerData spawner) { @@ -62,11 +62,11 @@ public void spawnLootToSpawner(SpawnerData spawner) { 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()); @@ -184,9 +184,8 @@ public void spawnLootToSpawner(SpawnerData spawner) { public LootResult generateLoot(int minMobs, int maxMobs, SpawnerData spawner) { - int mobCount = random.nextInt(maxMobs - minMobs + 1) + minMobs; - long totalExperienceLong = (long) spawner.getEntityExperienceValue() * mobCount; - long totalExperience = Math.min(totalExperienceLong, Long.MAX_VALUE); + 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(); @@ -201,29 +200,21 @@ public LootResult generateLoot(int minMobs, int maxMobs, SpawnerData spawner) { // Process mobs in batch rather than individually for (LootItem lootItem : validItems) { // Calculate the probability for the entire mob batch at once - int successfulDrops = 0; + int totalAmount; - // Calculate binomial distribution - how many mobs will drop this item - for (int i = 0; i < mobCount; i++) { - if (random.nextDouble() * 100 <= lootItem.chance()) { - successfulDrops++; - } + if (Config.get().isOptimizedLootgen() && shouldApproximate(lootItem.chance(), mobCount)) { + // O(1) binomial approximation + totalAmount = generateApproximatedLoot(lootItem, mobCount); + } else { + // O(n) binomial distribution + totalAmount = generateExactLoot(lootItem, mobCount); } - if (successfulDrops > 0) { + if (totalAmount > 0) { // Create item just once per loot type - ItemStack prototype = lootItem.createItemStack(random); + ItemStack prototype = lootItem.createItemStack(ThreadLocalRandom.current()); if (prototype != null) { - // Total amount across all mobs - int totalAmount = 0; - for (int i = 0; i < successfulDrops; i++) { - totalAmount += lootItem.generateAmount(random); - } - - if (totalAmount > 0) { - // Add to consolidated map - consolidatedLoot.merge(prototype, totalAmount, (a, b) -> a + b); - } + consolidatedLoot.merge(prototype, totalAmount, Integer::sum); } } } @@ -248,6 +239,41 @@ public LootResult generateLoot(int minMobs, int maxMobs, SpawnerData spawner) { return new LootResult(finalLoot, totalExperience); } + // Determines whether to use expected-value approximation + private boolean shouldApproximate(double chance, int mobCount) { + // simple heuristic: use expected if at least one item can be generated + if (chance <= 0D) return false; + return mobCount > 97.5D / chance; + } + + // O(n) simulation: exact per-mob drop calculation + private int generateExactLoot(LootItem lootItem, int mobCount) { + int successfulDrops = 0; + ThreadLocalRandom random = ThreadLocalRandom.current(); + double p = lootItem.chance() / 100.0; + for (int i = 0; i < mobCount; i++) { + if (random.nextDouble() < p) { + successfulDrops++; + } + } + int totalAmount = 0; + for (int i = 0; i < successfulDrops; i++) { + totalAmount += lootItem.generateAmount(random); + } + return totalAmount; + } + + // O(1) expected-value calculation with small jitter + private int generateApproximatedLoot(LootItem lootItem, int mobCount) { + double p = lootItem.chance() / 100.0; + double expectedDrops = mobCount * p; + double avgAmount = lootItem.getAverageAmount(); + double jitter = p != 1.0 + ? 0.95 + ThreadLocalRandom.current().nextDouble() * 0.10 + : 1.0; + return (int) Math.round(expectedDrops * avgAmount * jitter); + } + private List limitItemsToAvailableSlots(List items, SpawnerData spawner) { VirtualInventory currentInventory = spawner.getVirtualInventory(); int maxSlots = spawner.getMaxSpawnerLootSlots(); @@ -373,11 +399,11 @@ private void handleGuiUpdates(SpawnerData spawner) { spawner.updateHologramData(); } } - + /** * Pre-generates loot asynchronously for improved UX. * Loot is calculated in background before timer expires, then added instantly when ready. - * + * *

This method: *

    *
  • Checks spawner capacity before generation
  • @@ -385,7 +411,7 @@ private void handleGuiUpdates(SpawnerData spawner) { *
  • Invokes callback with generated items and experience
  • *
  • Handles thread-safety with proper locking
  • *
- * + * * @param spawner The spawner to pre-generate loot for * @param callback Callback invoked with generated loot (items, experience) */ @@ -410,13 +436,13 @@ public void preGenerateLoot(SpawnerData spawner, LootGenerationCallback callback 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(); - + if (atCapacity) { callback.onLootGenerated(Collections.emptyList(), 0); return; @@ -447,15 +473,15 @@ public void preGenerateLoot(SpawnerData spawner, LootGenerationCallback callback } private LootResult generateExperienceOnlyLoot(int minMobs, int maxMobs, SpawnerData spawner) { - int mobCount = random.nextInt(maxMobs - minMobs + 1) + minMobs; + 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. - * + * *

This method: *

    *
  • Validates pre-generated loot is not empty
  • @@ -464,9 +490,9 @@ private LootResult generateExperienceOnlyLoot(int minMobs, int maxMobs, SpawnerD *
  • Updates lastSpawnTime to maintain cycle timing
  • *
  • Triggers GUI updates and marks spawner for persistence
  • *
- * + * *

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 experience Pre-generated experience amount @@ -523,7 +549,7 @@ public void addPreGeneratedLoot(SpawnerData spawner, List items, long Scheduler.runTaskAsync(() -> { boolean changed = false; - + if (experience > 0 && spawner.getSpawnerExp() < spawner.getMaxStoredExp()) { long currentExp = spawner.getSpawnerExp(); long maxExp = spawner.getMaxStoredExp(); @@ -585,7 +611,7 @@ public void addPreGeneratedLoot(SpawnerData spawner, List items, long } }); } - + /** * Callback interface for asynchronous loot pre-generation. * Invoked when loot generation completes with the generated items and experience. @@ -594,10 +620,10 @@ public void addPreGeneratedLoot(SpawnerData spawner, List items, long public interface LootGenerationCallback { /** * Called when loot generation completes. - * + * * @param items Generated items list (never null, may be empty) * @param experience Generated experience amount */ void onLootGenerated(List items, long experience); } -} \ No newline at end of file +} diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/loot/LootItem.java b/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/loot/LootItem.java index f6e0fc17..95004ef0 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/loot/LootItem.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/lootgen/loot/LootItem.java @@ -45,7 +45,11 @@ public int generateAmount(Random random) { return random.nextInt(maxAmount - minAmount + 1) + minAmount; } + public double getAverageAmount() { + return (this.maxAmount + this.minAmount) / 2.0; + } + public boolean isAvailable() { return material != null; } -} \ No newline at end of file +} diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index bed53b70..3c188e43 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -156,6 +156,23 @@ hopper: check_delay: 3s # Time between collection checks (see time format guide above) stack_per_transfer: 5 # Number of item stacks transferred in one operation (max 5) +#--------------------------------------------------- +# Loot Generation Settings +#--------------------------------------------------- +# Configuration for internal loot generation algorithms +loot_generation: + # Enables the optimized hybrid loot generation system + # + # When enabled, SmartSpawner dynamically switches between: + # - Exact O(n) simulation for rare drops or small mob batches (binomial distribution) + # - Fast O(1) expected-value approximation for large mob batches (binomial approximation) + # + # This significantly improves performance with millions of stacked spawners + # while preserving realistic loot distribution and rare drop accuracy. + # + # Recommended: true + optimized_generation: false + #--------------------------------------------------- # Bedrock Player Support #--------------------------------------------------- From e9480a3949b37f393b0c9f0a7b9c1b5db7eb4150 Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Mon, 11 May 2026 19:08:29 +0200 Subject: [PATCH 02/19] Use fastutil --- .../spawner/properties/VirtualInventory.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 4a24be5b..0237bf72 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,5 +1,7 @@ package github.nighter.smartspawner.spawner.properties; +import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import lombok.Getter; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.Damageable; @@ -12,7 +14,7 @@ public class VirtualInventory { private final Map consolidatedItems; @Getter private int maxSlots; - private final Map displayInventoryCache; + private final Int2ObjectOpenHashMap displayInventoryCache; private boolean displayCacheDirty; private int usedSlotsCache; private long totalItemsCache; @@ -34,7 +36,7 @@ protected boolean removeEldestEntry(Map.Entry eldest) public VirtualInventory(int maxSlots) { this.maxSlots = maxSlots; this.consolidatedItems = new ConcurrentHashMap<>(); - this.displayInventoryCache = new HashMap<>(maxSlots); // Pre-size the map + this.displayInventoryCache = new Int2ObjectOpenHashMap<>(); // DO NOT pre-size the map, DONT! this.displayCacheDirty = true; this.metricsCacheDirty = true; this.usedSlotsCache = 0; @@ -218,7 +220,7 @@ public Map getDisplayInventory() { // Return cached result if available if (!displayCacheDirty) { // Return a shallow copy to prevent modification of the cache - return Collections.unmodifiableMap(displayInventoryCache); + return Int2ObjectMaps.unmodifiable(displayInventoryCache); } // Clear the cache for a fresh rebuild but reuse the existing map @@ -284,7 +286,7 @@ public Map getDisplayInventory() { usedSlotsCache = displayInventoryCache.size(); // Return unmodifiable map to prevent external changes - return Collections.unmodifiableMap(displayInventoryCache); + return Int2ObjectMaps.unmodifiable(displayInventoryCache); } public long getTotalItems() { @@ -403,4 +405,4 @@ public void resize(int newMaxSlots) { // but they won't be accessible in the display } } -} \ No newline at end of file +} From 7e663a70fc0282c46aed2e8e2e1db28b3a8b256d Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Tue, 12 May 2026 09:33:00 +0200 Subject: [PATCH 03/19] Other fastutil stuff --- .../spawner/gui/storage/SpawnerStorageUI.java | 14 ++++++++------ .../spawner/properties/VirtualInventory.java | 5 +++-- 2 files changed, 11 insertions(+), 8 deletions(-) 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 a1fe17ad..c437ea64 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 @@ -13,6 +13,8 @@ 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 it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import lombok.Getter; import net.kyori.adventure.text.Component; import org.bukkit.entity.EntityType; @@ -234,7 +236,7 @@ public void updateDisplay(Inventory inventory, SpawnerData spawner, int page, in } // Track both changes and slots that need to be emptied - Map updates = new HashMap<>(); + Int2ObjectMap updates = new Int2ObjectOpenHashMap<>(); Set slotsToEmpty = new HashSet<>(); // Clear storage area slots first @@ -261,8 +263,8 @@ public void updateDisplay(Inventory inventory, SpawnerData spawner, int page, in } } - for (Map.Entry entry : updates.entrySet()) { - inventory.setItem(entry.getKey(), entry.getValue()); + for (Int2ObjectMap.Entry entry : updates.int2ObjectEntrySet()) { + inventory.setItem(entry.getIntKey(), entry.getValue()); } // Update hologram if enabled @@ -289,7 +291,7 @@ private void addPageItems(Map updates, Set slotsToE try { // Get display items directly from VirtualInventory (source of truth) VirtualInventory virtualInv = spawner.getVirtualInventory(); - Map displayItems = virtualInv.getDisplayInventory(); + Int2ObjectMap displayItems = virtualInv.getDisplayInventory(); if (displayItems.isEmpty()) { return; @@ -299,8 +301,8 @@ private void addPageItems(Map updates, Set slotsToE int startIndex = (page - 1) * StoragePageHolder.MAX_ITEMS_PER_PAGE; // Add items for this page - for (Map.Entry entry : displayItems.entrySet()) { - int globalIndex = entry.getKey(); + for (Int2ObjectMap.Entry entry : displayItems.int2ObjectEntrySet()) { + int globalIndex = entry.getIntKey(); // Check if item belongs on this page if (globalIndex >= startIndex && globalIndex < startIndex + StoragePageHolder.MAX_ITEMS_PER_PAGE) { 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 0237bf72..8b18aaa7 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,5 +1,6 @@ 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; @@ -216,7 +217,7 @@ public boolean removeItems(List items) { } // Optimized getDisplayInventory method - public Map getDisplayInventory() { + public Int2ObjectMap getDisplayInventory() { // Return cached result if available if (!displayCacheDirty) { // Return a shallow copy to prevent modification of the cache @@ -229,7 +230,7 @@ public Map getDisplayInventory() { if (consolidatedItems.isEmpty()) { displayCacheDirty = false; usedSlotsCache = 0; - return Collections.emptyMap(); + return Int2ObjectMaps.emptyMap(); } // Get and sort the items - only use cached sort result if available From 8f17604507e0b313012146ca1f699f99b7c902c6 Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Tue, 12 May 2026 10:18:25 +0200 Subject: [PATCH 04/19] Scary inventory optimization --- .../smartspawner/extras/HopperTransfer.java | 50 +++--- .../spawner/gui/storage/SpawnerStorageUI.java | 19 +-- .../spawner/properties/VirtualInventory.java | 157 +++++++++++------- 3 files changed, 137 insertions(+), 89 deletions(-) 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/spawner/gui/storage/SpawnerStorageUI.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageUI.java index c437ea64..0232cd4b 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 @@ -289,27 +289,18 @@ public void updateDisplay(Inventory inventory, SpawnerData spawner, int page, in 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(); - Int2ObjectMap 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 (Int2ObjectMap.Entry entry : displayItems.int2ObjectEntrySet()) { - int globalIndex = entry.getIntKey(); - - // 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); - } + 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/properties/VirtualInventory.java b/core/src/main/java/github/nighter/smartspawner/spawner/properties/VirtualInventory.java index 8b18aaa7..43481e0d 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 @@ -216,6 +216,20 @@ public boolean removeItems(List items) { return true; } + public Int2ObjectMap getDisplayPage(int page, int pageSize) { + if (pageSize <= 0) { + return Int2ObjectMaps.emptyMap(); + } + + int safePage = Math.max(1, page); + int startSlot = (safePage - 1) * pageSize; + return buildDisplaySection(startSlot, pageSize); + } + + public Int2ObjectMap getDisplayRange(int startSlot, int maxResults) { + return buildDisplaySection(startSlot, maxResults); + } + // Optimized getDisplayInventory method public Int2ObjectMap getDisplayInventory() { // Return cached result if available @@ -224,63 +238,8 @@ public Int2ObjectMap getDisplayInventory() { return Int2ObjectMaps.unmodifiable(displayInventoryCache); } - // Clear the cache for a fresh rebuild but reuse the existing map displayInventoryCache.clear(); - - if (consolidatedItems.isEmpty()) { - displayCacheDirty = false; - usedSlotsCache = 0; - return Int2ObjectMaps.emptyMap(); - } - - // 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().getTemplateRef().getType() == preferredSortMaterial; - boolean e2Preferred = e2.getKey().getTemplateRef().getType() == preferredSortMaterial; - - 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())); - } - } - - // Process items directly to the display inventory - int currentSlot = 0; - - for (Map.Entry entry : sortedEntriesCache) { - if (currentSlot >= maxSlots) break; - - ItemSignature sig = entry.getKey(); - long totalAmount = entry.getValue(); - ItemStack templateItem = sig.getTemplateRef(); - int maxStackSize = templateItem.getMaxStackSize(); - - // Create as many stacks as needed for this item type - while (totalAmount > 0 && currentSlot < maxSlots) { - int stackSize = (int) Math.min(totalAmount, maxStackSize); - - // Create the display item only once per slot - ItemStack displayItem = templateItem.clone(); - displayItem.setAmount(stackSize); - - // Store in cache - displayInventoryCache.put(currentSlot, displayItem); - - totalAmount -= stackSize; - currentSlot++; - } - } + displayInventoryCache.putAll(buildDisplaySection(0, maxSlots)); // Update cache state displayCacheDirty = false; @@ -406,4 +365,90 @@ public void resize(int newMaxSlots) { // but they won't be accessible in the display } } + + 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(); + ItemStack templateItem = sig.getTemplateRef(); + int maxStackSize = templateItem.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 = templateItem.clone(); + 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; + } + + private void sortEntries(List> entries) { + if (preferredSortMaterial != null) { + entries.sort((e1, e2) -> { + boolean e1Preferred = e1.getKey().getTemplateRef().getType() == preferredSortMaterial; + boolean e2Preferred = e2.getKey().getTemplateRef().getType() == preferredSortMaterial; + + if (e1Preferred && !e2Preferred) return -1; + if (!e1Preferred && e2Preferred) return 1; + + return e1.getKey().getMaterialName().compareTo(e2.getKey().getMaterialName()); + }); + return; + } + + entries.sort(Comparator.comparing(e -> e.getKey().getMaterialName())); + } } From c7e398e5779800e3888812185414e55e7d334f08 Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Tue, 12 May 2026 14:05:15 +0200 Subject: [PATCH 05/19] Dumb sort button caching - TODO: make it per-spawner without EntityLootConfig(?) --- .../spawner/gui/storage/SpawnerStorageUI.java | 55 ++------- .../gui/storage/button/SortButton.java | 112 ++++++++++++++++++ 2 files changed, 124 insertions(+), 43 deletions(-) create mode 100644 core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/button/SortButton.java 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 0232cd4b..2c6afd60 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 @@ -7,12 +7,14 @@ import github.nighter.smartspawner.spawner.gui.layout.GuiButton; import github.nighter.smartspawner.spawner.gui.layout.GuiLayout; import github.nighter.smartspawner.spawner.gui.layout.GuiLayoutConfig; +import github.nighter.smartspawner.spawner.gui.storage.button.SortButton; import github.nighter.smartspawner.spawner.lootgen.loot.EntityLootConfig; import github.nighter.smartspawner.spawner.lootgen.loot.LootItem; import github.nighter.smartspawner.spawner.properties.VirtualInventory; import github.nighter.smartspawner.spawner.properties.SpawnerData; import github.nighter.smartspawner.Scheduler; import github.nighter.smartspawner.Scheduler.Task; +import io.papermc.paper.datacomponent.DataComponentTypes; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import lombok.Getter; @@ -355,7 +357,16 @@ private void addNavigationButtons(Map updates, SpawnerData s item = staticButtons.get("takeAll"); break; case "sort_items": - item = createSortButton(spawner, button.getMaterial()); + item = SortButton.getOrBuildSortButton( + spawner, + button.getMaterial(), + languageManager, + data -> createButton( + data.material(), + data.name(), + data.lore() + ) + ); break; case "drop_page": item = staticButtons.get("dropPage"); @@ -486,48 +497,6 @@ private ItemStack createCollectExpButton(SpawnerData spawner, Material material) return createButton(material, name, lore); } - private ItemStack createSortButton(SpawnerData spawner, Material material) { - Map placeholders = new HashMap<>(); - - // Get current sort item - Material currentSort = spawner.getPreferredSortItem(); - - // Get format strings from configuration - 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"); - - // Get available items from spawner drops - StringBuilder availableItems = new StringBuilder(); - if (spawner.getLootConfig() != null && spawner.getLootConfig().getAllItems() != null) { - boolean first = true; - var sortedLoot = spawner.getLootConfig().getAllItems().stream() - .sorted(Comparator.comparing(item -> item.material().name())) - .toList(); - - for (var lootItem : sortedLoot) { - if (!first) availableItems.append("\n"); - String itemName = languageManager.getVanillaItemName(lootItem.material()); - String format = currentSort == lootItem.material() ? selectedItemFormat : unselectedItemFormat; - - // Replace {item_name} placeholder in format string - String formattedItem = format.replace("{item_name}", itemName); - availableItems.append(formattedItem); - first = false; - } - } - - if (availableItems.isEmpty()) { - availableItems.append(noneText); - } - - placeholders.put("available_items", availableItems.toString()); - - String name = languageManager.getGuiItemName("sort_items_button.name", placeholders); - List lore = languageManager.getGuiItemLoreWithMultilinePlaceholders("sort_items_button.lore", placeholders); - return createButton(material, name, lore); - } - private ItemStack createStorageSpawnerInfoButton(SpawnerData spawner, Material material) { Map storedItems = spawner.getVirtualInventory().getConsolidatedItems(); List lootComponents = buildStorageInfoLootComponents(spawner, storedItems); 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..32396103 --- /dev/null +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/button/SortButton.java @@ -0,0 +1,112 @@ +package github.nighter.smartspawner.spawner.gui.storage.button; + +import github.nighter.smartspawner.language.LanguageManager; +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.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +public final class SortButton { + + private static final Map SORT_BUTTON_CACHE = new ConcurrentHashMap<>(); + + private static final EnumMap MATERIAL_NAME_CACHE = new EnumMap<>(Material.class); + + private record SortButtonCacheKey(EntityLootConfig lootConfig, Material selectedMaterial, Material buttonMaterial) {} + + private SortButton() {} + + public static ItemStack getOrBuildSortButton(SpawnerData spawner, Material buttonMaterial, + LanguageManager languageManager, Function buttonFactory) { + + EntityLootConfig lootConfig = spawner.getLootConfig(); + + return SORT_BUTTON_CACHE.computeIfAbsent( + 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) {} +} From 2daa7591aecaacd8b6f2b51c6b43d8a232a1ae1d Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Tue, 12 May 2026 14:09:08 +0200 Subject: [PATCH 06/19] Remove signatureCache as caching was actually slower --- .../spawner/properties/VirtualInventory.java | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) 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 43481e0d..0162ede9 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 @@ -26,13 +26,6 @@ public class VirtualInventory { // Add an LRU cache for expensive item operations private static final int ITEM_CACHE_SIZE = 128; - private static final Map signatureCache = - Collections.synchronizedMap(new LinkedHashMap(ITEM_CACHE_SIZE, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > ITEM_CACHE_SIZE; - } - }); public VirtualInventory(int maxSlots) { this.maxSlots = maxSlots; @@ -133,16 +126,7 @@ private int getItemDamage(ItemStack item) { } public static ItemSignature getSignature(ItemStack item) { - // First try to get from cache - ItemSignature cachedSig = signatureCache.get(item); - if (cachedSig != null) { - return cachedSig; - } - - // Create new signature and cache it - ItemSignature newSig = new ItemSignature(item); - signatureCache.put(item.clone(), newSig); - return newSig; + return new ItemSignature(item); } // Add items in bulk with minimal operations From f1513cc6fc0532a66531cee8188a5a28bcd1167b Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Tue, 12 May 2026 18:51:36 +0200 Subject: [PATCH 07/19] Cleanup for the whole display rewrite --- .../spawner/data/SpawnerFileHandler.java | 3 +- .../data/database/SpawnerDatabaseHandler.java | 3 +- .../spawner/gui/main/SpawnerMenuUI.java | 19 +- .../gui/sell/SpawnerSellConfirmUI.java | 11 +- .../spawner/gui/storage/SpawnerStorageUI.java | 11 +- .../spawner/lootgen/SpawnerLootGenerator.java | 13 +- .../spawner/properties/ItemSignature.java | 94 ++++++++ .../spawner/properties/SpawnerData.java | 34 +-- .../spawner/properties/VirtualInventory.java | 202 ++---------------- .../spawner/sell/SpawnerSellManager.java | 7 +- .../spawner/utils/ItemStackSerializer.java | 8 +- 11 files changed, 158 insertions(+), 247 deletions(-) create mode 100644 core/src/main/java/github/nighter/smartspawner/spawner/properties/ItemSignature.java 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 abf7adcc..f3d0933d 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 @@ -2,6 +2,7 @@ import github.nighter.smartspawner.SmartSpawner; import github.nighter.smartspawner.spawner.data.storage.SpawnerStorage; +import github.nighter.smartspawner.spawner.properties.ItemSignature; import github.nighter.smartspawner.spawner.properties.SpawnerData; import github.nighter.smartspawner.spawner.properties.VirtualInventory; import github.nighter.smartspawner.Scheduler; @@ -229,7 +230,7 @@ private boolean saveSpawnerBatch(Map spawners) { VirtualInventory virtualInv = spawner.getVirtualInventory(); if (virtualInv != null) { - Map items = virtualInv.getConsolidatedItems(); + Map items = virtualInv.getConsolidatedItems(); List serializedItems = ItemStackSerializer.serializeInventory(items); spawnerData.set(path + ".inventory", serializedItems); } 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 d99a0491..d28e4c1d 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 @@ -5,6 +5,7 @@ import github.nighter.smartspawner.commands.list.gui.CrossServerSpawnerData; import github.nighter.smartspawner.spawner.data.storage.SpawnerStorage; import github.nighter.smartspawner.spawner.data.storage.StorageMode; +import github.nighter.smartspawner.spawner.properties.ItemSignature; import github.nighter.smartspawner.spawner.properties.SpawnerData; import github.nighter.smartspawner.spawner.properties.VirtualInventory; import github.nighter.smartspawner.spawner.utils.ItemStackSerializer; @@ -595,7 +596,7 @@ private String serializeInventory(VirtualInventory virtualInv) { return null; } - Map items = virtualInv.getConsolidatedItems(); + Map items = virtualInv.getConsolidatedItems(); if (items.isEmpty()) { return null; } diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/main/SpawnerMenuUI.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/main/SpawnerMenuUI.java index e9469a11..2f040532 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/main/SpawnerMenuUI.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/main/SpawnerMenuUI.java @@ -1,5 +1,6 @@ package github.nighter.smartspawner.spawner.gui.main; +import github.nighter.smartspawner.spawner.properties.ItemSignature; import net.kyori.adventure.text.Component; import github.nighter.smartspawner.SmartSpawner; import github.nighter.smartspawner.nms.VersionInitializer; @@ -255,7 +256,7 @@ public ItemStack createLootStorageItem(SpawnerData spawner) { } List lootComponents = Collections.emptyList(); if (usedPlaceholders.contains("loot_items")) { - Map storedItems = virtualInventory.getConsolidatedItems(); + Map storedItems = virtualInventory.getConsolidatedItems(); lootComponents = buildLootItemComponents(spawner.getEntityType(), storedItems); } @@ -282,10 +283,10 @@ public ItemStack createLootStorageItem(SpawnerData spawner) { return chestItem; } - private String buildLootItemsText(EntityType entityType, Map storedItems) { + private String buildLootItemsText(EntityType entityType, Map storedItems) { // Create material-to-amount map for quick lookups Map materialAmountMap = new HashMap<>(); - for (Map.Entry entry : storedItems.entrySet()) { + for (Map.Entry entry : storedItems.entrySet()) { Material material = entry.getKey().getTemplateRef().getType(); materialAmountMap.merge(material, entry.getValue(), Long::sum); } @@ -327,11 +328,11 @@ private String buildLootItemsText(EntityType entityType, Map> sortedItems = + List> sortedItems = new ArrayList<>(storedItems.entrySet()); sortedItems.sort(Comparator.comparing(e -> e.getKey().getMaterialName())); - for (Map.Entry entry : sortedItems) { + for (Map.Entry entry : sortedItems) { ItemStack templateItem = entry.getKey().getTemplateRef(); Material material = templateItem.getType(); long amount = entry.getValue(); @@ -602,9 +603,9 @@ private int calculatePercentage(long current, long maximum) { return maximum > 0 ? (int) ((double) current / maximum * 100) : 0; } - private List buildLootItemComponents(EntityType entityType, Map storedItems) { + private List buildLootItemComponents(EntityType entityType, Map storedItems) { Map materialAmountMap = new HashMap<>(); - for (Map.Entry entry : storedItems.entrySet()) { + for (Map.Entry entry : storedItems.entrySet()) { Material material = entry.getKey().getTemplateRef().getType(); materialAmountMap.merge(material, entry.getValue(), Long::sum); } @@ -628,10 +629,10 @@ private List buildLootItemComponents(EntityType entityType, Map> sortedItems = + List> sortedItems = new ArrayList<>(storedItems.entrySet()); sortedItems.sort(Comparator.comparing(e -> e.getKey().getMaterialName())); - for (Map.Entry entry : sortedItems) { + for (Map.Entry entry : sortedItems) { Material material = entry.getKey().getTemplateRef().getType(); long amount = entry.getValue(); String formattedAmount = languageManager.formatNumber(amount); diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/sell/SpawnerSellConfirmUI.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/sell/SpawnerSellConfirmUI.java index b692fa71..b3155f7c 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/sell/SpawnerSellConfirmUI.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/sell/SpawnerSellConfirmUI.java @@ -1,5 +1,6 @@ package github.nighter.smartspawner.spawner.gui.sell; +import github.nighter.smartspawner.spawner.properties.ItemSignature; import net.kyori.adventure.text.Component; import github.nighter.smartspawner.SmartSpawner; import github.nighter.smartspawner.language.LanguageManager; @@ -164,7 +165,7 @@ private ItemStack createConfirmButton(Material material, Map pla private ItemStack createSpawnerInfoButton(Player player, SpawnerData spawner, Map placeholders) { // Build loot item components for {loot_items} placeholder - Map storedItems = spawner.getVirtualInventory().getConsolidatedItems(); + Map storedItems = spawner.getVirtualInventory().getConsolidatedItems(); List lootComponents = buildSellInfoLootComponents(spawner, storedItems); // Prepare the meta modifier consumer @@ -200,9 +201,9 @@ private ItemStack createSpawnerInfoButton(Player player, SpawnerData spawner, Ma return spawnerItem; } - private List buildSellInfoLootComponents(SpawnerData spawner, Map storedItems) { + private List buildSellInfoLootComponents(SpawnerData spawner, Map storedItems) { Map materialAmountMap = new HashMap<>(); - for (Map.Entry entry : storedItems.entrySet()) { + for (Map.Entry entry : storedItems.entrySet()) { Material material = entry.getKey().getTemplateRef().getType(); materialAmountMap.merge(material, entry.getValue(), Long::sum); } @@ -227,9 +228,9 @@ private List buildSellInfoLootComponents(SpawnerData spawner, Map> sortedItems = new ArrayList<>(storedItems.entrySet()); + List> sortedItems = new ArrayList<>(storedItems.entrySet()); sortedItems.sort(Comparator.comparing(e -> e.getKey().getMaterialName())); - for (Map.Entry entry : sortedItems) { + for (Map.Entry entry : sortedItems) { Material material = entry.getKey().getTemplateRef().getType(); long amount = entry.getValue(); String formattedAmount = languageManager.formatNumber(amount); 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 2c6afd60..abb2c91b 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 @@ -10,6 +10,7 @@ import github.nighter.smartspawner.spawner.gui.storage.button.SortButton; import github.nighter.smartspawner.spawner.lootgen.loot.EntityLootConfig; import github.nighter.smartspawner.spawner.lootgen.loot.LootItem; +import github.nighter.smartspawner.spawner.properties.ItemSignature; import github.nighter.smartspawner.spawner.properties.VirtualInventory; import github.nighter.smartspawner.spawner.properties.SpawnerData; import github.nighter.smartspawner.Scheduler; @@ -498,7 +499,7 @@ private ItemStack createCollectExpButton(SpawnerData spawner, Material material) } private ItemStack createStorageSpawnerInfoButton(SpawnerData spawner, Material material) { - Map storedItems = spawner.getVirtualInventory().getConsolidatedItems(); + Map storedItems = spawner.getVirtualInventory().getConsolidatedItems(); List lootComponents = buildStorageInfoLootComponents(spawner, storedItems); Map placeholders = new HashMap<>(); @@ -550,9 +551,9 @@ private ItemStack createStorageSpawnerInfoButton(SpawnerData spawner, Material m } private List buildStorageInfoLootComponents(SpawnerData spawner, - Map storedItems) { + Map storedItems) { Map materialAmountMap = new HashMap<>(); - for (Map.Entry entry : storedItems.entrySet()) { + for (Map.Entry entry : storedItems.entrySet()) { Material mat = entry.getKey().getTemplateRef().getType(); materialAmountMap.merge(mat, entry.getValue(), Long::sum); } @@ -581,9 +582,9 @@ private List buildStorageInfoLootComponents(SpawnerData spawner, "storage_spawner_info_button.loot_items", mat, formattedAmount, chance)); } } else { - List> sortedItems = new ArrayList<>(storedItems.entrySet()); + List> sortedItems = new ArrayList<>(storedItems.entrySet()); sortedItems.sort(Comparator.comparing(e -> e.getKey().getMaterialName())); - for (Map.Entry entry : sortedItems) { + for (Map.Entry entry : sortedItems) { Material mat = entry.getKey().getTemplateRef().getType(); long amount = entry.getValue(); String formattedAmount = languageManager.formatNumber(amount); 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 ff8a21ff..635a925c 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 @@ -3,6 +3,7 @@ import github.nighter.smartspawner.SmartSpawner; import github.nighter.smartspawner.config.Config; import github.nighter.smartspawner.spawner.gui.synchronization.SpawnerGuiViewManager; +import github.nighter.smartspawner.spawner.properties.ItemSignature; import github.nighter.smartspawner.spawner.properties.SpawnerData; import github.nighter.smartspawner.spawner.data.SpawnerManager; import github.nighter.smartspawner.spawner.properties.VirtualInventory; @@ -284,7 +285,7 @@ private List limitItemsToAvailableSlots(List items, Spawne } // Create a simulation inventory - Map simulatedInventory = new HashMap<>(currentInventory.getConsolidatedItems()); + Map simulatedInventory = new HashMap<>(currentInventory.getConsolidatedItems()); List acceptedItems = new ArrayList<>(); // Sort items by priority (you can change this sorting strategy) @@ -294,9 +295,9 @@ private List limitItemsToAvailableSlots(List items, Spawne if (item == null || item.getAmount() <= 0) continue; // Add to simulation and check slot count - Map tempSimulation = new HashMap<>(simulatedInventory); + Map tempSimulation = new HashMap<>(simulatedInventory); // Use cached signature to avoid excessive cloning - VirtualInventory.ItemSignature sig = VirtualInventory.getSignature(item); + ItemSignature sig = VirtualInventory.getSignature(item); tempSimulation.merge(sig, (long) item.getAmount(), (a, b) -> a + b); // Calculate slots needed @@ -335,7 +336,7 @@ private List limitItemsToAvailableSlots(List items, Spawne return acceptedItems; } - private int calculateSlots(Map items) { + private int calculateSlots(Map items) { // Use a more efficient calculation approach return items.entrySet().stream() .mapToInt(entry -> { @@ -349,7 +350,7 @@ private int calculateSlots(Map items) { private int calculateRequiredSlots(List items, VirtualInventory inventory) { // Create a temporary map to simulate how items would stack - Map simulatedItems = new HashMap<>(); + Map simulatedItems = new HashMap<>(); // First, get existing items if we need to account for them if (inventory != null) { @@ -361,7 +362,7 @@ private int calculateRequiredSlots(List items, VirtualInventory inven if (item == null || item.getAmount() <= 0) continue; // Use cached signature to avoid excessive cloning - VirtualInventory.ItemSignature sig = VirtualInventory.getSignature(item); + ItemSignature sig = VirtualInventory.getSignature(item); simulatedItems.merge(sig, (long) item.getAmount(), (a, b) -> a + b); } 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 new file mode 100644 index 00000000..5ac2fd33 --- /dev/null +++ b/core/src/main/java/github/nighter/smartspawner/spawner/properties/ItemSignature.java @@ -0,0 +1,94 @@ +package github.nighter.smartspawner.spawner.properties; + +import lombok.Getter; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.Damageable; +import org.bukkit.inventory.meta.ItemMeta; + +public class ItemSignature { + private final ItemStack template; + private final int hashCode; + @Getter + private final String materialName; + @Getter + private final int maxStackSize; // Cache purposes + + public ItemSignature(ItemStack item) { + this.template = item.clone(); + this.template.setAmount(1); + this.materialName = item.getType().name(); + this.hashCode = calculateHashCode(); + this.maxStackSize = item.getMaxStackSize(); + } + + // Replace the current calculateHashCode() method with: + private int calculateHashCode() { + // Use a faster hash algorithm and cache more item properties + int result = 31 * template.getType().ordinal(); // Using ordinal() instead of name() hashing + result = 31 * result + getItemDamage(template); + + // Only access ItemMeta when needed + if (template.hasItemMeta()) { + ItemMeta meta = template.getItemMeta(); + // Extract only the essential meta properties that determine similarity + result = 31 * result + (meta.hasDisplayName() ? meta.displayName().hashCode() : 0); + result = 31 * result + (meta.hasLore() ? meta.lore().hashCode() : 0); + result = 31 * result + (meta.hasEnchants() ? meta.getEnchants().hashCode() : 0); + } + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ItemSignature that)) return false; + + // First compare cheap properties + if (template.getType() != that.template.getType() || + getItemDamage(template) != getItemDamage(that.template)) { + return false; + } + + // Only check ItemMeta if types match + boolean thisHasMeta = template.hasItemMeta(); + boolean thatHasMeta = that.template.hasItemMeta(); + + if (thisHasMeta != thatHasMeta) { + return false; + } + + // If both have no meta, they're similar enough + if (!thisHasMeta) { + return true; + } + + // For complex items, fall back to isSimilar but only as a last resort + return template.isSimilar(that.template); + } + + @Override + public int hashCode() { + return hashCode; + } + + public ItemStack getTemplate() { + return template.clone(); + } + + // Non-cloning method for internal use + public ItemStack getTemplateRef() { + return template; + } + + private int getItemDamage(ItemStack item) { + if (!item.hasItemMeta()) { + return 0; + } + ItemMeta meta = item.getItemMeta(); + if (meta instanceof Damageable damageable) { + return damageable.getDamage(); + } + return 0; + } + +} 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 4800a1af..67d67879 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 @@ -192,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(); @@ -214,9 +212,7 @@ public void recalculateAfterConfigReload() { */ public void recalculateAfterAPIModification() { calculateStackBasedValues(); - if (virtualInventory != null && virtualInventory.getMaxSlots() != maxSpawnerLootSlots) { - recreateVirtualInventory(); - } + updateHologramData(); // Invalidate GUI cache after API modifications @@ -321,9 +317,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(); @@ -337,11 +330,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.min(Math.max(0L, exp), maxStoredExp); updateHologramData(); @@ -555,14 +543,14 @@ public void markSellValueDirty() { * @param itemsAdded Map of item signatures to quantities added * @param priceCache Price cache from loot config */ - public void incrementSellValue(Map itemsAdded, + public void incrementSellValue(Map itemsAdded, Map priceCache) { if (itemsAdded == null || itemsAdded.isEmpty()) { return; } double addedValue = 0.0; - for (Map.Entry entry : itemsAdded.entrySet()) { + for (Map.Entry entry : itemsAdded.entrySet()) { // Use getTemplateRef() to avoid cloning - we only need to read properties ItemStack template = entry.getKey().getTemplateRef(); long amount = entry.getValue(); @@ -587,16 +575,16 @@ public void decrementSellValue(List itemsRemoved, Map } // Consolidate removed items - Map consolidated = new java.util.HashMap<>(); + Map consolidated = new java.util.HashMap<>(); for (ItemStack item : itemsRemoved) { if (item == null || item.getAmount() <= 0) continue; // Use cached signature to avoid excessive cloning - VirtualInventory.ItemSignature sig = VirtualInventory.getSignature(item); + ItemSignature sig = VirtualInventory.getSignature(item); consolidated.merge(sig, (long) item.getAmount(), (a, b) -> a + b); } double removedValue = 0.0; - for (Map.Entry entry : consolidated.entrySet()) { + for (Map.Entry entry : consolidated.entrySet()) { // Use getTemplateRef() to avoid cloning - we only need to read properties ItemStack template = entry.getKey().getTemplateRef(); long amount = entry.getValue(); @@ -624,10 +612,10 @@ public void recalculateSellValue() { Map priceCache = createPriceCache(); // Calculate from current inventory - Map items = virtualInventory.getConsolidatedItems(); + Map items = virtualInventory.getConsolidatedItems(); double totalValue = 0.0; - for (Map.Entry entry : items.entrySet()) { + for (Map.Entry entry : items.entrySet()) { // Use getTemplateRef() to avoid cloning - we only need to read properties ItemStack template = entry.getKey().getTemplateRef(); long amount = entry.getValue(); @@ -735,11 +723,11 @@ public void addItemsAndUpdateSellValue(List items) { inventoryLock.lock(); try { // Consolidate items being added for efficient price lookup - Map itemsToAdd = new java.util.HashMap<>(); + Map itemsToAdd = new java.util.HashMap<>(); for (ItemStack item : items) { if (item == null || item.getAmount() <= 0) continue; // Use cached signature to avoid excessive cloning - VirtualInventory.ItemSignature sig = VirtualInventory.getSignature(item); + ItemSignature sig = VirtualInventory.getSignature(item); itemsToAdd.merge(sig, (long) item.getAmount(), (a, b) -> a + b); } 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 0162ede9..9725ce44 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 @@ -5,8 +5,6 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import lombok.Getter; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.Damageable; -import org.bukkit.inventory.meta.ItemMeta; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -15,116 +13,17 @@ public class VirtualInventory { private final Map consolidatedItems; @Getter private int maxSlots; - private final Int2ObjectOpenHashMap displayInventoryCache; - private boolean displayCacheDirty; - private int usedSlotsCache; - private long totalItemsCache; - private boolean metricsCacheDirty; // Cache sorted entries to avoid resorting when display isn't changing private List> sortedEntriesCache; private org.bukkit.Material preferredSortMaterial; - // Add an LRU cache for expensive item operations - private static final int ITEM_CACHE_SIZE = 128; - public VirtualInventory(int maxSlots) { this.maxSlots = maxSlots; this.consolidatedItems = new ConcurrentHashMap<>(); - this.displayInventoryCache = new Int2ObjectOpenHashMap<>(); // DO NOT pre-size the map, DONT! - this.displayCacheDirty = true; - this.metricsCacheDirty = true; - this.usedSlotsCache = 0; - this.totalItemsCache = 0; this.sortedEntriesCache = null; this.preferredSortMaterial = null; } - public static class ItemSignature { - private final ItemStack template; - private final int hashCode; - @Getter - private final String materialName; - - public ItemSignature(ItemStack item) { - this.template = item.clone(); - this.template.setAmount(1); - this.materialName = item.getType().name(); - this.hashCode = calculateHashCode(); - } - - // Replace the current calculateHashCode() method with: - private int calculateHashCode() { - // Use a faster hash algorithm and cache more item properties - int result = 31 * template.getType().ordinal(); // Using ordinal() instead of name() hashing - result = 31 * result + getItemDamage(template); - - // Only access ItemMeta when needed - if (template.hasItemMeta()) { - ItemMeta meta = template.getItemMeta(); - // Extract only the essential meta properties that determine similarity - result = 31 * result + (meta.hasDisplayName() ? meta.displayName().hashCode() : 0); - result = 31 * result + (meta.hasLore() ? meta.lore().hashCode() : 0); - result = 31 * result + (meta.hasEnchants() ? meta.getEnchants().hashCode() : 0); - } - return result; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof ItemSignature)) return false; - ItemSignature that = (ItemSignature) o; - - // First compare cheap properties - if (template.getType() != that.template.getType() || - getItemDamage(template) != getItemDamage(that.template)) { - return false; - } - - // Only check ItemMeta if types match - boolean thisHasMeta = template.hasItemMeta(); - boolean thatHasMeta = that.template.hasItemMeta(); - - if (thisHasMeta != thatHasMeta) { - return false; - } - - // If both have no meta, they're similar enough - if (!thisHasMeta) { - return true; - } - - // For complex items, fall back to isSimilar but only as a last resort - return template.isSimilar(that.template); - } - - @Override - public int hashCode() { - return hashCode; - } - - public ItemStack getTemplate() { - return template.clone(); - } - - // Non-cloning method for internal use - public ItemStack getTemplateRef() { - return template; - } - - private int getItemDamage(ItemStack item) { - if (!item.hasItemMeta()) { - return 0; - } - ItemMeta meta = item.getItemMeta(); - if (meta instanceof Damageable) { - return ((Damageable) meta).getDamage(); - } - return 0; - } - - } - public static ItemSignature getSignature(ItemStack item) { return new ItemSignature(item); } @@ -148,8 +47,6 @@ public void addItems(List items) { for (Map.Entry entry : itemBatch.entrySet()) { consolidatedItems.merge(entry.getKey(), entry.getValue(), (a, b) -> a + b); } - displayCacheDirty = true; - metricsCacheDirty = true; sortedEntriesCache = null; } } @@ -192,8 +89,6 @@ public boolean removeItems(List items) { } if (updated) { - displayCacheDirty = true; - metricsCacheDirty = true; sortedEntriesCache = null; // Invalidate sorted entries cache } @@ -214,69 +109,26 @@ public Int2ObjectMap getDisplayRange(int startSlot, int maxResults) { return buildDisplaySection(startSlot, maxResults); } - // Optimized getDisplayInventory method - public Int2ObjectMap getDisplayInventory() { - // Return cached result if available - if (!displayCacheDirty) { - // Return a shallow copy to prevent modification of the cache - return Int2ObjectMaps.unmodifiable(displayInventoryCache); - } - - displayInventoryCache.clear(); - displayInventoryCache.putAll(buildDisplaySection(0, maxSlots)); - - // Update cache state - displayCacheDirty = false; - usedSlotsCache = displayInventoryCache.size(); - - // Return unmodifiable map to prevent external changes - return Int2ObjectMaps.unmodifiable(displayInventoryCache); - } - - public long getTotalItems() { - if (metricsCacheDirty) { - updateMetricsCache(); - } - return totalItemsCache; - } - public Map getConsolidatedItems() { return new HashMap<>(consolidatedItems); } 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().getTemplateRef().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; } /** @@ -294,7 +146,6 @@ public void sortItems(org.bukkit.Material preferredMaterial) { // Only proceed if we have items to sort if (consolidatedItems.isEmpty()) { - this.displayCacheDirty = true; return; } @@ -319,35 +170,6 @@ 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 - } - - this.maxSlots = newMaxSlots; - - // Mark caches as dirty since slot count changed - this.displayCacheDirty = true; - - // 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 - } } private Int2ObjectMap buildDisplaySection(int startSlot, int maxResults) { 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 4221c791..878b431c 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 @@ -5,6 +5,7 @@ import github.nighter.smartspawner.api.events.SpawnerSellEvent; import github.nighter.smartspawner.language.MessageService; import github.nighter.smartspawner.spawner.gui.synchronization.SpawnerGuiViewManager; +import github.nighter.smartspawner.spawner.properties.ItemSignature; import github.nighter.smartspawner.spawner.properties.SpawnerData; import github.nighter.smartspawner.spawner.properties.VirtualInventory; @@ -83,7 +84,7 @@ public void sellAllItems(Player player, SpawnerData spawner, Runnable onComplete spawnerGuiViewManager.closeAllViewersInventory(spawner); // Lightweight snapshot – safe because isSelling prevents concurrent inventory changes - final Map itemSnapshot = virtualInv.getConsolidatedItems(); + final Map itemSnapshot = virtualInv.getConsolidatedItems(); final double accumulatedValue = spawner.getAccumulatedSellValue(); final Location spawnerLocation = spawner.getSpawnerLocation(); @@ -181,12 +182,12 @@ private void applySellResult(Player player, SpawnerData spawner, SellResult sell * Calculates the total sell value and constructs the list of {@link ItemStack}s to remove. * Pure computation – no Bukkit API calls, safe to run on an async thread. */ - private SellResult calculateSellValue(Map consolidatedItems, + private SellResult calculateSellValue(Map consolidatedItems, double totalValue) { long totalItemsSold = 0; ArrayList itemsToRemove = new ArrayList<>(); - for (Map.Entry entry : consolidatedItems.entrySet()) { + for (Map.Entry entry : consolidatedItems.entrySet()) { ItemStack templateRef = entry.getKey().getTemplateRef(); long amount = entry.getValue(); int maxStackSize = templateRef.getMaxStackSize(); diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/utils/ItemStackSerializer.java b/core/src/main/java/github/nighter/smartspawner/spawner/utils/ItemStackSerializer.java index 23db553b..b6c2f9de 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/utils/ItemStackSerializer.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/utils/ItemStackSerializer.java @@ -1,6 +1,6 @@ package github.nighter.smartspawner.spawner.utils; -import github.nighter.smartspawner.spawner.properties.VirtualInventory; +import github.nighter.smartspawner.spawner.properties.ItemSignature; import lombok.Getter; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; @@ -38,10 +38,10 @@ public void addPotionArrow(PotionType potionType, int count) { } } - public static List serializeInventory(Map items) { + public static List serializeInventory(Map items) { Map groupedItems = new HashMap<>(); - for (Map.Entry entry : items.entrySet()) { + for (Map.Entry entry : items.entrySet()) { // Use getTemplateRef() to avoid cloning - we only need to read properties ItemStack template = entry.getKey().getTemplateRef(); Material material = template.getType(); @@ -209,4 +209,4 @@ public static boolean isDestructibleItem(Material material) { || name.equals("WARPED_FUNGUS_ON_A_STICK") || name.equals("MACE"); } -} \ No newline at end of file +} From 19f51d2e660c014bb1a576ddf1f52008e5a16eb1 Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Tue, 12 May 2026 19:50:24 +0200 Subject: [PATCH 08/19] Start optimizing createButton itself --- .../spawner/gui/storage/SpawnerStorageUI.java | 18 ++++++++++++++++-- .../spawner/lootgen/SpawnerLootGenerator.java | 4 ++-- 2 files changed, 18 insertions(+), 4 deletions(-) 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 abb2c91b..cdad5c99 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 @@ -16,6 +16,7 @@ import github.nighter.smartspawner.Scheduler; import github.nighter.smartspawner.Scheduler.Task; import io.papermc.paper.datacomponent.DataComponentTypes; +import io.papermc.paper.datacomponent.item.ItemLore; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import lombok.Getter; @@ -289,8 +290,7 @@ 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 { // Read only the requested page instead of materializing the full logical inventory. VirtualInventory virtualInv = spawner.getVirtualInventory(); @@ -440,6 +440,20 @@ private ItemStack createButton(Material material, String name, List lore return item; } + private ItemStack createButtonModern(Material material, String name, List lore) { + ItemStack item = ItemStack.of(material); + + item.setData(DataComponentTypes.ITEM_NAME, Component.text("Next Page")); + item.setData(DataComponentTypes.LORE, ItemLore.lore(lore)); + + // Hide tooltip for BUNDLE material (prevents showing bundle contents) + if (material == Material.BUNDLE) { + github.nighter.smartspawner.nms.VersionInitializer.hideTooltip(item); + } + + return item; + } + private ItemStack createNavigationButton(String type, int targetPage, Material material) { Map placeholders = new HashMap<>(); placeholders.put("target_page", String.valueOf(targetPage)); 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 635a925c..0b6f28b7 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 @@ -341,7 +341,7 @@ private int calculateSlots(Map items) { return items.entrySet().stream() .mapToInt(entry -> { long amount = entry.getValue(); - int maxStackSize = entry.getKey().getTemplateRef().getMaxStackSize(); + int maxStackSize = entry.getKey().getMaxStackSize(); // Use integer division with ceiling function return (int) ((amount + maxStackSize - 1) / maxStackSize); }) @@ -363,7 +363,7 @@ private int calculateRequiredSlots(List items, VirtualInventory inven // Use cached signature to avoid excessive cloning ItemSignature sig = VirtualInventory.getSignature(item); - simulatedItems.merge(sig, (long) item.getAmount(), (a, b) -> a + b); + simulatedItems.merge(sig, (long) item.getAmount(), Long::sum); } // Calculate exact slots needed From 2981565bb227394fa9d98fea51b0412d5a34300a Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Wed, 13 May 2026 10:05:32 +0200 Subject: [PATCH 09/19] Make LRUCache a Guava wrapper and use it in SortButton --- .../smartspawner/language/LRUCache.java | 89 ----------- .../language/LanguageManager.java | 2 +- .../gui/storage/button/SortButton.java | 7 +- .../nighter/smartspawner/utils/LRUCache.java | 146 ++++++++++++++++++ 4 files changed, 151 insertions(+), 93 deletions(-) delete mode 100644 core/src/main/java/github/nighter/smartspawner/language/LRUCache.java create mode 100644 core/src/main/java/github/nighter/smartspawner/utils/LRUCache.java diff --git a/core/src/main/java/github/nighter/smartspawner/language/LRUCache.java b/core/src/main/java/github/nighter/smartspawner/language/LRUCache.java deleted file mode 100644 index 6df30b2e..00000000 --- a/core/src/main/java/github/nighter/smartspawner/language/LRUCache.java +++ /dev/null @@ -1,89 +0,0 @@ -package github.nighter.smartspawner.language; - -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * A simple LRU (Least Recently Used) cache implementation - * that automatically removes the least recently accessed entries - * when the cache reaches its capacity. - * - * @param The type of keys maintained by this cache - * @param The type of values maintained by this cache - */ -public class LRUCache { - private final LinkedHashMap cache; - private int capacity; - - /** - * Constructs an LRU cache with the specified capacity - * - * @param capacity The maximum number of entries in the cache - */ - public LRUCache(int capacity) { - this.capacity = capacity; - this.cache = new LinkedHashMap(capacity, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > LRUCache.this.capacity; - } - }; - } - - /** - * Returns the value associated with the specified key, - * or null if no mapping exists for the key - * - * @param key The key whose associated value is to be returned - * @return The value associated with the key, or null if no mapping exists - */ - public synchronized V get(K key) { - return cache.get(key); - } - - /** - * Associates the specified value with the specified key in this cache - * - * @param key The key with which the specified value is to be associated - * @param value The value to be associated with the specified key - * @return The previous value associated with the key, or null if no mapping existed - */ - public synchronized V put(K key, V value) { - return cache.put(key, value); - } - - /** - * Removes all entries from the cache - */ - public synchronized void clear() { - cache.clear(); - } - - /** - * Returns the number of key-value mappings in this cache - * - * @return The number of key-value mappings in this cache - */ - public synchronized int size() { - return cache.size(); - } - - /** - * Returns the capacity of this cache - * - * @return The capacity of this cache - */ - public synchronized int capacity() { - return capacity; - } - - /** - * Resize the cache capacity - * - * @param newCapacity The new capacity for the cache - */ - public synchronized void resize(int newCapacity) { - this.capacity = newCapacity; - // The LinkedHashMap will automatically adjust its size on the next put operation - } -} \ No newline at end of file diff --git a/core/src/main/java/github/nighter/smartspawner/language/LanguageManager.java b/core/src/main/java/github/nighter/smartspawner/language/LanguageManager.java index 455e9a25..bec8ad43 100644 --- a/core/src/main/java/github/nighter/smartspawner/language/LanguageManager.java +++ b/core/src/main/java/github/nighter/smartspawner/language/LanguageManager.java @@ -1,6 +1,7 @@ package github.nighter.smartspawner.language; import github.nighter.smartspawner.SmartSpawner; +import github.nighter.smartspawner.utils.LRUCache; import lombok.Getter; import org.bukkit.ChatColor; import org.bukkit.Material; @@ -17,7 +18,6 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; -import java.text.Normalizer; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; 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 index 32396103..e141f830 100644 --- 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 @@ -1,6 +1,7 @@ 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; @@ -8,12 +9,12 @@ import org.bukkit.inventory.ItemStack; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; public final class SortButton { - private static final Map SORT_BUTTON_CACHE = new ConcurrentHashMap<>(); + 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); @@ -26,7 +27,7 @@ public static ItemStack getOrBuildSortButton(SpawnerData spawner, Material butto EntityLootConfig lootConfig = spawner.getLootConfig(); - return SORT_BUTTON_CACHE.computeIfAbsent( + return SORT_BUTTON_CACHE.get( new SortButtonCacheKey( lootConfig, spawner.getPreferredSortItem(), diff --git a/core/src/main/java/github/nighter/smartspawner/utils/LRUCache.java b/core/src/main/java/github/nighter/smartspawner/utils/LRUCache.java new file mode 100644 index 00000000..eb6879a8 --- /dev/null +++ b/core/src/main/java/github/nighter/smartspawner/utils/LRUCache.java @@ -0,0 +1,146 @@ +package github.nighter.smartspawner.utils; + +import com.google.common.base.Preconditions; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +import java.util.function.Function; + +/** + * A lightweight LRU-style cache backed by Guava's {@link Cache}. + * + *

This cache automatically evicts least-recently-used entries + * when the configured maximum size is exceeded.

+ * + *

The preferred access pattern is {@link #get(Object, Function)}, + * which provides atomic lazy-loading behavior similar to + * {@code computeIfAbsent}.

+ * + * @param key type + * @param value type + */ +public final class LRUCache { + + private final Cache cache; + private final int capacity; + + /** + * Creates a new cache with the specified maximum capacity. + * + * @param capacity maximum number of entries allowed in the cache + * @throws IllegalArgumentException if capacity is less than or equal to zero + */ + public LRUCache(int capacity) { + if (capacity <= 0) { + throw new IllegalArgumentException("Capacity must be > 0"); + } + + this.capacity = capacity; + this.cache = createCache(capacity); + } + + /** + * Creates the underlying Guava cache instance. + * + * @param capacity maximum cache size + * @return configured cache instance + */ + private Cache createCache(int capacity) { + return CacheBuilder.newBuilder() + .maximumSize(capacity) + .build(); + } + + /** + * Retrieves a cached value if present. + * + * @param key cache key + * @return cached value, or {@code null} if absent + * @deprecated Prefer {@link #get(Object, Function)} for atomic lazy loading + */ + @Deprecated + public V get(K key) { + return cache.getIfPresent(key); + } + + /** + * Stores a value in the cache. + * + * @param key cache key + * @param value value to cache + * @return previously cached value, or {@code null} if absent + * @deprecated Prefer {@link #get(Object, Function)} for atomic lazy loading + */ + @Deprecated + public V put(K key, V value) { + V previous = cache.getIfPresent(key); + cache.put(key, value); + return previous; + } + + /** + * Retrieves a cached value, computing and caching it atomically + * if it is not already present. + * + *

The mapping function is only invoked when the key is absent.

+ * + * @param key cache key + * @param mappingFunction value supplier for cache misses + * @return cached or newly computed value + * @throws RuntimeException if the mapping function throws an exception + */ + public V get(K key, Function mappingFunction) { + Preconditions.checkNotNull(key); + Preconditions.checkNotNull(mappingFunction); + + try { + return cache.get(key, () -> mappingFunction.apply(key)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Removes all entries from the cache. + */ + public void clear() { + cache.invalidateAll(); + } + + /** + * Returns the approximate number of entries currently stored. + * + * @return estimated cache size + */ + public int size() { + return Math.toIntExact(cache.size()); + } + + /** + * Returns the configured maximum capacity of the cache. + * + * @return maximum cache size + */ + public int capacity() { + return capacity; + } + + /** + * Removes a specific entry from the cache. + * + * @param key cache key to invalidate + */ + public void remove(K key) { + cache.invalidate(key); + } + + /** + * Checks whether a key currently exists in the cache. + * + * @param key cache key + * @return {@code true} if the key is cached + */ + public boolean containsKey(K key) { + return cache.getIfPresent(key) != null; + } +} From 681501dbb23781069f1dd50161aee882456486f4 Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Wed, 13 May 2026 13:20:32 +0200 Subject: [PATCH 10/19] Optimize the UI further --- .../spawner/gui/storage/SpawnerStorageUI.java | 155 +++++++++--------- 1 file changed, 74 insertions(+), 81 deletions(-) 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 cdad5c99..814cab38 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 @@ -21,7 +21,6 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import lombok.Getter; import net.kyori.adventure.text.Component; -import org.bukkit.entity.EntityType; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; @@ -30,7 +29,6 @@ import org.bukkit.Material; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; public class SpawnerStorageUI { @@ -44,19 +42,19 @@ public class SpawnerStorageUI { // Precomputed buttons to avoid repeated creation private final Map staticButtons; - // Lightweight caches with better eviction strategies - private final Map navigationButtonCache; - private final Map pageIndicatorCache; + // Navigation buttons + private ItemStack previousNavigationButtonBase; + private ItemStack nextNavigationButtonBase; - // Cache expiry time reduced for more responsive updates - private static final int MAX_CACHE_SIZE = 100; + private List previousNavigationLore; + private List nextNavigationLore; + + private String previousNavigationFirstLine; + private String nextNavigationFirstLine; // Cache for title format to avoid repeated language lookups private String cachedStorageTitleFormat = null; - // Cleanup task to remove stale entries from caches - private Task cleanupTask; - public SpawnerStorageUI(SmartSpawner plugin) { this.plugin = plugin; this.languageManager = plugin.getLanguageManager(); @@ -64,11 +62,9 @@ public SpawnerStorageUI(SmartSpawner plugin) { // Initialize caches with appropriate initial capacity this.staticButtons = new HashMap<>(8); - this.navigationButtonCache = new ConcurrentHashMap<>(16); - this.pageIndicatorCache = new ConcurrentHashMap<>(16); initializeStaticButtons(); - startCleanupTask(); + initializeNavigationButtons(); } public void reload() { @@ -76,13 +72,14 @@ public void reload() { this.layoutConfig = plugin.getGuiLayoutConfig(); // Clear caches to force reloading of buttons - navigationButtonCache.clear(); - pageIndicatorCache.clear(); staticButtons.clear(); cachedStorageTitleFormat = null; // Reinitialize static buttons initializeStaticButtons(); + + // Reinitialize navigation buttons + initializeNavigationButtons(); } private void initializeStaticButtons() { @@ -310,6 +307,46 @@ private void addPageItems(Map updates, Set slotsToE } } + private void initializeNavigationButtons() { + GuiLayout layout = layoutConfig.getCurrentStorageLayout(); + + for (GuiButton button : layout.getAllButtons().values()) { + String action = getAnyActionFromButton(button); + + if (action == null) { + continue; + } + + switch (action) { + case "previous_page" -> { + previousNavigationLore = + languageManager.getGuiItemLoreAsList("navigation_button_previous.lore"); + + previousNavigationFirstLine = previousNavigationLore.getFirst(); + + previousNavigationButtonBase = createButton( + button.getMaterial(), + languageManager.getGuiItemName("navigation_button_previous.name"), + previousNavigationLore + ); + } + + case "next_page" -> { + nextNavigationLore = + languageManager.getGuiItemLoreAsList("navigation_button_next.lore"); + + nextNavigationFirstLine = nextNavigationLore.getFirst(); + + nextNavigationButtonBase = createButton( + button.getMaterial(), + languageManager.getGuiItemName("navigation_button_next.name"), + nextNavigationLore + ); + } + } + } + } + private void addNavigationButtons(Map updates, SpawnerData spawner, int page, int totalPages) { if (totalPages == -1) { totalPages = calculateTotalPages(spawner); @@ -342,16 +379,13 @@ private void addNavigationButtons(Map updates, SpawnerData s switch (action) { case "previous_page": if (page > 1) { - String cacheKey = "prev-" + (page - 1); - item = navigationButtonCache.computeIfAbsent( - cacheKey, k -> createNavigationButton("previous", page - 1, button.getMaterial())); + item = createNavigationButton(true, page - 1); } break; + case "next_page": if (page < totalPages) { - String cacheKey = "next-" + (page + 1); - item = navigationButtonCache.computeIfAbsent( - cacheKey, k -> createNavigationButton("next", page + 1, button.getMaterial())); + item = createNavigationButton(false, page + 1); } break; case "take_all": @@ -440,37 +474,31 @@ private ItemStack createButton(Material material, String name, List lore return item; } - private ItemStack createButtonModern(Material material, String name, List lore) { - ItemStack item = ItemStack.of(material); - - item.setData(DataComponentTypes.ITEM_NAME, Component.text("Next Page")); - item.setData(DataComponentTypes.LORE, ItemLore.lore(lore)); + private ItemStack createNavigationButton(boolean previous, int targetPage) { + ItemStack item = (previous + ? previousNavigationButtonBase + : nextNavigationButtonBase).clone(); - // Hide tooltip for BUNDLE material (prevents showing bundle contents) - if (material == Material.BUNDLE) { - github.nighter.smartspawner.nms.VersionInitializer.hideTooltip(item); - } + ItemMeta meta = item.getItemMeta(); - return item; - } + if (meta != null) { + List baseLore = previous + ? previousNavigationLore + : nextNavigationLore; - private ItemStack createNavigationButton(String type, int targetPage, Material material) { - Map placeholders = new HashMap<>(); - placeholders.put("target_page", String.valueOf(targetPage)); + List lore = new ArrayList<>(baseLore); - String buttonName; - String buttonKey; + lore.set(0, + (previous + ? previousNavigationFirstLine + : nextNavigationFirstLine).replace("{target_page}", Integer.toString(targetPage)) + ); - if (type.equals("previous")) { - buttonKey = "navigation_button_previous"; - } else { - buttonKey = "navigation_button_next"; + meta.setLore(lore); + item.setItemMeta(meta); } - buttonName = languageManager.getGuiItemName(buttonKey + ".name", placeholders); - String[] buttonLore = languageManager.getGuiItemLore(buttonKey + ".lore", placeholders); - - return createButton(material, buttonName, Arrays.asList(buttonLore)); + return item; } private ItemStack createSellButton(SpawnerData spawner, Material material) { @@ -609,46 +637,11 @@ private List buildStorageInfoLootComponents(SpawnerData spawner, return components; } - private void startCleanupTask() { - cleanupTask = Scheduler.runTaskTimer(this::cleanupCaches, 20L * 30, 20L * 30); // Run every 30 seconds - } - - public void cancelTasks() { - if (cleanupTask != null) { - cleanupTask.cancel(); - cleanupTask = null; - } - } - - private void cleanupCaches() { - // LRU-like cleanup for navigation buttons - if (navigationButtonCache.size() > MAX_CACHE_SIZE) { - int toRemove = navigationButtonCache.size() - (MAX_CACHE_SIZE / 2); - List keysToRemove = new ArrayList<>(navigationButtonCache.keySet()); - for (int i = 0; i < Math.min(toRemove, keysToRemove.size()); i++) { - navigationButtonCache.remove(keysToRemove.get(i)); - } - } - - // LRU-like cleanup for page indicators - if (pageIndicatorCache.size() > MAX_CACHE_SIZE) { - int toRemove = pageIndicatorCache.size() - (MAX_CACHE_SIZE / 2); - List keysToRemove = new ArrayList<>(pageIndicatorCache.keySet()); - for (int i = 0; i < Math.min(toRemove, keysToRemove.size()); i++) { - pageIndicatorCache.remove(keysToRemove.get(i)); - } - } - } - public void cleanup() { - navigationButtonCache.clear(); - pageIndicatorCache.clear(); cachedStorageTitleFormat = null; - // Cancel scheduled tasks - cancelTasks(); - // Re-initialize static buttons (just in case language has changed) initializeStaticButtons(); + initializeNavigationButtons(); } } From 64f32509551b279c1308fb143367983f8d03a38b Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Wed, 13 May 2026 15:01:05 +0200 Subject: [PATCH 11/19] Improve ItemSignature by caching the Material --- .../spawner/gui/main/SpawnerMenuUI.java | 9 ++++----- .../spawner/gui/sell/SpawnerSellConfirmUI.java | 4 ++-- .../spawner/gui/storage/SpawnerStorageUI.java | 4 ++-- .../spawner/properties/ItemSignature.java | 18 ++++++++++-------- .../spawner/properties/VirtualInventory.java | 12 ++++++------ .../spawner/sell/SpawnerSellManager.java | 5 +++-- .../spawner/utils/ItemStackSerializer.java | 5 +++-- 7 files changed, 30 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/main/SpawnerMenuUI.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/main/SpawnerMenuUI.java index 2f040532..a70a2b66 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/main/SpawnerMenuUI.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/main/SpawnerMenuUI.java @@ -287,7 +287,7 @@ private String buildLootItemsText(EntityType entityType, Map materialAmountMap = new HashMap<>(); for (Map.Entry entry : storedItems.entrySet()) { - Material material = entry.getKey().getTemplateRef().getType(); + Material material = entry.getKey().getMaterial(); materialAmountMap.merge(material, entry.getValue(), Long::sum); } @@ -333,8 +333,7 @@ private String buildLootItemsText(EntityType entityType, Map e.getKey().getMaterialName())); for (Map.Entry entry : sortedItems) { - ItemStack templateItem = entry.getKey().getTemplateRef(); - Material material = templateItem.getType(); + Material material = entry.getKey().getMaterial(); long amount = entry.getValue(); String materialName = languageManager.getVanillaItemName(material); @@ -606,7 +605,7 @@ private int calculatePercentage(long current, long maximum) { private List buildLootItemComponents(EntityType entityType, Map storedItems) { Map materialAmountMap = new HashMap<>(); for (Map.Entry entry : storedItems.entrySet()) { - Material material = entry.getKey().getTemplateRef().getType(); + Material material = entry.getKey().getMaterial(); materialAmountMap.merge(material, entry.getValue(), Long::sum); } @@ -633,7 +632,7 @@ private List buildLootItemComponents(EntityType entityType, Map(storedItems.entrySet()); sortedItems.sort(Comparator.comparing(e -> e.getKey().getMaterialName())); for (Map.Entry entry : sortedItems) { - Material material = entry.getKey().getTemplateRef().getType(); + Material material = entry.getKey().getMaterial(); long amount = entry.getValue(); String formattedAmount = languageManager.formatNumber(amount); components.add(languageManager.buildTranslatableGuiLootLine( diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/sell/SpawnerSellConfirmUI.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/sell/SpawnerSellConfirmUI.java index b3155f7c..6e105110 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/sell/SpawnerSellConfirmUI.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/sell/SpawnerSellConfirmUI.java @@ -204,7 +204,7 @@ private ItemStack createSpawnerInfoButton(Player player, SpawnerData spawner, Ma private List buildSellInfoLootComponents(SpawnerData spawner, Map storedItems) { Map materialAmountMap = new HashMap<>(); for (Map.Entry entry : storedItems.entrySet()) { - Material material = entry.getKey().getTemplateRef().getType(); + Material material = entry.getKey().getMaterial(); materialAmountMap.merge(material, entry.getValue(), Long::sum); } @@ -231,7 +231,7 @@ private List buildSellInfoLootComponents(SpawnerData spawner, Map> sortedItems = new ArrayList<>(storedItems.entrySet()); sortedItems.sort(Comparator.comparing(e -> e.getKey().getMaterialName())); for (Map.Entry entry : sortedItems) { - Material material = entry.getKey().getTemplateRef().getType(); + Material material = entry.getKey().getMaterial(); long amount = entry.getValue(); String formattedAmount = languageManager.formatNumber(amount); components.add(languageManager.buildTranslatableGuiLootLine( 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 814cab38..4b1f3d03 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 @@ -596,7 +596,7 @@ private List buildStorageInfoLootComponents(SpawnerData spawner, Map storedItems) { Map materialAmountMap = new HashMap<>(); for (Map.Entry entry : storedItems.entrySet()) { - Material mat = entry.getKey().getTemplateRef().getType(); + Material mat = entry.getKey().getMaterial(); materialAmountMap.merge(mat, entry.getValue(), Long::sum); } @@ -627,7 +627,7 @@ private List buildStorageInfoLootComponents(SpawnerData spawner, List> sortedItems = new ArrayList<>(storedItems.entrySet()); sortedItems.sort(Comparator.comparing(e -> e.getKey().getMaterialName())); for (Map.Entry entry : sortedItems) { - Material mat = entry.getKey().getTemplateRef().getType(); + Material mat = entry.getKey().getMaterial(); long amount = entry.getValue(); String formattedAmount = languageManager.formatNumber(amount); components.add(languageManager.buildTranslatableGuiLootLine( 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 5ac2fd33..d5bed147 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 @@ -1,6 +1,7 @@ package github.nighter.smartspawner.spawner.properties; import lombok.Getter; +import org.bukkit.Material; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.Damageable; import org.bukkit.inventory.meta.ItemMeta; @@ -8,15 +9,13 @@ public class ItemSignature { private final ItemStack template; private final int hashCode; - @Getter - private final String materialName; - @Getter - private final int maxStackSize; // Cache purposes + @Getter private final Material material; + @Getter private final int maxStackSize; // Cache purposes public ItemSignature(ItemStack item) { this.template = item.clone(); this.template.setAmount(1); - this.materialName = item.getType().name(); + this.material = template.getType(); this.hashCode = calculateHashCode(); this.maxStackSize = item.getMaxStackSize(); } @@ -24,7 +23,7 @@ public ItemSignature(ItemStack item) { // Replace the current calculateHashCode() method with: private int calculateHashCode() { // Use a faster hash algorithm and cache more item properties - int result = 31 * template.getType().ordinal(); // Using ordinal() instead of name() hashing + int result = 31 * this.material.ordinal(); // Using ordinal() instead of name() hashing result = 31 * result + getItemDamage(template); // Only access ItemMeta when needed @@ -44,8 +43,7 @@ public boolean equals(Object o) { if (!(o instanceof ItemSignature that)) return false; // First compare cheap properties - if (template.getType() != that.template.getType() || - getItemDamage(template) != getItemDamage(that.template)) { + if (material != that.material || getItemDamage(template) != getItemDamage(that.template)) { return false; } @@ -80,6 +78,10 @@ public ItemStack getTemplateRef() { return template; } + public String getMaterialName() { + return material.name(); + } + private int getItemDamage(ItemStack item) { if (!item.hasItemMeta()) { return 0; 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 9725ce44..3ea476d6 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 @@ -154,8 +154,8 @@ public void sortItems(org.bukkit.Material preferredMaterial) { this.sortedEntriesCache = consolidatedItems.entrySet().stream() .sorted((e1, e2) -> { // Use getTemplateRef() to avoid cloning - we only need to read the type - boolean e1Preferred = e1.getKey().getTemplateRef().getType() == preferredMaterial; - boolean e2Preferred = e2.getKey().getTemplateRef().getType() == preferredMaterial; + boolean e1Preferred = e1.getKey().getMaterial() == preferredMaterial; + boolean e2Preferred = e2.getKey().getMaterial() == preferredMaterial; if (e1Preferred && !e2Preferred) return -1; if (!e1Preferred && e2Preferred) return 1; @@ -199,8 +199,7 @@ private Int2ObjectMap buildDisplaySection(int startSlot, int maxResul } ItemSignature sig = entry.getKey(); - ItemStack templateItem = sig.getTemplateRef(); - int maxStackSize = templateItem.getMaxStackSize(); + int maxStackSize = sig.getMaxStackSize(); if (maxStackSize <= 0) { continue; } @@ -220,6 +219,7 @@ private Int2ObjectMap buildDisplaySection(int startSlot, int maxResul long remainingAmount = totalAmount - ((long) stacksToSkip * maxStackSize); currentGlobalSlot += stacksToSkip; + ItemStack templateItem = sig.getTemplateRef(); while (remainingAmount > 0 && relativeSlot < sectionLimit && currentGlobalSlot < maxSlots) { ItemStack displayItem = templateItem.clone(); displayItem.setAmount((int) Math.min(remainingAmount, maxStackSize)); @@ -244,8 +244,8 @@ private List> getSortedEntries() { private void sortEntries(List> entries) { if (preferredSortMaterial != null) { entries.sort((e1, e2) -> { - boolean e1Preferred = e1.getKey().getTemplateRef().getType() == preferredSortMaterial; - boolean e2Preferred = e2.getKey().getTemplateRef().getType() == preferredSortMaterial; + boolean e1Preferred = e1.getKey().getMaterial() == preferredSortMaterial; + boolean e2Preferred = e2.getKey().getMaterial() == preferredSortMaterial; if (e1Preferred && !e2Preferred) return -1; if (!e1Preferred && e2Preferred) return 1; 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 878b431c..73cce4fd 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 @@ -188,9 +188,10 @@ private SellResult calculateSellValue(Map consolidatedItems ArrayList itemsToRemove = new ArrayList<>(); for (Map.Entry entry : consolidatedItems.entrySet()) { - ItemStack templateRef = entry.getKey().getTemplateRef(); + ItemSignature signature = entry.getKey(); + ItemStack templateRef = signature.getTemplateRef(); long amount = entry.getValue(); - int maxStackSize = templateRef.getMaxStackSize(); + int maxStackSize = signature.getMaxStackSize(); totalItemsSold += amount; diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/utils/ItemStackSerializer.java b/core/src/main/java/github/nighter/smartspawner/spawner/utils/ItemStackSerializer.java index b6c2f9de..701f6c5b 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/utils/ItemStackSerializer.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/utils/ItemStackSerializer.java @@ -43,8 +43,9 @@ public static List serializeInventory(Map items) { for (Map.Entry entry : items.entrySet()) { // Use getTemplateRef() to avoid cloning - we only need to read properties - ItemStack template = entry.getKey().getTemplateRef(); - Material material = template.getType(); + ItemSignature signature = entry.getKey(); + ItemStack template = signature.getTemplateRef(); + Material material = signature.getMaterial(); ItemGroup group = groupedItems.computeIfAbsent(material, ItemGroup::new); if (material == Material.TIPPED_ARROW) { From 73b6b3d54ab44efa24c69dfc2645eead2b12076e Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Wed, 13 May 2026 17:04:31 +0200 Subject: [PATCH 12/19] Improve start times and addItems logic --- .../spawner/data/SpawnerFileHandler.java | 8 +- .../data/database/SpawnerDatabaseHandler.java | 8 +- .../spawner/properties/VirtualInventory.java | 107 ++++++++++++------ 3 files changed, 73 insertions(+), 50 deletions(-) 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 f3d0933d..a04572b8 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 @@ -437,13 +437,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 d28e4c1d..5a11ed9b 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/properties/VirtualInventory.java b/core/src/main/java/github/nighter/smartspawner/spawner/properties/VirtualInventory.java index 3ea476d6..4b57ed83 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 @@ -11,8 +11,7 @@ public class VirtualInventory { private final Map consolidatedItems; - @Getter - private int maxSlots; + @Getter private int maxSlots; // Cache sorted entries to avoid resorting when display isn't changing private List> sortedEntriesCache; private org.bukkit.Material preferredSortMaterial; @@ -28,69 +27,105 @@ public static ItemSignature getSignature(ItemStack item) { return new ItemSignature(item); } - // Add items in bulk with minimal operations + /* + * FAST PATH + * Used for loading already-consolidated storage data. + */ + public void addItem(ItemStack item, long amount) { + if (item == null || amount <= 0) { + return; + } + + ItemSignature signature = getSignature(item); + + consolidatedItems.merge(signature, amount, Long::sum); + + sortedEntriesCache = null; + } + + /* + * Bulk insert for physical item stacks. + */ public void addItems(List items) { - if (items.isEmpty()) return; + if (items.isEmpty()) { + return; + } - // Pre-allocate space for batch processing Map itemBatch = new HashMap<>(items.size()); - // 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); - } + if (item == null) { + continue; + } + + int amount = item.getAmount(); - // 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); + if (amount <= 0) { + continue; } - sortedEntriesCache = null; + + ItemSignature signature = getSignature(item); + + itemBatch.merge(signature, (long) amount, Long::sum); } + + if (itemBatch.isEmpty()) { + return; + } + + for (Map.Entry entry : itemBatch.entrySet()) { + consolidatedItems.merge(entry.getKey(), entry.getValue(), Long::sum); + } + + sortedEntriesCache = null; } - // Remove items in bulk with minimal operations + public boolean removeItems(List items) { - if (items.isEmpty()) return true; + 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); + if (item == null) { + continue; + } + + int amount = item.getAmount(); + + if (amount <= 0) { + continue; + } + + ItemSignature signature = getSignature(item); + + toRemove.merge(signature, (long) amount, Long::sum); } - if (toRemove.isEmpty()) return true; + if (toRemove.isEmpty()) { + return true; + } - // Verify we have enough of each item for (Map.Entry entry : toRemove.entrySet()) { - Long currentAmount = consolidatedItems.getOrDefault(entry.getKey(), 0L); + long currentAmount = consolidatedItems.getOrDefault(entry.getKey(), 0L); + if (currentAmount < entry.getValue()) { return false; } } - // Perform removals all at once - boolean updated = false; for (Map.Entry entry : toRemove.entrySet()) { - ItemSignature sig = entry.getKey(); + ItemSignature signature = entry.getKey(); long amountToRemove = entry.getValue(); - consolidatedItems.computeIfPresent(sig, (key, current) -> { - long newAmount = current - amountToRemove; - return newAmount <= 0 ? null : newAmount; + consolidatedItems.computeIfPresent(signature, (key, current) -> { + long remaining = current - amountToRemove; + return remaining <= 0 ? null : remaining; }); - - updated = true; } - if (updated) { - sortedEntriesCache = null; // Invalidate sorted entries cache - } + sortedEntriesCache = null; return true; } From 31adc458b20cd98008fa24d9d7f093d82166b969 Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Thu, 14 May 2026 08:08:04 +0200 Subject: [PATCH 13/19] LinkedHashMap is better --- .../nighter/smartspawner/utils/LRUCache.java | 146 +++++++----------- 1 file changed, 56 insertions(+), 90 deletions(-) diff --git a/core/src/main/java/github/nighter/smartspawner/utils/LRUCache.java b/core/src/main/java/github/nighter/smartspawner/utils/LRUCache.java index eb6879a8..b6f78b9b 100644 --- a/core/src/main/java/github/nighter/smartspawner/utils/LRUCache.java +++ b/core/src/main/java/github/nighter/smartspawner/utils/LRUCache.java @@ -1,146 +1,112 @@ package github.nighter.smartspawner.utils; import com.google.common.base.Preconditions; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.function.Function; /** - * A lightweight LRU-style cache backed by Guava's {@link Cache}. + * A simple LRU (Least Recently Used) cache implementation + * that automatically removes the least recently accessed entries + * when the cache reaches its capacity. * - *

This cache automatically evicts least-recently-used entries - * when the configured maximum size is exceeded.

- * - *

The preferred access pattern is {@link #get(Object, Function)}, - * which provides atomic lazy-loading behavior similar to - * {@code computeIfAbsent}.

- * - * @param key type - * @param value type + * @param The type of keys maintained by this cache + * @param The type of values maintained by this cache */ -public final class LRUCache { - - private final Cache cache; - private final int capacity; +public class LRUCache { + private final LinkedHashMap cache; + private int capacity; /** - * Creates a new cache with the specified maximum capacity. + * Constructs an LRU cache with the specified capacity * - * @param capacity maximum number of entries allowed in the cache - * @throws IllegalArgumentException if capacity is less than or equal to zero + * @param capacity The maximum number of entries in the cache */ public LRUCache(int capacity) { - if (capacity <= 0) { - throw new IllegalArgumentException("Capacity must be > 0"); - } - this.capacity = capacity; - this.cache = createCache(capacity); - } - - /** - * Creates the underlying Guava cache instance. - * - * @param capacity maximum cache size - * @return configured cache instance - */ - private Cache createCache(int capacity) { - return CacheBuilder.newBuilder() - .maximumSize(capacity) - .build(); + this.cache = new LinkedHashMap(capacity, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > LRUCache.this.capacity; + } + }; } /** - * Retrieves a cached value if present. + * Returns the value associated with the specified key, + * or null if no mapping exists for the key * - * @param key cache key - * @return cached value, or {@code null} if absent - * @deprecated Prefer {@link #get(Object, Function)} for atomic lazy loading + * @param key The key whose associated value is to be returned + * @return The value associated with the key, or null if no mapping exists */ - @Deprecated - public V get(K key) { - return cache.getIfPresent(key); + public synchronized V get(K key) { + return cache.get(key); } /** - * Stores a value in the cache. + * Associates the specified value with the specified key in this cache * - * @param key cache key - * @param value value to cache - * @return previously cached value, or {@code null} if absent - * @deprecated Prefer {@link #get(Object, Function)} for atomic lazy loading + * @param key The key with which the specified value is to be associated + * @param value The value to be associated with the specified key + * @return The previous value associated with the key, or null if no mapping existed */ - @Deprecated - public V put(K key, V value) { - V previous = cache.getIfPresent(key); - cache.put(key, value); - return previous; + public synchronized V put(K key, V value) { + return cache.put(key, value); } /** - * Retrieves a cached value, computing and caching it atomically - * if it is not already present. + * Returns the value associated with the specified key, computing and + * caching it with the supplied mapping function when no mapping exists. * - *

The mapping function is only invoked when the key is absent.

+ *

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 cache key - * @param mappingFunction value supplier for cache misses - * @return cached or newly computed value - * @throws RuntimeException if the mapping function throws an exception + * @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} or {@code mappingFunction} is null */ - public V get(K key, Function mappingFunction) { + public synchronized V get(K key, Function mappingFunction) { Preconditions.checkNotNull(key); Preconditions.checkNotNull(mappingFunction); - try { - return cache.get(key, () -> mappingFunction.apply(key)); - } catch (Exception e) { - throw new RuntimeException(e); - } + return cache.computeIfAbsent(key, mappingFunction); } /** - * Removes all entries from the cache. + * Removes all entries from the cache */ - public void clear() { - cache.invalidateAll(); + public synchronized void clear() { + cache.clear(); } /** - * Returns the approximate number of entries currently stored. + * Returns the number of key-value mappings in this cache * - * @return estimated cache size + * @return The number of key-value mappings in this cache */ - public int size() { - return Math.toIntExact(cache.size()); + public synchronized int size() { + return cache.size(); } /** - * Returns the configured maximum capacity of the cache. + * Returns the capacity of this cache * - * @return maximum cache size + * @return The capacity of this cache */ - public int capacity() { + public synchronized int capacity() { return capacity; } /** - * Removes a specific entry from the cache. - * - * @param key cache key to invalidate - */ - public void remove(K key) { - cache.invalidate(key); - } - - /** - * Checks whether a key currently exists in the cache. + * Resize the cache capacity * - * @param key cache key - * @return {@code true} if the key is cached + * @param newCapacity The new capacity for the cache */ - public boolean containsKey(K key) { - return cache.getIfPresent(key) != null; + public synchronized void resize(int newCapacity) { + this.capacity = newCapacity; + // The LinkedHashMap will automatically adjust its size on the next put operation } } From 916d18495d2f3b1d62a1995ec4ed1f17f8820fb5 Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Thu, 14 May 2026 09:01:50 +0200 Subject: [PATCH 14/19] Optimize navigation buttons --- .../spawner/gui/storage/SpawnerStorageUI.java | 84 +----------- .../storage/button/NavigationButtonCache.java | 129 ++++++++++++++++++ 2 files changed, 135 insertions(+), 78 deletions(-) create mode 100644 core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/button/NavigationButtonCache.java 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 4b1f3d03..47825d06 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 @@ -7,6 +7,7 @@ import github.nighter.smartspawner.spawner.gui.layout.GuiButton; import github.nighter.smartspawner.spawner.gui.layout.GuiLayout; import github.nighter.smartspawner.spawner.gui.layout.GuiLayoutConfig; +import github.nighter.smartspawner.spawner.gui.storage.button.NavigationButtonCache; import github.nighter.smartspawner.spawner.gui.storage.button.SortButton; import github.nighter.smartspawner.spawner.lootgen.loot.EntityLootConfig; import github.nighter.smartspawner.spawner.lootgen.loot.LootItem; @@ -14,9 +15,6 @@ import github.nighter.smartspawner.spawner.properties.VirtualInventory; import github.nighter.smartspawner.spawner.properties.SpawnerData; import github.nighter.smartspawner.Scheduler; -import github.nighter.smartspawner.Scheduler.Task; -import io.papermc.paper.datacomponent.DataComponentTypes; -import io.papermc.paper.datacomponent.item.ItemLore; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import lombok.Getter; @@ -42,15 +40,7 @@ public class SpawnerStorageUI { // Precomputed buttons to avoid repeated creation private final Map staticButtons; - // Navigation buttons - private ItemStack previousNavigationButtonBase; - private ItemStack nextNavigationButtonBase; - - private List previousNavigationLore; - private List nextNavigationLore; - - private String previousNavigationFirstLine; - private String nextNavigationFirstLine; + private final NavigationButtonCache navigationButtons; // Cache for title format to avoid repeated language lookups private String cachedStorageTitleFormat = null; @@ -62,6 +52,7 @@ public SpawnerStorageUI(SmartSpawner plugin) { // Initialize caches with appropriate initial capacity this.staticButtons = new HashMap<>(8); + this.navigationButtons = new NavigationButtonCache(data -> createButton(data.material(), data.name(), data.lore())); initializeStaticButtons(); initializeNavigationButtons(); @@ -308,43 +299,7 @@ private void addPageItems(Map updates, Set slotsToE } private void initializeNavigationButtons() { - GuiLayout layout = layoutConfig.getCurrentStorageLayout(); - - for (GuiButton button : layout.getAllButtons().values()) { - String action = getAnyActionFromButton(button); - - if (action == null) { - continue; - } - - switch (action) { - case "previous_page" -> { - previousNavigationLore = - languageManager.getGuiItemLoreAsList("navigation_button_previous.lore"); - - previousNavigationFirstLine = previousNavigationLore.getFirst(); - - previousNavigationButtonBase = createButton( - button.getMaterial(), - languageManager.getGuiItemName("navigation_button_previous.name"), - previousNavigationLore - ); - } - - case "next_page" -> { - nextNavigationLore = - languageManager.getGuiItemLoreAsList("navigation_button_next.lore"); - - nextNavigationFirstLine = nextNavigationLore.getFirst(); - - nextNavigationButtonBase = createButton( - button.getMaterial(), - languageManager.getGuiItemName("navigation_button_next.name"), - nextNavigationLore - ); - } - } - } + navigationButtons.reload(layoutConfig.getCurrentStorageLayout(), languageManager); } private void addNavigationButtons(Map updates, SpawnerData spawner, int page, int totalPages) { @@ -379,13 +334,13 @@ private void addNavigationButtons(Map updates, SpawnerData s switch (action) { case "previous_page": if (page > 1) { - item = createNavigationButton(true, page - 1); + item = navigationButtons.getPreviousButton(page - 1); } break; case "next_page": if (page < totalPages) { - item = createNavigationButton(false, page + 1); + item = navigationButtons.getNextButton(page + 1); } break; case "take_all": @@ -474,33 +429,6 @@ private ItemStack createButton(Material material, String name, List lore return item; } - private ItemStack createNavigationButton(boolean previous, int targetPage) { - ItemStack item = (previous - ? previousNavigationButtonBase - : nextNavigationButtonBase).clone(); - - ItemMeta meta = item.getItemMeta(); - - if (meta != null) { - List baseLore = previous - ? previousNavigationLore - : nextNavigationLore; - - List lore = new ArrayList<>(baseLore); - - lore.set(0, - (previous - ? previousNavigationFirstLine - : nextNavigationFirstLine).replace("{target_page}", Integer.toString(targetPage)) - ); - - meta.setLore(lore); - item.setItemMeta(meta); - } - - return item; - } - private ItemStack createSellButton(SpawnerData spawner, Material material) { // Create placeholders for total sell price Map placeholders = new HashMap<>(); 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) {} +} From 70991ab8bbb4a01e641b6b1fa97a26ec9545fa7f Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Thu, 14 May 2026 09:27:30 +0200 Subject: [PATCH 15/19] Do not check mappingFunction --- .../main/java/github/nighter/smartspawner/utils/LRUCache.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/main/java/github/nighter/smartspawner/utils/LRUCache.java b/core/src/main/java/github/nighter/smartspawner/utils/LRUCache.java index b6f78b9b..fe96fea9 100644 --- a/core/src/main/java/github/nighter/smartspawner/utils/LRUCache.java +++ b/core/src/main/java/github/nighter/smartspawner/utils/LRUCache.java @@ -66,11 +66,10 @@ public synchronized V put(K key, V value) { * @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} or {@code mappingFunction} is null + * @throws NullPointerException if {@code key} is null */ public synchronized V get(K key, Function mappingFunction) { Preconditions.checkNotNull(key); - Preconditions.checkNotNull(mappingFunction); return cache.computeIfAbsent(key, mappingFunction); } From bfd09f812e7845752048a3820acb410ee2bc7f12 Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Thu, 14 May 2026 09:27:57 +0200 Subject: [PATCH 16/19] Improve hashCode and equals in SortButtonCacheKey --- .../gui/storage/button/SortButton.java | 64 +++++++++++++------ 1 file changed, 43 insertions(+), 21 deletions(-) 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 index e141f830..bed4523c 100644 --- 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 @@ -18,8 +18,6 @@ public final class SortButton { private static final EnumMap MATERIAL_NAME_CACHE = new EnumMap<>(Material.class); - private record SortButtonCacheKey(EntityLootConfig lootConfig, Material selectedMaterial, Material buttonMaterial) {} - private SortButton() {} public static ItemStack getOrBuildSortButton(SpawnerData spawner, Material buttonMaterial, @@ -35,8 +33,8 @@ public static ItemStack getOrBuildSortButton(SpawnerData spawner, Material butto ), key -> buildSortButton( lootConfig, - key.selectedMaterial(), - key.buttonMaterial(), + key.selectedMaterial, + key.buttonMaterial, languageManager, buttonFactory ) @@ -52,19 +50,13 @@ private static ItemStack buildSortButton(EntityLootConfig lootConfig, Material c String availableItemsString; - if (lootConfig != null - && lootConfig.getAllItems() != null - && !lootConfig.getAllItems().isEmpty()) { + if (lootConfig != null && lootConfig.getAllItems() != null && !lootConfig.getAllItems().isEmpty()) { - List sortedLoot = - new ArrayList<>(lootConfig.getAllItems()); + List sortedLoot = new ArrayList<>(lootConfig.getAllItems()); - sortedLoot.sort( - Comparator.comparing(item -> item.material().name()) - ); + sortedLoot.sort(Comparator.comparing(item -> item.material().name())); - StringBuilder availableItems = - new StringBuilder(sortedLoot.size() * 32); + StringBuilder availableItems = new StringBuilder(sortedLoot.size() * 32); boolean first = true; @@ -80,14 +72,11 @@ private static ItemStack buildSortButton(EntityLootConfig lootConfig, Material c languageManager::getVanillaItemName ); - String format = - currentSort == lootMaterial + String format = currentSort == lootMaterial ? selectedItemFormat : unselectedItemFormat; - availableItems.append( - format.replace("{item_name}", itemName) - ); + availableItems.append(format.replace("{item_name}", itemName)); first = false; } @@ -101,8 +90,7 @@ private static ItemStack buildSortButton(EntityLootConfig lootConfig, Material c placeholders.put("available_items", availableItemsString); return buttonFactory.apply( - new ButtonData( - buttonMaterial, + new ButtonData(buttonMaterial, languageManager.getGuiItemName("sort_items_button.name", placeholders), languageManager.getGuiItemLoreWithMultilinePlaceholders("sort_items_button.lore", placeholders) ) @@ -110,4 +98,38 @@ private static ItemStack buildSortButton(EntityLootConfig lootConfig, Material c } 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; + } + } } From 1a9e61dd5cd932adf6942af9ac85541c437373dd Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Thu, 14 May 2026 18:45:24 +0200 Subject: [PATCH 17/19] Add -DEV to the version --- build.gradle.kts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index e9664cca..f0d3dd6b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ allprojects { apply(plugin = "maven-publish") group = "github.nighter" - version = "1.6.6" + version = "1.6.6-DEV" repositories { mavenCentral() @@ -98,4 +98,3 @@ tasks.withType().configureEach { options.release.set(targetJavaVersion) } } - From 2d46fcad6da6e9708cebc340d2e8a87dbe878aef Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Thu, 14 May 2026 19:15:52 +0200 Subject: [PATCH 18/19] Very scary: Completely remove physical ItemStacks in the lootgen logic --- .../utils/LootPreGenerationHelper.java | 6 +- .../spawner/lootgen/LootResult.java | 7 +- .../spawner/lootgen/SpawnerLootGenerator.java | 348 +++++++++--------- .../spawner/lootgen/SpawnerRangeChecker.java | 5 +- .../spawner/properties/SpawnerData.java | 148 +++++--- .../spawner/properties/VirtualInventory.java | 71 ++-- .../smartspawner/spawner/sell/SellResult.java | 16 +- .../spawner/sell/SpawnerSellManager.java | 46 ++- 8 files changed, 343 insertions(+), 304 deletions(-) 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..54c7c0c5 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..3475d849 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 0b6f28b7..e439ec46 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,190 +174,205 @@ public void spawnLootToSpawner(SpawnerData spawner) { } public LootResult generateLoot(int minMobs, int maxMobs, SpawnerData spawner) { + ThreadLocalRandom random = ThreadLocalRandom.current(); - int mobCount = ThreadLocalRandom.current().nextInt(maxMobs - minMobs + 1) + minMobs; + int mobCount = generateMobCount(minMobs, maxMobs, random); 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<>(validItems.size()); + + boolean optimizedLootgen = Config.get().isOptimizedLootgen(); - // Process mobs in batch rather than individually for (LootItem lootItem : validItems) { - // Calculate the probability for the entire mob batch at once int totalAmount; - if (Config.get().isOptimizedLootgen() && shouldApproximate(lootItem.chance(), mobCount)) { - // O(1) binomial approximation - totalAmount = generateApproximatedLoot(lootItem, mobCount); + if (optimizedLootgen && shouldApproximate(lootItem.chance(), mobCount)) { + totalAmount = generateApproximatedLoot(lootItem, mobCount, random); } else { - // O(n) binomial distribution - totalAmount = generateExactLoot(lootItem, mobCount); + totalAmount = generateExactLoot(lootItem, mobCount, random); } - if (totalAmount > 0) { - // Create item just once per loot type - ItemStack prototype = lootItem.createItemStack(ThreadLocalRandom.current()); - 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(random); + if (prototype == null || prototype.getType() == Material.AIR) { + continue; } + + ItemSignature signature = VirtualInventory.getSignature(prototype); + consolidatedLoot.merge(signature, totalAmount, Integer::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) { - // simple heuristic: use expected if at least one item can be generated - if (chance <= 0D) return false; - return mobCount > 97.5D / chance; + private int generateMobCount(int minMobs, int maxMobs, ThreadLocalRandom random) { + int lowerBound = Math.max(0, Math.min(minMobs, maxMobs)); + int upperBound = Math.max(0, Math.max(minMobs, maxMobs)); + + if (upperBound == lowerBound) { + return upperBound; + } + + return random.nextInt(lowerBound, upperBound + 1); } - // O(n) simulation: exact per-mob drop calculation - private int generateExactLoot(LootItem lootItem, int mobCount) { + private int generateExactLoot(LootItem lootItem, int mobCount, ThreadLocalRandom random) { int successfulDrops = 0; - ThreadLocalRandom random = ThreadLocalRandom.current(); + double p = lootItem.chance() / 100.0; + for (int i = 0; i < mobCount; i++) { if (random.nextDouble() < p) { successfulDrops++; } } + + if (successfulDrops == 0) { + return 0; + } + int totalAmount = 0; + for (int i = 0; i < successfulDrops; i++) { totalAmount += lootItem.generateAmount(random); } + return totalAmount; } - // O(1) expected-value calculation with small jitter - private int generateApproximatedLoot(LootItem lootItem, int mobCount) { + private int generateApproximatedLoot(LootItem lootItem, int mobCount, ThreadLocalRandom random) { double p = lootItem.chance() / 100.0; double expectedDrops = mobCount * p; double avgAmount = lootItem.getAverageAmount(); + double jitter = p != 1.0 - ? 0.95 + ThreadLocalRandom.current().nextDouble() * 0.10 + ? 0.95 + random.nextDouble() * 0.10 : 1.0; + 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); - } - } + int 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; - // We've filled all slots, stop processing - break; + continue; + } + + int remainingSlots = Math.max(0, maxSlots - usedSlots); + long maxAddAmount = ((long) (oldSlots + remainingSlots) * maxStackSize) - currentAmount; + + if (maxAddAmount <= 0) { + continue; + } + + int acceptedAmount = (int) Math.min(maxAddAmount, amount); + + 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(), Long::sum); + 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(); + Integer amount = entry.getValue(); + if (signature == null || amount == null || amount <= 0) { + continue; + } + copy.merge(signature, amount, Integer::sum); + } + + return copy; + } + + // Determines whether to use expected-value approximation + private boolean shouldApproximate(double chance, int mobCount) { + // simple heuristic: use expected if at least one item can be generated + if (chance <= 0D) return false; + return mobCount > 97.5D / chance; } /** @@ -418,34 +423,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 +458,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 +470,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 +485,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 +497,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 +554,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 +602,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..56f09655 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/SpawnerData.java b/core/src/main/java/github/nighter/smartspawner/spawner/properties/SpawnerData.java index 67d67879..519fd889 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.nms.VersionInitializer; @@ -15,6 +16,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; @@ -81,7 +83,7 @@ public class SpawnerData { // Calculated values based on stackSize @Getter private int maxStoragePages; - @Getter @Setter + @Getter private int maxSpawnerLootSlots; @Getter @Setter private long maxStoredExp; @@ -95,7 +97,7 @@ public class SpawnerData { @Getter @Setter private int maxStackSize; - @Getter @Setter + @Getter private VirtualInventory virtualInventory; @Getter private final Set filteredItems = new HashSet<>(); @@ -109,8 +111,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; @@ -124,7 +125,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; @@ -168,7 +169,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; } @@ -227,12 +228,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; if (baseSpawnerDelay <= 0) { @@ -361,6 +376,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; @@ -443,7 +459,7 @@ public List getValidLootItems() { } private boolean isLootItemValid(LootItem item) { - ItemStack example = item.createItemStack(new Random()); + ItemStack example = item.createItemStack(ThreadLocalRandom.current()); return example != null && !filteredItems.contains(example.getType()); } @@ -538,29 +554,38 @@ 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; } double addedValue = 0.0; - for (Map.Entry entry : itemsAdded.entrySet()) { + for (Map.Entry entry : itemsAdded.entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) { + continue; + } + // Use getTemplateRef() to avoid cloning - we only need to read properties ItemStack template = entry.getKey().getTemplateRef(); - long amount = entry.getValue(); + long amount = entry.getValue().longValue(); double itemPrice = findItemPrice(template, priceCache); if (itemPrice > 0.0) { addedValue += itemPrice * amount; } } - this.accumulatedSellValue += addedValue; + if (addedValue > 0.0) { + this.accumulatedSellValue.addAndGet(addedValue); + } this.sellValueDirty = false; } @@ -574,27 +599,42 @@ 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; + } + // Use getTemplateRef() to avoid cloning - we only need to read properties ItemStack template = entry.getKey().getTemplateRef(); - long amount = entry.getValue(); + long amount = entry.getValue().longValue(); double itemPrice = findItemPrice(template, priceCache); if (itemPrice > 0.0) { removedValue += itemPrice * amount; } } - this.accumulatedSellValue = Math.max(0.0, this.accumulatedSellValue - removedValue); + subtractAccumulatedSellValue(removedValue); } /** @@ -603,7 +643,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,10 +665,23 @@ public void recalculateSellValue() { } } - 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 @@ -709,35 +762,22 @@ private String createItemKey(ItemStack item) { } /** - * 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; } - // 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(); @@ -755,13 +795,31 @@ 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 boolean removed = virtualInventory.removeItems(items); - // Update sell value atomically if removal was successful if (removed && !sellValueDirty) { Map priceCache = createPriceCache(); decrementSellValue(items, priceCache); @@ -773,13 +831,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 4b57ed83..1108b9e0 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 @@ -4,6 +4,7 @@ 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.*; @@ -14,7 +15,7 @@ public class VirtualInventory { @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; @@ -27,6 +28,10 @@ public static ItemSignature getSignature(ItemStack item) { return new ItemSignature(item); } + public void setMaxSlots(int maxSlots) { + this.maxSlots = Math.max(0, maxSlots); + } + /* * FAST PATH * Used for loading already-consolidated storage data. @@ -44,63 +49,58 @@ public void addItem(ItemStack item, long amount) { } /* - * Bulk insert for physical item stacks. + * Bulk insert for already-consolidated storage data. */ - public void addItems(List items) { - if (items.isEmpty()) { + public void addItems(Map items) { + if (items == null || items.isEmpty()) { return; } - Map itemBatch = new HashMap<>(items.size()); + boolean changed = false; - for (ItemStack item : items) { - if (item == null) { + for (Map.Entry entry : items.entrySet()) { + ItemSignature signature = entry.getKey(); + Number amountValue = entry.getValue(); + + if (signature == null || amountValue == null) { continue; } - int amount = item.getAmount(); - + long amount = amountValue.longValue(); if (amount <= 0) { continue; } - ItemSignature signature = getSignature(item); - - itemBatch.merge(signature, (long) amount, Long::sum); - } - - if (itemBatch.isEmpty()) { - return; + consolidatedItems.merge(signature, amount, Long::sum); + changed = true; } - for (Map.Entry entry : itemBatch.entrySet()) { - consolidatedItems.merge(entry.getKey(), entry.getValue(), Long::sum); + if (changed) { + sortedEntriesCache = null; } - - sortedEntriesCache = null; } - public boolean removeItems(List items) { - if (items.isEmpty()) { + public boolean removeItems(Map items) { + if (items == null || items.isEmpty()) { return true; } - Map toRemove = new HashMap<>(); + Map toRemove = new HashMap<>(items.size()); - for (ItemStack item : items) { - if (item == null) { + for (Map.Entry entry : items.entrySet()) { + ItemSignature signature = entry.getKey(); + Number amountValue = entry.getValue(); + + if (signature == null || amountValue == null) { continue; } - int amount = item.getAmount(); - + long amount = amountValue.longValue(); if (amount <= 0) { continue; } - ItemSignature signature = getSignature(item); - - toRemove.merge(signature, (long) amount, Long::sum); + toRemove.merge(signature, amount, Long::sum); } if (toRemove.isEmpty()) { @@ -108,19 +108,14 @@ public boolean removeItems(List items) { } for (Map.Entry entry : toRemove.entrySet()) { - long currentAmount = consolidatedItems.getOrDefault(entry.getKey(), 0L); - - if (currentAmount < entry.getValue()) { + if (consolidatedItems.getOrDefault(entry.getKey(), 0L) < entry.getValue()) { return false; } } for (Map.Entry entry : toRemove.entrySet()) { - ItemSignature signature = entry.getKey(); - long amountToRemove = entry.getValue(); - - consolidatedItems.computeIfPresent(signature, (key, current) -> { - long remaining = current - amountToRemove; + consolidatedItems.computeIfPresent(entry.getKey(), (key, current) -> { + long remaining = current - entry.getValue(); return remaining <= 0 ? null : remaining; }); } 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 73cce4fd..70d2afcf 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 @@ -140,7 +140,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(); @@ -179,34 +183,38 @@ 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(); - ItemStack templateRef = signature.getTemplateRef(); - 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 = templateRef.clone(); - 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(); + if (signature == null || amount == null || amount <= 0) { + continue; } + + ItemStack stack = signature.getTemplate(); + stack.setAmount((int) Math.min(amount, Integer.MAX_VALUE)); + apiItems.add(stack); } - return new SellResult(totalValue, totalItemsSold, itemsToRemove); + return apiItems; } } From da1dc86102e895b56d8356412f1220e3df50af8b Mon Sep 17 00:00:00 2001 From: RVSkeLe Date: Thu, 14 May 2026 20:39:56 +0200 Subject: [PATCH 19/19] Improve ItemSignature --- .../spawner/properties/ItemSignature.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 d5bed147..a5348f73 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 @@ -9,22 +9,25 @@ public class ItemSignature { private final ItemStack template; private final int hashCode; + // Cache purposes @Getter private final Material material; - @Getter private final int maxStackSize; // Cache purposes + @Getter private final int maxStackSize; + @Getter private final int damage; public ItemSignature(ItemStack item) { this.template = item.clone(); this.template.setAmount(1); this.material = template.getType(); - this.hashCode = calculateHashCode(); this.maxStackSize = item.getMaxStackSize(); + this.damage = getItemDamage(template); + this.hashCode = calculateHashCode(); } // Replace the current calculateHashCode() method with: private int calculateHashCode() { // Use a faster hash algorithm and cache more item properties int result = 31 * this.material.ordinal(); // Using ordinal() instead of name() hashing - result = 31 * result + getItemDamage(template); + result = 31 * result + this.damage; // Only access ItemMeta when needed if (template.hasItemMeta()) { @@ -43,7 +46,7 @@ public boolean equals(Object o) { if (!(o instanceof ItemSignature that)) return false; // First compare cheap properties - if (material != that.material || getItemDamage(template) != getItemDamage(that.template)) { + if (material != that.material || this.damage != that.damage) { return false; }