From a7970aba41400120296cf23b60ec533afb5afb44 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 16:26:33 +0000 Subject: [PATCH 1/6] Initial plan From 02cd932dee4e618be03c557608a42aebc3243a63 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 16:32:17 +0000 Subject: [PATCH 2/6] Fix timer sync: convert ticks to milliseconds in SpawnerLootGenerator Co-authored-by: ptthanh02 <73684260+ptthanh02@users.noreply.github.com> --- .../smartspawner/spawner/lootgen/SpawnerLootGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e28d0597..7db626fb 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 @@ -66,7 +66,7 @@ public void spawnLootToSpawner(SpawnerData spawner) { try { long lastSpawnTime = spawner.getLastSpawnTime(); - long spawnDelay = spawner.getSpawnDelay(); + long spawnDelay = spawner.getSpawnDelay() * 50L; // Convert ticks to milliseconds if (currentTime - lastSpawnTime < spawnDelay) { return; From e8f69382ed1ed65a5f00b46c47f8f1dc4fd18ee5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 17:28:01 +0000 Subject: [PATCH 3/6] Refactor: SpawnerRangeChecker manages timer, SpawnerGuiViewManager triggers spawn Co-authored-by: ptthanh02 <73684260+ptthanh02@users.noreply.github.com> --- .../gui/synchronization/SpawnerGuiViewManager.java | 10 ++++------ .../spawner/lootgen/SpawnerLootGenerator.java | 10 +++------- .../spawner/lootgen/SpawnerRangeChecker.java | 10 ++++++---- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java index 718559e5..75ad9c10 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java @@ -1346,7 +1346,7 @@ private long calculateTimeUntilNextSpawn(SpawnerData spawner) { // Ensure we don't go below 0 or above the delay timeUntilNextSpawn = Math.max(0, Math.min(timeUntilNextSpawn, cachedDelay)); - // If the timer has expired, handle spawn timing + // If the timer has expired, trigger loot spawn if (timeUntilNextSpawn <= 0) { try { // Try to acquire dataLock with timeout to prevent deadlock @@ -1358,15 +1358,13 @@ private long calculateTimeUntilNextSpawn(SpawnerData spawner) { timeElapsed = currentTime - lastSpawnTime; if (timeElapsed >= cachedDelay) { - // Update last spawn time to current time for next cycle - spawner.setLastSpawnTime(currentTime); - // Get the spawner location to schedule on the right region Location spawnerLocation = spawner.getSpawnerLocation(); if (spawnerLocation != null) { - // Schedule activation on the appropriate region thread for Folia compatibility + // Schedule loot spawn on the appropriate region thread for Folia compatibility + // SpawnerRangeChecker manages the timer, SpawnerGuiViewManager triggers the spawn Scheduler.runLocationTask(spawnerLocation, () -> { - plugin.getRangeChecker().activateSpawner(spawner); + plugin.getSpawnerLootGenerator().spawnLootToSpawner(spawner); }); } return cachedDelay; // Start new cycle with full delay 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 7db626fb..68ac4496 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 @@ -65,13 +65,9 @@ public void spawnLootToSpawner(SpawnerData spawner) { final AtomicInteger maxSlots; try { - long lastSpawnTime = spawner.getLastSpawnTime(); - long spawnDelay = spawner.getSpawnDelay() * 50L; // Convert ticks to milliseconds - - if (currentTime - lastSpawnTime < spawnDelay) { - return; - } - + // 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()); 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 3870e207..4c15d449 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 @@ -135,12 +135,14 @@ public void activateSpawner(SpawnerData spawner) { long currentTime = System.currentTimeMillis(); spawner.setLastSpawnTime(currentTime); + // Timer doesn't spawn loot directly - it just runs periodically to keep the spawner active + // SpawnerGuiViewManager will check the timer and trigger spawns via SpawnerLootGenerator Scheduler.Task task = Scheduler.runTaskTimer(() -> { - if (!spawner.getSpawnerStop().get()) { - plugin.getLogger().info("Spawning loot for spawner " + spawner.getSpawnerId()); - spawnerLootGenerator.spawnLootToSpawner(spawner); + // Task just keeps the spawner active - actual spawning logic moved to GUI manager + if (spawner.getSpawnerStop().get()) { + plugin.getLogger().info("Spawner " + spawner.getSpawnerId() + " is stopped, timer running but no spawn"); } - }, spawner.getSpawnDelay(), spawner.getSpawnDelay()); // Start after one delay period + }, spawner.getSpawnDelay(), spawner.getSpawnDelay()); spawnerTasks.put(spawner.getSpawnerId(), task); From 8e0a8722980e9433a50b6b25bd2f127a4f04a4ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 18:01:07 +0000 Subject: [PATCH 4/6] Optimize timer sync: add pre-generation, remove debug logging, check spawner active state Co-authored-by: ptthanh02 <73684260+ptthanh02@users.noreply.github.com> --- .../SpawnerGuiViewManager.java | 85 +++++++-- .../spawner/lootgen/SpawnerLootGenerator.java | 178 ++++++++++++++++++ .../spawner/lootgen/SpawnerRangeChecker.java | 16 +- 3 files changed, 255 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java index 75ad9c10..1646f79f 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java @@ -91,6 +91,29 @@ public class SpawnerGuiViewManager implements Listener { // Cache for storage title format to avoid repeated language lookups private String cachedStorageTitleFormat = null; + // Pre-generation cache: stores loot generated before timer expires + // Map - loot ready to be added when timer hits 0 + private final Map preGeneratedLoot = new ConcurrentHashMap<>(); + + // Threshold for pre-generation (in milliseconds) - start generating when this much time remains + private static final long PRE_GENERATION_THRESHOLD = 2000L; // 2 seconds before spawn + + // Track which spawners are currently generating loot + private final Set currentlyGenerating = ConcurrentHashMap.newKeySet(); + + // Static class to hold pre-generated loot + private static class PendingLoot { + final List items; + final int experience; + final long generatedAt; + + PendingLoot(List items, int experience) { + this.items = items; + this.experience = experience; + this.generatedAt = System.currentTimeMillis(); + } + } + // Static class to hold viewer info more efficiently private static class SpawnerViewerInfo { final SpawnerData spawnerData; @@ -1331,25 +1354,48 @@ private long calculateTimeUntilNextSpawn(SpawnerData spawner) { long lastSpawnTime = spawner.getLastSpawnTime(); long timeElapsed = currentTime - lastSpawnTime; - // Check if spawner is truly inactive (not just in initial stopped state) - // This mirrors the logic from SpawnerMenuUI.calculateInitialTimerValue() to ensure consistency - // between initial display and ongoing timer updates + // Check if spawner is active before processing boolean isSpawnerInactive = !spawner.getSpawnerActive() || - (spawner.getSpawnerStop().get() && timeElapsed > cachedDelay * 2); // Only inactive if stopped for more than 2 cycles + (spawner.getSpawnerStop().get() && timeElapsed > cachedDelay * 2); if (isSpawnerInactive) { + // Clean up any pre-generated loot for inactive spawner + String spawnerId = spawner.getSpawnerId(); + preGeneratedLoot.remove(spawnerId); + currentlyGenerating.remove(spawnerId); return -1; } long timeUntilNextSpawn = cachedDelay - timeElapsed; - - // Ensure we don't go below 0 or above the delay timeUntilNextSpawn = Math.max(0, Math.min(timeUntilNextSpawn, cachedDelay)); - // If the timer has expired, trigger loot spawn + String spawnerId = spawner.getSpawnerId(); + + // Pre-generation: Start generating loot when timer is close to expiring + if (timeUntilNextSpawn > 0 && timeUntilNextSpawn <= PRE_GENERATION_THRESHOLD) { + // Only start pre-generation once per cycle + if (!currentlyGenerating.contains(spawnerId) && !preGeneratedLoot.containsKey(spawnerId)) { + currentlyGenerating.add(spawnerId); + + // Pre-generate loot asynchronously + Location spawnerLocation = spawner.getSpawnerLocation(); + if (spawnerLocation != null) { + Scheduler.runLocationTask(spawnerLocation, () -> { + plugin.getSpawnerLootGenerator().preGenerateLoot(spawner, (items, experience) -> { + // Store the pre-generated loot + if (items != null && (!items.isEmpty() || experience > 0)) { + preGeneratedLoot.put(spawnerId, new PendingLoot(items, experience)); + } + currentlyGenerating.remove(spawnerId); + }); + }); + } + } + } + + // Timer expired: Add pre-generated loot to spawner if (timeUntilNextSpawn <= 0) { try { - // Try to acquire dataLock with timeout to prevent deadlock if (spawner.getDataLock().tryLock(100, TimeUnit.MILLISECONDS)) { try { // Re-check timing after acquiring lock @@ -1358,16 +1404,23 @@ private long calculateTimeUntilNextSpawn(SpawnerData spawner) { timeElapsed = currentTime - lastSpawnTime; if (timeElapsed >= cachedDelay) { - // Get the spawner location to schedule on the right region + // Check for pre-generated loot + PendingLoot pending = preGeneratedLoot.remove(spawnerId); + Location spawnerLocation = spawner.getSpawnerLocation(); if (spawnerLocation != null) { - // Schedule loot spawn on the appropriate region thread for Folia compatibility - // SpawnerRangeChecker manages the timer, SpawnerGuiViewManager triggers the spawn Scheduler.runLocationTask(spawnerLocation, () -> { - plugin.getSpawnerLootGenerator().spawnLootToSpawner(spawner); + if (pending != null) { + // Use pre-generated loot for better UX + plugin.getSpawnerLootGenerator().addPreGeneratedLoot(spawner, pending.items, pending.experience); + } else { + // Fallback: Generate and add immediately if pre-generation didn't complete + plugin.getSpawnerLootGenerator().spawnLootToSpawner(spawner); + } }); } - return cachedDelay; // Start new cycle with full delay + + return cachedDelay; // Start new cycle } return cachedDelay - timeElapsed; @@ -1375,13 +1428,11 @@ private long calculateTimeUntilNextSpawn(SpawnerData spawner) { spawner.getDataLock().unlock(); } } else { - // If can't acquire lock, return minimal time to try again soon - return 1000; // 1 second + return 1000; // 1 second retry } } catch (InterruptedException e) { - // Handle interruption Thread.currentThread().interrupt(); - return 1000; // 1 second + return 1000; } } 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 68ac4496..a634453d 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 @@ -362,4 +362,182 @@ private void handleGuiUpdates(SpawnerData spawner) { spawner.updateHologramData(); } } + + /** + * Pre-generate loot asynchronously without adding it to the spawner. + * This improves UX by preparing loot before the timer expires. + * + * @param spawner The spawner to generate loot for + * @param callback Callback to receive the generated items and experience + */ + public void preGenerateLoot(SpawnerData spawner, LootGenerationCallback callback) { + // Acquire lock to prevent concurrent generation + boolean lockAcquired = spawner.getLootGenerationLock().tryLock(); + if (!lockAcquired) { + callback.onLootGenerated(Collections.emptyList(), 0); + return; + } + + try { + // Check if spawner is at capacity + boolean dataLockAcquired = false; + try { + dataLockAcquired = spawner.getDataLock().tryLock(50, java.util.concurrent.TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + callback.onLootGenerated(Collections.emptyList(), 0); + return; + } + + if (!dataLockAcquired) { + callback.onLootGenerated(Collections.emptyList(), 0); + return; + } + + final int minMobs; + final int maxMobs; + + try { + // Check capacity + int usedSlots = spawner.getVirtualInventory().getUsedSlots(); + int maxSlots = spawner.getMaxSpawnerLootSlots(); + + if (usedSlots >= maxSlots && spawner.getSpawnerExp() >= spawner.getMaxStoredExp()) { + callback.onLootGenerated(Collections.emptyList(), 0); + return; + } + + minMobs = spawner.getMinMobs(); + maxMobs = spawner.getMaxMobs(); + } finally { + spawner.getDataLock().unlock(); + } + + // Generate loot asynchronously + Scheduler.runTaskAsync(() -> { + LootResult loot = generateLoot(minMobs, maxMobs, spawner); + + // Convert to list format for callback + List items = new ArrayList<>(); + if (loot.getItems() != null) { + items.addAll(loot.getItems()); + } + + callback.onLootGenerated(items, loot.getExperience()); + }); + } finally { + spawner.getLootGenerationLock().unlock(); + } + } + + /** + * Add pre-generated loot to the spawner immediately. + * This is called when the timer expires and loot has been pre-generated. + * + * @param spawner The spawner to add loot to + * @param items Pre-generated items + * @param experience Pre-generated experience + */ + public void addPreGeneratedLoot(SpawnerData spawner, List items, int experience) { + if (items == null || (items.isEmpty() && experience == 0)) { + return; + } + + // Acquire lock + boolean lockAcquired = spawner.getLootGenerationLock().tryLock(); + if (!lockAcquired) { + return; + } + + try { + boolean dataLockAcquired = false; + try { + dataLockAcquired = spawner.getDataLock().tryLock(50, java.util.concurrent.TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + + if (!dataLockAcquired) { + return; + } + + final long spawnTime = System.currentTimeMillis(); + final AtomicInteger usedSlots; + final AtomicInteger maxSlots; + + try { + usedSlots = new AtomicInteger(spawner.getVirtualInventory().getUsedSlots()); + maxSlots = new AtomicInteger(spawner.getMaxSpawnerLootSlots()); + + // Recheck capacity (in case it changed since pre-generation) + if (usedSlots.get() >= maxSlots.get() && spawner.getSpawnerExp() >= spawner.getMaxStoredExp()) { + return; + } + } finally { + spawner.getDataLock().unlock(); + } + + // Add items and exp to spawner + Scheduler.runTaskAsync(() -> { + boolean changed = false; + + // Add experience + if (experience > 0) { + int added = spawner.addExperience(experience); + if (added > 0) { + changed = true; + } + } + + // Add items + if (!items.isEmpty()) { + List itemsToAdd = new ArrayList<>(); + for (ItemStack item : items) { + if (item != null && item.getType() != Material.AIR) { + itemsToAdd.add(item.clone()); + } + } + + if (!itemsToAdd.isEmpty()) { + spawner.addItemsAndUpdateSellValue(itemsToAdd); + changed = true; + } + } + + if (!changed) { + return; + } + + // Update lastSpawnTime after successful addition + boolean updateLockAcquired = spawner.getDataLock().tryLock(); + if (updateLockAcquired) { + try { + spawner.setLastSpawnTime(spawnTime); + } finally { + spawner.getDataLock().unlock(); + } + } + + // Update capacity status + spawner.updateCapacityStatus(); + + // Update GUIs + handleGuiUpdates(spawner); + + // Mark for saving + spawnerManager.markSpawnerModified(spawner.getSpawnerId()); + }); + } finally { + spawner.getLootGenerationLock().unlock(); + } + } + + /** + * Callback interface for pre-generation + */ + @FunctionalInterface + public interface LootGenerationCallback { + void onLootGenerated(List items, int experience); + } } \ No newline at end of file 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 4c15d449..e9092986 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 @@ -128,19 +128,21 @@ private void handleSpawnerStateChange(SpawnerData spawner, boolean shouldStop) { public void activateSpawner(SpawnerData spawner) { deactivateSpawner(spawner); - plugin.getLogger().info("Activating spawner " + spawner.getSpawnerId() + " for loot spawning."); + + // Check if spawner is actually active before starting countdown + if (!spawner.getSpawnerActive()) { + return; + } // Set lastSpawnTime to current time to start countdown immediately - // This ensures timer shows full delay countdown when spawner activates long currentTime = System.currentTimeMillis(); spawner.setLastSpawnTime(currentTime); - // Timer doesn't spawn loot directly - it just runs periodically to keep the spawner active - // SpawnerGuiViewManager will check the timer and trigger spawns via SpawnerLootGenerator + // Timer manages the spawner active state - actual loot spawning triggered by SpawnerGuiViewManager Scheduler.Task task = Scheduler.runTaskTimer(() -> { - // Task just keeps the spawner active - actual spawning logic moved to GUI manager - if (spawner.getSpawnerStop().get()) { - plugin.getLogger().info("Spawner " + spawner.getSpawnerId() + " is stopped, timer running but no spawn"); + // Verify spawner is still active on each tick + if (!spawner.getSpawnerActive() || spawner.getSpawnerStop().get()) { + return; } }, spawner.getSpawnDelay(), spawner.getSpawnDelay()); From 1be392ea54b50dfc3ba264f85f4f9525dd8c4bae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 18:14:18 +0000 Subject: [PATCH 5/6] Refactor pre-generation: cleaner code, comprehensive Javadocs, better thread-safety Co-authored-by: ptthanh02 <73684260+ptthanh02@users.noreply.github.com> --- .../SpawnerGuiViewManager.java | 63 ++---- .../spawner/lootgen/SpawnerLootGenerator.java | 196 ++++++++++-------- .../spawner/properties/SpawnerData.java | 40 ++++ 3 files changed, 158 insertions(+), 141 deletions(-) diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java index 1646f79f..3a17f34a 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java @@ -90,29 +90,8 @@ public class SpawnerGuiViewManager implements Listener { // Cache for storage title format to avoid repeated language lookups private String cachedStorageTitleFormat = null; - - // Pre-generation cache: stores loot generated before timer expires - // Map - loot ready to be added when timer hits 0 - private final Map preGeneratedLoot = new ConcurrentHashMap<>(); - - // Threshold for pre-generation (in milliseconds) - start generating when this much time remains - private static final long PRE_GENERATION_THRESHOLD = 2000L; // 2 seconds before spawn - - // Track which spawners are currently generating loot - private final Set currentlyGenerating = ConcurrentHashMap.newKeySet(); - // Static class to hold pre-generated loot - private static class PendingLoot { - final List items; - final int experience; - final long generatedAt; - - PendingLoot(List items, int experience) { - this.items = items; - this.experience = experience; - this.generatedAt = System.currentTimeMillis(); - } - } + private static final long PRE_GENERATION_THRESHOLD = 2000L; // Static class to hold viewer info more efficiently private static class SpawnerViewerInfo { @@ -1343,7 +1322,6 @@ private String updateExistingTimerLine(String line, String newTimeDisplay) { } private long calculateTimeUntilNextSpawn(SpawnerData spawner) { - // Cache spawn delay calculation outside of lock long cachedDelay = spawner.getCachedSpawnDelay(); if (cachedDelay == 0) { cachedDelay = spawner.getSpawnDelay() * 50L; @@ -1354,73 +1332,56 @@ private long calculateTimeUntilNextSpawn(SpawnerData spawner) { long lastSpawnTime = spawner.getLastSpawnTime(); long timeElapsed = currentTime - lastSpawnTime; - // Check if spawner is active before processing boolean isSpawnerInactive = !spawner.getSpawnerActive() || (spawner.getSpawnerStop().get() && timeElapsed > cachedDelay * 2); if (isSpawnerInactive) { - // Clean up any pre-generated loot for inactive spawner - String spawnerId = spawner.getSpawnerId(); - preGeneratedLoot.remove(spawnerId); - currentlyGenerating.remove(spawnerId); + spawner.clearPreGeneratedLoot(); return -1; } long timeUntilNextSpawn = cachedDelay - timeElapsed; timeUntilNextSpawn = Math.max(0, Math.min(timeUntilNextSpawn, cachedDelay)); - - String spawnerId = spawner.getSpawnerId(); - // Pre-generation: Start generating loot when timer is close to expiring if (timeUntilNextSpawn > 0 && timeUntilNextSpawn <= PRE_GENERATION_THRESHOLD) { - // Only start pre-generation once per cycle - if (!currentlyGenerating.contains(spawnerId) && !preGeneratedLoot.containsKey(spawnerId)) { - currentlyGenerating.add(spawnerId); + if (!spawner.isPreGenerating() && !spawner.hasPreGeneratedLoot()) { + spawner.setPreGenerating(true); - // Pre-generate loot asynchronously Location spawnerLocation = spawner.getSpawnerLocation(); if (spawnerLocation != null) { Scheduler.runLocationTask(spawnerLocation, () -> { plugin.getSpawnerLootGenerator().preGenerateLoot(spawner, (items, experience) -> { - // Store the pre-generated loot - if (items != null && (!items.isEmpty() || experience > 0)) { - preGeneratedLoot.put(spawnerId, new PendingLoot(items, experience)); - } - currentlyGenerating.remove(spawnerId); + spawner.storePreGeneratedLoot(items, experience); + spawner.setPreGenerating(false); }); }); } } } - // Timer expired: Add pre-generated loot to spawner if (timeUntilNextSpawn <= 0) { try { if (spawner.getDataLock().tryLock(100, TimeUnit.MILLISECONDS)) { try { - // Re-check timing after acquiring lock currentTime = System.currentTimeMillis(); lastSpawnTime = spawner.getLastSpawnTime(); timeElapsed = currentTime - lastSpawnTime; if (timeElapsed >= cachedDelay) { - // Check for pre-generated loot - PendingLoot pending = preGeneratedLoot.remove(spawnerId); - Location spawnerLocation = spawner.getSpawnerLocation(); if (spawnerLocation != null) { Scheduler.runLocationTask(spawnerLocation, () -> { - if (pending != null) { - // Use pre-generated loot for better UX - plugin.getSpawnerLootGenerator().addPreGeneratedLoot(spawner, pending.items, pending.experience); + if (spawner.hasPreGeneratedLoot()) { + List items = spawner.getAndClearPreGeneratedItems(); + int exp = spawner.getAndClearPreGeneratedExperience(); + plugin.getSpawnerLootGenerator().addPreGeneratedLoot(spawner, items, exp); } else { - // Fallback: Generate and add immediately if pre-generation didn't complete plugin.getSpawnerLootGenerator().spawnLootToSpawner(spawner); } }); } - return cachedDelay; // Start new cycle + return cachedDelay; } return cachedDelay - timeElapsed; @@ -1428,7 +1389,7 @@ private long calculateTimeUntilNextSpawn(SpawnerData spawner) { spawner.getDataLock().unlock(); } } else { - return 1000; // 1 second retry + return 1000; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); 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 a634453d..c16a54bc 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 @@ -364,22 +364,27 @@ private void handleGuiUpdates(SpawnerData spawner) { } /** - * Pre-generate loot asynchronously without adding it to the spawner. - * This improves UX by preparing loot before the timer expires. + * Pre-generates loot asynchronously for improved UX. + * Loot is calculated in background before timer expires, then added instantly when ready. * - * @param spawner The spawner to generate loot for - * @param callback Callback to receive the generated items and experience + *

This method: + *

    + *
  • Checks spawner capacity before generation
  • + *
  • Generates loot asynchronously to avoid blocking
  • + *
  • 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) */ public void preGenerateLoot(SpawnerData spawner, LootGenerationCallback callback) { - // Acquire lock to prevent concurrent generation - boolean lockAcquired = spawner.getLootGenerationLock().tryLock(); - if (!lockAcquired) { + if (!spawner.getLootGenerationLock().tryLock()) { callback.onLootGenerated(Collections.emptyList(), 0); return; } try { - // Check if spawner is at capacity boolean dataLockAcquired = false; try { dataLockAcquired = spawner.getDataLock().tryLock(50, java.util.concurrent.TimeUnit.MILLISECONDS); @@ -396,13 +401,14 @@ public void preGenerateLoot(SpawnerData spawner, LootGenerationCallback callback final int minMobs; final int maxMobs; + final boolean atCapacity; try { - // Check capacity int usedSlots = spawner.getVirtualInventory().getUsedSlots(); int maxSlots = spawner.getMaxSpawnerLootSlots(); + atCapacity = usedSlots >= maxSlots && spawner.getSpawnerExp() >= spawner.getMaxStoredExp(); - if (usedSlots >= maxSlots && spawner.getSpawnerExp() >= spawner.getMaxStoredExp()) { + if (atCapacity) { callback.onLootGenerated(Collections.emptyList(), 0); return; } @@ -413,17 +419,12 @@ public void preGenerateLoot(SpawnerData spawner, LootGenerationCallback callback spawner.getDataLock().unlock(); } - // Generate loot asynchronously Scheduler.runTaskAsync(() -> { LootResult loot = generateLoot(minMobs, maxMobs, spawner); - - // Convert to list format for callback - List items = new ArrayList<>(); - if (loot.getItems() != null) { - items.addAll(loot.getItems()); - } - - callback.onLootGenerated(items, loot.getExperience()); + callback.onLootGenerated( + loot.getItems() != null ? new ArrayList<>(loot.getItems()) : Collections.emptyList(), + loot.getExperience() + ); }); } finally { spawner.getLootGenerationLock().unlock(); @@ -431,113 +432,128 @@ public void preGenerateLoot(SpawnerData spawner, LootGenerationCallback callback } /** - * Add pre-generated loot to the spawner immediately. - * This is called when the timer expires and loot has been pre-generated. + * Adds pre-generated loot to spawner instantly when timer expires. + * + *

This method: + *

    + *
  • Validates pre-generated loot is not empty
  • + *
  • Rechecks capacity (may have changed since pre-generation)
  • + *
  • Adds items and experience to spawner
  • + *
  • 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 - * @param experience Pre-generated experience + * @param items Pre-generated items list + * @param experience Pre-generated experience amount */ public void addPreGeneratedLoot(SpawnerData spawner, List items, int experience) { - if (items == null || (items.isEmpty() && experience == 0)) { + if ((items == null || items.isEmpty()) && experience == 0) { return; } - // Acquire lock - boolean lockAcquired = spawner.getLootGenerationLock().tryLock(); - if (!lockAcquired) { + Location spawnerLocation = spawner.getSpawnerLocation(); + if (spawnerLocation == null) { return; } - try { - boolean dataLockAcquired = false; - try { - dataLockAcquired = spawner.getDataLock().tryLock(50, java.util.concurrent.TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return; - } - - if (!dataLockAcquired) { + Scheduler.runLocationTask(spawnerLocation, () -> { + if (!spawner.getLootGenerationLock().tryLock()) { return; } - final long spawnTime = System.currentTimeMillis(); - final AtomicInteger usedSlots; - final AtomicInteger maxSlots; - try { - usedSlots = new AtomicInteger(spawner.getVirtualInventory().getUsedSlots()); - maxSlots = new AtomicInteger(spawner.getMaxSpawnerLootSlots()); + boolean dataLockAcquired = false; + try { + dataLockAcquired = spawner.getDataLock().tryLock(50, java.util.concurrent.TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } - // Recheck capacity (in case it changed since pre-generation) - if (usedSlots.get() >= maxSlots.get() && spawner.getSpawnerExp() >= spawner.getMaxStoredExp()) { + if (!dataLockAcquired) { return; } - } finally { - spawner.getDataLock().unlock(); - } - // Add items and exp to spawner - Scheduler.runTaskAsync(() -> { - boolean changed = false; + final long spawnTime = System.currentTimeMillis(); + final boolean capacityCheck; - // Add experience - if (experience > 0) { - int added = spawner.addExperience(experience); - if (added > 0) { - changed = true; + try { + int usedSlots = spawner.getVirtualInventory().getUsedSlots(); + int maxSlots = spawner.getMaxSpawnerLootSlots(); + capacityCheck = usedSlots >= maxSlots && spawner.getSpawnerExp() >= spawner.getMaxStoredExp(); + + if (capacityCheck) { + return; } + } finally { + spawner.getDataLock().unlock(); } - // Add items - if (!items.isEmpty()) { - List itemsToAdd = new ArrayList<>(); - for (ItemStack item : items) { - if (item != null && item.getType() != Material.AIR) { - itemsToAdd.add(item.clone()); + Scheduler.runTaskAsync(() -> { + boolean changed = false; + + if (experience > 0 && spawner.getSpawnerExp() < spawner.getMaxStoredExp()) { + int currentExp = spawner.getSpawnerExp(); + int maxExp = spawner.getMaxStoredExp(); + int newExp = Math.min(currentExp + experience, maxExp); + + if (newExp != currentExp) { + spawner.setSpawnerExp(newExp); + changed = true; } } - if (!itemsToAdd.isEmpty()) { - spawner.addItemsAndUpdateSellValue(itemsToAdd); - changed = true; - } - } - - if (!changed) { - return; - } + if (items != null && !items.isEmpty()) { + List validItems = new ArrayList<>(); + for (ItemStack item : items) { + if (item != null && item.getType() != Material.AIR) { + validItems.add(item.clone()); + } + } - // Update lastSpawnTime after successful addition - boolean updateLockAcquired = spawner.getDataLock().tryLock(); - if (updateLockAcquired) { - try { - spawner.setLastSpawnTime(spawnTime); - } finally { - spawner.getDataLock().unlock(); + if (!validItems.isEmpty()) { + spawner.addItemsAndUpdateSellValue(validItems); + changed = true; + } } - } - // Update capacity status - spawner.updateCapacityStatus(); + if (!changed) { + return; + } - // Update GUIs - handleGuiUpdates(spawner); + if (spawner.getDataLock().tryLock()) { + try { + spawner.setLastSpawnTime(spawnTime); + } finally { + spawner.getDataLock().unlock(); + } + } - // Mark for saving - spawnerManager.markSpawnerModified(spawner.getSpawnerId()); - }); - } finally { - spawner.getLootGenerationLock().unlock(); - } + spawner.updateCapacityStatus(); + handleGuiUpdates(spawner); + spawnerManager.markSpawnerModified(spawner.getSpawnerId()); + }); + } finally { + spawner.getLootGenerationLock().unlock(); + } + }); } /** - * Callback interface for pre-generation + * Callback interface for asynchronous loot pre-generation. + * Invoked when loot generation completes with the generated items and experience. */ @FunctionalInterface 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, int experience); } } \ No newline at end of file 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 4496dd34..d0bdd274 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 @@ -112,6 +112,11 @@ public class SpawnerData { // Sort preference for spawner storage @Getter @Setter private Material preferredSortItem; + + // CRITICAL: Pre-generated loot storage for better UX - access must be synchronized via lootGenerationLock + private volatile List preGeneratedItems; + private volatile int preGeneratedExperience; + private volatile boolean isPreGenerating; public SpawnerData(String id, Location location, EntityType type, SmartSpawner plugin) { super(); @@ -603,4 +608,39 @@ public boolean removeItemsAndUpdateSellValue(List items) { return removed; } + + public synchronized void storePreGeneratedLoot(List items, int experience) { + this.preGeneratedItems = items; + this.preGeneratedExperience = experience; + } + + public synchronized List getAndClearPreGeneratedItems() { + List items = preGeneratedItems; + preGeneratedItems = null; + return items; + } + + public synchronized int getAndClearPreGeneratedExperience() { + int exp = preGeneratedExperience; + preGeneratedExperience = 0; + return exp; + } + + public synchronized boolean hasPreGeneratedLoot() { + return (preGeneratedItems != null && !preGeneratedItems.isEmpty()) || preGeneratedExperience > 0; + } + + public synchronized void setPreGenerating(boolean generating) { + this.isPreGenerating = generating; + } + + public synchronized boolean isPreGenerating() { + return isPreGenerating; + } + + public synchronized void clearPreGeneratedLoot() { + preGeneratedItems = null; + preGeneratedExperience = 0; + isPreGenerating = false; + } } From d1c79a0735453cd5e5f429597e17faf417f1ceaf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 18:48:24 +0000 Subject: [PATCH 6/6] Add spawner active checks before pre-generation and spawn loot Co-authored-by: ptthanh02 <73684260+ptthanh02@users.noreply.github.com> --- .../SpawnerGuiViewManager.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java index 3a17f34a..65a9f65d 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/synchronization/SpawnerGuiViewManager.java @@ -1345,11 +1345,20 @@ private long calculateTimeUntilNextSpawn(SpawnerData spawner) { if (timeUntilNextSpawn > 0 && timeUntilNextSpawn <= PRE_GENERATION_THRESHOLD) { if (!spawner.isPreGenerating() && !spawner.hasPreGeneratedLoot()) { + if (!spawner.getSpawnerActive() || spawner.getSpawnerStop().get()) { + return timeUntilNextSpawn; + } + spawner.setPreGenerating(true); Location spawnerLocation = spawner.getSpawnerLocation(); if (spawnerLocation != null) { Scheduler.runLocationTask(spawnerLocation, () -> { + if (!spawner.getSpawnerActive() || spawner.getSpawnerStop().get()) { + spawner.setPreGenerating(false); + return; + } + plugin.getSpawnerLootGenerator().preGenerateLoot(spawner, (items, experience) -> { spawner.storePreGeneratedLoot(items, experience); spawner.setPreGenerating(false); @@ -1368,9 +1377,19 @@ private long calculateTimeUntilNextSpawn(SpawnerData spawner) { timeElapsed = currentTime - lastSpawnTime; if (timeElapsed >= cachedDelay) { + if (!spawner.getSpawnerActive() || spawner.getSpawnerStop().get()) { + spawner.clearPreGeneratedLoot(); + return cachedDelay; + } + Location spawnerLocation = spawner.getSpawnerLocation(); if (spawnerLocation != null) { Scheduler.runLocationTask(spawnerLocation, () -> { + if (!spawner.getSpawnerActive() || spawner.getSpawnerStop().get()) { + spawner.clearPreGeneratedLoot(); + return; + } + if (spawner.hasPreGeneratedLoot()) { List items = spawner.getAndClearPreGeneratedItems(); int exp = spawner.getAndClearPreGeneratedExperience();