From 014dea2dfa937d8ad66b2e98fef6558347152e47 Mon Sep 17 00:00:00 2001 From: VulpineFriend87 Date: Fri, 8 May 2026 23:23:17 +0200 Subject: [PATCH] feat: general implementation --- .../top/vulpine/simpleLobby/SimpleLobby.java | 19 +++ .../simpleLobby/command/SpawnCommand.java | 35 ++--- .../scheduler/BukkitSchedulerAdapter.java | 56 ++++++++ .../simpleLobby/scheduler/Cancellable.java | 13 ++ .../simpleLobby/scheduler/FoliaScheduler.java | 129 ++++++++++++++++++ .../scheduler/SchedulerAdapter.java | 27 ++++ .../simpleLobby/utils/ActionParser.java | 13 +- .../simpleLobby/utils/PlayerUtils.java | 2 +- 8 files changed, 266 insertions(+), 28 deletions(-) create mode 100644 src/main/java/top/vulpine/simpleLobby/scheduler/BukkitSchedulerAdapter.java create mode 100644 src/main/java/top/vulpine/simpleLobby/scheduler/Cancellable.java create mode 100644 src/main/java/top/vulpine/simpleLobby/scheduler/FoliaScheduler.java create mode 100644 src/main/java/top/vulpine/simpleLobby/scheduler/SchedulerAdapter.java diff --git a/src/main/java/top/vulpine/simpleLobby/SimpleLobby.java b/src/main/java/top/vulpine/simpleLobby/SimpleLobby.java index 15abde7..2ce45d3 100644 --- a/src/main/java/top/vulpine/simpleLobby/SimpleLobby.java +++ b/src/main/java/top/vulpine/simpleLobby/SimpleLobby.java @@ -5,6 +5,9 @@ import top.vulpine.simpleLobby.command.SpawnCommand; import top.vulpine.simpleLobby.listener.PlayerListener; import top.vulpine.simpleLobby.listener.WorldListener; +import top.vulpine.simpleLobby.scheduler.BukkitSchedulerAdapter; +import top.vulpine.simpleLobby.scheduler.FoliaScheduler; +import top.vulpine.simpleLobby.scheduler.SchedulerAdapter; import top.vulpine.simpleLobby.utils.ActionParser; import top.vulpine.simpleLobby.utils.logger.LogLevel; import top.vulpine.simpleLobby.utils.logger.Logger; @@ -17,6 +20,7 @@ public final class SimpleLobby extends JavaPlugin { private ActionParser actionParser; + private SchedulerAdapter scheduler; private static final int PLUGIN_ID = 28227; @@ -34,6 +38,17 @@ public void onEnable() { } Logger.init(logLevel); + boolean folia; + try { + Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); + folia = true; + } catch (ClassNotFoundException e) { + folia = false; + } + this.scheduler = folia ? new FoliaScheduler(this) : new BukkitSchedulerAdapter(this); + Logger.debug("Detected " + (folia ? "Folia" : "Bukkit/Spigot") + " server, using " + + scheduler.getClass().getSimpleName()); + String[] message = { "", "&f&l _____ &a&l__", @@ -74,4 +89,8 @@ public void onEnable() { public ActionParser getActionParser() { return actionParser; } + + public SchedulerAdapter scheduler() { + return scheduler; + } } diff --git a/src/main/java/top/vulpine/simpleLobby/command/SpawnCommand.java b/src/main/java/top/vulpine/simpleLobby/command/SpawnCommand.java index 9a237f3..419eb90 100644 --- a/src/main/java/top/vulpine/simpleLobby/command/SpawnCommand.java +++ b/src/main/java/top/vulpine/simpleLobby/command/SpawnCommand.java @@ -10,9 +10,8 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerMoveEvent; -import org.bukkit.scheduler.BukkitRunnable; -import org.bukkit.scheduler.BukkitTask; import top.vulpine.simpleLobby.SimpleLobby; +import top.vulpine.simpleLobby.scheduler.Cancellable; import top.vulpine.simpleLobby.utils.ActionParser; import top.vulpine.simpleLobby.utils.Colorize; import top.vulpine.simpleLobby.utils.PermissionChecker; @@ -29,7 +28,7 @@ public class SpawnCommand implements CommandExecutor, TabCompleter, Listener { private final SimpleLobby plugin; private final ActionParser actionParser; - private final Map tasks = new ConcurrentHashMap<>(); + private final Map tasks = new ConcurrentHashMap<>(); private final Map locations = new ConcurrentHashMap<>(); public SpawnCommand(SimpleLobby plugin) { @@ -76,30 +75,22 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String UUID uuid = player.getUniqueId(); locations.put(uuid, player.getLocation().clone()); - BukkitTask task = new BukkitRunnable() { + Cancellable task = plugin.scheduler().runEntityLater(player, () -> { + PlayerUtils.teleportPlayer(plugin, player); + tasks.remove(uuid); + locations.remove(uuid); - @Override - public void run() { - PlayerUtils.teleportPlayer(plugin, player); - tasks.remove(uuid); - locations.remove(uuid); - - List teleportActions = plugin.getConfig().getStringList("spawn.actions.teleported"); - actionParser.executeActions(teleportActions, player, 0, new HashMap<>()); - } - - }.runTaskLater(plugin, seconds * 20L); + List teleportActions = plugin.getConfig().getStringList("spawn.actions.teleported"); + actionParser.executeActions(teleportActions, player, 0, new HashMap<>()); + }, seconds * 20L); tasks.put(uuid, task); } else { - new BukkitRunnable() { - @Override - public void run() { - PlayerUtils.teleportPlayer(plugin, player); - } - }.runTaskLater(plugin, seconds * 20L); + plugin.scheduler().runEntityLater(player, + () -> PlayerUtils.teleportPlayer(plugin, player), + seconds * 20L); } @@ -134,7 +125,7 @@ public void onPlayerMove(PlayerMoveEvent event) { if (from.getBlockX() != to.getBlockX() || from.getBlockY() != to.getBlockY() || from.getBlockZ() != to.getBlockZ()) { - BukkitTask task = tasks.remove(uuid); + Cancellable task = tasks.remove(uuid); if (task != null) task.cancel(); locations.remove(uuid); diff --git a/src/main/java/top/vulpine/simpleLobby/scheduler/BukkitSchedulerAdapter.java b/src/main/java/top/vulpine/simpleLobby/scheduler/BukkitSchedulerAdapter.java new file mode 100644 index 0000000..3f22d98 --- /dev/null +++ b/src/main/java/top/vulpine/simpleLobby/scheduler/BukkitSchedulerAdapter.java @@ -0,0 +1,56 @@ +package top.vulpine.simpleLobby.scheduler; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitTask; + +public class BukkitSchedulerAdapter implements SchedulerAdapter { + + private final Plugin plugin; + + public BukkitSchedulerAdapter(Plugin plugin) { + this.plugin = plugin; + } + + @Override + public void runGlobal(Runnable task) { + if (Bukkit.isPrimaryThread()) { + task.run(); + } else { + Bukkit.getScheduler().runTask(plugin, task); + } + } + + @Override + public void runEntity(Entity entity, Runnable task) { + if (Bukkit.isPrimaryThread()) { + task.run(); + } else { + Bukkit.getScheduler().runTask(plugin, task); + } + } + + @Override + public Cancellable runEntityLater(Entity entity, Runnable task, long ticks) { + BukkitTask bt = Bukkit.getScheduler().runTaskLater(plugin, task, Math.max(1L, ticks)); + return bt::cancel; + } + + @Override + public Cancellable runGlobalLater(Runnable task, long ticks) { + BukkitTask bt = Bukkit.getScheduler().runTaskLater(plugin, task, Math.max(1L, ticks)); + return bt::cancel; + } + + @Override + public void teleport(Player player, Location location) { + if (Bukkit.isPrimaryThread()) { + player.teleport(location); + } else { + Bukkit.getScheduler().runTask(plugin, () -> player.teleport(location)); + } + } +} diff --git a/src/main/java/top/vulpine/simpleLobby/scheduler/Cancellable.java b/src/main/java/top/vulpine/simpleLobby/scheduler/Cancellable.java new file mode 100644 index 0000000..4e46d3a --- /dev/null +++ b/src/main/java/top/vulpine/simpleLobby/scheduler/Cancellable.java @@ -0,0 +1,13 @@ +package top.vulpine.simpleLobby.scheduler; + +/** + * Handle to a scheduled task that can be cancelled. + * Returned by {@link SchedulerAdapter} delayed-task methods. + */ +@FunctionalInterface +public interface Cancellable { + + Cancellable NOOP = () -> {}; + + void cancel(); +} diff --git a/src/main/java/top/vulpine/simpleLobby/scheduler/FoliaScheduler.java b/src/main/java/top/vulpine/simpleLobby/scheduler/FoliaScheduler.java new file mode 100644 index 0000000..4f31699 --- /dev/null +++ b/src/main/java/top/vulpine/simpleLobby/scheduler/FoliaScheduler.java @@ -0,0 +1,129 @@ +package top.vulpine.simpleLobby.scheduler; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.Method; +import java.util.function.Consumer; + +/** + * Folia implementation of {@link SchedulerAdapter}, using reflection so the plugin + * still compiles against and runs on plain Spigot. + * + * This class is only loaded when Folia is detected at startup, so the missing + * Paper/Folia classes never trigger a {@link NoClassDefFoundError} on Spigot. + */ +public class FoliaScheduler implements SchedulerAdapter { + + private final Plugin plugin; + + private final Method entityGetScheduler; + private final Method entitySchedulerRun; + private final Method entitySchedulerRunDelayed; + private final Method bukkitGetGlobalScheduler; + private final Method globalSchedulerExecute; + private final Method globalSchedulerRunDelayed; + private final Method scheduledTaskCancel; + private final Method playerTeleportAsync; + + public FoliaScheduler(Plugin plugin) { + this.plugin = plugin; + try { + this.entityGetScheduler = Entity.class.getMethod("getScheduler"); + + Class entitySchedulerCls = Class.forName( + "io.papermc.paper.threadedregions.scheduler.EntityScheduler"); + this.entitySchedulerRun = entitySchedulerCls.getMethod( + "run", Plugin.class, Consumer.class, Runnable.class); + this.entitySchedulerRunDelayed = entitySchedulerCls.getMethod( + "runDelayed", Plugin.class, Consumer.class, Runnable.class, long.class); + + this.bukkitGetGlobalScheduler = Bukkit.class.getMethod("getGlobalRegionScheduler"); + Class globalSchedulerCls = Class.forName( + "io.papermc.paper.threadedregions.scheduler.GlobalRegionScheduler"); + this.globalSchedulerExecute = globalSchedulerCls.getMethod( + "execute", Plugin.class, Runnable.class); + this.globalSchedulerRunDelayed = globalSchedulerCls.getMethod( + "runDelayed", Plugin.class, Consumer.class, long.class); + + Class scheduledTaskCls = Class.forName( + "io.papermc.paper.threadedregions.scheduler.ScheduledTask"); + this.scheduledTaskCancel = scheduledTaskCls.getMethod("cancel"); + + this.playerTeleportAsync = Player.class.getMethod("teleportAsync", Location.class); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Failed to bind Folia scheduler reflection", e); + } + } + + @Override + public void runGlobal(Runnable task) { + try { + Object globalSched = bukkitGetGlobalScheduler.invoke(null); + globalSchedulerExecute.invoke(globalSched, plugin, task); + } catch (ReflectiveOperationException e) { + plugin.getLogger().warning("FoliaScheduler#runGlobal failed: " + e.getMessage()); + } + } + + @Override + public void runEntity(Entity entity, Runnable task) { + try { + Object entSched = entityGetScheduler.invoke(entity); + Consumer consumer = ignored -> task.run(); + entitySchedulerRun.invoke(entSched, plugin, consumer, null); + } catch (ReflectiveOperationException e) { + plugin.getLogger().warning("FoliaScheduler#runEntity failed: " + e.getMessage()); + } + } + + @Override + public Cancellable runEntityLater(Entity entity, Runnable task, long ticks) { + try { + Object entSched = entityGetScheduler.invoke(entity); + Consumer consumer = ignored -> task.run(); + Object scheduledTask = entitySchedulerRunDelayed.invoke( + entSched, plugin, consumer, null, Math.max(1L, ticks)); + return cancellableOf(scheduledTask); + } catch (ReflectiveOperationException e) { + plugin.getLogger().warning("FoliaScheduler#runEntityLater failed: " + e.getMessage()); + return Cancellable.NOOP; + } + } + + @Override + public Cancellable runGlobalLater(Runnable task, long ticks) { + try { + Object globalSched = bukkitGetGlobalScheduler.invoke(null); + Consumer consumer = ignored -> task.run(); + Object scheduledTask = globalSchedulerRunDelayed.invoke( + globalSched, plugin, consumer, Math.max(1L, ticks)); + return cancellableOf(scheduledTask); + } catch (ReflectiveOperationException e) { + plugin.getLogger().warning("FoliaScheduler#runGlobalLater failed: " + e.getMessage()); + return Cancellable.NOOP; + } + } + + @Override + public void teleport(Player player, Location location) { + try { + playerTeleportAsync.invoke(player, location); + } catch (ReflectiveOperationException e) { + plugin.getLogger().warning("FoliaScheduler#teleport failed: " + e.getMessage()); + } + } + + private Cancellable cancellableOf(Object scheduledTask) { + if (scheduledTask == null) return Cancellable.NOOP; + return () -> { + try { + scheduledTaskCancel.invoke(scheduledTask); + } catch (ReflectiveOperationException ignored) { + } + }; + } +} diff --git a/src/main/java/top/vulpine/simpleLobby/scheduler/SchedulerAdapter.java b/src/main/java/top/vulpine/simpleLobby/scheduler/SchedulerAdapter.java new file mode 100644 index 0000000..dc9615e --- /dev/null +++ b/src/main/java/top/vulpine/simpleLobby/scheduler/SchedulerAdapter.java @@ -0,0 +1,27 @@ +package top.vulpine.simpleLobby.scheduler; + +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; + +/** + * Abstraction over the Bukkit and Folia schedulers. + * Implementations route work to the correct thread for the running server flavor. + */ +public interface SchedulerAdapter { + + /** Run on the global region (Folia) or the main thread (Bukkit). */ + void runGlobal(Runnable task); + + /** Run on the entity's region (Folia) or the main thread (Bukkit). */ + void runEntity(Entity entity, Runnable task); + + /** Schedule a task on the entity's region after the given tick delay. */ + Cancellable runEntityLater(Entity entity, Runnable task, long ticks); + + /** Schedule a task on the global region after the given tick delay. */ + Cancellable runGlobalLater(Runnable task, long ticks); + + /** Teleport a player in a way that is safe on both Bukkit and Folia. */ + void teleport(Player player, Location location); +} diff --git a/src/main/java/top/vulpine/simpleLobby/utils/ActionParser.java b/src/main/java/top/vulpine/simpleLobby/utils/ActionParser.java index 22febfb..849ab7c 100644 --- a/src/main/java/top/vulpine/simpleLobby/utils/ActionParser.java +++ b/src/main/java/top/vulpine/simpleLobby/utils/ActionParser.java @@ -123,9 +123,9 @@ private void executeCommand(String params, Player player) { String command = parts[1].trim().replace("%player%", player.getName()); if (target.equalsIgnoreCase("console")) { - Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command); + plugin.scheduler().runGlobal(() -> Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command)); } else if (target.equalsIgnoreCase("player")) { - player.performCommand(command); + plugin.scheduler().runEntity(player, () -> player.performCommand(command)); } } @@ -153,10 +153,10 @@ private void executeGamemode(String params, Player player) { if (target.equalsIgnoreCase("global")) { for (Player p : Bukkit.getOnlinePlayers()) { - p.setGameMode(gamemode); + plugin.scheduler().runEntity(p, () -> p.setGameMode(gamemode)); } } else if (target.equalsIgnoreCase("player")) { - player.setGameMode(gamemode); + plugin.scheduler().runEntity(player, () -> player.setGameMode(gamemode)); } } @@ -282,8 +282,11 @@ private void executeSound(String params, Player player) { private void executeDelay(String params, List actions, Player player, int nextIndex, Map placeholders) { int delay = Integer.parseInt(params.trim()); + long ticks = delay / 50L; - plugin.getServer().getScheduler().runTaskLater(plugin, () -> executeActions(actions, player, nextIndex, placeholders), delay / 50L); + plugin.scheduler().runEntityLater(player, + () -> executeActions(actions, player, nextIndex, placeholders), + ticks); } diff --git a/src/main/java/top/vulpine/simpleLobby/utils/PlayerUtils.java b/src/main/java/top/vulpine/simpleLobby/utils/PlayerUtils.java index 4e79372..64709e5 100644 --- a/src/main/java/top/vulpine/simpleLobby/utils/PlayerUtils.java +++ b/src/main/java/top/vulpine/simpleLobby/utils/PlayerUtils.java @@ -35,7 +35,7 @@ public static void teleportPlayer(SimpleLobby plugin, Player player) { float yaw = (float) config.getDouble("spawn.location.yaw"); float pitch = (float) config.getDouble("spawn.location.pitch"); - player.teleport(new Location(world, x, y, z, yaw, pitch)); + plugin.scheduler().teleport(player, new Location(world, x, y, z, yaw, pitch)); Logger.debug("Teleported player " + player.getName() + " to spawn at " + world + " (" + x + ", " + y + ", " + z + ")");