From dcde42026c80662b0d6036f9af237a59b3c16929 Mon Sep 17 00:00:00 2001 From: tdaue <62252661+tdaue@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:43:26 +0100 Subject: [PATCH] Add Folia support Route all scheduling through a small Bukkit/Folia bridge (oneblock.utils.Scheduler) that detects Folia at runtime and uses its global, region, entity and async schedulers via reflection, so the plugin still compiles against the Spigot API and runs on Java 8 / legacy servers with unchanged behavior. - Repeating tasks: block generation runs per player on the entity thread, then on the island's region thread; particles spawn on the island's region thread - Teleports use teleportAsync on Folia - /ob join builds the island on its region thread before teleporting the player - Island set/clear/scan and member kicks run on the owning region/entity thread - Console commands from rewards and command blocks run on the global thread - plugin.yml: folia-supported: true Co-Authored-By: Claude Opus 5.5 --- src/oneblock/CommandHandler.java | 57 ++++-- src/oneblock/OneBlock.java | 19 +- src/oneblock/RewardManager.java | 6 +- src/oneblock/events/BlockEvent.java | 3 +- src/oneblock/invitation/Invitation.java | 4 +- src/oneblock/placement/Place.java | 5 +- src/oneblock/tasks/IslandBlockGenTask.java | 61 ++++--- src/oneblock/tasks/IslandParticleTask.java | 12 +- .../tasks/PlayerCacheRefreshTask.java | 3 +- src/oneblock/utils/Scheduler.java | 171 ++++++++++++++++++ src/plugin.yml | 1 + 11 files changed, 274 insertions(+), 68 deletions(-) create mode 100644 src/oneblock/utils/Scheduler.java diff --git a/src/oneblock/CommandHandler.java b/src/oneblock/CommandHandler.java index d5f645a..fe7af1f 100644 --- a/src/oneblock/CommandHandler.java +++ b/src/oneblock/CommandHandler.java @@ -26,6 +26,7 @@ import oneblock.gui.GUI; import oneblock.invitation.Guest; import oneblock.invitation.Invitation; +import oneblock.utils.Scheduler; import oneblock.utils.Utils; import oneblock.worldguard.OBWorldGuard; @@ -76,15 +77,19 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String if (player == null) return false; UUID uuid = player.getUniqueId(); int X_pl = 0, Z_pl = 0; + Runnable newIsland = null; int plID = PlayerInfo.getId(uuid); if (plID == -1) { PlayerInfo inf = new PlayerInfo(uuid); plID = PlayerInfo.getFreeId(useEmptyIslands); int result[] = plugin.getIslandCoordinates(plID); X_pl = result[0]; Z_pl = result[1]; - if (plID != PlayerInfo.size()) - Island.clear(getWorld(), X_pl, getY(), Z_pl, getOffset()/4); - Island.place(getWorld(), X_pl, getY(), Z_pl); + final boolean reused = plID != PlayerInfo.size(); + final int X_isl = X_pl, Z_isl = Z_pl; + newIsland = () -> { + if (reused) Island.clear(getWorld(), X_isl, getY(), Z_isl, getOffset()/4); + Island.place(getWorld(), X_isl, getY(), Z_isl); + }; plugin.worldGuard.createRegion(uuid, X_pl, Z_pl, getOffset(), plID); PlayerInfo.set(plID, inf); if (!superlegacy) @@ -96,7 +101,13 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String } if (!plugin.enabled) plugin.runMainTask(); if (progress_bar) PlayerInfo.get(plID).bar.setVisible(true); - player.teleport(new Location(getWorld(), X_pl + 0.5, getY() + 1.2013, Z_pl + 0.5)); + final Location spawn = new Location(getWorld(), X_pl + 0.5, getY() + 1.2013, Z_pl + 0.5); + if (newIsland == null) Scheduler.teleport(player, spawn); + else { + // Build the island on its own region thread (Folia), then bring the player over. + final Runnable build = newIsland; + Scheduler.runAt(spawn, () -> { build.run(); Scheduler.teleport(player, spawn); }); + } if (OBWorldGuard.isEnabled()) plugin.worldGuard.addMember(uuid, plID); return true; } @@ -108,7 +119,7 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String sender.sendMessage(Messages.leave_not_set); return true; } - player.teleport(plugin.getLeave()); + Scheduler.teleport(player, plugin.getLeave()); return true; } case ("v"): @@ -141,7 +152,7 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String final int X_pl = result[0], Z_pl = result[1]; if (protection) Guest.list.add(new Guest(uuid, player.getUniqueId())); - player.teleport(new Location(getWorld(), X_pl + 0.5, getY() + 1.2013, Z_pl + 0.5)); + Scheduler.teleport(player, new Location(getWorld(), X_pl + 0.5, getY() + 1.2013, Z_pl + 0.5)); PlayerInfo.removeBarFor(player); return true; } @@ -212,13 +223,15 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String } if (!(member instanceof Player)) return true; Player member_ex = (Player) member; - int memberID = plugin.findNearestRegionId(member_ex.getLocation()); - if (memberID == ownerID) { - if (!member_ex.hasPermission("oneblock.set")) - member_ex.performCommand("ob j"); - info.removeBar(member_ex); - sender.sendMessage(member.getName() + Messages.kicked); - } + Scheduler.runFor(member_ex, () -> { + int memberID = plugin.findNearestRegionId(member_ex.getLocation()); + if (memberID == ownerID) { + if (!member_ex.hasPermission("oneblock.set")) + member_ex.performCommand("ob j"); + info.removeBar(member_ex); + sender.sendMessage(member_ex.getName() + Messages.kicked); + } + }); return true; } case ("accept"):{ @@ -266,7 +279,7 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String if (requirePermission(sender, "oneblock.set")) { config = YamlConfiguration.loadConfiguration(LegacyConfigSaver.file); // Loading the config.yml file before making changes. - Bukkit.getScheduler().runTaskLater(plugin, () -> { LegacyConfigSaver.Save(config); }, 2L); // Saving the config.yml file after making changes. + Scheduler.runLater(() -> { LegacyConfigSaver.Save(config); }, 2L); // Saving the config.yml file after making changes. switch (parametr) { case ("set"): { if (player == null && args.length < 6) { @@ -312,7 +325,8 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String if (!plugin.enabled) plugin.runMainTask(); - getWorld().getBlockAt(getX(), getY(), getZ()).setType(GRASS_BLOCK.get()); + Scheduler.runAt(new Location(getWorld(), getX(), getY(), getZ()), + () -> getWorld().getBlockAt(getX(), getY(), getZ()).setType(GRASS_BLOCK.get())); plugin.worldGuard.recreateRegions(); LegacyConfigSaver.Save(config); @@ -356,7 +370,7 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String // ReloadBorders is scheduled 2 ticks later so the fall-through // below can first persist `border=true/false` via the shared // bool-toggle body; the ordering is load-bearing. - Bukkit.getScheduler().runTaskLater(plugin, () -> { plugin.ReloadBorders(); }, 2L); + Scheduler.runLater(() -> { plugin.ReloadBorders(); }, 2L); // fall through to shared bool-toggle body: case ("circlemode"): case ("useemptyislands"): @@ -435,7 +449,8 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String if (progress_bar) inf.bar.setVisible(false); int result[] = plugin.getIslandCoordinates(id); - Island.clear(getWorld(), result[0], getY(), result[1], getOffset()/4); + Scheduler.runAt(new Location(getWorld(), result[0], getY(), result[1]), + () -> Island.clear(getWorld(), result[0], getY(), result[1], getOffset()/4)); sender.sendMessage(String.format("%splayer %s island is destroyed! :D", ChatColor.GREEN, args[1])); return true; } @@ -590,9 +605,11 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String UUID uuid = p.getUniqueId(); if (PlayerInfo.getId(uuid) != -1) { int result[] = plugin.getIslandCoordinates(PlayerInfo.getId(uuid)); - Island.scan(getWorld(), result[0], getY(), result[1]); - sender.sendMessage(ChatColor.GREEN + "A copy of your island has been successfully saved!"); - config.set("custom_island", Island.map()); + Scheduler.runAt(new Location(getWorld(), result[0], getY(), result[1]), () -> { + Island.scan(getWorld(), result[0], getY(), result[1]); + sender.sendMessage(ChatColor.GREEN + "A copy of your island has been successfully saved!"); + config.set("custom_island", Island.map()); + }); } else sender.sendMessage(ChatColor.RED + "You don't have an island!"); return true; diff --git a/src/oneblock/OneBlock.java b/src/oneblock/OneBlock.java index 04379eb..a77c0b5 100644 --- a/src/oneblock/OneBlock.java +++ b/src/oneblock/OneBlock.java @@ -145,6 +145,7 @@ public static String getBarTitle(Player p, int lvl) { @Override public void onEnable() { plugin = this; + Scheduler.init(this); GUI.legacy = !Utils.findMethod(SkullMeta.class, "setOwningPlayer"); final Metrics metrics = new Metrics(this, 14477); final PluginManager pluginManager = Bukkit.getPluginManager(); @@ -181,7 +182,7 @@ public void onEnable() { if (getOffset() == 0) return; - Bukkit.getScheduler().runTaskTimerAsynchronously(this, new WorldInitTask(this), 32, 80); + Scheduler.runTimerAsync(new WorldInitTask(this), 32, 80); } private Place.Type determinePlaceType(PluginManager pluginManager) { @@ -207,12 +208,12 @@ private void setupMetrics(Metrics metrics) { } public void runMainTask() { - Bukkit.getScheduler().cancelTasks(this); + Scheduler.cancelTasks(); if (getOffset() == 0) return; - Bukkit.getScheduler().runTaskTimerAsynchronously(this, new PlayerCacheRefreshTask(this), 0, 120); - Bukkit.getScheduler().runTaskTimerAsynchronously(this, new PlayerDataSaveTask(this), 200, 6000); - if (!superlegacy) Bukkit.getScheduler().runTaskTimerAsynchronously(this, new IslandParticleTask(this), 40, 40); - Bukkit.getScheduler().runTaskTimer(this, new IslandBlockGenTask(this), 40, 80); + Scheduler.runTimerAsync(new PlayerCacheRefreshTask(this), 0, 120); + Scheduler.runTimerAsync(new PlayerDataSaveTask(this), 200, 6000); + if (!superlegacy) Scheduler.runTimerAsync(new IslandParticleTask(this), 40, 40); + Scheduler.runTimer(new IslandBlockGenTask(this), 40, 80); enabled = true; if (OBWorldGuard.canUse && Bukkit.getPluginManager().isPluginEnabled("WorldGuard")) { @@ -293,7 +294,7 @@ public void UpdateBorderLocation(Player pl, Location loc) { public void UpdateBorder(final Player pl) { WorldBorder border = pl.getWorldBorder(); - Bukkit.getScheduler().runTaskLater(this, + Scheduler.runForLater(pl, () -> { pl.setWorldBorder(border); }, 10L); } @@ -301,8 +302,8 @@ public void ReloadBorders() { if (!isBorderSupported) return; World w = getWor(); if (w == null) return; - if (border) w.getPlayers().forEach(pl -> plugin.UpdateBorderLocation(pl, pl.getLocation())); - else w.getPlayers().forEach(pl -> pl.setWorldBorder(null)); + if (border) Scheduler.playersIn(w).forEach(pl -> Scheduler.runFor(pl, () -> plugin.UpdateBorderLocation(pl, pl.getLocation()))); + else Scheduler.playersIn(w).forEach(pl -> Scheduler.runFor(pl, () -> pl.setWorldBorder(null))); } public boolean isWithinIslandBounds(Location loc, int centerX, int centerZ) { diff --git a/src/oneblock/RewardManager.java b/src/oneblock/RewardManager.java index cab1548..743b534 100644 --- a/src/oneblock/RewardManager.java +++ b/src/oneblock/RewardManager.java @@ -4,6 +4,7 @@ import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; +import oneblock.utils.Scheduler; import oneblock.utils.Utils; import java.io.File; @@ -83,8 +84,9 @@ private void executeCommandList(Player player, List commands, Map Bukkit.dispatchCommand(Bukkit.getConsoleSender(), dispatched)); } } diff --git a/src/oneblock/events/BlockEvent.java b/src/oneblock/events/BlockEvent.java index 9030f23..3a6bf61 100644 --- a/src/oneblock/events/BlockEvent.java +++ b/src/oneblock/events/BlockEvent.java @@ -17,6 +17,7 @@ import org.bukkit.util.Vector; import oneblock.PlayerInfo; +import oneblock.utils.Scheduler; public class BlockEvent implements Listener { protected static final double DROP_TELEPORT_HEIGHT_OFFSET = 0.8; @@ -56,6 +57,6 @@ public void BlockBreak(final BlockBreakEvent e) { if (block.getX() != result[0]) return; if (block.getZ() != result[1]) return; - Bukkit.getScheduler().runTaskLater(plugin, () -> {plugin.BlockGen(result[0], result[1], plID, ponl, block);}, 1L); + Scheduler.runAtLater(block.getLocation(), () -> {plugin.BlockGen(result[0], result[1], plID, ponl, block);}, 1L); } } \ No newline at end of file diff --git a/src/oneblock/invitation/Invitation.java b/src/oneblock/invitation/Invitation.java index 659e7ce..07c8bff 100644 --- a/src/oneblock/invitation/Invitation.java +++ b/src/oneblock/invitation/Invitation.java @@ -8,6 +8,7 @@ import org.bukkit.entity.Player; import oneblock.CommandHandler; +import oneblock.utils.Scheduler; import oneblock.OneBlock; import oneblock.PlayerInfo; @@ -35,8 +36,7 @@ public static void add(UUID name, UUID to) { return; Invitation inv_ = new Invitation(name, to); Invitation.list.add(inv_); - Bukkit.getScheduler().runTaskLater(OneBlock.plugin, - () -> { Invitation.list.remove(inv_); }, 300L); + Scheduler.runLater(() -> { Invitation.list.remove(inv_); }, 300L); } public static Invitation check(UUID uuid) { diff --git a/src/oneblock/placement/Place.java b/src/oneblock/placement/Place.java index 5fc26c0..65946db 100644 --- a/src/oneblock/placement/Place.java +++ b/src/oneblock/placement/Place.java @@ -3,6 +3,8 @@ import org.bukkit.Bukkit; import org.bukkit.block.Block; +import oneblock.utils.Scheduler; + public abstract class Place { public enum Type @@ -49,7 +51,8 @@ public boolean executeCommand(Block block, String command, String playerName) { if (!playerName.isEmpty()) //if (playerName != null) dispatched = dispatched.replace("{player}", playerName); - Bukkit.dispatchCommand(Bukkit.getConsoleSender(), dispatched); + final String command_ = dispatched; + Scheduler.run(() -> Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command_)); return true; } } diff --git a/src/oneblock/tasks/IslandBlockGenTask.java b/src/oneblock/tasks/IslandBlockGenTask.java index 1bd9227..3fa3b98 100644 --- a/src/oneblock/tasks/IslandBlockGenTask.java +++ b/src/oneblock/tasks/IslandBlockGenTask.java @@ -12,6 +12,7 @@ import oneblock.OneBlock; import oneblock.PlayerInfo; import oneblock.invitation.Guest; +import oneblock.utils.Scheduler; /** * Main-thread block-generation pulse: every four seconds (80 ticks) it @@ -28,34 +29,40 @@ public final class IslandBlockGenTask implements Runnable { @Override public void run() { // SubBlockGen - for (Player player : plugin.cache.getPlayers()) { - final World wor = OneBlock.getWor(); - if (player.getWorld() != wor) continue; - final UUID uuid = player.getUniqueId(); - final int result[] = plugin.cache.getIslandCoordinates(player); - final int X_pl = result[0], Z_pl = result[1], plID = result[2]; - - if (OneBlock.protection && !player.hasPermission("Oneblock.ignoreBarrier")) { - boolean CheckGuest = false; - Location loc = player.getLocation(); - PlayerInfo inf = Guest.getPlayerInfo(uuid); - if (inf != null) { - int crd[] = plugin.getIslandCoordinates(PlayerInfo.getId(inf.uuid)); - CheckGuest = plugin.isWithinIslandBounds(loc, crd[0], crd[1]); - if (!CheckGuest) Guest.remove(uuid); - } - if (!plugin.isWithinIslandBounds(loc, X_pl, Z_pl) && !CheckGuest) { - player.performCommand("ob j"); - player.sendMessage(Messages.protection); - continue; - } + for (Player player : plugin.cache.getPlayers()) + Scheduler.runFor(player, () -> pulse(player)); + } + + private void pulse(Player player) { + final World wor = OneBlock.getWor(); + if (player.getWorld() != wor) return; + final UUID uuid = player.getUniqueId(); + final int result[] = plugin.cache.getIslandCoordinates(player); + final int X_pl = result[0], Z_pl = result[1], plID = result[2]; + + if (OneBlock.protection && !player.hasPermission("Oneblock.ignoreBarrier")) { + boolean CheckGuest = false; + Location loc = player.getLocation(); + PlayerInfo inf = Guest.getPlayerInfo(uuid); + if (inf != null) { + int crd[] = plugin.getIslandCoordinates(PlayerInfo.getId(inf.uuid)); + CheckGuest = plugin.isWithinIslandBounds(loc, crd[0], crd[1]); + if (!CheckGuest) Guest.remove(uuid); + } + if (!plugin.isWithinIslandBounds(loc, X_pl, Z_pl) && !CheckGuest) { + player.performCommand("ob j"); + player.sendMessage(Messages.protection); + return; } - - final Block block = wor.getBlockAt(X_pl, OneBlock.getY(), Z_pl); - if (block.getType() != Material.AIR) continue; - if (PlayerInfo.getId(uuid) == -1) continue; - - plugin.BlockGen(X_pl, Z_pl, plID, player, block); } + + // On Folia the island block may belong to another region than the player. + Scheduler.runAt(new Location(wor, X_pl, OneBlock.getY(), Z_pl), () -> { + final Block block = wor.getBlockAt(X_pl, OneBlock.getY(), Z_pl); + if (block.getType() != Material.AIR) return; + if (PlayerInfo.getId(uuid) == -1) return; + + plugin.BlockGen(X_pl, Z_pl, plID, player, block); + }); } } diff --git a/src/oneblock/tasks/IslandParticleTask.java b/src/oneblock/tasks/IslandParticleTask.java index fa2de34..9c9b181 100644 --- a/src/oneblock/tasks/IslandParticleTask.java +++ b/src/oneblock/tasks/IslandParticleTask.java @@ -6,6 +6,7 @@ import org.bukkit.entity.Player; import oneblock.OneBlock; +import oneblock.utils.Scheduler; public final class IslandParticleTask implements Runnable { private static final double[][] PARTICLE_OFFSETS = {{0, 0}, {0, 1}, {1, 0}, {1, 1}}; @@ -25,12 +26,13 @@ public void run() { if (result == null) continue; int X_pl = result[0], Z_pl = result[1]; double baseY = OneBlock.getY() + 0.5; - - for (double[] offset : PARTICLE_OFFSETS) { - Location loc = new Location(wor, X_pl + offset[0], baseY, Z_pl + offset[1]); - wor.spawnParticle(Particle.PORTAL, loc, PARTICLE_COUNT, 0, 0, 0, 0); - } + Scheduler.runAt(new Location(wor, X_pl, baseY, Z_pl), () -> { + for (double[] offset : PARTICLE_OFFSETS) { + Location loc = new Location(wor, X_pl + offset[0], baseY, Z_pl + offset[1]); + wor.spawnParticle(Particle.PORTAL, loc, PARTICLE_COUNT, 0, 0, 0, 0); + } + }); } } } diff --git a/src/oneblock/tasks/PlayerCacheRefreshTask.java b/src/oneblock/tasks/PlayerCacheRefreshTask.java index ce1fa43..782f385 100644 --- a/src/oneblock/tasks/PlayerCacheRefreshTask.java +++ b/src/oneblock/tasks/PlayerCacheRefreshTask.java @@ -3,6 +3,7 @@ import org.bukkit.World; import oneblock.OneBlock; +import oneblock.utils.Scheduler; /** * Async-scheduled refresh of {@link oneblock.PlayerCache} contents from @@ -18,6 +19,6 @@ public final class PlayerCacheRefreshTask implements Runnable { @Override public void run() { World w = OneBlock.getWorld(); - if (w != null) plugin.cache.updateCache(w.getPlayers()); + if (w != null) plugin.cache.updateCache(Scheduler.playersIn(w)); } } diff --git a/src/oneblock/utils/Scheduler.java b/src/oneblock/utils/Scheduler.java new file mode 100644 index 0000000..3450e47 --- /dev/null +++ b/src/oneblock/utils/Scheduler.java @@ -0,0 +1,171 @@ +package oneblock.utils; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +/** + * Bukkit / Folia scheduling bridge. + * + *

On Folia, world and entity state may only be touched from the region + * thread that owns it, so work is routed to the global, region or entity + * schedulers. The Folia API is reached through reflection so the plugin + * keeps compiling against the plain Spigot API and running on Java 8. + * + *

Everywhere else every method behaves exactly like the pre-Folia code: + * {@link #runAt} and {@link #runFor} run immediately on the calling thread, + * and the timed methods delegate to the {@code BukkitScheduler}. + */ +public final class Scheduler { + public static final boolean FOLIA = classExists("io.papermc.paper.threadedregions.RegionizedServer"); + private static final String PKG = "io.papermc.paper.threadedregions.scheduler."; + private static final long MS_PER_TICK = 50L; + + private static Plugin plugin; + private static Object global, async, region; + private static Method globalRun, globalRunDelayed, globalRunAtFixedRate, globalCancel; + private static Method asyncRunAtFixedRate, asyncCancel; + private static Method regionRun, regionRunDelayed; + private static Method entityGetScheduler, entityRun, entityRunDelayed, teleportAsync; + + private Scheduler() {} + + public static void init(Plugin pl) { + plugin = pl; + if (!FOLIA) return; + try { + Class g = Class.forName(PKG + "GlobalRegionScheduler"); + Class a = Class.forName(PKG + "AsyncScheduler"); + Class r = Class.forName(PKG + "RegionScheduler"); + Class e = Class.forName(PKG + "EntityScheduler"); + global = Bukkit.class.getMethod("getGlobalRegionScheduler").invoke(null); + async = Bukkit.class.getMethod("getAsyncScheduler").invoke(null); + region = Bukkit.class.getMethod("getRegionScheduler").invoke(null); + globalRun = g.getMethod("run", Plugin.class, Consumer.class); + globalRunDelayed = g.getMethod("runDelayed", Plugin.class, Consumer.class, long.class); + globalRunAtFixedRate = g.getMethod("runAtFixedRate", Plugin.class, Consumer.class, long.class, long.class); + globalCancel = g.getMethod("cancelTasks", Plugin.class); + asyncRunAtFixedRate = a.getMethod("runAtFixedRate", Plugin.class, Consumer.class, long.class, long.class, TimeUnit.class); + asyncCancel = a.getMethod("cancelTasks", Plugin.class); + regionRun = r.getMethod("run", Plugin.class, Location.class, Consumer.class); + regionRunDelayed = r.getMethod("runDelayed", Plugin.class, Location.class, Consumer.class, long.class); + entityGetScheduler = Entity.class.getMethod("getScheduler"); + entityRun = e.getMethod("run", Plugin.class, Consumer.class, Runnable.class); + entityRunDelayed = e.getMethod("runDelayed", Plugin.class, Consumer.class, Runnable.class, long.class); + teleportAsync = Entity.class.getMethod("teleportAsync", Location.class); + } catch (ReflectiveOperationException ex) { + throw new IllegalStateException("Folia detected, but its scheduler API could not be resolved", ex); + } + } + + /** Server-wide work (console commands, config). Folia: global region thread. */ + public static void run(Runnable task) { + if (FOLIA) call(globalRun, global, plugin, wrap(task)); + else task.run(); + } + + public static void runLater(Runnable task, long delay) { + if (FOLIA) call(globalRunDelayed, global, plugin, wrap(task), Math.max(1, delay)); + else Bukkit.getScheduler().runTaskLater(plugin, task, delay); + } + + public static void runTimer(Runnable task, long delay, long period) { + if (FOLIA) call(globalRunAtFixedRate, global, plugin, wrap(task), Math.max(1, delay), Math.max(1, period)); + else Bukkit.getScheduler().runTaskTimer(plugin, task, delay, period); + } + + public static void runTimerAsync(Runnable task, long delay, long period) { + if (FOLIA) call(asyncRunAtFixedRate, async, plugin, wrap(task), + Math.max(1, delay) * MS_PER_TICK, Math.max(1, period) * MS_PER_TICK, TimeUnit.MILLISECONDS); + else Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, task, delay, period); + } + + /** Work on blocks around {@code loc}. Folia: the region thread owning that location. */ + public static void runAt(Location loc, Runnable task) { + if (FOLIA) call(regionRun, region, plugin, loc, wrap(task)); + else task.run(); + } + + public static void runAtLater(Location loc, Runnable task, long delay) { + if (FOLIA) call(regionRunDelayed, region, plugin, loc, wrap(task), Math.max(1, delay)); + else Bukkit.getScheduler().runTaskLater(plugin, task, delay); + } + + /** Work on an entity. Folia: the entity's own thread; skipped if it was removed. */ + public static void runFor(Entity entity, Runnable task) { + if (FOLIA) call(entityRun, entityScheduler(entity), plugin, wrap(task), null); + else task.run(); + } + + public static void runForLater(Entity entity, Runnable task, long delay) { + if (FOLIA) call(entityRunDelayed, entityScheduler(entity), plugin, wrap(task), null, Math.max(1, delay)); + else Bukkit.getScheduler().runTaskLater(plugin, task, delay); + } + + /** Folia forbids synchronous teleports; {@code teleportAsync} is used there instead. */ + public static void teleport(Entity entity, Location loc) { + if (FOLIA) runFor(entity, () -> call(teleportAsync, entity, loc)); + else entity.teleport(loc); + } + + public static void cancelTasks() { + if (FOLIA) { + call(globalCancel, global, plugin); + call(asyncCancel, async, plugin); + } + else Bukkit.getScheduler().cancelTasks(plugin); + } + + /** Players in {@code world}, readable from any thread. */ + public static Collection playersIn(World world) { + if (!FOLIA) return world.getPlayers(); + List players = new ArrayList<>(); + for (Player p : Bukkit.getOnlinePlayers()) + if (world.equals(p.getWorld())) players.add(p); + return players; + } + + private static Object entityScheduler(Entity entity) { + try { + return entityGetScheduler.invoke(entity); + } catch (ReflectiveOperationException ex) { + throw new IllegalStateException(ex); + } + } + + private static Consumer wrap(Runnable task) { + return scheduledTask -> task.run(); + } + + private static Object call(Method method, Object target, Object... args) { + try { + return method.invoke(target, args); + } catch (InvocationTargetException ex) { + Throwable cause = ex.getCause(); + if (cause instanceof RuntimeException) throw (RuntimeException) cause; + throw new IllegalStateException(cause); + } catch (IllegalAccessException ex) { + throw new IllegalStateException(ex); + } + } + + private static boolean classExists(String name) { + try { + Class.forName(name); + return true; + } catch (ClassNotFoundException ex) { + return false; + } + } +} diff --git a/src/plugin.yml b/src/plugin.yml index 1602c27..2258a0f 100644 --- a/src/plugin.yml +++ b/src/plugin.yml @@ -2,6 +2,7 @@ name: Oneblock main: oneblock.OneBlock version: 1.6.3 api-version: 1.13 +folia-supported: true author: MrMarL loadbefore: ['Multiverse-Core'] softdepend: ['PlaceholderAPI', 'WorldGuard', 'ItemsAdder', 'Oraxen', 'Nexo', 'CraftEngine']