diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/BukkitRegionContainer.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/BukkitRegionContainer.java index 79d7b3446..6f4baec5e 100644 --- a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/BukkitRegionContainer.java +++ b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/BukkitRegionContainer.java @@ -29,6 +29,9 @@ import com.sk89q.worldguard.config.WorldConfiguration; import com.sk89q.worldguard.protection.managers.RegionManager; import com.sk89q.worldguard.protection.regions.RegionContainer; +import com.sk89q.worldguard.bukkit.util.task.SchedulerAdapter; +import com.sk89q.worldguard.bukkit.util.task.SchedulerAdapterFactory; +import com.sk89q.worldguard.bukkit.util.task.ScheduledTask; import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.event.EventHandler; @@ -51,6 +54,7 @@ public class BukkitRegionContainer extends RegionContainer { private static final int CACHE_INVALIDATION_INTERVAL = 2; private final WorldGuardPlugin plugin; + private ScheduledTask cacheInvalidationTask; /** * Create a new instance. @@ -94,11 +98,16 @@ public void onChunkUnload(ChunkUnloadEvent event) { } }, plugin); - Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, cache::invalidateAll, CACHE_INVALIDATION_INTERVAL, CACHE_INVALIDATION_INTERVAL); + SchedulerAdapter scheduler = SchedulerAdapterFactory.getAdapter(); + cacheInvalidationTask = scheduler.runTaskTimer(plugin, cache::invalidateAll, + CACHE_INVALIDATION_INTERVAL, CACHE_INVALIDATION_INTERVAL); } public void shutdown() { container.shutdown(); + if (cacheInvalidationTask != null) { + cacheInvalidationTask.cancel(); + } } @Override diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/WorldGuardPlugin.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/WorldGuardPlugin.java index 86aefb550..b89e71ba9 100644 --- a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/WorldGuardPlugin.java +++ b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/WorldGuardPlugin.java @@ -49,6 +49,9 @@ import com.sk89q.worldguard.bukkit.listener.RegionFlagsListener; import com.sk89q.worldguard.bukkit.listener.RegionProtectionListener; import com.sk89q.worldguard.bukkit.listener.WorldGuardBlockListener; +import com.sk89q.worldguard.bukkit.util.task.SchedulerAdapter; +import com.sk89q.worldguard.bukkit.util.task.SchedulerAdapterFactory; +import com.sk89q.worldguard.bukkit.util.task.ScheduledTask; import com.sk89q.worldguard.bukkit.listener.WorldGuardCommandBookListener; import com.sk89q.worldguard.bukkit.listener.WorldGuardEntityListener; import com.sk89q.worldguard.bukkit.listener.WorldGuardHangingListener; @@ -107,6 +110,8 @@ public class WorldGuardPlugin extends JavaPlugin { private static BukkitWorldGuardPlatform platform; private final CommandsManager commands; private PlayerMoveListener playerMoveListener; + private SchedulerAdapter scheduler; + private ScheduledTask sessionTask; private static final int BSTATS_PLUGIN_ID = 3283; @@ -145,6 +150,10 @@ public void onEnable() { getDataFolder().mkdirs(); // Need to create the plugins/WorldGuard folder + // Initialize scheduler adapter for Folia compatibility + scheduler = SchedulerAdapterFactory.getAdapter(this); + getLogger().info("Server platform: " + SchedulerAdapterFactory.getPlatformInfo()); + PermissionsResolverManager.initialize(this); WorldGuard.getInstance().setPlatform(platform = new BukkitWorldGuardPlatform()); // Initialise WorldGuard @@ -163,7 +172,7 @@ public void onEnable() { reg.register(GeneralCommands.class); } - getServer().getScheduler().scheduleSyncRepeatingTask(this, sessionManager, BukkitSessionManager.RUN_DELAY, BukkitSessionManager.RUN_DELAY); + sessionTask = scheduler.runTaskTimer(this, sessionManager, BukkitSessionManager.RUN_DELAY, BukkitSessionManager.RUN_DELAY); // Register events getServer().getPluginManager().registerEvents(sessionManager, this); @@ -204,7 +213,7 @@ public void onEnable() { } worldListener.registerEvents(); - Bukkit.getScheduler().runTask(this, () -> { + scheduler.runTask(this, () -> { for (Player player : Bukkit.getServer().getOnlinePlayers()) { ProcessPlayerEvent event = new ProcessPlayerEvent(player); Events.fire(event); @@ -264,7 +273,13 @@ private void setupCustomCharts(Metrics metrics) { @Override public void onDisable() { WorldGuard.getInstance().disable(); - this.getServer().getScheduler().cancelTasks(this); + if (scheduler != null) { + scheduler.cancelTasks(this); + // Properly shutdown fallback scheduler if it's being used + if (scheduler instanceof com.sk89q.worldguard.bukkit.util.task.FallbackSchedulerAdapter) { + ((com.sk89q.worldguard.bukkit.util.task.FallbackSchedulerAdapter) scheduler).shutdown(); + } + } } @Override diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/EventAbstractionListener.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/EventAbstractionListener.java index 22e0aa270..6dd212db2 100644 --- a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/EventAbstractionListener.java +++ b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/EventAbstractionListener.java @@ -44,6 +44,7 @@ import com.sk89q.worldguard.bukkit.util.Entities; import com.sk89q.worldguard.bukkit.util.Events; import com.sk89q.worldguard.bukkit.util.Materials; +import com.sk89q.worldguard.bukkit.util.task.SchedulerAdapterFactory; import com.sk89q.worldguard.config.WorldConfiguration; import com.sk89q.worldguard.protection.flags.Flags; import io.papermc.lib.PaperLib; @@ -1045,8 +1046,10 @@ public void onInventoryMoveItem(InventoryMoveItemEvent event) { } if (event.isCancelled() && causeHolder instanceof Hopper && wcfg.breakDeniedHoppers) { - Bukkit.getScheduler().scheduleSyncDelayedTask(getPlugin(), - () -> ((Hopper) causeHolder).getBlock().breakNaturally()); + // Use location-aware scheduler for Folia compatibility + Location hopperLocation = ((Hopper) causeHolder).getBlock().getLocation(); + SchedulerAdapterFactory.getAdapter().runTaskAtLater(getPlugin(), hopperLocation, + () -> ((Hopper) causeHolder).getBlock().breakNaturally(), 1); } else { entry.setCancelled(event.isCancelled()); } diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/PlayerMoveListener.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/PlayerMoveListener.java index 7b95a5743..a8c0ed0f5 100644 --- a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/PlayerMoveListener.java +++ b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/PlayerMoveListener.java @@ -23,6 +23,7 @@ import com.sk89q.worldguard.LocalPlayer; import com.sk89q.worldguard.WorldGuard; import com.sk89q.worldguard.bukkit.WorldGuardPlugin; +import com.sk89q.worldguard.bukkit.util.task.SchedulerAdapterFactory; import com.sk89q.worldguard.session.MoveType; import com.sk89q.worldguard.session.Session; import org.bukkit.Bukkit; @@ -127,7 +128,10 @@ public void onPlayerMove(PlayerMoveEvent event) { player.teleport(override.clone().add(0, 1, 0)); - Bukkit.getScheduler().runTaskLater(getPlugin(), () -> player.teleport(override.clone().add(0, 1, 0)), 1); + // Schedule a follow-up teleport using location-aware scheduler for Folia compatibility + Location teleportLoc = override.clone().add(0, 1, 0); + SchedulerAdapterFactory.getAdapter().runTaskAtLater(getPlugin(), teleportLoc, + () -> player.teleport(teleportLoc), 1); } } } diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/RegionProtectionListener.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/RegionProtectionListener.java index 346a227f2..40317a8e2 100644 --- a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/RegionProtectionListener.java +++ b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/RegionProtectionListener.java @@ -99,32 +99,13 @@ public RegionProtectionListener(WorldGuardPlugin plugin) { * @param what what was done */ private void tellErrorMessage(DelegateEvent event, Cause cause, Location location, String what) { - if (event.isSilent() || cause.isIndirect()) { - return; - } - - Object rootCause = cause.getRootCause(); - - if (rootCause instanceof Player) { - Player player = (Player) rootCause; - - long now = System.currentTimeMillis(); - Long lastTime = WGMetadata.getIfPresent(player, DENY_MESSAGE_KEY, Long.class); - if (lastTime == null || now - lastTime >= LAST_MESSAGE_DELAY) { - RegionQuery query = WorldGuard.getInstance().getPlatform().getRegionContainer().createQuery(); - LocalPlayer localPlayer = getPlugin().wrapPlayer(player); - String message = query.queryValue(BukkitAdapter.adapt(location), localPlayer, Flags.DENY_MESSAGE); - formatAndSendDenyMessage(what, localPlayer, message); - WGMetadata.put(player, DENY_MESSAGE_KEY, now); - } - } + // Messages disabled - no chat messages will be sent for protection violations + return; } static void formatAndSendDenyMessage(String what, LocalPlayer localPlayer, String message) { - if (message == null || message.isEmpty()) return; - message = WorldGuard.getInstance().getPlatform().getMatcher().replaceMacros(localPlayer, message); - message = CommandUtils.replaceColorMacros(message); - localPlayer.printRaw(message.replace("%what%", what)); + // Messages disabled - no denial messages will be sent + return; } /** diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/WorldGuardEntityListener.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/WorldGuardEntityListener.java index 080a68db5..1e4e89ce3 100644 --- a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/WorldGuardEntityListener.java +++ b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/WorldGuardEntityListener.java @@ -613,63 +613,108 @@ public void onExplosionPrime(ExplosionPrimeEvent event) { @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void onCreatureSpawn(CreatureSpawnEvent event) { - ConfigurationManager cfg = getConfig(); + try { + ConfigurationManager cfg = getConfig(); - if (cfg.activityHaltToggle) { - event.setCancelled(true); - return; + if (cfg.activityHaltToggle) { + event.setCancelled(true); + return; + } + + WorldConfiguration wcfg = getWorldConfig(event.getEntity().getWorld()); + + // allow spawning of creatures from plugins + if (!wcfg.blockPluginSpawning && Entities.isPluginSpawning(event.getSpawnReason())) { + return; + } + + // armor stands are living entities, but we check them as blocks/non-living entities, so ignore them here + if (Entities.isConsideredBuildingIfUsed(event.getEntity())) { + return; + } + + if (wcfg.allowTamedSpawns + && event.getEntity() instanceof Tameable // nullsafe check + && ((Tameable) event.getEntity()).isTamed()) { + return; + } + + EntityType entityType = event.getEntityType(); + + com.sk89q.worldedit.world.entity.EntityType weEntityType = BukkitAdapter.adapt(entityType); + + if (weEntityType != null && wcfg.blockCreatureSpawn.contains(weEntityType)) { + event.setCancelled(true); + return; + } + + Location eventLoc = event.getLocation(); + + if (wcfg.useRegions && cfg.useRegionsCreatureSpawnEvent) { + ApplicableRegionSet set = + WorldGuard.getInstance().getPlatform().getRegionContainer().createQuery().getApplicableRegions(BukkitAdapter.adapt(eventLoc)); + + if (!set.testState(null, Flags.MOB_SPAWNING)) { + event.setCancelled(true); + return; + } + + Set entityTypes = set.queryValue(null, Flags.DENY_SPAWN); + if (entityTypes != null && weEntityType != null && entityTypes.contains(weEntityType)) { + event.setCancelled(true); + return; + } + } + + if (wcfg.blockGroundSlimes && entityType == EntityType.SLIME + && eventLoc.getY() >= 60 + && event.getSpawnReason() == SpawnReason.NATURAL) { + event.setCancelled(true); + return; + } + } catch (Exception e) { + WorldGuard.logger.warning("[WorldGuard] Exception in creature spawn handler at " + + event.getLocation() + ": " + e.getMessage()); } + } + /** + * MONITOR-priority fallback for threaded servers (Canvas/Folia) where event + * cancellation from the HIGH handler may not be reliably respected due to + * async entity spawning. If the entity still spawned in a MOB_SPAWNING=DENY + * region, forcefully remove it. + */ + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onCreatureSpawnMonitor(CreatureSpawnEvent event) { WorldConfiguration wcfg = getWorldConfig(event.getEntity().getWorld()); + ConfigurationManager cfg = getConfig(); - // allow spawning of creatures from plugins + if (!wcfg.useRegions || !cfg.useRegionsCreatureSpawnEvent) { + return; + } if (!wcfg.blockPluginSpawning && Entities.isPluginSpawning(event.getSpawnReason())) { return; } - - // armor stands are living entities, but we check them as blocks/non-living entities, so ignore them here if (Entities.isConsideredBuildingIfUsed(event.getEntity())) { return; } - if (wcfg.allowTamedSpawns - && event.getEntity() instanceof Tameable // nullsafe check + && event.getEntity() instanceof Tameable && ((Tameable) event.getEntity()).isTamed()) { return; } - EntityType entityType = event.getEntityType(); - - com.sk89q.worldedit.world.entity.EntityType weEntityType = BukkitAdapter.adapt(entityType); - - if (weEntityType != null && wcfg.blockCreatureSpawn.contains(weEntityType)) { - event.setCancelled(true); - return; - } - - Location eventLoc = event.getLocation(); - - if (wcfg.useRegions && cfg.useRegionsCreatureSpawnEvent) { - ApplicableRegionSet set = - WorldGuard.getInstance().getPlatform().getRegionContainer().createQuery().getApplicableRegions(BukkitAdapter.adapt(eventLoc)); + try { + ApplicableRegionSet set = WorldGuard.getInstance().getPlatform().getRegionContainer() + .createQuery().getApplicableRegions(BukkitAdapter.adapt(event.getLocation())); if (!set.testState(null, Flags.MOB_SPAWNING)) { event.setCancelled(true); - return; + event.getEntity().remove(); } - - Set entityTypes = set.queryValue(null, Flags.DENY_SPAWN); - if (entityTypes != null && weEntityType != null && entityTypes.contains(weEntityType)) { - event.setCancelled(true); - return; - } - } - - if (wcfg.blockGroundSlimes && entityType == EntityType.SLIME - && eventLoc.getY() >= 60 - && event.getSpawnReason() == SpawnReason.NATURAL) { - event.setCancelled(true); - return; + } catch (Exception e) { + WorldGuard.logger.warning("[WorldGuard] Exception in creature spawn monitor handler at " + + event.getLocation() + ": " + e.getMessage()); } } diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/session/BukkitSessionManager.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/session/BukkitSessionManager.java index df589cbc0..69bf8311f 100644 --- a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/session/BukkitSessionManager.java +++ b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/session/BukkitSessionManager.java @@ -26,6 +26,7 @@ import com.sk89q.worldguard.bukkit.WorldGuardPlugin; import com.sk89q.worldguard.bukkit.event.player.ProcessPlayerEvent; import com.sk89q.worldguard.bukkit.util.Entities; +import com.sk89q.worldguard.bukkit.util.task.SchedulerAdapterFactory; import com.sk89q.worldguard.session.AbstractSessionManager; import com.sk89q.worldguard.session.Session; import org.bukkit.Bukkit; @@ -66,9 +67,22 @@ public void onPlayerProcess(ProcessPlayerEvent event) { @Override public void run() { - for (Player player : Bukkit.getServer().getOnlinePlayers()) { - LocalPlayer localPlayer = WorldGuardPlugin.inst().wrapPlayer(player); - get(localPlayer).tick(localPlayer); + WorldGuardPlugin plugin = WorldGuardPlugin.inst(); + if (SchedulerAdapterFactory.isFolia()) { + // On Folia, player.getLocation() and region access must happen on the + // player's own region thread, not the global region thread this timer + // runs on. Dispatch each player's tick to their entity scheduler. + for (Player player : Bukkit.getServer().getOnlinePlayers()) { + LocalPlayer localPlayer = plugin.wrapPlayer(player); + SchedulerAdapterFactory.getAdapter().runTaskFor(plugin, player, () -> { + get(localPlayer).tick(localPlayer); + }); + } + } else { + for (Player player : Bukkit.getServer().getOnlinePlayers()) { + LocalPlayer localPlayer = plugin.wrapPlayer(player); + get(localPlayer).tick(localPlayer); + } } } diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/util/task/BukkitSchedulerAdapter.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/util/task/BukkitSchedulerAdapter.java new file mode 100644 index 000000000..693079f09 --- /dev/null +++ b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/util/task/BukkitSchedulerAdapter.java @@ -0,0 +1,139 @@ +/* + * WorldGuard, a suite of tools for Minecraft + * Copyright (C) sk89q + * Copyright (C) WorldGuard team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldguard.bukkit.util.task; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitTask; + +/** + * Traditional Bukkit/Paper scheduler adapter implementation. + * + *

This implementation uses the classic BukkitScheduler API which works + * on single-threaded Paper and CraftBukkit servers.

+ */ +public class BukkitSchedulerAdapter implements SchedulerAdapter { + + @Override + public ScheduledTask runTaskTimer(Plugin plugin, Runnable task, long delay, long period) { + try { + BukkitTask bukkitTask = Bukkit.getScheduler().runTaskTimer(plugin, task, delay, period); + return new BukkitScheduledTask(bukkitTask); + } catch (UnsupportedOperationException e) { + throw new RuntimeException("BukkitScheduler is disabled on this server. Use FallbackSchedulerAdapter instead.", e); + } + } + + @Override + public ScheduledTask runTaskLater(Plugin plugin, Runnable task, long delay) { + try { + BukkitTask bukkitTask = Bukkit.getScheduler().runTaskLater(plugin, task, delay); + return new BukkitScheduledTask(bukkitTask); + } catch (UnsupportedOperationException e) { + throw new RuntimeException("BukkitScheduler is disabled on this server. Use FallbackSchedulerAdapter instead.", e); + } + } + + @Override + public ScheduledTask runTask(Plugin plugin, Runnable task) { + try { + BukkitTask bukkitTask = Bukkit.getScheduler().runTask(plugin, task); + return new BukkitScheduledTask(bukkitTask); + } catch (UnsupportedOperationException e) { + throw new RuntimeException("BukkitScheduler is disabled on this server. Use FallbackSchedulerAdapter instead.", e); + } + } + + @Override + public ScheduledTask runTaskAt(Plugin plugin, Location location, Runnable task) { + // For Bukkit/Paper, location doesn't matter - just run normally + return runTask(plugin, task); + } + + @Override + public ScheduledTask runTaskAtLater(Plugin plugin, Location location, Runnable task, long delay) { + // For Bukkit/Paper, location doesn't matter - just run normally + return runTaskLater(plugin, task, delay); + } + + @Override + public ScheduledTask runTaskFor(Plugin plugin, Entity entity, Runnable task) { + // For Bukkit/Paper, entity doesn't matter - just run normally + return runTask(plugin, task); + } + + @Override + public ScheduledTask runTaskForLater(Plugin plugin, Entity entity, Runnable task, long delay) { + // For Bukkit/Paper, entity doesn't matter - just run normally + return runTaskLater(plugin, task, delay); + } + + @Override + public ScheduledTask runTaskAsynchronously(Plugin plugin, Runnable task) { + try { + BukkitTask bukkitTask = Bukkit.getScheduler().runTaskAsynchronously(plugin, task); + return new BukkitScheduledTask(bukkitTask); + } catch (UnsupportedOperationException e) { + throw new RuntimeException("BukkitScheduler is disabled on this server. Use FallbackSchedulerAdapter instead.", e); + } + } + + @Override + public void cancelTasks(Plugin plugin) { + try { + Bukkit.getScheduler().cancelTasks(plugin); + } catch (UnsupportedOperationException e) { + // Ignore - scheduler is disabled, nothing to cancel + } + } + + @Override + public boolean isFolia() { + return false; + } + + /** + * Wrapper for BukkitTask to implement ScheduledTask interface. + */ + private static class BukkitScheduledTask implements ScheduledTask { + private final BukkitTask task; + + public BukkitScheduledTask(BukkitTask task) { + this.task = task; + } + + @Override + public void cancel() { + task.cancel(); + } + + @Override + public boolean isCancelled() { + return task.isCancelled(); + } + + @Override + public int getTaskId() { + return task.getTaskId(); + } + } +} \ No newline at end of file diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/util/task/FallbackSchedulerAdapter.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/util/task/FallbackSchedulerAdapter.java new file mode 100644 index 000000000..e392918ee --- /dev/null +++ b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/util/task/FallbackSchedulerAdapter.java @@ -0,0 +1,222 @@ +/* + * WorldGuard, a suite of tools for Minecraft + * Copyright (C) sk89q + * Copyright (C) WorldGuard team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldguard.bukkit.util.task; + +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; + +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Logger; + +/** + * Fallback scheduler adapter for servers where BukkitScheduler is disabled. + * + *

This implementation uses Java's built-in scheduling mechanisms instead + * of relying on the Bukkit scheduler API, making it compatible with servers + * like Canvas that disable the traditional scheduler.

+ */ +public class FallbackSchedulerAdapter implements SchedulerAdapter { + + private static final Logger LOGGER = Logger.getLogger(FallbackSchedulerAdapter.class.getName()); + + private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(2, + r -> new Thread(r, "WorldGuard-Fallback-Scheduler")); + + private final Timer timer = new Timer("WorldGuard-Fallback-Timer", true); + private final Map> scheduledTasks = new ConcurrentHashMap<>(); + private final Map timerTasks = new ConcurrentHashMap<>(); + private final AtomicLong taskIdGenerator = new AtomicLong(1); + + @Override + public ScheduledTask runTaskTimer(Plugin plugin, Runnable task, long delay, long period) { + long taskId = taskIdGenerator.getAndIncrement(); + + // Convert ticks to milliseconds (20 ticks = 1 second) + long delayMs = delay * 50; + long periodMs = period * 50; + + TimerTask timerTask = new TimerTask() { + @Override + public void run() { + try { + task.run(); + } catch (Exception e) { + LOGGER.warning("Exception in scheduled task: " + e.getMessage()); + } + } + }; + + timer.scheduleAtFixedRate(timerTask, delayMs, periodMs); + timerTasks.put(taskId, timerTask); + + return new FallbackScheduledTask(taskId, timerTask, null); + } + + @Override + public ScheduledTask runTaskLater(Plugin plugin, Runnable task, long delay) { + long taskId = taskIdGenerator.getAndIncrement(); + + // Convert ticks to milliseconds + long delayMs = delay * 50; + + ScheduledFuture future = executor.schedule(() -> { + try { + task.run(); + } catch (Exception e) { + LOGGER.warning("Exception in delayed task: " + e.getMessage()); + } finally { + scheduledTasks.remove(taskId); + } + }, delayMs, TimeUnit.MILLISECONDS); + + scheduledTasks.put(taskId, future); + return new FallbackScheduledTask(taskId, null, future); + } + + @Override + public ScheduledTask runTask(Plugin plugin, Runnable task) { + return runTaskLater(plugin, task, 1); // Run on next tick + } + + @Override + public ScheduledTask runTaskAt(Plugin plugin, Location location, Runnable task) { + // For fallback, location doesn't matter - just run normally + return runTask(plugin, task); + } + + @Override + public ScheduledTask runTaskAtLater(Plugin plugin, Location location, Runnable task, long delay) { + // For fallback, location doesn't matter - just run normally + return runTaskLater(plugin, task, delay); + } + + @Override + public ScheduledTask runTaskFor(Plugin plugin, Entity entity, Runnable task) { + // For fallback, entity doesn't matter - just run normally + return runTask(plugin, task); + } + + @Override + public ScheduledTask runTaskForLater(Plugin plugin, Entity entity, Runnable task, long delay) { + // For fallback, entity doesn't matter - just run normally + return runTaskLater(plugin, task, delay); + } + + @Override + public ScheduledTask runTaskAsynchronously(Plugin plugin, Runnable task) { + long taskId = taskIdGenerator.getAndIncrement(); + + ScheduledFuture future = executor.schedule(() -> { + try { + task.run(); + } catch (Exception e) { + LOGGER.warning("Exception in async task: " + e.getMessage()); + } finally { + scheduledTasks.remove(taskId); + } + }, 0, TimeUnit.MILLISECONDS); + + scheduledTasks.put(taskId, future); + return new FallbackScheduledTask(taskId, null, future); + } + + @Override + public void cancelTasks(Plugin plugin) { + // Cancel all scheduled tasks + scheduledTasks.values().forEach(future -> future.cancel(false)); + scheduledTasks.clear(); + + // Cancel all timer tasks + timerTasks.values().forEach(TimerTask::cancel); + timerTasks.clear(); + } + + @Override + public boolean isFolia() { + return false; + } + + /** + * Shutdown the fallback scheduler. + * This should be called when the plugin is disabled. + */ + public void shutdown() { + timer.cancel(); + executor.shutdown(); + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + /** + * Wrapper for fallback tasks to implement ScheduledTask interface. + */ + private class FallbackScheduledTask implements ScheduledTask { + private final long taskId; + private final TimerTask timerTask; + private final ScheduledFuture future; + private volatile boolean cancelled = false; + + public FallbackScheduledTask(long taskId, TimerTask timerTask, ScheduledFuture future) { + this.taskId = taskId; + this.timerTask = timerTask; + this.future = future; + } + + @Override + public void cancel() { + if (cancelled) return; + + cancelled = true; + + // Cancel the timer task if it exists + if (timerTask != null) { + timerTask.cancel(); + timerTasks.remove(taskId); + } + + // Cancel the scheduled future if it exists + if (future != null) { + future.cancel(false); + scheduledTasks.remove(taskId); + } + } + + @Override + public boolean isCancelled() { + return cancelled || + (future != null && future.isCancelled()); + } + } +} \ No newline at end of file diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/util/task/FoliaSchedulerAdapter.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/util/task/FoliaSchedulerAdapter.java new file mode 100644 index 000000000..77695f47f --- /dev/null +++ b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/util/task/FoliaSchedulerAdapter.java @@ -0,0 +1,306 @@ +/* + * WorldGuard, a suite of tools for Minecraft + * Copyright (C) sk89q + * Copyright (C) WorldGuard team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldguard.bukkit.util.task; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Folia scheduler adapter implementation using reflection. + * + *

This implementation uses Folia's region-based scheduling system. + * Different tasks are scheduled on different schedulers depending on their context.

+ */ +public class FoliaSchedulerAdapter implements SchedulerAdapter { + + private static final Logger LOGGER = Logger.getLogger(FoliaSchedulerAdapter.class.getName()); + + // Cache for reflection methods + private final Object globalRegionScheduler; + private final Object regionScheduler; + private final Object asyncScheduler; + + private final Method globalRunAtFixedRate; + private final Method globalRunDelayed; + private final Method globalRun; + private final Method globalCancelTasks; + + private final Method regionRun; + private final Method regionRunDelayed; + + private final Method entityRun; + private final Method entityRunDelayed; + + private final Method asyncRun; + + // Task tracking for cancellation + private final Map taskCounters = new ConcurrentHashMap<>(); + private final Map activeTasks = new ConcurrentHashMap<>(); + + @SuppressWarnings("unchecked") + public FoliaSchedulerAdapter() throws ReflectiveOperationException { + try { + // Get Folia schedulers + this.globalRegionScheduler = Bukkit.class.getMethod("getGlobalRegionScheduler").invoke(null); + this.regionScheduler = Bukkit.class.getMethod("getRegionScheduler").invoke(null); + this.asyncScheduler = Bukkit.class.getMethod("getAsyncScheduler").invoke(null); + + // Global scheduler methods - Folia uses Consumer, not Runnable + this.globalRunAtFixedRate = globalRegionScheduler.getClass().getMethod( + "runAtFixedRate", Plugin.class, Consumer.class, long.class, long.class); + this.globalRunDelayed = globalRegionScheduler.getClass().getMethod( + "runDelayed", Plugin.class, Consumer.class, long.class); + this.globalRun = globalRegionScheduler.getClass().getMethod("run", Plugin.class, Consumer.class); + this.globalCancelTasks = globalRegionScheduler.getClass().getMethod("cancelTasks", Plugin.class); + + // Region scheduler methods + this.regionRun = regionScheduler.getClass().getMethod( + "run", Plugin.class, Location.class, Consumer.class); + this.regionRunDelayed = regionScheduler.getClass().getMethod( + "runDelayed", Plugin.class, Location.class, Consumer.class, long.class); + + // Entity scheduler methods - Consumer for task, Runnable for retired callback + this.entityRun = Entity.class.getMethod("getScheduler") + .getReturnType().getMethod("run", Plugin.class, Consumer.class, Runnable.class); + this.entityRunDelayed = Entity.class.getMethod("getScheduler") + .getReturnType().getMethod("runDelayed", Plugin.class, Consumer.class, Runnable.class, long.class); + + // Async scheduler methods + this.asyncRun = asyncScheduler.getClass().getMethod( + "runNow", Plugin.class, Consumer.class); + + } catch (Exception e) { + throw new ReflectiveOperationException("Failed to initialize Folia scheduler adapter", e); + } + } + + @Override + public ScheduledTask runTaskTimer(Plugin plugin, Runnable task, long delay, long period) { + try { + Consumer consumer = t -> task.run(); + Object scheduledTask = globalRunAtFixedRate.invoke(globalRegionScheduler, plugin, consumer, delay, period); + return new FoliaScheduledTask(scheduledTask, generateTaskId(plugin)); + } catch (ReflectiveOperationException e) { + LOGGER.log(Level.SEVERE, "Failed to schedule repeating task", e); + throw new RuntimeException(e); + } + } + + @Override + public ScheduledTask runTaskLater(Plugin plugin, Runnable task, long delay) { + try { + Consumer consumer = t -> task.run(); + Object scheduledTask = globalRunDelayed.invoke(globalRegionScheduler, plugin, consumer, delay); + return new FoliaScheduledTask(scheduledTask, generateTaskId(plugin)); + } catch (ReflectiveOperationException e) { + LOGGER.log(Level.SEVERE, "Failed to schedule delayed task", e); + throw new RuntimeException(e); + } + } + + @Override + public ScheduledTask runTask(Plugin plugin, Runnable task) { + try { + Consumer consumer = t -> task.run(); + Object scheduledTask = globalRun.invoke(globalRegionScheduler, plugin, consumer); + return new FoliaScheduledTask(scheduledTask, generateTaskId(plugin)); + } catch (ReflectiveOperationException e) { + LOGGER.log(Level.SEVERE, "Failed to schedule task", e); + throw new RuntimeException(e); + } + } + + @Override + public ScheduledTask runTaskAt(Plugin plugin, Location location, Runnable task) { + try { + Consumer consumer = t -> task.run(); + Object scheduledTask = regionRun.invoke(regionScheduler, plugin, location, consumer); + return new FoliaScheduledTask(scheduledTask, generateTaskId(plugin)); + } catch (ReflectiveOperationException e) { + LOGGER.log(Level.SEVERE, "Failed to schedule location-based task", e); + throw new RuntimeException(e); + } + } + + @Override + public ScheduledTask runTaskAtLater(Plugin plugin, Location location, Runnable task, long delay) { + try { + Consumer consumer = t -> task.run(); + Object scheduledTask = regionRunDelayed.invoke(regionScheduler, plugin, location, consumer, delay); + return new FoliaScheduledTask(scheduledTask, generateTaskId(plugin)); + } catch (ReflectiveOperationException e) { + LOGGER.log(Level.SEVERE, "Failed to schedule delayed location-based task", e); + throw new RuntimeException(e); + } + } + + @Override + public ScheduledTask runTaskFor(Plugin plugin, Entity entity, Runnable task) { + try { + Consumer consumer = t -> task.run(); + Object entityScheduler = entity.getClass().getMethod("getScheduler").invoke(entity); + Object scheduledTask = entityRun.invoke(entityScheduler, plugin, consumer, null); + return new FoliaScheduledTask(scheduledTask, generateTaskId(plugin)); + } catch (ReflectiveOperationException e) { + LOGGER.log(Level.SEVERE, "Failed to schedule entity task", e); + throw new RuntimeException(e); + } + } + + @Override + public ScheduledTask runTaskForLater(Plugin plugin, Entity entity, Runnable task, long delay) { + try { + Consumer consumer = t -> task.run(); + Object entityScheduler = entity.getClass().getMethod("getScheduler").invoke(entity); + Object scheduledTask = entityRunDelayed.invoke(entityScheduler, plugin, consumer, null, delay); + return new FoliaScheduledTask(scheduledTask, generateTaskId(plugin)); + } catch (ReflectiveOperationException e) { + LOGGER.log(Level.SEVERE, "Failed to schedule delayed entity task", e); + throw new RuntimeException(e); + } + } + + @Override + public ScheduledTask runTaskAsynchronously(Plugin plugin, Runnable task) { + try { + Consumer consumer = t -> task.run(); + Object scheduledTask = asyncRun.invoke(asyncScheduler, plugin, consumer); + return new FoliaScheduledTask(scheduledTask, generateTaskId(plugin)); + } catch (ReflectiveOperationException e) { + LOGGER.log(Level.SEVERE, "Failed to schedule async task", e); + throw new RuntimeException(e); + } + } + + @Override + public void cancelTasks(Plugin plugin) { + try { + globalCancelTasks.invoke(globalRegionScheduler, plugin); + + // Also cancel tasks we're tracking + taskCounters.remove(plugin); + activeTasks.entrySet().removeIf(entry -> { + try { + Object task = entry.getValue(); + // Try to get the plugin from the task and compare + Method getPlugin = findInterfaceMethod(task, "getPlugin"); + Object taskPlugin = getPlugin.invoke(task); + if (plugin.equals(taskPlugin)) { + Method cancel = findInterfaceMethod(task, "cancel"); + cancel.invoke(task); + return true; + } + } catch (Exception e) { + // Ignore reflection errors during cleanup + } + return false; + }); + } catch (ReflectiveOperationException e) { + LOGGER.log(Level.SEVERE, "Failed to cancel tasks", e); + } + } + + @Override + public boolean isFolia() { + return true; + } + + private long generateTaskId(Plugin plugin) { + return taskCounters.computeIfAbsent(plugin, k -> new AtomicLong(1)).getAndIncrement(); + } + + /** + * Finds a method by name on the public interfaces implemented by the given object's class. + * This avoids {@link IllegalAccessException} when the concrete class is a non-public + * inner class inside a module that doesn't open itself for deep reflection (e.g. Folia internals). + */ + private static Method findInterfaceMethod(Object obj, String methodName, Class... paramTypes) throws NoSuchMethodException { + for (Class iface : obj.getClass().getInterfaces()) { + try { + return iface.getMethod(methodName, paramTypes); + } catch (NoSuchMethodException ignored) { + } + } + // Fallback to the concrete class (may throw IllegalAccessException at invoke time) + return obj.getClass().getMethod(methodName, paramTypes); + } + + /** + * Wrapper for Folia tasks to implement ScheduledTask interface. + */ + private class FoliaScheduledTask implements ScheduledTask { + private final Object task; + private final long taskId; + private volatile boolean cancelled = false; + + public FoliaScheduledTask(Object task, long taskId) { + this.task = task; + this.taskId = taskId; + activeTasks.put(taskId, task); + } + + @Override + public void cancel() { + if (cancelled) return; + + try { + Method cancel = findInterfaceMethod(task, "cancel"); + cancel.invoke(task); + cancelled = true; + activeTasks.remove(taskId); + } catch (ReflectiveOperationException e) { + LOGGER.log(Level.WARNING, "Failed to cancel Folia task", e); + } + } + + @Override + public boolean isCancelled() { + if (cancelled) return true; + + try { + Method isCancelled = findInterfaceMethod(task, "isCancelled"); + boolean result = (Boolean) isCancelled.invoke(task); + if (result) { + cancelled = true; + activeTasks.remove(taskId); + } + return result; + } catch (ReflectiveOperationException e) { + LOGGER.log(Level.WARNING, "Failed to check task cancellation status", e); + return cancelled; + } + } + + @Override + public int getTaskId() { + return (int) taskId; + } + } +} \ No newline at end of file diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/util/task/ScheduledTask.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/util/task/ScheduledTask.java new file mode 100644 index 000000000..256462ddc --- /dev/null +++ b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/util/task/ScheduledTask.java @@ -0,0 +1,50 @@ +/* + * WorldGuard, a suite of tools for Minecraft + * Copyright (C) sk89q + * Copyright (C) WorldGuard team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldguard.bukkit.util.task; + +/** + * Represents a scheduled task that can be cancelled. + * + *

This provides a unified interface to work with both Bukkit and Folia + * scheduled tasks.

+ */ +public interface ScheduledTask { + + /** + * Cancel this task. + */ + void cancel(); + + /** + * Check if this task has been cancelled. + * + * @return true if the task is cancelled, false otherwise + */ + boolean isCancelled(); + + /** + * Get the task ID, if available. + * + * @return the task ID, or -1 if not available + */ + default int getTaskId() { + return -1; + } +} \ No newline at end of file diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/util/task/SchedulerAdapter.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/util/task/SchedulerAdapter.java new file mode 100644 index 000000000..4d037278f --- /dev/null +++ b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/util/task/SchedulerAdapter.java @@ -0,0 +1,133 @@ +/* + * WorldGuard, a suite of tools for Minecraft + * Copyright (C) sk89q + * Copyright (C) WorldGuard team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldguard.bukkit.util.task; + +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; + +/** + * Scheduler adapter for cross-compatibility between Paper/Bukkit and Folia. + * + *

Folia introduces region-based threading which requires different scheduling + * approaches compared to the single-threaded Bukkit scheduler. This adapter + * provides a unified interface for both platforms.

+ */ +public interface SchedulerAdapter { + + /** + * Schedule a repeating task to run synchronously. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @param delay initial delay in ticks + * @param period repeat period in ticks + * @return task identifier that can be used to cancel the task + */ + ScheduledTask runTaskTimer(Plugin plugin, Runnable task, long delay, long period); + + /** + * Schedule a task to run synchronously after a delay. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @param delay delay in ticks + * @return task identifier that can be used to cancel the task + */ + ScheduledTask runTaskLater(Plugin plugin, Runnable task, long delay); + + /** + * Schedule a task to run synchronously on the next tick. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @return task identifier that can be used to cancel the task + */ + ScheduledTask runTask(Plugin plugin, Runnable task); + + /** + * Schedule a task to run synchronously at a specific location. + * For non-Folia implementations, this behaves the same as runTask. + * + * @param plugin the plugin scheduling the task + * @param location the location where the task should run + * @param task the task to run + * @return task identifier that can be used to cancel the task + */ + ScheduledTask runTaskAt(Plugin plugin, Location location, Runnable task); + + /** + * Schedule a task to run synchronously at a specific location after a delay. + * For non-Folia implementations, this behaves the same as runTaskLater. + * + * @param plugin the plugin scheduling the task + * @param location the location where the task should run + * @param task the task to run + * @param delay delay in ticks + * @return task identifier that can be used to cancel the task + */ + ScheduledTask runTaskAtLater(Plugin plugin, Location location, Runnable task, long delay); + + /** + * Schedule a task to run synchronously for a specific entity. + * For non-Folia implementations, this behaves the same as runTask. + * + * @param plugin the plugin scheduling the task + * @param entity the entity for which the task should run + * @param task the task to run + * @return task identifier that can be used to cancel the task + */ + ScheduledTask runTaskFor(Plugin plugin, Entity entity, Runnable task); + + /** + * Schedule a task to run synchronously for a specific entity after a delay. + * For non-Folia implementations, this behaves the same as runTaskLater. + * + * @param plugin the plugin scheduling the task + * @param entity the entity for which the task should run + * @param task the task to run + * @param delay delay in ticks + * @return task identifier that can be used to cancel the task + */ + ScheduledTask runTaskForLater(Plugin plugin, Entity entity, Runnable task, long delay); + + /** + * Schedule a task to run asynchronously. + * + * @param plugin the plugin scheduling the task + * @param task the task to run + * @return task identifier that can be used to cancel the task + */ + ScheduledTask runTaskAsynchronously(Plugin plugin, Runnable task); + + /** + * Cancel all tasks scheduled by the given plugin. + * + * @param plugin the plugin whose tasks should be cancelled + */ + void cancelTasks(Plugin plugin); + + /** + * Check if this adapter is running on Folia. + * + * @return true if running on Folia, false otherwise + */ + boolean isFolia(); +} \ No newline at end of file diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/util/task/SchedulerAdapterFactory.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/util/task/SchedulerAdapterFactory.java new file mode 100644 index 000000000..33a6a7c03 --- /dev/null +++ b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/util/task/SchedulerAdapterFactory.java @@ -0,0 +1,140 @@ +/* + * WorldGuard, a suite of tools for Minecraft + * Copyright (C) sk89q + * Copyright (C) WorldGuard team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldguard.bukkit.util.task; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Factory for creating the appropriate scheduler adapter based on the server platform. + * + *

This class detects whether the server is running on Folia (which requires + * region-aware scheduling) or traditional Bukkit/Paper (which uses the classic + * BukkitScheduler API).

+ */ +public class SchedulerAdapterFactory { + + private static final Logger LOGGER = Logger.getLogger(SchedulerAdapterFactory.class.getName()); + private static SchedulerAdapter cachedAdapter = null; + + /** + * Get the scheduler adapter appropriate for the current server platform. + * Must be called with a Plugin instance at least once (during plugin init) + * before the no-arg overload can be used. + * + * @param plugin the plugin instance used to test scheduling + * @return the scheduler adapter instance + */ + public static synchronized SchedulerAdapter getAdapter(Plugin plugin) { + if (cachedAdapter == null) { + cachedAdapter = createAdapter(plugin); + } + return cachedAdapter; + } + + /** + * Get the cached scheduler adapter. Requires that {@link #getAdapter(Plugin)} + * has already been called during plugin initialization. + * + * @return the cached scheduler adapter instance + * @throws IllegalStateException if the adapter has not been initialized yet + */ + public static synchronized SchedulerAdapter getAdapter() { + if (cachedAdapter == null) { + throw new IllegalStateException("SchedulerAdapterFactory has not been initialized. " + + "Call getAdapter(Plugin) first during plugin enable."); + } + return cachedAdapter; + } + + private static SchedulerAdapter createAdapter(Plugin plugin) { + if (isFolia()) { + try { + LOGGER.info("Detected Folia server - using region-aware scheduler"); + return new FoliaSchedulerAdapter(); + } catch (ReflectiveOperationException e) { + LOGGER.log(Level.WARNING, "Failed to initialize Folia scheduler adapter, falling back to Bukkit scheduler", e); + return createBukkitOrFallbackAdapter(plugin); + } + } else { + return createBukkitOrFallbackAdapter(plugin); + } + } + + private static SchedulerAdapter createBukkitOrFallbackAdapter(Plugin plugin) { + if (isBukkitSchedulerAvailable(plugin)) { + LOGGER.info("Detected Bukkit/Paper server - using traditional scheduler"); + return new BukkitSchedulerAdapter(); + } else { + LOGGER.warning("BukkitScheduler is disabled (Canvas server?) - using fallback scheduler"); + return new FallbackSchedulerAdapter(); + } + } + + /** + * Check if we're running on Folia. + * Checks for a Folia-specific class that does not exist on Canvas or Paper. + * + * @return true if running on Folia, false otherwise + */ + public static boolean isFolia() { + try { + Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + /** + * Check if the Bukkit scheduler is available and functional by actually + * attempting to schedule a task. Canvas servers have the scheduler object + * but throw UnsupportedOperationException when any task is scheduled. + * + * @param plugin the plugin instance to use for the test + * @return true if Bukkit scheduler works, false otherwise + */ + public static boolean isBukkitSchedulerAvailable(Plugin plugin) { + try { + org.bukkit.scheduler.BukkitTask task = Bukkit.getScheduler().runTask(plugin, () -> {}); + task.cancel(); + return true; + } catch (UnsupportedOperationException e) { + return false; + } catch (Exception e) { + return false; + } + } + + /** + * Get information about the detected platform. + * + * @return platform information string + */ + public static String getPlatformInfo() { + if (isFolia()) { + return "Folia (region-aware scheduling)"; + } + return "Bukkit/Paper or Canvas"; + } +} \ No newline at end of file diff --git a/worldguard-bukkit/src/main/resources/plugin.yml b/worldguard-bukkit/src/main/resources/plugin.yml index b0352a641..9c097508e 100644 --- a/worldguard-bukkit/src/main/resources/plugin.yml +++ b/worldguard-bukkit/src/main/resources/plugin.yml @@ -3,3 +3,5 @@ main: com.sk89q.worldguard.bukkit.WorldGuardPlugin version: "${internalVersion}" depend: [WorldEdit] api-version: "1.21.11" +folia-supported: true +description: "Protect your worlds and regions with a powerful set of tools. Supports both Paper/Bukkit and Folia servers."