diff --git a/core/src/main/java/github/nighter/smartspawner/Scheduler.java b/core/src/main/java/github/nighter/smartspawner/Scheduler.java index 55a73d72..84cddc5c 100644 --- a/core/src/main/java/github/nighter/smartspawner/Scheduler.java +++ b/core/src/main/java/github/nighter/smartspawner/Scheduler.java @@ -13,538 +13,528 @@ import java.util.logging.Level; /** - * Universal scheduler utility that supports both traditional Bukkit scheduling - * and Folia's region-based scheduling system. - * - * This class automatically detects which server implementation is being used - * and provides appropriate scheduling methods. + * Universal scheduler utility supporting both traditional Bukkit scheduling + * and Folia's region-based scheduling system using a Strategy Pattern. */ public final class Scheduler { - private static final Plugin plugin; - private static final boolean isFolia; + private static final long TICK_MS = 50L; - static { - plugin = SmartSpawner.getInstance(); + private static Plugin plugin; + private static PlatformScheduler impl; - // Check if we're running on Folia - boolean foliaDetected = false; - try { - Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); - foliaDetected = true; - plugin.getLogger().info("Folia detected! Using region-based threading system."); - } catch (final ClassNotFoundException e) { - plugin.getLogger().info("Running on standard Paper server."); - } catch (Exception e) { - plugin.getLogger().warning("Unexpected error while detecting server type: " + e.getMessage()); - } - isFolia = foliaDetected; + private Scheduler() { } /** - * Runs a task on the main thread (or global region in Folia). - * - * @param runnable The task to run - * @return A Task object representing the scheduled task + * Initializes the scheduler. Must be called from the plugin's onEnable method. */ - public static Task runTask(Runnable runnable) { - if (isFolia) { - try { - io.papermc.paper.threadedregions.scheduler.ScheduledTask task = - Bukkit.getGlobalRegionScheduler().run(plugin, scheduledTask -> runnable.run()); - return new Task(task); - } catch (Exception e) { - plugin.getLogger().log(Level.SEVERE, "Error scheduling task in Folia", e); - return new Task(null); - } + public static void init(Plugin pluginInstance) { + if (impl != null) { + pluginInstance.getLogger().warning("Scheduler.init() called more than once; ignoring."); + return; + } + plugin = pluginInstance; + if (detectFolia()) { + impl = new FoliaSchedulerImpl(); + plugin.getLogger().info("Folia detected! Using region-based threading system."); } else { - return new Task(Bukkit.getScheduler().runTask(plugin, runnable)); + impl = new BukkitSchedulerImpl(); + plugin.getLogger().info("Running on standard Paper server."); } } - /** - * Runs a task asynchronously. - * - * @param runnable The task to run - * @return A Task object representing the scheduled task - */ - public static Task runTaskAsync(Runnable runnable) { - if (isFolia) { - try { - io.papermc.paper.threadedregions.scheduler.ScheduledTask task = - Bukkit.getAsyncScheduler().runNow(plugin, scheduledTask -> runnable.run()); - return new Task(task); - } catch (Exception e) { - plugin.getLogger().log(Level.SEVERE, "Error scheduling async task in Folia", e); - return new Task(null); - } - } else { - return new Task(Bukkit.getScheduler().runTaskAsynchronously(plugin, runnable)); + private static boolean detectFolia() { + try { + Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); + return true; + } catch (ClassNotFoundException e) { + return false; } } - /** - * Runs a task after a specified delay. - * - * @param runnable The task to run - * @param delayTicks The delay in ticks before running the task - * @return A Task object representing the scheduled task - */ - public static Task runTaskLater(Runnable runnable, long delayTicks) { - if (isFolia) { - try { - io.papermc.paper.threadedregions.scheduler.ScheduledTask task = - Bukkit.getGlobalRegionScheduler().runDelayed(plugin, scheduledTask -> runnable.run(), - delayTicks < 1 ? 1 : delayTicks); - return new Task(task); - } catch (Exception e) { - plugin.getLogger().log(Level.SEVERE, "Error scheduling delayed task in Folia", e); - return new Task(null); - } - } else { - return new Task(Bukkit.getScheduler().runTaskLater(plugin, runnable, delayTicks)); + private static void ensureInitialized() { + if (impl == null) { + throw new IllegalStateException("Scheduler.init() was not called"); } } - /** - * Runs a task asynchronously after a specified delay. - * - * @param runnable The task to run - * @param delayTicks The delay in ticks before running the task - * @return A Task object representing the scheduled task - */ + private static long foliaDelay(long delayTicks) { + return delayTicks < 1 ? 1 : delayTicks; + } + + // --- Public API --- + + public static Task runTask(Runnable runnable) { + ensureInitialized(); + return impl.runTask(runnable); + } + + public static Task runTaskAsync(Runnable runnable) { + ensureInitialized(); + return impl.runTaskAsync(runnable); + } + + public static Task runTaskLater(Runnable runnable, long delayTicks) { + ensureInitialized(); + return impl.runTaskLater(runnable, delayTicks); + } + public static Task runTaskLaterAsync(Runnable runnable, long delayTicks) { - if (isFolia) { - try { - long delayMs = delayTicks * 50; // Convert ticks to milliseconds - io.papermc.paper.threadedregions.scheduler.ScheduledTask task = - Bukkit.getAsyncScheduler().runDelayed(plugin, scheduledTask -> runnable.run(), - delayMs, TimeUnit.MILLISECONDS); - return new Task(task); - } catch (Exception e) { - plugin.getLogger().log(Level.SEVERE, "Error scheduling delayed async task in Folia", e); - return new Task(null); - } - } else { - return new Task(Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, runnable, delayTicks)); - } + ensureInitialized(); + return impl.runTaskLaterAsync(runnable, delayTicks); } - /** - * Runs a task repeatedly at fixed intervals. - * - * @param runnable The task to run - * @param delayTicks The initial delay in ticks before the first execution - * @param periodTicks The period in ticks between subsequent executions - * @return A Task object representing the scheduled task - */ public static Task runTaskTimer(Runnable runnable, long delayTicks, long periodTicks) { - if (isFolia) { - try { - io.papermc.paper.threadedregions.scheduler.ScheduledTask task = - Bukkit.getGlobalRegionScheduler().runAtFixedRate(plugin, scheduledTask -> runnable.run(), - delayTicks < 1 ? 1 : delayTicks, periodTicks); - return new Task(task); - } catch (Exception e) { - plugin.getLogger().log(Level.SEVERE, "Error scheduling timer task in Folia", e); - return new Task(null); - } - } else { - return new Task(Bukkit.getScheduler().runTaskTimer(plugin, runnable, delayTicks, periodTicks)); - } + ensureInitialized(); + return impl.runTaskTimer(runnable, delayTicks, periodTicks); } - /** - * Runs a task repeatedly at fixed intervals asynchronously. - * - * @param runnable The task to run - * @param delayTicks The initial delay in ticks before the first execution - * @param periodTicks The period in ticks between subsequent executions - * @return A Task object representing the scheduled task - */ public static Task runTaskTimerAsync(Runnable runnable, long delayTicks, long periodTicks) { - if (isFolia) { - try { - // Convert ticks to milliseconds (1 tick = 50ms) - long delayMs = delayTicks * 50; - long periodMs = periodTicks * 50; - - io.papermc.paper.threadedregions.scheduler.ScheduledTask task = - Bukkit.getAsyncScheduler().runAtFixedRate(plugin, scheduledTask -> runnable.run(), - delayMs, periodMs, TimeUnit.MILLISECONDS); - return new Task(task); - } catch (Exception e) { - plugin.getLogger().log(Level.SEVERE, "Error scheduling timer async task in Folia", e); - return new Task(null); - } - } else { - return new Task(Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, runnable, delayTicks, periodTicks)); - } + ensureInitialized(); + return impl.runTaskTimerAsync(runnable, delayTicks, periodTicks); } - /** - * Runs a task in the region of a specific entity. - * Falls back to regular scheduling on non-Folia servers. - * - * @param entity The entity in whose region to run the task - * @param runnable The task to run - * @return A Task object representing the scheduled task - */ public static Task runEntityTask(Entity entity, Runnable runnable) { - if (isFolia && entity != null) { - try { - io.papermc.paper.threadedregions.scheduler.ScheduledTask task = - entity.getScheduler().run(plugin, scheduledTask -> runnable.run(), null); - return new Task(task); - } catch (Exception e) { - plugin.getLogger().log(Level.WARNING, "Error scheduling entity task in Folia, falling back to global scheduler", e); - return runTask(runnable); - } - } else { + ensureInitialized(); + if (entity == null || !entity.isValid()) { return runTask(runnable); } + return impl.runEntityTask(entity, runnable); } - /** - * Runs a delayed task in the region of a specific entity. - * - * @param entity The entity in whose region to run the task - * @param runnable The task to run - * @param delayTicks The delay in ticks before running the task - * @return A Task object representing the scheduled task - */ public static Task runEntityTaskLater(Entity entity, Runnable runnable, long delayTicks) { - if (isFolia && entity != null) { - try { - io.papermc.paper.threadedregions.scheduler.ScheduledTask task = - entity.getScheduler().runDelayed(plugin, scheduledTask -> runnable.run(), null, - delayTicks < 1 ? 1 : delayTicks); - return new Task(task); - } catch (Exception e) { - plugin.getLogger().log(Level.WARNING, "Error scheduling delayed entity task in Folia, falling back to global scheduler", e); - return runTaskLater(runnable, delayTicks); - } - } else { + ensureInitialized(); + if (entity == null || !entity.isValid()) { return runTaskLater(runnable, delayTicks); } + return impl.runEntityTaskLater(entity, runnable, delayTicks); } - /** - * Runs a repeated task in the region of a specific entity. - * - * @param entity The entity in whose region to run the task - * @param runnable The task to run - * @param delayTicks The initial delay in ticks before the first execution - * @param periodTicks The period in ticks between subsequent executions - * @return A Task object representing the scheduled task - */ public static Task runEntityTaskTimer(Entity entity, Runnable runnable, long delayTicks, long periodTicks) { - if (isFolia && entity != null) { - try { - io.papermc.paper.threadedregions.scheduler.ScheduledTask task = - entity.getScheduler().runAtFixedRate(plugin, scheduledTask -> runnable.run(), null, - delayTicks < 1 ? 1 : delayTicks, periodTicks); - return new Task(task); - } catch (Exception e) { - plugin.getLogger().log(Level.WARNING, "Error scheduling timer entity task in Folia, falling back to global scheduler", e); - return runTaskTimer(runnable, delayTicks, periodTicks); - } - } else { + ensureInitialized(); + if (entity == null || !entity.isValid()) { return runTaskTimer(runnable, delayTicks, periodTicks); } + return impl.runEntityTaskTimer(entity, runnable, delayTicks, periodTicks); } - /** - * Runs a task in the region of a specific location. - * Falls back to regular scheduling on non-Folia servers. - * - * @param location The location in whose region to run the task - * @param runnable The task to run - * @return A Task object representing the scheduled task - */ public static Task runLocationTask(Location location, Runnable runnable) { - if (isFolia && location != null && location.getWorld() != null) { - try { - io.papermc.paper.threadedregions.scheduler.ScheduledTask task = - Bukkit.getRegionScheduler().run(plugin, location, scheduledTask -> runnable.run()); - return new Task(task); - } catch (Exception e) { - plugin.getLogger().log(Level.WARNING, "Error scheduling location task in Folia, falling back to global scheduler", e); - return runTask(runnable); - } - } else { + ensureInitialized(); + if (location == null || location.getWorld() == null) { return runTask(runnable); } + return impl.runLocationTask(location, runnable); } - /** - * Runs a task in the region of a specific chunk in a world. - * If the server is running Folia, the task will be scheduled on the chunk's region thread. - * On non-Folia servers or in case of an error, the task will fall back to global scheduling. - * - * @param world The world containing the chunk where the task will be executed. - * @param chunkX The X-coordinate of the chunk. - * @param chunkZ The Z-coordinate of the chunk. - * @param runnable The task to be run in the specified chunk's region. - * @return A Task object representing the scheduled task. - */ public static Task runChunkTask(World world, int chunkX, int chunkZ, Runnable runnable) { - if (isFolia && world != null) { - try { - io.papermc.paper.threadedregions.scheduler.ScheduledTask task = - Bukkit.getRegionScheduler().run(plugin, world, chunkX, chunkZ, scheduledTask -> runnable.run()); - return new Task(task); - } catch (Exception e) { - plugin.getLogger().log(Level.WARNING, "Error scheduling location task in Folia, falling back to global scheduler", e); - return runTask(runnable); - } - } else { + ensureInitialized(); + if (world == null) { return runTask(runnable); } + return impl.runChunkTask(world, chunkX, chunkZ, runnable); } - /** - * Runs a delayed task in the region of a specific location. - * - * @param location The location in whose region to run the task - * @param runnable The task to run - * @param delayTicks The delay in ticks before running the task - * @return A Task object representing the scheduled task - */ public static Task runLocationTaskLater(Location location, Runnable runnable, long delayTicks) { - if (isFolia && location != null && location.getWorld() != null) { - try { - io.papermc.paper.threadedregions.scheduler.ScheduledTask task = - Bukkit.getRegionScheduler().runDelayed(plugin, location, scheduledTask -> runnable.run(), - delayTicks < 1 ? 1 : delayTicks); - return new Task(task); - } catch (Exception e) { - plugin.getLogger().log(Level.WARNING, "Error scheduling delayed location task in Folia, falling back to global scheduler", e); - return runTaskLater(runnable, delayTicks); - } - } else { + ensureInitialized(); + if (location == null || location.getWorld() == null) { return runTaskLater(runnable, delayTicks); } + return impl.runLocationTaskLater(location, runnable, delayTicks); } - /** - * Runs a repeated task in the region of a specific location. - * - * @param location The location in whose region to run the task - * @param runnable The task to run - * @param delayTicks The initial delay in ticks before the first execution - * @param periodTicks The period in ticks between subsequent executions - * @return A Task object representing the scheduled task - */ public static Task runLocationTaskTimer(Location location, Runnable runnable, long delayTicks, long periodTicks) { - if (isFolia && location != null && location.getWorld() != null) { - try { - io.papermc.paper.threadedregions.scheduler.ScheduledTask task = - Bukkit.getRegionScheduler().runAtFixedRate(plugin, location, scheduledTask -> runnable.run(), - delayTicks < 1 ? 1 : delayTicks, periodTicks); - return new Task(task); - } catch (Exception e) { - plugin.getLogger().log(Level.WARNING, "Error scheduling timer location task in Folia, falling back to global scheduler", e); - return runTaskTimer(runnable, delayTicks, periodTicks); - } - } else { + ensureInitialized(); + if (location == null || location.getWorld() == null) { return runTaskTimer(runnable, delayTicks, periodTicks); } + return impl.runLocationTaskTimer(location, runnable, delayTicks, periodTicks); } - /** - * Runs a task in the region of a specific location in a world. - * Falls back to regular scheduling on non-Folia servers. - * - * @param location The location in whose region to run the task - * @param runnable The task to run - * @return A Task object representing the scheduled task - */ public static Task runWorldTask(Location location, Runnable runnable) { - if (isFolia && location != null && location.getWorld() != null) { - try { - io.papermc.paper.threadedregions.scheduler.ScheduledTask task = - Bukkit.getRegionScheduler().run(plugin, location, scheduledTask -> runnable.run()); - return new Task(task); - } catch (Exception e) { - plugin.getLogger().log(Level.WARNING, "Error scheduling world task in Folia, falling back to global scheduler", e); - return runTask(runnable); - } - } else { - return runTask(runnable); - } + return runLocationTask(location, runnable); } - /** - * Runs a delayed task in the region of a specific location in a world. - * - * @param location The location in whose region to run the task - * @param runnable The task to run - * @param delayTicks The delay in ticks before running the task - * @return A Task object representing the scheduled task - */ public static Task runWorldTaskLater(Location location, Runnable runnable, long delayTicks) { - if (isFolia && location != null && location.getWorld() != null) { - try { - io.papermc.paper.threadedregions.scheduler.ScheduledTask task = - Bukkit.getRegionScheduler().runDelayed(plugin, location, scheduledTask -> runnable.run(), - delayTicks < 1 ? 1 : delayTicks); - return new Task(task); - } catch (Exception e) { - plugin.getLogger().log(Level.WARNING, "Error scheduling delayed world task in Folia, falling back to global scheduler", e); - return runTaskLater(runnable, delayTicks); - } - } else { - return runTaskLater(runnable, delayTicks); - } + return runLocationTaskLater(location, runnable, delayTicks); } - /** - * Creates a CompletableFuture that will be completed on the main thread or global region. - * - * @param The type of the result - * @param supplier The supplier providing the result - * @return A CompletableFuture that will be completed with the result - */ public static CompletableFuture supplySync(Supplier supplier) { - CompletableFuture future = new CompletableFuture<>(); + ensureInitialized(); + return impl.supplySync(supplier); + } - try { - if (isFolia) { - Bukkit.getGlobalRegionScheduler().run(plugin, task -> { - try { - future.complete(supplier.get()); - } catch (Throwable t) { - future.completeExceptionally(t); - plugin.getLogger().log(Level.SEVERE, "Error while executing sync task", t); - } - }); - } else { - Bukkit.getScheduler().runTask(plugin, () -> { - try { - future.complete(supplier.get()); - } catch (Throwable t) { - future.completeExceptionally(t); - plugin.getLogger().log(Level.SEVERE, "Error while executing sync task", t); + public static CompletableFuture supplyAsync(Supplier supplier) { + ensureInitialized(); + return impl.supplyAsync(supplier); + } + + // --- Task wrapper --- + + public interface Task { + void cancel(); + + boolean isCancelled(); + } + + // --- Platform strategy --- + + private interface PlatformScheduler { + Task runTask(Runnable runnable); + + Task runTaskAsync(Runnable runnable); + + Task runTaskLater(Runnable runnable, long delayTicks); + + Task runTaskLaterAsync(Runnable runnable, long delayTicks); + + Task runTaskTimer(Runnable runnable, long delayTicks, long periodTicks); + + Task runTaskTimerAsync(Runnable runnable, long delayTicks, long periodTicks); + + Task runEntityTask(Entity entity, Runnable runnable); + + Task runEntityTaskLater(Entity entity, Runnable runnable, long delayTicks); + + Task runEntityTaskTimer(Entity entity, Runnable runnable, long delayTicks, long periodTicks); + + Task runLocationTask(Location location, Runnable runnable); + + Task runLocationTaskLater(Location location, Runnable runnable, long delayTicks); + + Task runLocationTaskTimer(Location location, Runnable runnable, long delayTicks, long periodTicks); + + Task runChunkTask(World world, int chunkX, int chunkZ, Runnable runnable); + + CompletableFuture supplySync(Supplier supplier); + + CompletableFuture supplyAsync(Supplier supplier); + } + + // --- Bukkit implementation --- + + private static final class BukkitSchedulerImpl implements PlatformScheduler { + + private Task wrap(BukkitTask task) { + return new Task() { + @Override + public void cancel() { + if (task != null) { + task.cancel(); } - }); - } - } catch (Throwable t) { - future.completeExceptionally(t); + } + + @Override + public boolean isCancelled() { + return task == null || task.isCancelled(); + } + }; } - return future; - } + @Override + public Task runTask(Runnable runnable) { + return wrap(Bukkit.getScheduler().runTask(plugin, runnable)); + } - /** - * Creates a CompletableFuture that will be completed asynchronously. - * - * @param The type of the result - * @param supplier The supplier providing the result - * @return A CompletableFuture that will be completed with the result - */ - public static CompletableFuture supplyAsync(Supplier supplier) { - CompletableFuture future = new CompletableFuture<>(); + @Override + public Task runTaskAsync(Runnable runnable) { + return wrap(Bukkit.getScheduler().runTaskAsynchronously(plugin, runnable)); + } - try { - if (isFolia) { - Bukkit.getAsyncScheduler().runNow(plugin, task -> { + @Override + public Task runTaskLater(Runnable runnable, long delayTicks) { + return wrap(Bukkit.getScheduler().runTaskLater(plugin, runnable, delayTicks)); + } + + @Override + public Task runTaskLaterAsync(Runnable runnable, long delayTicks) { + return wrap(Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, runnable, delayTicks)); + } + + @Override + public Task runTaskTimer(Runnable runnable, long delayTicks, long periodTicks) { + return wrap(Bukkit.getScheduler().runTaskTimer(plugin, runnable, delayTicks, periodTicks)); + } + + @Override + public Task runTaskTimerAsync(Runnable runnable, long delayTicks, long periodTicks) { + return wrap(Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, runnable, delayTicks, periodTicks)); + } + + @Override + public Task runEntityTask(Entity entity, Runnable runnable) { + return runTask(runnable); + } + + @Override + public Task runEntityTaskLater(Entity entity, Runnable runnable, long delayTicks) { + return runTaskLater(runnable, delayTicks); + } + + @Override + public Task runEntityTaskTimer(Entity entity, Runnable runnable, long delayTicks, long periodTicks) { + return runTaskTimer(runnable, delayTicks, periodTicks); + } + + @Override + public Task runLocationTask(Location location, Runnable runnable) { + return runTask(runnable); + } + + @Override + public Task runLocationTaskLater(Location location, Runnable runnable, long delayTicks) { + return runTaskLater(runnable, delayTicks); + } + + @Override + public Task runLocationTaskTimer(Location location, Runnable runnable, long delayTicks, long periodTicks) { + return runTaskTimer(runnable, delayTicks, periodTicks); + } + + @Override + public Task runChunkTask(World world, int chunkX, int chunkZ, Runnable runnable) { + return runTask(runnable); + } + + @Override + public CompletableFuture supplySync(Supplier supplier) { + CompletableFuture future = new CompletableFuture<>(); + try { + Bukkit.getScheduler().runTask(plugin, () -> { try { future.complete(supplier.get()); - } catch (Throwable t) { - future.completeExceptionally(t); - plugin.getLogger().log(Level.SEVERE, "Error while executing async task", t); + } catch (Throwable ex) { + future.completeExceptionally(ex); + plugin.getLogger().log(Level.SEVERE, "Error while executing sync task", ex); } }); - } else { + } catch (Throwable ex) { + future.completeExceptionally(ex); + } + return future; + } + + @Override + public CompletableFuture supplyAsync(Supplier supplier) { + CompletableFuture future = new CompletableFuture<>(); + try { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { try { future.complete(supplier.get()); - } catch (Throwable t) { - future.completeExceptionally(t); - plugin.getLogger().log(Level.SEVERE, "Error while executing async task", t); + } catch (Throwable ex) { + future.completeExceptionally(ex); + plugin.getLogger().log(Level.SEVERE, "Error while executing async task", ex); } }); + } catch (Throwable ex) { + future.completeExceptionally(ex); } - } catch (Throwable t) { - future.completeExceptionally(t); + return future; } - - return future; } - /** - * Wrapper class for both Bukkit and Folia tasks. - */ - public static class Task { - private final Object task; - - /** - * Creates a new Task. - * - * @param task The underlying task object - */ - Task(Object task) { - this.task = task; - } - - /** - * Cancels the task. - */ - public void cancel() { - if (task == null) { - return; - } + // --- Folia implementation --- - try { - if (isFolia) { - if (task instanceof io.papermc.paper.threadedregions.scheduler.ScheduledTask) { - ((io.papermc.paper.threadedregions.scheduler.ScheduledTask) task).cancel(); - } - } else { - if (task instanceof BukkitTask) { - ((BukkitTask) task).cancel(); + private static final class FoliaSchedulerImpl implements PlatformScheduler { + + private Task wrap(io.papermc.paper.threadedregions.scheduler.ScheduledTask task) { + return new Task() { + @Override + public void cancel() { + if (task != null) { + task.cancel(); } } + + @Override + public boolean isCancelled() { + return task == null || task.isCancelled(); + } + }; + } + + @Override + public Task runTask(Runnable runnable) { + try { + return wrap(Bukkit.getGlobalRegionScheduler().run(plugin, scheduledTask -> runnable.run())); + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Error scheduling task in Folia", e); + return wrap(null); + } + } + + @Override + public Task runTaskAsync(Runnable runnable) { + try { + return wrap(Bukkit.getAsyncScheduler().runNow(plugin, scheduledTask -> runnable.run())); + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Error scheduling async task in Folia", e); + return wrap(null); + } + } + + @Override + public Task runTaskLater(Runnable runnable, long delayTicks) { + try { + return wrap(Bukkit.getGlobalRegionScheduler().runDelayed(plugin, scheduledTask -> runnable.run(), + foliaDelay(delayTicks))); } catch (Exception e) { - plugin.getLogger().log(Level.WARNING, "Failed to cancel task", e); + plugin.getLogger().log(Level.SEVERE, "Error scheduling delayed task in Folia", e); + return wrap(null); } } - /** - * Gets the underlying task object. - * - * @return The underlying task object - */ - public Object getTask() { - return task; + @Override + public Task runTaskLaterAsync(Runnable runnable, long delayTicks) { + try { + long delayMs = delayTicks * TICK_MS; + return wrap(Bukkit.getAsyncScheduler().runDelayed(plugin, scheduledTask -> runnable.run(), + delayMs, TimeUnit.MILLISECONDS)); + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Error scheduling delayed async task in Folia", e); + return wrap(null); + } } - /** - * Checks if this task is cancelled. - * - * @return true if the task is cancelled - */ - public boolean isCancelled() { - if (task == null) { - return true; + @Override + public Task runTaskTimer(Runnable runnable, long delayTicks, long periodTicks) { + try { + return wrap(Bukkit.getGlobalRegionScheduler().runAtFixedRate(plugin, scheduledTask -> runnable.run(), + foliaDelay(delayTicks), periodTicks)); + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Error scheduling timer task in Folia", e); + return wrap(null); } + } + @Override + public Task runTaskTimerAsync(Runnable runnable, long delayTicks, long periodTicks) { try { - if (isFolia) { - if (task instanceof io.papermc.paper.threadedregions.scheduler.ScheduledTask) { - return ((io.papermc.paper.threadedregions.scheduler.ScheduledTask) task).isCancelled(); - } - } else { - if (task instanceof BukkitTask) { - return ((BukkitTask) task).isCancelled(); + long delayMs = delayTicks * TICK_MS; + long periodMs = periodTicks * TICK_MS; + return wrap(Bukkit.getAsyncScheduler().runAtFixedRate(plugin, scheduledTask -> runnable.run(), + delayMs, periodMs, TimeUnit.MILLISECONDS)); + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Error scheduling timer async task in Folia", e); + return wrap(null); + } + } + + @Override + public Task runEntityTask(Entity entity, Runnable runnable) { + try { + return wrap(entity.getScheduler().run(plugin, scheduledTask -> runnable.run(), null)); + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, + "Error scheduling entity task in Folia, falling back to global scheduler", e); + return runTask(runnable); + } + } + + @Override + public Task runEntityTaskLater(Entity entity, Runnable runnable, long delayTicks) { + try { + return wrap(entity.getScheduler().runDelayed(plugin, scheduledTask -> runnable.run(), null, + foliaDelay(delayTicks))); + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, + "Error scheduling delayed entity task in Folia, falling back to global scheduler", e); + return runTaskLater(runnable, delayTicks); + } + } + + @Override + public Task runEntityTaskTimer(Entity entity, Runnable runnable, long delayTicks, long periodTicks) { + try { + return wrap(entity.getScheduler().runAtFixedRate(plugin, scheduledTask -> runnable.run(), null, + foliaDelay(delayTicks), periodTicks)); + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, + "Error scheduling timer entity task in Folia, falling back to global scheduler", e); + return runTaskTimer(runnable, delayTicks, periodTicks); + } + } + + @Override + public Task runLocationTask(Location location, Runnable runnable) { + try { + return wrap(Bukkit.getRegionScheduler().run(plugin, location, scheduledTask -> runnable.run())); + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, + "Error scheduling location task in Folia, falling back to global scheduler", e); + return runTask(runnable); + } + } + + @Override + public Task runLocationTaskLater(Location location, Runnable runnable, long delayTicks) { + try { + return wrap(Bukkit.getRegionScheduler().runDelayed(plugin, location, scheduledTask -> runnable.run(), + foliaDelay(delayTicks))); + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, + "Error scheduling delayed location task in Folia, falling back to global scheduler", e); + return runTaskLater(runnable, delayTicks); + } + } + + @Override + public Task runLocationTaskTimer(Location location, Runnable runnable, long delayTicks, long periodTicks) { + try { + return wrap(Bukkit.getRegionScheduler().runAtFixedRate(plugin, location, scheduledTask -> runnable.run(), + foliaDelay(delayTicks), periodTicks)); + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, + "Error scheduling timer location task in Folia, falling back to global scheduler", e); + return runTaskTimer(runnable, delayTicks, periodTicks); + } + } + + @Override + public Task runChunkTask(World world, int chunkX, int chunkZ, Runnable runnable) { + try { + return wrap(Bukkit.getRegionScheduler().run(plugin, world, chunkX, chunkZ, scheduledTask -> runnable.run())); + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, + "Error scheduling chunk task in Folia, falling back to global scheduler", e); + return runTask(runnable); + } + } + + @Override + public CompletableFuture supplySync(Supplier supplier) { + CompletableFuture future = new CompletableFuture<>(); + try { + Bukkit.getGlobalRegionScheduler().run(plugin, scheduledTask -> { + try { + future.complete(supplier.get()); + } catch (Throwable ex) { + future.completeExceptionally(ex); + plugin.getLogger().log(Level.SEVERE, "Error while executing sync task", ex); } - } - } catch (Exception ignored) { - // Task may have already been garbage collected or is invalid + }); + } catch (Throwable ex) { + future.completeExceptionally(ex); } + return future; + } - return true; + @Override + public CompletableFuture supplyAsync(Supplier supplier) { + CompletableFuture future = new CompletableFuture<>(); + try { + Bukkit.getAsyncScheduler().runNow(plugin, scheduledTask -> { + try { + future.complete(supplier.get()); + } catch (Throwable ex) { + future.completeExceptionally(ex); + plugin.getLogger().log(Level.SEVERE, "Error while executing async task", ex); + } + }); + } catch (Throwable ex) { + future.completeExceptionally(ex); + } + return future; } } } diff --git a/core/src/main/java/github/nighter/smartspawner/SmartSpawner.java b/core/src/main/java/github/nighter/smartspawner/SmartSpawner.java index ced99a19..31b5d221 100644 --- a/core/src/main/java/github/nighter/smartspawner/SmartSpawner.java +++ b/core/src/main/java/github/nighter/smartspawner/SmartSpawner.java @@ -180,6 +180,7 @@ public class SmartSpawner extends JavaPlugin implements SmartSpawnerPlugin { public void onEnable() { long startTime = System.currentTimeMillis(); instance = this; + Scheduler.init(this); Config.load(this); // Initialize plugin integrations diff --git a/core/src/main/java/github/nighter/smartspawner/logging/LoggingConfig.java b/core/src/main/java/github/nighter/smartspawner/logging/LoggingConfig.java index 8864948e..3654662d 100644 --- a/core/src/main/java/github/nighter/smartspawner/logging/LoggingConfig.java +++ b/core/src/main/java/github/nighter/smartspawner/logging/LoggingConfig.java @@ -97,7 +97,7 @@ public Set getEnabledEvents() { return new HashSet<>(enabledEvents); } - public boolean isEventEnabled(SpawnerEventType eventType) { - return !enabled || !enabledEvents.contains(eventType); + public boolean shouldLogEvent(SpawnerEventType eventType) { + return enabled && enabledEvents.contains(eventType); } } diff --git a/core/src/main/java/github/nighter/smartspawner/logging/SpawnerActionLogger.java b/core/src/main/java/github/nighter/smartspawner/logging/SpawnerActionLogger.java index 295b92ad..edb80429 100644 --- a/core/src/main/java/github/nighter/smartspawner/logging/SpawnerActionLogger.java +++ b/core/src/main/java/github/nighter/smartspawner/logging/SpawnerActionLogger.java @@ -8,12 +8,14 @@ import java.io.BufferedWriter; import java.io.File; -import java.io.FileWriter; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; @@ -24,183 +26,219 @@ * Handles asynchronous logging with file rotation and multiple output formats. */ public class SpawnerActionLogger { + private static final int MAX_DRAIN_PER_TICK = 500; + private static final double SIZE_RECONCILE_THRESHOLD = 0.9; + + private static final DateTimeFormatter FILE_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final DateTimeFormatter ROTATE_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); + private final SmartSpawner plugin; private final LoggingConfig config; private final Queue logQueue; private final AtomicBoolean isShuttingDown; + private final Object fileLock = new Object(); + private final AtomicBoolean rotationPending = new AtomicBoolean(false); + private Scheduler.Task logTask; - private DiscordWebhookLogger discordLogger; - + private volatile DiscordWebhookLogger discordLogger; + private File currentLogFile; - private static final ThreadLocal dateFormat = - ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); - + private volatile long currentLogFileSize; + public SpawnerActionLogger(SmartSpawner plugin, LoggingConfig config) { this.plugin = plugin; this.config = config; this.logQueue = new ConcurrentLinkedQueue<>(); this.isShuttingDown = new AtomicBoolean(false); - + if (config.isEnabled()) { setupLogDirectory(); startLoggingTask(); } - - // Initialize Discord webhook logger (only when enabled – no memory cost otherwise) + DiscordWebhookConfig discordConfig = new DiscordWebhookConfig(plugin); if (discordConfig.isEnabled()) { DiscordEmbedConfigManager embedManager = new DiscordEmbedConfigManager(plugin, discordConfig); this.discordLogger = new DiscordWebhookLogger(plugin, discordConfig, embedManager); } } - + /** * Logs a spawner action asynchronously. */ public void log(SpawnerLogEntry entry) { - if (!config.isEnabled() || config.isEventEnabled(entry.getEventType())) { + if (!config.isEnabled() || isShuttingDown.get()) { return; } - + if (!config.shouldLogEvent(entry.getEventType())) { + return; + } + if (config.isConsoleOutput()) { plugin.getLogger().info("[SpawnerLog] " + entry.toReadableString()); } - - // Always use async logging + logQueue.offer(entry); - - // Also send to Discord if enabled - if (discordLogger != null) { - discordLogger.queueWebhook(entry); + + DiscordWebhookLogger currentDiscord = this.discordLogger; + if (currentDiscord != null) { + currentDiscord.queueWebhook(entry); } } - + /** * Logs a spawner action using a builder pattern. */ public void log(SpawnerEventType eventType, LogEntryConsumer consumer) { - if (!config.isEnabled() || config.isEventEnabled(eventType)) { + if (!config.isEnabled() || isShuttingDown.get() || !config.shouldLogEvent(eventType)) { return; } - + SpawnerLogEntry.Builder builder = new SpawnerLogEntry.Builder(eventType); consumer.accept(builder); log(builder.build()); } - + @FunctionalInterface public interface LogEntryConsumer { void accept(SpawnerLogEntry.Builder builder); } - + private void setupLogDirectory() { try { Path logPath = Paths.get(plugin.getDataFolder().getAbsolutePath(), config.getLogDirectory()); Files.createDirectories(logPath); - - String fileName = "spawner-" + dateFormat.get().format(new Date()) + - (config.isJsonFormat() ? ".json" : ".log"); + + String fileName = "spawner-" + LocalDate.now().format(FILE_DATE_FORMAT) + + (config.isJsonFormat() ? ".json" : ".log"); currentLogFile = logPath.resolve(fileName).toFile(); - - // Perform log rotation if needed + currentLogFileSize = currentLogFile.exists() ? currentLogFile.length() : 0L; + rotateLogsIfNeeded(); } catch (IOException e) { plugin.getLogger().log(Level.SEVERE, "Failed to setup log directory", e); } } - + private void startLoggingTask() { - // Process log queue every 2 seconds (always async) logTask = Scheduler.runTaskTimerAsync(() -> { - if (isShuttingDown.get()) { + if (isShuttingDown.get() || logQueue.isEmpty()) { return; } processLogQueue(); }, 40L, 40L); } - + private void processLogQueue() { - if (logQueue.isEmpty()) { - return; - } - List entries = new ArrayList<>(); SpawnerLogEntry entry; - while ((entry = logQueue.poll()) != null) { + int drained = 0; + while ((entry = logQueue.poll()) != null && drained < MAX_DRAIN_PER_TICK) { entries.add(entry); + drained++; } - + if (!entries.isEmpty()) { writeLogEntries(entries); } } - + private void writeLogEntries(List entries) { if (currentLogFile == null || entries.isEmpty()) { return; } - - try (BufferedWriter writer = new BufferedWriter(new FileWriter(currentLogFile, true))) { - for (SpawnerLogEntry entry : entries) { - String logLine = config.isJsonFormat() ? entry.toJson() : entry.toReadableString(); - writer.write(logLine); - writer.newLine(); + + synchronized (fileLock) { + try (BufferedWriter writer = Files.newBufferedWriter( + currentLogFile.toPath(), StandardCharsets.UTF_8, + java.nio.file.StandardOpenOption.CREATE, + java.nio.file.StandardOpenOption.APPEND)) { + for (SpawnerLogEntry logEntry : entries) { + String logLine = config.isJsonFormat() ? logEntry.toJson() : logEntry.toReadableString(); + writer.write(logLine); + writer.newLine(); + currentLogFileSize += logLine.getBytes(StandardCharsets.UTF_8).length + 1; + } + writer.flush(); + } catch (IOException e) { + plugin.getLogger().log(Level.WARNING, "Failed to write log entries", e); + return; } - writer.flush(); - - // Check if rotation is needed after writing - checkAndRotateLog(); - } catch (IOException e) { - plugin.getLogger().log(Level.WARNING, "Failed to write log entries", e); + + maybeScheduleRotation(); } } - - private void checkAndRotateLog() { - if (currentLogFile == null || !currentLogFile.exists()) { + + private void maybeScheduleRotation() { + if (isShuttingDown.get() || currentLogFile == null) { + return; + } + + long maxSizeBytes = config.getMaxLogSizeMB() * 1024L * 1024L; + if (currentLogFileSize < maxSizeBytes * SIZE_RECONCILE_THRESHOLD) { return; } - - long fileSizeBytes = currentLogFile.length(); - long maxSizeBytes = config.getMaxLogSizeMB() * 1024 * 1024; - - if (fileSizeBytes > maxSizeBytes) { - rotateLog(); + + long actual = currentLogFile.length(); + currentLogFileSize = actual; + if (actual <= maxSizeBytes) { + return; + } + + if (rotationPending.compareAndSet(false, true)) { + Scheduler.runTaskAsync(() -> { + try { + rotateLog(); + } finally { + rotationPending.set(false); + } + }); } } - + private void rotateLog() { - try { - String timestamp = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(new Date()); - String extension = config.isJsonFormat() ? ".json" : ".log"; - Path logPath = Paths.get(plugin.getDataFolder().getAbsolutePath(), config.getLogDirectory()); - - File rotatedFile = logPath.resolve("spawner-" + timestamp + extension).toFile(); - Files.move(currentLogFile.toPath(), rotatedFile.toPath()); - - String fileName = "spawner-" + dateFormat.get().format(new Date()) + extension; - currentLogFile = logPath.resolve(fileName).toFile(); - - plugin.getLogger().info("Rotated spawner log to: " + rotatedFile.getName()); - - // Clean up old logs - cleanupOldLogs(); - } catch (IOException e) { - plugin.getLogger().log(Level.WARNING, "Failed to rotate log file", e); + synchronized (fileLock) { + if (currentLogFile == null || !currentLogFile.exists()) { + return; + } + + long maxSizeBytes = config.getMaxLogSizeMB() * 1024L * 1024L; + if (currentLogFile.length() <= maxSizeBytes) { + currentLogFileSize = currentLogFile.length(); + return; + } + + try { + String timestamp = LocalDateTime.now().format(ROTATE_DATE_FORMAT); + String extension = config.isJsonFormat() ? ".json" : ".log"; + Path logPath = Paths.get(plugin.getDataFolder().getAbsolutePath(), config.getLogDirectory()); + + File rotatedFile = logPath.resolve("spawner-" + timestamp + extension).toFile(); + Files.move(currentLogFile.toPath(), rotatedFile.toPath()); + + String fileName = "spawner-" + LocalDate.now().format(FILE_DATE_FORMAT) + extension; + currentLogFile = logPath.resolve(fileName).toFile(); + currentLogFileSize = 0L; + + plugin.getLogger().info("Rotated spawner log to: " + rotatedFile.getName()); + + cleanupOldLogs(); + } catch (IOException e) { + plugin.getLogger().log(Level.WARNING, "Failed to rotate log file", e); + } } } - + private void rotateLogsIfNeeded() { try { Path logPath = Paths.get(plugin.getDataFolder().getAbsolutePath(), config.getLogDirectory()); - - File[] logFiles = logPath.toFile().listFiles((dir, name) -> + + File[] logFiles = logPath.toFile().listFiles((dir, name) -> name.startsWith("spawner-") && (name.endsWith(".log") || name.endsWith(".json"))); - + if (logFiles != null && logFiles.length > config.getMaxLogFiles()) { - // Sort by last modified date Arrays.sort(logFiles, Comparator.comparingLong(File::lastModified)); - - // Delete oldest files + int filesToDelete = logFiles.length - config.getMaxLogFiles(); for (int i = 0; i < filesToDelete; i++) { if (logFiles[i].delete()) { @@ -212,11 +250,11 @@ private void rotateLogsIfNeeded() { plugin.getLogger().log(Level.WARNING, "Failed to rotate old logs", e); } } - + private void cleanupOldLogs() { rotateLogsIfNeeded(); } - + /** * Reloads the Discord webhook logger from {@code discord_logging.yml}. * The file-logging task is NOT interrupted; only the Discord side is restarted. @@ -226,10 +264,8 @@ public void reloadDiscord() { DiscordEmbedConfigManager newEmbedManager = new DiscordEmbedConfigManager(plugin, newDiscordConfig); if (discordLogger != null) { - // Hot-reload: swap config and restart background task if needed discordLogger.reload(newDiscordConfig, newEmbedManager); } else if (newDiscordConfig.isEnabled()) { - // Discord was disabled before; create a fresh logger now this.discordLogger = new DiscordWebhookLogger(plugin, newDiscordConfig, newEmbedManager); } } @@ -239,17 +275,18 @@ public void reloadDiscord() { */ public void shutdown() { isShuttingDown.set(true); - + if (logTask != null) { logTask.cancel(); } - - // Flush remaining entries - processLogQueue(); - - // Shutdown Discord logger - if (discordLogger != null) { - discordLogger.shutdown(); + + synchronized (fileLock) { + processLogQueue(); + } + + DiscordWebhookLogger currentDiscord = discordLogger; + if (currentDiscord != null) { + currentDiscord.shutdown(); } } } diff --git a/core/src/main/java/github/nighter/smartspawner/migration/SpawnerDataConverter.java b/core/src/main/java/github/nighter/smartspawner/migration/SpawnerDataConverter.java index 88ccf66a..e40652e4 100644 --- a/core/src/main/java/github/nighter/smartspawner/migration/SpawnerDataConverter.java +++ b/core/src/main/java/github/nighter/smartspawner/migration/SpawnerDataConverter.java @@ -1,28 +1,33 @@ package github.nighter.smartspawner.migration; +import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import github.nighter.smartspawner.SmartSpawner; +import github.nighter.smartspawner.spawner.utils.ItemStackSerializer; import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.Damageable; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.PotionMeta; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import java.util.*; - -import com.google.gson.Gson; +import java.util.logging.Level; public class SpawnerDataConverter { + private static final Gson GSON = new Gson(); + private final SmartSpawner plugin; private final FileConfiguration oldConfig; private final FileConfiguration newConfig; - private static final Gson gson = new Gson(); public SpawnerDataConverter(SmartSpawner plugin, FileConfiguration oldConfig, FileConfiguration newConfig) { this.plugin = plugin; @@ -32,14 +37,15 @@ public SpawnerDataConverter(SmartSpawner plugin, FileConfiguration oldConfig, Fi public void convertData() { ConfigurationSection spawnersSection = oldConfig.getConfigurationSection("spawners"); - if (spawnersSection == null) return; + if (spawnersSection == null) { + return; + } for (String spawnerId : spawnersSection.getKeys(false)) { try { convertSpawner(spawnerId); } catch (Exception e) { - plugin.getLogger().severe("Failed to convert spawner " + spawnerId); - e.printStackTrace(); + plugin.getLogger().log(Level.SEVERE, "Failed to convert spawner " + spawnerId, e); } } } @@ -47,195 +53,196 @@ public void convertData() { private void convertSpawner(String spawnerId) { String oldPath = "spawners." + spawnerId; - try { - // Format location - String worldName = oldConfig.getString(oldPath + ".world"); - int x = oldConfig.getInt(oldPath + ".x"); - int y = oldConfig.getInt(oldPath + ".y"); - int z = oldConfig.getInt(oldPath + ".z"); - - // Format settings string - String settings = String.format("%d,%b,%d,%b,%d,%d,%d,%d,%d,%d,%d,%b", - oldConfig.getInt(oldPath + ".spawnerExp"), - oldConfig.getBoolean(oldPath + ".spawnerActive"), - oldConfig.getInt(oldPath + ".spawnerRange"), - oldConfig.getBoolean(oldPath + ".spawnerStop"), - oldConfig.getInt(oldPath + ".spawnDelay"), - oldConfig.getInt(oldPath + ".maxSpawnerLootSlots"), - oldConfig.getInt(oldPath + ".maxStoredExp"), - oldConfig.getInt(oldPath + ".minMobs"), - oldConfig.getInt(oldPath + ".maxMobs"), - oldConfig.getInt(oldPath + ".stackSize"), - oldConfig.getLong(oldPath + ".lastSpawnTime"), - oldConfig.getBoolean(oldPath + ".allowEquipmentItems") - ); - - // Format inventory - List newInventoryFormat = new ArrayList<>(); - ConfigurationSection invSection = oldConfig.getConfigurationSection(oldPath + ".virtualInventory"); - if (invSection != null) { - List serializedItems = invSection.getStringList("items"); - Map> durabilityItems = new HashMap<>(); // Material -> (Durability -> Count) - Map regularItems = new HashMap<>(); // For items without durability - - for (String serialized : serializedItems) { - try { - String[] parts = serialized.split(":", 2); - if (parts.length == 2) { - ItemStack item = itemStackFromJson(parts[1]); - if (item != null) { - if (item.getType() == Material.TIPPED_ARROW) { - ItemMeta meta = item.getItemMeta(); - if (meta instanceof PotionMeta && ((PotionMeta) meta).hasCustomEffects()) { - PotionEffect effect = ((PotionMeta) meta).getCustomEffects().get(0); - String itemKey = String.format("TIPPED_ARROW#%s;%d;%d", - effect.getType().getName(), - effect.getDuration(), - effect.getAmplifier()); - regularItems.merge(itemKey, item.getAmount(), Integer::sum); - } else { - regularItems.merge("ARROW", item.getAmount(), Integer::sum); - } - } else if (isDestructibleItem(item.getType())) { - // Handle items with durability - String itemType = item.getType().name(); - durabilityItems.computeIfAbsent(itemType, k -> new TreeMap<>()) - .merge((int) item.getDurability(), item.getAmount(), Integer::sum); - } else { - // Handle regular items - regularItems.merge(item.getType().name(), item.getAmount(), Integer::sum); - } - } - } - } catch (Exception e) { - plugin.getLogger().warning("Failed to convert item in spawner " + spawnerId + ": " + e.getMessage()); - } - } + String worldName = oldConfig.getString(oldPath + ".world"); + int x = oldConfig.getInt(oldPath + ".x"); + int y = oldConfig.getInt(oldPath + ".y"); + int z = oldConfig.getInt(oldPath + ".z"); + + String settings = String.format("%d,%b,%d,%b,%d,%d,%d,%d,%d,%d,%d,%b", + oldConfig.getInt(oldPath + ".spawnerExp"), + oldConfig.getBoolean(oldPath + ".spawnerActive"), + oldConfig.getInt(oldPath + ".spawnerRange"), + oldConfig.getBoolean(oldPath + ".spawnerStop"), + oldConfig.getInt(oldPath + ".spawnDelay"), + oldConfig.getInt(oldPath + ".maxSpawnerLootSlots"), + oldConfig.getInt(oldPath + ".maxStoredExp"), + oldConfig.getInt(oldPath + ".minMobs"), + oldConfig.getInt(oldPath + ".maxMobs"), + oldConfig.getInt(oldPath + ".stackSize"), + oldConfig.getLong(oldPath + ".lastSpawnTime"), + oldConfig.getBoolean(oldPath + ".allowEquipmentItems") + ); - // Add regular items to the output - for (Map.Entry entry : regularItems.entrySet()) { - newInventoryFormat.add(entry.getKey() + ":" + entry.getValue()); + List newInventoryFormat = new ArrayList<>(); + ConfigurationSection invSection = oldConfig.getConfigurationSection(oldPath + ".virtualInventory"); + if (invSection != null) { + List serializedItems = invSection.getStringList("items"); + Map> durabilityItems = new HashMap<>(); + Map regularItems = new HashMap<>(); + + for (String serialized : serializedItems) { + int colonIndex = serialized.indexOf(':'); + if (colonIndex == -1) { + continue; } - // Add items with durability to the output using the correct format - for (Map.Entry> itemEntry : durabilityItems.entrySet()) { - StringBuilder itemString = new StringBuilder(itemEntry.getKey()); - if (!itemEntry.getValue().isEmpty()) { - itemString.append(";"); - boolean first = true; - for (Map.Entry durabilityEntry : itemEntry.getValue().entrySet()) { - if (!first) { - itemString.append(","); - } - itemString.append(durabilityEntry.getKey()) - .append(":") - .append(durabilityEntry.getValue()); - first = false; + try { + ItemStack item = itemStackFromJson(serialized.substring(colonIndex + 1)); + if (item == null || item.getType().isAir()) { + continue; + } + + Material type = item.getType(); + + if (type == Material.TIPPED_ARROW) { + ItemMeta meta = item.getItemMeta(); + if (meta instanceof PotionMeta potionMeta && potionMeta.hasCustomEffects()) { + PotionEffect effect = potionMeta.getCustomEffects().get(0); + String itemKey = String.format("TIPPED_ARROW#%s;%d;%d", + effect.getType().getName(), + effect.getDuration(), + effect.getAmplifier()); + regularItems.merge(itemKey, item.getAmount(), Integer::sum); + } else { + regularItems.merge("ARROW", item.getAmount(), Integer::sum); } + } else if (ItemStackSerializer.isDestructibleItem(type)) { + int damage = 0; + if (item.getItemMeta() instanceof Damageable damageable) { + damage = damageable.getDamage(); + } + durabilityItems.computeIfAbsent(type.name(), k -> new TreeMap<>()) + .merge(damage, item.getAmount(), Integer::sum); + } else { + regularItems.merge(type.name(), item.getAmount(), Integer::sum); } - newInventoryFormat.add(itemString.toString()); + } catch (Exception e) { + plugin.getLogger().warning("Failed to convert item in spawner " + spawnerId + ": " + e.getMessage()); } } - // Build the complete spawner section - String spawnerPath = "spawners." + spawnerId; - newConfig.set(spawnerPath + ".location", String.format("%s,%d,%d,%d", worldName, x, y, z)); - newConfig.set(spawnerPath + ".entityType", oldConfig.getString(oldPath + ".entityType")); - newConfig.set(spawnerPath + ".settings", settings); - newConfig.set(spawnerPath + ".inventory", newInventoryFormat); + for (Map.Entry entry : regularItems.entrySet()) { + newInventoryFormat.add(entry.getKey() + ":" + entry.getValue()); + } - } catch (Exception e) { - plugin.getLogger().severe("Failed to convert spawner " + spawnerId + ": " + e.getMessage()); - throw e; + for (Map.Entry> itemEntry : durabilityItems.entrySet()) { + StringBuilder itemString = new StringBuilder(itemEntry.getKey()); + if (!itemEntry.getValue().isEmpty()) { + itemString.append(";"); + boolean first = true; + for (Map.Entry durabilityEntry : itemEntry.getValue().entrySet()) { + if (!first) { + itemString.append(","); + } + itemString.append(durabilityEntry.getKey()) + .append(":") + .append(durabilityEntry.getValue()); + first = false; + } + } + newInventoryFormat.add(itemString.toString()); + } } - } - - private boolean isDestructibleItem(Material material) { - String name = material.name(); - return name.endsWith("_SWORD") - || name.endsWith("_PICKAXE") - || name.endsWith("_AXE") - || name.endsWith("_SPADE") - || name.endsWith("_HOE") - || name.equals("BOW") - || name.equals("FISHING_ROD") - || name.equals("FLINT_AND_STEEL") - || name.equals("SHEARS") - || name.equals("SHIELD") - || name.equals("ELYTRA") - || name.equals("TRIDENT") - || name.equals("CROSSBOW") - || name.startsWith("LEATHER_") - || name.startsWith("CHAINMAIL_") - || name.startsWith("IRON_") - || name.startsWith("GOLDEN_") - || name.startsWith("DIAMOND_") - || name.startsWith("NETHERITE_"); + String spawnerPath = "spawners." + spawnerId; + newConfig.set(spawnerPath + ".location", String.format("%s,%d,%d,%d", worldName, x, y, z)); + newConfig.set(spawnerPath + ".entityType", oldConfig.getString(oldPath + ".entityType")); + newConfig.set(spawnerPath + ".settings", settings); + newConfig.set(spawnerPath + ".inventory", newInventoryFormat); } public static ItemStack itemStackFromJson(String data) { - JsonObject json = gson.fromJson(data, JsonObject.class); - ItemStack item = new ItemStack( - Material.valueOf(json.get("type").getAsString()), - json.get("amount").getAsInt(), - (short) json.get("durability").getAsInt() - ); + JsonObject json = GSON.fromJson(data, JsonObject.class); + if (json == null || !json.has("type")) { + return null; + } + + Material material = Material.valueOf(json.get("type").getAsString()); + int amount = json.has("amount") ? json.get("amount").getAsInt() : 1; + ItemStack item = new ItemStack(material, amount); ItemMeta meta = item.getItemMeta(); - if (meta != null) { - if (json.has("displayName")) { - meta.setDisplayName(json.get("displayName").getAsString()); - } + if (meta == null) { + return item; + } - if (json.has("lore")) { - List lore = new ArrayList<>(); - JsonArray loreArray = json.getAsJsonArray("lore"); - for (JsonElement element : loreArray) { - lore.add(element.getAsString()); - } - meta.setLore(lore); + if (json.has("durability") && meta instanceof Damageable damageable) { + damageable.setDamage(json.get("durability").getAsInt()); + meta = damageable; + } + + if (json.has("displayName")) { + meta.setDisplayName(json.get("displayName").getAsString()); + } + + if (json.has("lore")) { + List lore = new ArrayList<>(); + JsonArray loreArray = json.getAsJsonArray("lore"); + for (JsonElement element : loreArray) { + lore.add(element.getAsString()); } + meta.setLore(lore); + } - if (json.has("enchantments")) { - JsonObject enchants = json.getAsJsonObject("enchantments"); - for (Map.Entry entry : enchants.entrySet()) { - Enchantment enchantment = Enchantment.getByName(entry.getKey()); - if (enchantment != null) { - meta.addEnchant(enchantment, entry.getValue().getAsInt(), true); - } + if (json.has("enchantments")) { + JsonObject enchants = json.getAsJsonObject("enchantments"); + for (Map.Entry entry : enchants.entrySet()) { + Enchantment enchantment = resolveEnchantment(entry.getKey()); + if (enchantment != null) { + meta.addEnchant(enchantment, entry.getValue().getAsInt(), true); } } + } - if (meta instanceof PotionMeta && json.has("potionData")) { - PotionMeta potionMeta = (PotionMeta) meta; - JsonObject potionData = json.getAsJsonObject("potionData"); - - if (potionData.has("customEffects")) { - JsonArray customEffects = potionData.getAsJsonArray("customEffects"); - for (JsonElement element : customEffects) { - JsonObject effectObj = element.getAsJsonObject(); - PotionEffectType type = PotionEffectType.getByName( - effectObj.get("type").getAsString() + if (meta instanceof PotionMeta potionMeta && json.has("potionData")) { + JsonObject potionData = json.getAsJsonObject("potionData"); + + if (potionData.has("customEffects")) { + JsonArray customEffects = potionData.getAsJsonArray("customEffects"); + for (JsonElement element : customEffects) { + JsonObject effectObj = element.getAsJsonObject(); + PotionEffectType type = resolvePotionEffectType(effectObj.get("type").getAsString()); + if (type != null) { + PotionEffect effect = new PotionEffect( + type, + effectObj.get("duration").getAsInt(), + effectObj.get("amplifier").getAsInt(), + effectObj.get("ambient").getAsBoolean(), + effectObj.get("particles").getAsBoolean(), + effectObj.get("icon").getAsBoolean() ); - if (type != null) { - PotionEffect effect = new PotionEffect( - type, - effectObj.get("duration").getAsInt(), - effectObj.get("amplifier").getAsInt(), - effectObj.get("ambient").getAsBoolean(), - effectObj.get("particles").getAsBoolean(), - effectObj.get("icon").getAsBoolean() - ); - potionMeta.addCustomEffect(effect, true); - } + potionMeta.addCustomEffect(effect, true); } } } - - item.setItemMeta(meta); + meta = potionMeta; } + item.setItemMeta(meta); return item; } -} \ No newline at end of file + + private static Enchantment resolveEnchantment(String key) { + Enchantment enchantment = Registry.ENCHANTMENT.get(NamespacedKey.minecraft(key.toLowerCase(Locale.ROOT))); + if (enchantment != null) { + return enchantment; + } + enchantment = Registry.ENCHANTMENT.get(NamespacedKey.fromString(key)); + if (enchantment != null) { + return enchantment; + } + return Enchantment.getByName(key); + } + + private static PotionEffectType resolvePotionEffectType(String id) { + PotionEffectType type = Registry.POTION_EFFECT_TYPE.get(NamespacedKey.minecraft(id.toLowerCase(Locale.ROOT))); + if (type != null) { + return type; + } + type = Registry.POTION_EFFECT_TYPE.get(NamespacedKey.fromString(id)); + if (type != null) { + return type; + } + return PotionEffectType.getByName(id); + } +}