diff --git a/build.gradle b/build.gradle index 0e35325d..71a76f6e 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,6 @@ allprojects { maven { name = "nightexpress-releases"; url = "https://repo.nightexpressdev.com/releases"} maven { name = "iridiumdevelopment"; url = "https://nexus.iridiumdevelopment.net/repository/maven-releases/" } maven { name = "Lumine Releases"; url = 'https://mvn.lumine.io/repository/maven-public/' } - maven { name = "groupez"; url = uri("https://repo.groupez.dev/releases") } maven { name = "minecodes-repository-releases"; url = "https://maven.minecodes.pl/releases"} } } diff --git a/core/build.gradle b/core/build.gradle index 5109fc1b..7fb0b6ca 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -20,8 +20,6 @@ dependencies { compileOnly("io.github.fabiozumbi12.RedProtect:RedProtect-Spigot:8.1.2") { exclude group: "*" } compileOnly 'dev.aurelium:auraskills-api-bukkit:2.3.9' compileOnly 'pl.minecodes.plots:plugin-api:4.6.2' - compileOnly("fr.maxlego08.shop:zshop-api:3.3.1") - compileOnly("fr.maxlego08.menu:zmenu-api:1.1.0.6") // Implementation dependencies implementation 'com.github.GriefPrevention:GriefPrevention:18.0.0' diff --git a/core/src/main/java/github/nighter/smartspawner/SmartSpawner.java b/core/src/main/java/github/nighter/smartspawner/SmartSpawner.java index 738e0b1a..cbdb04f2 100644 --- a/core/src/main/java/github/nighter/smartspawner/SmartSpawner.java +++ b/core/src/main/java/github/nighter/smartspawner/SmartSpawner.java @@ -340,6 +340,7 @@ private void registerListeners() { PluginManager pm = getServer().getPluginManager(); // Register core listeners + getServer().getPluginManager().registerEvents(new WorldEventHandler(this), this); pm.registerEvents(naturalSpawnerListener, this); pm.registerEvents(spawnerBreakListener, this); pm.registerEvents(spawnerPlaceListener, this); diff --git a/core/src/main/java/github/nighter/smartspawner/commands/clear/ClearGhostSpawnersSubCommand.java b/core/src/main/java/github/nighter/smartspawner/commands/clear/ClearGhostSpawnersSubCommand.java index 2bfa0fa3..3d50fb2d 100644 --- a/core/src/main/java/github/nighter/smartspawner/commands/clear/ClearGhostSpawnersSubCommand.java +++ b/core/src/main/java/github/nighter/smartspawner/commands/clear/ClearGhostSpawnersSubCommand.java @@ -48,20 +48,33 @@ public int execute(CommandContext context) { // Track how many spawners are being checked using thread-safe counter final AtomicInteger removedCount = new AtomicInteger(0); final int totalSpawners = allSpawners.size(); - + // Check each spawner on its location thread for Folia compatibility for (SpawnerData spawner : allSpawners) { org.bukkit.Location loc = spawner.getSpawnerLocation(); - if (loc != null && loc.getWorld() != null) { - Scheduler.runLocationTask(loc, () -> { - if (plugin.getSpawnerManager().isGhostSpawner(spawner)) { - plugin.getSpawnerManager().removeGhostSpawner(spawner.getSpawnerId()); - removedCount.incrementAndGet(); - } - }); + if (loc == null) continue; + + org.bukkit.World world = null; + try { + world = loc.getWorld(); + } catch (IllegalArgumentException ignored) { + // Leaf can throw "World unloaded" here + } + + if (world == null) { + // World is currently unloaded -> don't crash, just skip + continue; } + + Scheduler.runLocationTask(loc, () -> { + if (plugin.getSpawnerManager().isGhostSpawner(spawner)) { + plugin.getSpawnerManager().removeGhostSpawner(spawner.getSpawnerId()); + removedCount.incrementAndGet(); + } + }); } - + + // Schedule a delayed message to report results (give time for checks to complete) Scheduler.runTaskLater(() -> { int count = removedCount.get(); diff --git a/core/src/main/java/github/nighter/smartspawner/extras/WorldEventHandler.java b/core/src/main/java/github/nighter/smartspawner/extras/WorldEventHandler.java new file mode 100644 index 00000000..8f84ffe6 --- /dev/null +++ b/core/src/main/java/github/nighter/smartspawner/extras/WorldEventHandler.java @@ -0,0 +1,275 @@ +package github.nighter.smartspawner.extras; + +import github.nighter.smartspawner.SmartSpawner; +import github.nighter.smartspawner.spawner.properties.SpawnerData; +import github.nighter.smartspawner.Scheduler; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.world.WorldInitEvent; +import org.bukkit.event.world.WorldLoadEvent; +import org.bukkit.event.world.WorldSaveEvent; +import org.bukkit.event.world.WorldUnloadEvent; + +import java.util.Map; +import java.util.Set; +import java.util.HashSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +/** + * Handles world-related events to manage spawner loading and unloading + */ +public class WorldEventHandler implements Listener { + private final SmartSpawner plugin; + private final Logger logger; + + // Track which worlds have been processed for spawner loading + private final Set processedWorlds = ConcurrentHashMap.newKeySet(); + + // Store spawner data that couldn't be loaded due to missing worlds + private final Map pendingSpawners = new ConcurrentHashMap<>(); + + // Flag to track if initial loading has been attempted + private volatile boolean initialLoadAttempted = false; + + public WorldEventHandler(SmartSpawner plugin) { + this.plugin = plugin; + this.logger = plugin.getLogger(); + } + + /** + * Called when a world is initialized (before it's fully loaded) + */ + @EventHandler(priority = EventPriority.MONITOR) + public void onWorldInit(WorldInitEvent event) { + World world = event.getWorld(); + plugin.debug("World initialized: " + world.getName()); + } + + /** + * Called when a world is fully loaded and ready for use + */ + @EventHandler(priority = EventPriority.MONITOR) + public void onWorldLoad(WorldLoadEvent event) { + World world = event.getWorld(); + String worldName = world.getName(); + + plugin.debug("World loaded: " + worldName); + + // Mark world as processed + processedWorlds.add(worldName); + + // Try to load any pending spawners for this world + loadPendingSpawnersForWorld(worldName); + + // If this is during server startup, also attempt initial load + if (!initialLoadAttempted) { + // Delay slightly to ensure world is fully ready + Scheduler.runTaskLater(() -> attemptInitialSpawnerLoad(), 20L); + } + } + + /** + * Called when a world is being saved + */ + @EventHandler(priority = EventPriority.MONITOR) + public void onWorldSave(WorldSaveEvent event) { + World world = event.getWorld(); + plugin.debug("World saving: " + world.getName()); + + // Flush any pending spawner changes for this world + plugin.getSpawnerFileHandler().flushChanges(); + } + + /** + * Called when a world is being unloaded + */ + @EventHandler(priority = EventPriority.MONITOR) + public void onWorldUnload(WorldUnloadEvent event) { + World world = event.getWorld(); + String worldName = world.getName(); + + plugin.debug("World unloading: " + worldName); + + // Remove world from processed set + processedWorlds.remove(worldName); + + // Unload spawners from this world + unloadSpawnersFromWorld(worldName); + + // Save any pending changes before unloading + plugin.getSpawnerFileHandler().flushChanges(); + } + + /** + * Attempt to perform initial spawner loading, checking for available worlds + */ + public void attemptInitialSpawnerLoad() { + if (initialLoadAttempted) { + return; + } + + initialLoadAttempted = true; + plugin.debug("Attempting initial spawner load..."); + + // Load spawner data from file + Map allSpawnerData = plugin.getSpawnerFileHandler().loadAllSpawnersRaw(); + + int loadedCount = 0; + int pendingCount = 0; + + for (Map.Entry entry : allSpawnerData.entrySet()) { + String spawnerId = entry.getKey(); + SpawnerData spawner = entry.getValue(); + + if (spawner != null) { + // Successfully loaded spawner + plugin.getSpawnerManager().addSpawnerToIndexes(spawnerId, spawner); + loadedCount++; + } else { + // Spawner couldn't be loaded, likely due to missing world + // Store as pending for later loading + PendingSpawnerData pending = loadPendingSpawnerFromFile(spawnerId); + if (pending != null) { + pendingSpawners.put(spawnerId, pending); + pendingCount++; + } + } + } + + logger.info("Initial spawner load complete. Loaded: " + loadedCount + + ", Pending (missing worlds): " + pendingCount); + + if (pendingCount > 0) { + logger.info("Pending spawners will be loaded when their worlds become available."); + } + } + + /** + * Load pending spawners for a specific world that just became available + */ + private void loadPendingSpawnersForWorld(String worldName) { + if (pendingSpawners.isEmpty()) { + return; + } + + int loadedCount = 0; + + // Create a copy of the keys to avoid concurrent modification + Set spawnerIds = new HashSet<>(pendingSpawners.keySet()); + + for (String spawnerId : spawnerIds) { + PendingSpawnerData pending = pendingSpawners.get(spawnerId); + + if (pending != null && worldName.equals(pending.worldName)) { + // Try to load this spawner now that its world is available + SpawnerData spawner = plugin.getSpawnerFileHandler().loadSpecificSpawner(spawnerId); + + if (spawner != null) { + plugin.getSpawnerManager().addSpawnerToIndexes(spawnerId, spawner); + pendingSpawners.remove(spawnerId); + loadedCount++; + plugin.debug("Loaded pending spawner " + spawnerId + " for world " + worldName); + } + } + } + + if (loadedCount > 0) { + logger.info("Loaded " + loadedCount + " pending spawners for world: " + worldName); + + // Restart hoppers for the spawners in this world + if (plugin.getHopperHandler() != null) { + plugin.getHopperHandler().restartAllHoppers(); + } + } + } + + /** + * Unload all spawners from a specific world + */ + private void unloadSpawnersFromWorld(String worldName) { + Set worldSpawners = plugin.getSpawnerManager().getSpawnersInWorld(worldName); + + if (worldSpawners != null && !worldSpawners.isEmpty()) { + int unloadedCount = 0; + + // IMPORTANT: + // In setups where island worlds are frequently unloaded/reloaded (e.g., SlimeWorld/ASP per-island worlds), + // keeping SpawnerData objects in memory across world unloads can leave them bound to a stale World instance. + // That causes holograms and region-thread tasks to never recover when the world is loaded again. + // + // Strategy: + // 1) Remove the spawners from runtime indexes on world unload + // 2) Mark them as pending so they will be re-loaded from file when the world loads again + // + // This keeps behavior aligned with your requirement: + // - offline: spawners do NOT run + // - when player returns and world loads: spawners resume + holograms are recreated + for (SpawnerData spawner : new HashSet<>(worldSpawners)) { + if (spawner == null) continue; + + String spawnerId = spawner.getSpawnerId(); + if (spawnerId != null) { + pendingSpawners.put(spawnerId, new PendingSpawnerData(spawnerId, worldName)); + // Remove from manager indexes immediately (hologram removal is handled inside removeSpawner) + plugin.getSpawnerManager().removeSpawner(spawnerId); + } else { + // Fallback: at least remove hologram to avoid stuck displays + spawner.removeHologram(); + } + unloadedCount++; + } + + logger.info("Unloaded " + unloadedCount + " spawners from world: " + worldName + " (marked pending for reload)"); + } + } + + /** + * Load basic spawner information without creating the full SpawnerData object + */ + private PendingSpawnerData loadPendingSpawnerFromFile(String spawnerId) { + try { + String locationString = plugin.getSpawnerFileHandler().getRawLocationString(spawnerId); + if (locationString != null) { + String[] locParts = locationString.split(","); + if (locParts.length >= 1) { + return new PendingSpawnerData(spawnerId, locParts[0]); + } + } + } catch (Exception e) { + plugin.debug("Error loading pending spawner data for " + spawnerId + ": " + e.getMessage()); + } + + return null; + } + + /** + * Check if a world is currently loaded and available + */ + public boolean isWorldLoaded(String worldName) { + return processedWorlds.contains(worldName) && Bukkit.getWorld(worldName) != null; + } + + /** + * Get the count of pending spawners waiting for worlds to load + */ + public int getPendingSpawnerCount() { + return pendingSpawners.size(); + } + + /** + * Simple data class to store basic spawner information for pending loading + */ + private static class PendingSpawnerData { + final String spawnerId; + final String worldName; + + PendingSpawnerData(String spawnerId, String worldName) { + this.spawnerId = spawnerId; + this.worldName = worldName; + } + } +} diff --git a/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/ShopIntegrationManager.java b/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/ShopIntegrationManager.java index 9dc31e09..30a0e3c6 100644 --- a/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/ShopIntegrationManager.java +++ b/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/ShopIntegrationManager.java @@ -7,7 +7,6 @@ import github.nighter.smartspawner.hooks.economy.shops.providers.excellentshop.ExcellentShopProvider; import github.nighter.smartspawner.hooks.economy.shops.providers.shopguiplus.ShopGuiPlusProvider; import github.nighter.smartspawner.hooks.economy.shops.providers.shopguiplus.SpawnerHook; -import github.nighter.smartspawner.hooks.economy.shops.providers.zshop.ZShopProvider; import lombok.RequiredArgsConstructor; import org.bukkit.Material; import org.bukkit.plugin.Plugin; @@ -121,12 +120,6 @@ private boolean tryRegisterSpecificProvider(String providerName) { return !availableProviders.isEmpty(); } break; - case "zshop": - if (isPluginAvailable("ZShop")) { - registerProviderIfAvailable("ZShop", () -> new ZShopProvider(plugin)); - return !availableProviders.isEmpty(); - } - break; case "excellentshop": if (isPluginAvailable("ExcellentShop")) { registerProviderIfAvailable("ExcellentShop", () -> new ExcellentShopProvider(plugin)); diff --git a/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/providers/zshop/ZShopProvider.java b/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/providers/zshop/ZShopProvider.java deleted file mode 100644 index 4cca2e09..00000000 --- a/core/src/main/java/github/nighter/smartspawner/hooks/economy/shops/providers/zshop/ZShopProvider.java +++ /dev/null @@ -1,92 +0,0 @@ -package github.nighter.smartspawner.hooks.economy.shops.providers.zshop; - -import fr.maxlego08.shop.api.ShopManager; -import fr.maxlego08.shop.api.buttons.ItemButton; -import github.nighter.smartspawner.SmartSpawner; -import github.nighter.smartspawner.hooks.economy.shops.providers.ShopProvider; -import lombok.RequiredArgsConstructor; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.plugin.Plugin; - -import java.util.Optional; -import java.util.logging.Level; - -@RequiredArgsConstructor -public class ZShopProvider implements ShopProvider { - private final SmartSpawner plugin; - private ShopManager shopManager; - - @Override - public String getPluginName() { - return "zShop"; - } - - @Override - public boolean isAvailable() { - try { - Plugin zShopPlugin = Bukkit.getPluginManager().getPlugin("zShop"); - if (zShopPlugin != null && zShopPlugin.isEnabled()) { - // Check if the zShop API classes are available - Class.forName("fr.maxlego08.shop.api.ShopManager"); - Class.forName("fr.maxlego08.shop.api.buttons.ItemButton"); - - // Try to get the ShopManager service - ShopManager manager = getShopManager(); - return manager != null; - } - } catch (ClassNotFoundException | NoClassDefFoundError e) { - plugin.debug("zShop API not found: " + e.getMessage()); - } catch (Exception e) { - plugin.getLogger().warning("Error initializing zShop integration: " + e.getMessage()); - } - return false; - } - - @Override - public double getSellPrice(Material material) { - try { - Optional itemButtonOpt = getItemButton(material); - if (itemButtonOpt.isEmpty()) { - return 0.0; - } - - ItemButton itemButton = itemButtonOpt.get(); - double sellPrice = itemButton.getSellPrice(); - return sellPrice > 0 ? sellPrice : 0.0; - - } catch (Exception e) { - plugin.debug("Error getting sell price for " + material + " from zShop: " + e.getMessage()); - return 0.0; - } - } - - private ShopManager getShopManager() { - if (this.shopManager != null) { - return this.shopManager; - } - - try { - this.shopManager = plugin.getServer().getServicesManager() - .getRegistration(ShopManager.class) - .getProvider(); - return this.shopManager; - } catch (Exception e) { - plugin.debug("Failed to get zShop ShopManager: " + e.getMessage()); - return null; - } - } - - private Optional getItemButton(Material material) { - try { - ShopManager manager = getShopManager(); - if (manager == null) { - return Optional.empty(); - } - return manager.getItemButton(material); - } catch (Exception e) { - plugin.getLogger().log(Level.SEVERE, "Error getting item button from zShop", e); - return Optional.empty(); - } - } -} 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 70c82235..abf52da6 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 @@ -1,21 +1,34 @@ package github.nighter.smartspawner.spawner.lootgen; import github.nighter.smartspawner.SmartSpawner; +import github.nighter.smartspawner.Scheduler; import github.nighter.smartspawner.spawner.data.SpawnerManager; 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.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; - +import java.util.concurrent.TimeUnit; + +/** + * Island-world gate: + * - NO "nearby player" requirement. + * - Spawner runs ONLY if: + * 1) its chunk is loaded AND + * 2) there is at least 1 eligible player in the SAME WORLD as the spawner (i.e., someone is on the island) + * + * HARD PAUSE: + * - If not allowed, timer is frozen (no offline progress / no catch-up). + */ public class SpawnerRangeChecker { + private static final long CHECK_INTERVAL = 20L; // 1 second in ticks + private final SmartSpawner plugin; private final SpawnerManager spawnerManager; private final ExecutorService executor; @@ -23,107 +36,96 @@ public class SpawnerRangeChecker { public SpawnerRangeChecker(SmartSpawner plugin) { this.plugin = plugin; this.spawnerManager = plugin.getSpawnerManager(); - this.executor = Executors.newSingleThreadExecutor(r -> new Thread(r, "SmartSpawner-RangeCheck")); - initializeRangeCheckTask(); - } - - private void initializeRangeCheckTask() { - // Using the global scheduler, but only for coordinating region-specific checks - Scheduler.runTaskTimer(this::scheduleRegionSpecificCheck, CHECK_INTERVAL, CHECK_INTERVAL); + this.executor = Executors.newSingleThreadExecutor(r -> new Thread(r, "SmartSpawner-IslandGate")); + Scheduler.runTaskTimer(this::tick, CHECK_INTERVAL, CHECK_INTERVAL); } - private void scheduleRegionSpecificCheck() { - PlayerRangeWrapper[] rangePlayers = getRangePlayers(); - + private void tick() { this.executor.execute(() -> { final List allSpawners = spawnerManager.getAllSpawners(); - final RangeMath rangeCheck = new RangeMath(rangePlayers, allSpawners); - final boolean[] spawnersPlayerFound = rangeCheck.getActiveSpawners(); - - for (int i = 0; i < spawnersPlayerFound.length; i++) { - final boolean expectedStop = !spawnersPlayerFound[i]; - final SpawnerData sd = allSpawners.get(i); - final String spawnerId = sd.getSpawnerId(); - - // Atomically update spawner stop flag only if it has changed - if (sd.getSpawnerStop().compareAndSet(!expectedStop, expectedStop)) { - // Schedule main-thread task for actual state change - Scheduler.runLocationTask(sd.getSpawnerLocation(), () -> { - if (!isSpawnerValid(sd)) { - cleanupRemovedSpawner(spawnerId); - return; - } - - // Double-check atomic boolean before applying - if (sd.getSpawnerStop().get() == expectedStop) { - handleSpawnerStateChange(sd, expectedStop); - } - }); - } else { - // Spawner state hasn't changed, but check if it's time to spawn loot - // Only process active spawners that are not stopped - if (sd.getSpawnerActive() && !sd.getSpawnerStop().get()) { - checkAndSpawnLoot(sd); + for (SpawnerData spawner : allSpawners) { + if (spawner == null) continue; + + final Location loc = spawner.getSpawnerLocation(); + if (loc == null) continue; + + // Folia/Leaf safety: do world/chunk/player-world checks on region thread + Scheduler.runLocationTask(loc, () -> { + if (!isSpawnerValid(spawner)) { + cleanupRemovedSpawner(spawner.getSpawnerId()); + return; } - } + + final Location liveLoc = spawner.getSpawnerLocation(); + if (liveLoc == null || liveLoc.getWorld() == null) return; + + boolean chunkLoaded; + try { + chunkLoaded = liveLoc.getChunk().isLoaded(); + } catch (Throwable t) { + chunkLoaded = false; + } + + // ✅ Gate: at least 1 eligible player must be in the SAME WORLD + boolean hasPlayerInSameWorld = hasEligiblePlayerInWorld(liveLoc); + + // Allowed only if chunk loaded AND world has player (island not empty) + final boolean allowed = chunkLoaded && hasPlayerInSameWorld; + final boolean shouldStop = !allowed; + + boolean previous = spawner.getSpawnerStop().getAndSet(shouldStop); + if (previous != shouldStop) { + handleSpawnerStateChange(spawner, shouldStop); + } + + // HARD PAUSE: freeze timer & clear pre-gen loot when not allowed + if (shouldStop) { + spawner.setLastSpawnTime(System.currentTimeMillis()); + spawner.clearPreGeneratedLoot(); + return; + } + + if (spawner.getSpawnerActive() && !spawner.getSpawnerStop().get()) { + checkAndSpawnLoot(spawner); + } + }); } }); } - private PlayerRangeWrapper[] getRangePlayers() { - final Player[] onlinePlayers = Bukkit.getOnlinePlayers().toArray(new Player[0]); - final PlayerRangeWrapper[] rangePlayers = new PlayerRangeWrapper[onlinePlayers.length]; - int i = 0; - - for (Player p : onlinePlayers) { + private boolean hasEligiblePlayerInWorld(Location spawnerLoc) { + // Same-world presence check (island has someone on it) + for (Player p : Bukkit.getOnlinePlayers()) { + if (p == null) continue; + if (!p.isConnected() || p.isDead()) continue; + if (p.getGameMode() == GameMode.SPECTATOR) continue; - boolean conditions = p.isConnected() && !p.isDead() - && p.getGameMode() != GameMode.SPECTATOR; - - // Store data in wrapper for faster access - rangePlayers[i++] = new PlayerRangeWrapper(p.getWorld().getUID(), - p.getX(), p.getY(), p.getZ(), - conditions - ); + if (p.getWorld() != null && p.getWorld().equals(spawnerLoc.getWorld())) { + return true; + } } - - return rangePlayers; + return false; } private boolean isSpawnerValid(SpawnerData spawner) { - // Check 1: Still in manager? SpawnerData current = spawnerManager.getSpawnerById(spawner.getSpawnerId()); - if (current == null) { - return false; - } - - // Check 2: Same instance? (prevents processing stale copies) - if (current != spawner) { - return false; - } + if (current == null) return false; + if (current != spawner) return false; - // Check 3: Location still valid? Location loc = spawner.getSpawnerLocation(); return loc != null && loc.getWorld() != null; } private void cleanupRemovedSpawner(String spawnerId) { - // Clear any pre-generated loot when spawner is removed SpawnerData spawner = spawnerManager.getSpawnerById(spawnerId); - if (spawner != null) { - spawner.clearPreGeneratedLoot(); - } + if (spawner != null) spawner.clearPreGeneratedLoot(); } private void handleSpawnerStateChange(SpawnerData spawner, boolean shouldStop) { - if (!shouldStop) { - activateSpawner(spawner); - } else { - deactivateSpawner(spawner); - } + if (!shouldStop) activateSpawner(spawner); + else deactivateSpawner(spawner); - // Force GUI update when spawner state changes if (plugin.getSpawnerGuiViewManager().hasViewers(spawner)) { plugin.getSpawnerGuiViewManager().forceStateChangeUpdate(spawner); } @@ -131,118 +133,105 @@ private void handleSpawnerStateChange(SpawnerData spawner, boolean shouldStop) { public void activateSpawner(SpawnerData spawner) { deactivateSpawner(spawner); + if (!spawner.getSpawnerActive()) return; - // Check if spawner is actually active before starting - if (!spawner.getSpawnerActive()) { - return; - } - - // Set lastSpawnTime to current time to start countdown immediately - long currentTime = System.currentTimeMillis(); - spawner.setLastSpawnTime(currentTime); + spawner.setLastSpawnTime(System.currentTimeMillis()); - // Immediately update any open GUIs to show the countdown if (plugin.getSpawnerGuiViewManager().hasViewers(spawner)) { plugin.getSpawnerGuiViewManager().updateSpawnerMenuViewers(spawner); } } public void deactivateSpawner(SpawnerData spawner) { - // Clear any pre-generated loot when deactivating spawner.clearPreGeneratedLoot(); } - /** - * Checks if a spawner should spawn loot based on its timer and spawns if needed. - * This runs independently of GUI updates to ensure loot spawns even when no one is viewing. - * - * @param spawner The spawner to check - */ private void checkAndSpawnLoot(SpawnerData spawner) { - // Calculate spawn delay long cachedDelay = spawner.getCachedSpawnDelay(); if (cachedDelay == 0) { - cachedDelay = (spawner.getSpawnDelay() + 20L) * 50L; // Convert ticks to milliseconds + cachedDelay = (spawner.getSpawnDelay() + 20L) * 50L; spawner.setCachedSpawnDelay(cachedDelay); } + final long finalCachedDelay = cachedDelay; - final long finalCachedDelay = cachedDelay; // Make effectively final for lambda + long now = System.currentTimeMillis(); + long last = spawner.getLastSpawnTime(); + long elapsed = now - last; - long currentTime = System.currentTimeMillis(); - long lastSpawnTime = spawner.getLastSpawnTime(); - long timeElapsed = currentTime - lastSpawnTime; + if (elapsed < cachedDelay) { + if (plugin.getSpawnerGuiViewManager().hasViewers(spawner)) { + plugin.getSpawnerGuiViewManager().updateSpawnerMenuViewers(spawner); + } + return; + } - // Check if it's time to spawn loot - if (timeElapsed >= cachedDelay) { - // Try to acquire lock with short timeout to avoid blocking + try { + if (!spawner.getDataLock().tryLock(50, TimeUnit.MILLISECONDS)) return; try { - if (spawner.getDataLock().tryLock(50, java.util.concurrent.TimeUnit.MILLISECONDS)) { - try { - // Double-check time and state after acquiring lock - currentTime = System.currentTimeMillis(); - lastSpawnTime = spawner.getLastSpawnTime(); - timeElapsed = currentTime - lastSpawnTime; - - if (timeElapsed >= cachedDelay && spawner.getSpawnerActive() && !spawner.getSpawnerStop().get()) { - Location spawnerLocation = spawner.getSpawnerLocation(); - if (spawnerLocation != null) { - // Schedule loot spawning on the correct region thread - Scheduler.runLocationTask(spawnerLocation, () -> { - // Final check before spawning - if (!spawner.getSpawnerActive() || spawner.getSpawnerStop().get()) { - spawner.clearPreGeneratedLoot(); - return; - } - - // Check if loot was already added early (for smooth UX) - // If so, just update the timer without spawning again - long timeSinceLastSpawn = System.currentTimeMillis() - spawner.getLastSpawnTime(); - if (timeSinceLastSpawn < finalCachedDelay - 100) { // 100ms tolerance - // Loot was already added early, just update GUI - if (plugin.getSpawnerGuiViewManager().hasViewers(spawner)) { - plugin.getSpawnerGuiViewManager().updateSpawnerMenuViewers(spawner); - } - return; - } - - // Spawn loot (pre-generated if available, otherwise generate new) - if (spawner.hasPreGeneratedLoot()) { - List items = spawner.getAndClearPreGeneratedItems(); - int exp = spawner.getAndClearPreGeneratedExperience(); - plugin.getSpawnerLootGenerator().addPreGeneratedLoot(spawner, items, exp); - } else { - plugin.getSpawnerLootGenerator().spawnLootToSpawner(spawner); - } - - // Update last spawn time is handled by addPreGeneratedLoot/spawnLootToSpawner - - // Update any open GUIs to show the new loot - if (plugin.getSpawnerGuiViewManager().hasViewers(spawner)) { - plugin.getSpawnerGuiViewManager().updateSpawnerMenuViewers(spawner); - } - }); - } - } - } finally { - spawner.getDataLock().unlock(); + now = System.currentTimeMillis(); + last = spawner.getLastSpawnTime(); + elapsed = now - last; + + if (elapsed < cachedDelay) return; + + if (!spawner.getSpawnerActive() || spawner.getSpawnerStop().get()) { + spawner.clearPreGeneratedLoot(); + return; + } + + final Location loc = spawner.getSpawnerLocation(); + if (loc == null || loc.getWorld() == null) return; + + // Final safety: chunk must still be loaded + if (!loc.getChunk().isLoaded()) { + spawner.setLastSpawnTime(System.currentTimeMillis()); + spawner.clearPreGeneratedLoot(); + return; + } + + // Also ensure island world still has a player (prevents edge cases mid-tick) + if (!hasEligiblePlayerInWorld(loc)) { + spawner.setLastSpawnTime(System.currentTimeMillis()); + spawner.clearPreGeneratedLoot(); + spawner.getSpawnerStop().set(true); + return; + } + + long timeSinceLast = System.currentTimeMillis() - spawner.getLastSpawnTime(); + if (timeSinceLast < finalCachedDelay - 100) { + if (plugin.getSpawnerGuiViewManager().hasViewers(spawner)) { + plugin.getSpawnerGuiViewManager().updateSpawnerMenuViewers(spawner); } + return; + } + + if (spawner.hasPreGeneratedLoot()) { + List items = spawner.getAndClearPreGeneratedItems(); + int exp = spawner.getAndClearPreGeneratedExperience(); + plugin.getSpawnerLootGenerator().addPreGeneratedLoot(spawner, items, exp); + } else { + plugin.getSpawnerLootGenerator().spawnLootToSpawner(spawner); } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + + if (plugin.getSpawnerGuiViewManager().hasViewers(spawner)) { + plugin.getSpawnerGuiViewManager().updateSpawnerMenuViewers(spawner); + } + + } finally { + spawner.getDataLock().unlock(); } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } } public void cleanup() { executor.shutdown(); try { - if (!executor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS)) { - executor.shutdownNow(); - } + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) executor.shutdownNow(); } catch (InterruptedException e) { executor.shutdownNow(); Thread.currentThread().interrupt(); } } } -