diff --git a/core/src/main/java/github/nighter/smartspawner/Scheduler.java b/core/src/main/java/github/nighter/smartspawner/Scheduler.java index 55a73d72..33188ab7 100644 --- a/core/src/main/java/github/nighter/smartspawner/Scheduler.java +++ b/core/src/main/java/github/nighter/smartspawner/Scheduler.java @@ -13,538 +13,177 @@ 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 high-performance Strategy Pattern. */ public final class Scheduler { + + private static Plugin plugin; + private static PlatformScheduler schedulerImpl; - private static final Plugin plugin; - private static final boolean isFolia; - - static { - plugin = SmartSpawner.getInstance(); - - // Check if we're running on Folia - boolean foliaDetected = false; + /** + * Initialisiert den Scheduler. Sollte in der onEnable-Methode des Plugins aufgerufen werden. + */ + public static void init(Plugin pluginInstance) { + plugin = pluginInstance; + + boolean isFolia = 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; - } + isFolia = true; + } catch (ClassNotFoundException ignored) {} - /** - * 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 - */ - 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); - } + schedulerImpl = new FoliaScheduler(); + plugin.getLogger().info("Folia detected! Using region-based threading system."); } else { - return new Task(Bukkit.getScheduler().runTask(plugin, runnable)); + schedulerImpl = new BukkitScheduler(); + plugin.getLogger().info("Standard Paper/Bukkit server detected."); } } - /** - * 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)); - } - } + // --- ÖFFENTLICHE API --- - /** - * 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)); - } + public static Task runTask(Runnable runnable) { return schedulerImpl.runTask(runnable); } + public static Task runTaskAsync(Runnable runnable) { return schedulerImpl.runTaskAsync(runnable); } + + public static Task runTaskLater(Runnable runnable, long delayTicks) { + return delayTicks <= 0 ? runTask(runnable) : schedulerImpl.runTaskLater(runnable, delayTicks); } - - /** - * 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 - */ - 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)); - } + public static Task runTaskLaterAsync(Runnable runnable, long delayTicks) { + return delayTicks <= 0 ? runTaskAsync(runnable) : schedulerImpl.runTaskLaterAsync(runnable, delayTicks); } + + public static Task runTaskTimer(Runnable runnable, long delayTicks, long periodTicks) { return schedulerImpl.runTaskTimer(runnable, delayTicks, periodTicks); } + public static Task runTaskTimerAsync(Runnable runnable, long delayTicks, long periodTicks) { return schedulerImpl.runTaskTimerAsync(runnable, delayTicks, periodTicks); } - /** - * 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)); - } - } - - /** - * 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)); - } - } - - /** - * 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 { - return runTask(runnable); - } + if (entity == null || !entity.isValid()) return runTask(runnable); + return schedulerImpl.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 { - return runTaskLater(runnable, delayTicks); - } + if (entity == null || !entity.isValid()) return runTaskLater(runnable, delayTicks); + return delayTicks <= 0 ? schedulerImpl.runEntityTask(entity, runnable) : schedulerImpl.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 { - return runTaskTimer(runnable, delayTicks, periodTicks); - } + if (entity == null || !entity.isValid()) return runTaskTimer(runnable, delayTicks, periodTicks); + return schedulerImpl.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 { - return runTask(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 { - return runTask(runnable); - } + if (location == null || location.getWorld() == null) return runTask(runnable); + return schedulerImpl.runLocationTask(location, 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 { - return runTaskLater(runnable, delayTicks); - } + if (location == null || location.getWorld() == null) return runTaskLater(runnable, delayTicks); + return delayTicks <= 0 ? schedulerImpl.runLocationTask(location, runnable) : schedulerImpl.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 { - return runTaskTimer(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); - } - } - - /** - * 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); - } + if (location == null || location.getWorld() == null) return runTaskTimer(runnable, delayTicks, periodTicks); + return schedulerImpl.runLocationTaskTimer(location, runnable, delayTicks, periodTicks); } - /** - * 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<>(); - - 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); - } - }); - } - } catch (Throwable t) { - future.completeExceptionally(t); - } - - return future; - } - - /** - * 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<>(); - - try { - if (isFolia) { - Bukkit.getAsyncScheduler().runNow(plugin, task -> { - try { - future.complete(supplier.get()); - } catch (Throwable t) { - future.completeExceptionally(t); - plugin.getLogger().log(Level.SEVERE, "Error while executing async task", t); - } - }); - } else { - 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 t) { - future.completeExceptionally(t); - } - - 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; - } - - 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(); - } - } - } catch (Exception e) { - plugin.getLogger().log(Level.WARNING, "Failed to cancel task", e); - } - } - - /** - * Gets the underlying task object. - * - * @return The underlying task object - */ - public Object getTask() { - return task; - } - - /** - * Checks if this task is cancelled. - * - * @return true if the task is cancelled - */ - public boolean isCancelled() { - if (task == null) { - return true; - } - - 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(); - } - } - } catch (Exception ignored) { - // Task may have already been garbage collected or is invalid - } - - return true; + public static Task runChunkTask(World world, int chunkX, int chunkZ, Runnable runnable) { + if (world == null) return runTask(runnable); + return schedulerImpl.runChunkTask(world, chunkX, chunkZ, runnable); + } + + public static CompletableFuture supplySync(Supplier supplier) { return schedulerImpl.supplySync(supplier); } + public static CompletableFuture supplyAsync(Supplier supplier) { return schedulerImpl.supplyAsync(supplier); } + + // --- INTERFACES & WRAPPER --- + + public interface Task { + void cancel(); + boolean isCancelled(); + } + + private interface PlatformScheduler { + Task runTask(Runnable r); + Task runTaskAsync(Runnable r); + Task runTaskLater(Runnable r, long delay); + Task runTaskLaterAsync(Runnable r, long delay); + Task runTaskTimer(Runnable r, long delay, long period); + Task runTaskTimerAsync(Runnable r, long delay, long period); + Task runEntityTask(Entity e, Runnable r); + Task runEntityTaskLater(Entity e, Runnable r, long delay); + Task runEntityTaskTimer(Entity e, Runnable r, long delay, long period); + Task runLocationTask(Location l, Runnable r); + Task runLocationTaskLater(Location l, Runnable r, long delay); + Task runLocationTaskTimer(Location l, Runnable r, long delay, long period); + Task runChunkTask(World w, int cx, int cz, Runnable r); + CompletableFuture supplySync(Supplier s); + CompletableFuture supplyAsync(Supplier s); + } + + // --- BUKKIT IMPLEMENTIERUNG --- + + private static final class BukkitScheduler implements PlatformScheduler { + private Task wrap(BukkitTask 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 r) { return wrap(Bukkit.getScheduler().runTask(plugin, r)); } + @Override public Task runTaskAsync(Runnable r) { return wrap(Bukkit.getScheduler().runTaskAsynchronously(plugin, r)); } + @Override public Task runTaskLater(Runnable r, long d) { return wrap(Bukkit.getScheduler().runTaskLater(plugin, r, d)); } + @Override public Task runTaskLaterAsync(Runnable r, long d) { return wrap(Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, r, d)); } + @Override public Task runTaskTimer(Runnable r, long d, long p) { return wrap(Bukkit.getScheduler().runTaskTimer(plugin, r, d, p)); } + @Override public Task runTaskTimerAsync(Runnable r, long d, long p) { return wrap(Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, r, d, p)); } + @Override public Task runEntityTask(Entity e, Runnable r) { return runTask(r); } + @Override public Task runEntityTaskLater(Entity e, Runnable r, long d) { return runTaskLater(r, d); } + @Override public Task runEntityTaskTimer(Entity e, Runnable r, long d, long p) { return runTaskTimer(r, d, p); } + @Override public Task runLocationTask(Location l, Runnable r) { return runTask(r); } + @Override public Task runLocationTaskLater(Location l, Runnable r, long d) { return runTaskLater(r, d); } + @Override public Task runLocationTaskTimer(Location l, Runnable r, long d, long p) { return runTaskTimer(r, d, p); } + @Override public Task runChunkTask(World w, int cx, int cz, Runnable r) { return runTask(r); } + + @Override public CompletableFuture supplySync(Supplier s) { + CompletableFuture f = new CompletableFuture<>(); + Bukkit.getScheduler().runTask(plugin, () -> { try { f.complete(s.get()); } catch (Throwable t) { f.completeExceptionally(t); } }); + return f; + } + @Override public CompletableFuture supplyAsync(Supplier s) { + CompletableFuture f = new CompletableFuture<>(); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { try { f.complete(s.get()); } catch (Throwable t) { f.completeExceptionally(t); } }); + return f; + } + } + + // --- FOLIA IMPLEMENTIERUNG --- + + private static final class FoliaScheduler 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 r) { return wrap(Bukkit.getGlobalRegionScheduler().run(plugin, t -> r.run())); } + @Override public Task runTaskAsync(Runnable r) { return wrap(Bukkit.getAsyncScheduler().runNow(plugin, t -> r.run())); } + @Override public Task runTaskLater(Runnable r, long d) { return wrap(Bukkit.getGlobalRegionScheduler().runDelayed(plugin, t -> r.run(), d)); } + @Override public Task runTaskLaterAsync(Runnable r, long d) { return wrap(Bukkit.getAsyncScheduler().runDelayed(plugin, t -> r.run(), d * 50, TimeUnit.MILLISECONDS)); } + @Override public Task runTaskTimer(Runnable r, long d, long p) { return wrap(Bukkit.getGlobalRegionScheduler().runAtFixedRate(plugin, t -> r.run(), d, p)); } + @Override public Task runTaskTimerAsync(Runnable r, long d, long p) { return wrap(Bukkit.getAsyncScheduler().runAtFixedRate(plugin, t -> r.run(), d * 50, p * 50, TimeUnit.MILLISECONDS)); } + + @Override public Task runEntityTask(Entity e, Runnable r) { return wrap(e.getScheduler().run(plugin, t -> r.run(), null)); } + @Override public Task runEntityTaskLater(Entity e, Runnable r, long d) { return wrap(e.getScheduler().runDelayed(plugin, t -> r.run(), null, d)); } + @Override public Task runEntityTaskTimer(Entity e, Runnable r, long d, long p) { return wrap(e.getScheduler().runAtFixedRate(plugin, t -> r.run(), null, d, p)); } + + @Override public Task runLocationTask(Location l, Runnable r) { return wrap(Bukkit.getRegionScheduler().run(plugin, l, t -> r.run())); } + @Override public Task runLocationTaskLater(Location l, Runnable r, long d) { return wrap(Bukkit.getRegionScheduler().runDelayed(plugin, l, t -> r.run(), d)); } + @Override public Task runLocationTaskTimer(Location l, Runnable r, long d, long p) { return wrap(Bukkit.getRegionScheduler().runAtFixedRate(plugin, l, t -> r.run(), d, p)); } + + @Override public Task runChunkTask(World w, int cx, int cz, Runnable r) { return wrap(Bukkit.getRegionScheduler().run(plugin, w, cx, cz, t -> r.run())); } + + @Override public CompletableFuture supplySync(Supplier s) { + CompletableFuture f = new CompletableFuture<>(); + Bukkit.getGlobalRegionScheduler().run(plugin, t -> { try { f.complete(s.get()); } catch (Throwable t) { f.completeExceptionally(t); } }); + return f; + } + @Override public CompletableFuture supplyAsync(Supplier s) { + CompletableFuture f = new CompletableFuture<>(); + Bukkit.getAsyncScheduler().runNow(plugin, t -> { try { f.complete(s.get()); } catch (Throwable t) { f.completeExceptionally(t); } }); + return f; } } } 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..44917fda 100644 --- a/core/src/main/java/github/nighter/smartspawner/logging/SpawnerActionLogger.java +++ b/core/src/main/java/github/nighter/smartspawner/logging/SpawnerActionLogger.java @@ -13,7 +13,9 @@ 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; @@ -21,235 +23,167 @@ /** * Main logging interface for spawner actions. - * Handles asynchronous logging with file rotation and multiple output formats. + * Handles asynchronous logging with decoupled background file rotation. */ public class SpawnerActionLogger { private final SmartSpawner plugin; private final LoggingConfig config; private final Queue logQueue; private final AtomicBoolean isShuttingDown; - private Scheduler.Task logTask; - private DiscordWebhookLogger discordLogger; + private Scheduler.Task logTask; + private volatile DiscordWebhookLogger discordLogger; private File currentLogFile; - private static final ThreadLocal dateFormat = - ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); - + private long currentLogFileSize; + + // Thread-safe und modernere Alternative zu SimpleDateFormat + 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"); + 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); + + // Discord erst laden, wenn die Haupt-Config Logging überhaupt erlaubt + reloadDiscord(); } } - + /** * Logs a spawner action asynchronously. */ public void log(SpawnerLogEntry entry) { - if (!config.isEnabled() || config.isEventEnabled(entry.getEventType())) { + if (!config.isEnabled() || isShuttingDown.get()) { return; } - + + // FEHLERBEHEBUNG: Logge nur, wenn das Event in der Config auch AKTIVIERT ist + if (!config.isEventEnabled(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); + + // Discord-Logger via volatile-Feld thread-sicher abfragen + 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.isEventEnabled(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()) + + + String fileName = "spawner-" + LocalDate.now().format(FILE_DATE_FORMAT) + (config.isJsonFormat() ? ".json" : ".log"); + currentLogFile = logPath.resolve(fileName).toFile(); - // Perform log rotation if needed - rotateLogsIfNeeded(); + // Initialen Size-Check machen, um I/O im Loop zu verringern + currentLogFileSize = currentLogFile.exists() ? currentLogFile.length() : 0; + + // Altweltliche Log-Rotation beim Start asynchron triggern + Scheduler.runTaskAsync(this::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) + // Intervall auf 20 Ticks (1 Sekunde) herabgesetzt für flüssigeren I/O-Fluss logTask = Scheduler.runTaskTimerAsync(() -> { - if (isShuttingDown.get()) { - return; + if (!logQueue.isEmpty()) { + processLogQueue(); } - processLogQueue(); - }, 40L, 40L); + }, 20L, 20L); } - + private void processLogQueue() { - if (logQueue.isEmpty()) { - return; - } - List entries = new ArrayList<>(); SpawnerLogEntry entry; - while ((entry = logQueue.poll()) != null) { + + // Begrenze maximale Entnahmen pro Durchlauf, um Heap-Spikes zu verhindern + int drained = 0; + while ((entry = logQueue.poll()) != null && drained < 500) { entries.add(entry); + drained++; } - + if (!entries.isEmpty()) { writeLogEntries(entries); } } - + private void writeLogEntries(List entries) { - if (currentLogFile == null || entries.isEmpty()) { + if (currentLogFile == null) { 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(); + + // Speicher-Größe im RAM tracken anstatt bei jeder Zeile die Festplatte zu fragen + currentLogFileSize += logLine.length() + System.lineSeparator().length(); } writer.flush(); - - // Check if rotation is needed after writing - checkAndRotateLog(); + + // Wenn kritische Größe überschritten, I/O-schwere Rotation asynchron auslagern + long maxSizeBytes = (long) config.getMaxLogSizeMB() * 1024 * 1024; + if (currentLogFileSize > maxSizeBytes) { + Scheduler.runTaskAsync(this::rotateLog); + } } catch (IOException e) { plugin.getLogger().log(Level.WARNING, "Failed to write log entries", e); } } - - private void checkAndRotateLog() { - if (currentLogFile == null || !currentLogFile.exists()) { + + private synchronized void rotateLog() { + // Erneuter Check im synchronisierten Block, um doppelte Rotation zu verhindern + long maxSizeBytes = (long) config.getMaxLogSizeMB() * 1024 * 1024; + if (currentLogFile == null || !currentLogFile.exists() || currentLogFile.length() <= maxSizeBytes) { return; } - - long fileSizeBytes = currentLogFile.length(); - long maxSizeBytes = config.getMaxLogSizeMB() * 1024 * 1024; - - if (fileSizeBytes > maxSizeBytes) { - rotateLog(); - } - } - - private void rotateLog() { + try { - String timestamp = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(new Date()); + 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-" + 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); - } - } - - private void rotateLogsIfNeeded() { - try { - Path logPath = Paths.get(plugin.getDataFolder().getAbsolutePath(), config.getLogDirectory()); - - 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()) { - plugin.getLogger().info("Deleted old log file: " + logFiles[i].getName()); - } - } - } - } catch (Exception e) { - 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. - */ - public void reloadDiscord() { - DiscordWebhookConfig newDiscordConfig = new DiscordWebhookConfig(plugin); - 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); - } - } - /** - * Flushes remaining log entries and shuts down the logger. - */ - public void shutdown() { - isShuttingDown.set(true); - - if (logTask != null) { - logTask.cancel(); - } - - // Flush remaining entries - processLogQueue(); - - // Shutdown Discord logger - if (discordLogger != null) { - discordLogger.shutdown(); - } - } -} + String fileName = "spawner-" + LocalDate.now().format(FILE_DATE_FORMAT) + extension; + currentLogFile = logPath.resolve(fileName 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..98047a2c 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,31 @@ 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 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 final SmartSpawner plugin; private final FileConfiguration oldConfig; private final FileConfiguration newConfig; - private static final Gson gson = new Gson(); + private static final Gson GSON = new Gson(); public SpawnerDataConverter(SmartSpawner plugin, FileConfiguration oldConfig, FileConfiguration newConfig) { this.plugin = plugin; @@ -38,152 +41,132 @@ public void convertData() { 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); } } } 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); - } - } + String oldPath = "spawners." + spawnerId + "."; + + // Location einlesen + String worldName = oldConfig.getString(oldPath + "world"); + int x = oldConfig.getInt(oldPath + "x"); + int y = oldConfig.getInt(oldPath + "y"); + int z = oldConfig.getInt(oldPath + "z"); + + // Settings-String kompakt bauen + 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") + ); + + // Inventar konvertieren + 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 (String serialized : serializedItems) { + int colonIndex = serialized.indexOf(':'); + if (colonIndex == -1) continue; + + try { + ItemStack item = itemStackFromJson(serialized.substring(colonIndex + 1)); + if (item == null || item.getType().isAir()) continue; + + Material type = item.getType(); + + // Spezialfall: Tipped Arrows + 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().getKey().getKey(), + effect.getDuration(), + effect.getAmplifier()); + regularItems.merge(itemKey, item.getAmount(), Integer::sum); + } else { + regularItems.merge("ARROW", item.getAmount(), Integer::sum); } - } catch (Exception e) { - plugin.getLogger().warning("Failed to convert item in spawner " + spawnerId + ": " + e.getMessage()); + } + // Items mit Haltbarkeit (Werkzeuge, Waffen, Rüstungen) + else if (type.getMaxDurability() > 0) { + int durability = 0; + if (item.getItemMeta() instanceof Damageable damageable) { + durability = damageable.getDamage(); + } + durabilityItems.computeIfAbsent(type.name(), k -> new TreeMap<>()) + .merge(durability, item.getAmount(), Integer::sum); + } + // Normale Stack-Items + else { + regularItems.merge(type.name(), item.getAmount(), Integer::sum); } + } catch (Exception e) { + plugin.getLogger().warning("Failed to convert item in spawner " + spawnerId + ": " + e.getMessage()); } + } - // Add regular items to the output - for (Map.Entry entry : regularItems.entrySet()) { - newInventoryFormat.add(entry.getKey() + ":" + entry.getValue()); - } + // Normale Items ins neue Format schreiben + for (Map.Entry entry : regularItems.entrySet()) { + newInventoryFormat.add(entry.getKey() + ":" + entry.getValue()); + } - // 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; - } + // Durability Items sortiert hinzufügen + 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()); } + newInventoryFormat.add(itemString.toString()); } - - // 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); - - } catch (Exception e) { - plugin.getLogger().severe("Failed to convert spawner " + spawnerId + ": " + e.getMessage()); - throw e; } - } - - 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_"); + // Neues Daten-Layout sichern + 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) { + // Haltbarkeit modern setzen + if (json.has("durability") && meta instanceof Damageable damageable) { + damageable.setDamage(json.get("durability").getAsInt()); + } + if (json.has("displayName")) { meta.setDisplayName(json.get("displayName").getAsString()); } @@ -191,51 +174,4 @@ public static ItemStack itemStackFromJson(String data) { 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 (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 (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); - } - } - } - } - - item.setItemMeta(meta); - } - - return item; - } -} \ No newline at end of file + for (JsonElement