Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
}
}
Expand Down
2 changes: 0 additions & 2 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,33 @@ public int execute(CommandContext<CommandSourceStack> 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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> processedWorlds = ConcurrentHashMap.newKeySet();

// Store spawner data that couldn't be loaded due to missing worlds
private final Map<String, PendingSpawnerData> 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<String, SpawnerData> allSpawnerData = plugin.getSpawnerFileHandler().loadAllSpawnersRaw();

int loadedCount = 0;
int pendingCount = 0;

for (Map.Entry<String, SpawnerData> 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<String> 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<SpawnerData> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down
Loading