diff --git a/build.gradle b/build.gradle index a6912f9..e95e434 100644 --- a/build.gradle +++ b/build.gradle @@ -16,14 +16,14 @@ group 'sh.okx' version '3.15.2' java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } repositories { mavenCentral() maven { - url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' + url 'https://repo.papermc.io/repository/maven-public/' } maven { url 'https://repo.extendedclip.com/content/repositories/placeholderapi/' @@ -51,7 +51,7 @@ dependencies { implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.30' compileOnly 'org.jetbrains:annotations:22.0.0' - compileOnly 'org.spigotmc:spigot-api:1.20.1-R0.1-SNAPSHOT' + compileOnly 'io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT' compileOnly('com.github.Realizedd:TokenManager:3.2.4') { transitive = false } diff --git a/src/main/java/sh/okx/rankup/AutoRankup.java b/src/main/java/sh/okx/rankup/AutoRankup.java index 2c88dbd..bcdb0f8 100644 --- a/src/main/java/sh/okx/rankup/AutoRankup.java +++ b/src/main/java/sh/okx/rankup/AutoRankup.java @@ -4,11 +4,17 @@ import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import sh.okx.rankup.util.folia.FoliaScheduler; +import sh.okx.rankup.util.folia.TaskWrapper; @RequiredArgsConstructor -public class AutoRankup extends BukkitRunnable { +public class AutoRankup implements Runnable { private final RankupPlugin rankup; + private TaskWrapper task; + private BukkitTask bukkitTask; + @Override public void run() { if (rankup.error()) { @@ -26,4 +32,29 @@ public void run() { } } } + + public void runTaskTimer(RankupPlugin rankupPlugin, long delay, long period) { + if (FoliaScheduler.isFolia()) { + task = FoliaScheduler.getAsyncScheduler().runAtFixedRate(rankupPlugin, $ -> this.run(), delay, period); + } else { + bukkitTask = new BukkitRunnable() { + @Override + public void run() { + AutoRankup.this.run(); + } + }.runTaskTimer(rankupPlugin, delay, period); + } + } + + public boolean isCancelled() { + return task == null ? bukkitTask.isCancelled() : task.isCancelled(); + } + + public void cancel() { + if (task == null) { + bukkitTask.cancel(); + } else { + task.cancel(); + } + } } diff --git a/src/main/java/sh/okx/rankup/Metrics.java b/src/main/java/sh/okx/rankup/Metrics.java index c260714..60b0fa2 100644 --- a/src/main/java/sh/okx/rankup/Metrics.java +++ b/src/main/java/sh/okx/rankup/Metrics.java @@ -3,12 +3,14 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import lombok.Getter; import org.bukkit.Bukkit; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.ServicePriority; +import sh.okx.rankup.util.folia.FoliaScheduler; import javax.net.ssl.HttpsURLConnection; import java.io.*; @@ -50,7 +52,8 @@ public class Metrics { private static final String URL = "https://bStats.org/submitData/bukkit"; // Is bStats enabled on this server? - private boolean enabled; + @Getter + private final boolean enabled; // Should failed requests be logged? private static boolean logFailedRequests; @@ -138,15 +141,6 @@ public Metrics(Plugin plugin) { } } - /** - * Checks if bStats is enabled. - * - * @return Whether bStats is enabled or not. - */ - public boolean isEnabled() { - return enabled; - } - /** * Adds a custom chart. * @@ -163,19 +157,30 @@ public void addCustomChart(CustomChart chart) { * Starts the Scheduler which submits our data every 30 minutes. */ private void startSubmitting() { - final Timer timer = new Timer(true); // We use a timer cause the Bukkit scheduler is affected by server lags - timer.scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - if (!plugin.isEnabled()) { // Plugin was disabled - timer.cancel(); - return; + if (FoliaScheduler.isFolia()) { + FoliaScheduler.getGlobalRegionScheduler().runAtFixedRate(plugin, + (ignored) -> { + if (!plugin.isEnabled()) { // Plugin was disabled + return; + } + submitData(); + }, 20L * 60 * 5, 20L * 60 * 30 + ); + } else { + final Timer timer = new Timer(true); // We use a timer cause the Bukkit scheduler is affected by server lags + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + if (!plugin.isEnabled()) { // Plugin was disabled + timer.cancel(); + return; + } + // Nevertheless we want our code to run in the Bukkit main thread, so we have to use the Bukkit scheduler + // Don't be afraid! The connection to the bStats server is still async, only the stats collection is sync ;) + Bukkit.getScheduler().runTask(plugin, () -> submitData()); } - // Nevertheless we want our code to run in the Bukkit main thread, so we have to use the Bukkit scheduler - // Don't be afraid! The connection to the bStats server is still async, only the stats collection is sync ;) - Bukkit.getScheduler().runTask(plugin, () -> submitData()); - } - }, 1000 * 60 * 5, 1000 * 60 * 30); + }, 1000 * 60 * 5, 1000 * 60 * 30); + } // Submit the data every 30 minutes, first time after 5 minutes to give other plugins enough time to start // WARNING: Changing the frequency has no effect but your plugin WILL be blocked/deleted! // WARNING: Just don't do it! @@ -257,7 +262,7 @@ private JsonObject getServerData() { } /** - * Collects the data and sends it afterwards. + * Collects the data and sends it afterward. */ private void submitData() { final JsonObject data = getServerData(); @@ -280,7 +285,7 @@ private void submitData() { Method jsonStringGetter = jsonObjectJsonSimple.getDeclaredMethod("toJSONString"); jsonStringGetter.setAccessible(true); String jsonString = (String) jsonStringGetter.invoke(plugin); - JsonObject object = new JsonParser().parse(jsonString).getAsJsonObject(); + JsonObject object = JsonParser.parseString(jsonString).getAsJsonObject(); pluginData.add(object); } } catch (ClassNotFoundException e) { @@ -330,7 +335,7 @@ private static void sendData(Plugin plugin, JsonObject data) throws Exception { throw new IllegalAccessException("This method must not be called from the main thread!"); } if (logSentData) { - plugin.getLogger().info("Sending data to bStats: " + data.toString()); + plugin.getLogger().info("Sending data to bStats: " + data); } HttpsURLConnection connection = (HttpsURLConnection) new URL(URL).openConnection(); diff --git a/src/main/java/sh/okx/rankup/RankupPlugin.java b/src/main/java/sh/okx/rankup/RankupPlugin.java index d54ff24..4fd8296 100644 --- a/src/main/java/sh/okx/rankup/RankupPlugin.java +++ b/src/main/java/sh/okx/rankup/RankupPlugin.java @@ -82,6 +82,7 @@ import sh.okx.rankup.serialization.YamlDeserializer; import sh.okx.rankup.util.UpdateNotifier; import sh.okx.rankup.util.VersionChecker; +import sh.okx.rankup.util.folia.FoliaScheduler; import java.io.File; import java.io.FileNotFoundException; @@ -280,15 +281,18 @@ private void addAllRequirements(Map map, RankList { + final Runnable runnable = () -> { refreshRanks(); error(); - }); + }; + + if (FoliaScheduler.isFolia()) FoliaScheduler.getGlobalRegionScheduler().execute(this, runnable); + else Bukkit.getScheduler().runTask(this, runnable); } else { refreshRanks(); } @@ -317,16 +324,12 @@ public void refreshRanks() { if (config.getBoolean("prestige")) { prestiges = new Prestiges(this, loadConfig("prestiges.yml")); -// prestiges.getOrderedList(); } else { prestiges = null; } rankups = new Rankups(this, loadRankupConfig("rankups")); // check rankups are not in an infinite loop -// rankups.getOrderedList(); - - } catch (RuntimeException e) { this.errorMessage = e.getClass().getName() + ": " + e.getMessage(); e.printStackTrace(); diff --git a/src/main/java/sh/okx/rankup/commands/InfoCommand.java b/src/main/java/sh/okx/rankup/commands/InfoCommand.java index 279fee9..b45619d 100644 --- a/src/main/java/sh/okx/rankup/commands/InfoCommand.java +++ b/src/main/java/sh/okx/rankup/commands/InfoCommand.java @@ -319,11 +319,14 @@ public boolean onCommand(CommandSender sender, Command command, String label, St sender.sendMessage( ChatColor.GREEN + "" + ChatColor.BOLD + description.getName() + " " + version + ChatColor.YELLOW + " by " + ChatColor.BLUE + ChatColor.BOLD + String.join(", ", description.getAuthors())); + if (sender.hasPermission("rankup.reload")) { sender.sendMessage(ChatColor.GREEN + "/" + label + " reload " + ChatColor.YELLOW + "Reloads configuration files."); } + if (sender.hasPermission("rankup.force")) { sender.sendMessage(ChatColor.GREEN + "/" + label + " forcerankup " + ChatColor.YELLOW + "Force a player to rankup, bypassing requirements."); + if (plugin.getPrestiges() != null) { sender.sendMessage( ChatColor.GREEN + "/" + label + " forceprestige " + ChatColor.YELLOW @@ -331,6 +334,7 @@ public boolean onCommand(CommandSender sender, Command command, String label, St } sender.sendMessage(ChatColor.GREEN + "/" + label + " rankdown " + ChatColor.YELLOW + "Force a player to move down one rank."); } + if (sender.hasPermission("rankup.playtime")) { sender.sendMessage(ChatColor.GREEN + "/" + label + " playtime " + ChatColor.YELLOW + "View your playtime"); } diff --git a/src/main/java/sh/okx/rankup/gui/GuiListener.java b/src/main/java/sh/okx/rankup/gui/GuiListener.java index 841f660..97e6e63 100644 --- a/src/main/java/sh/okx/rankup/gui/GuiListener.java +++ b/src/main/java/sh/okx/rankup/gui/GuiListener.java @@ -8,6 +8,7 @@ import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.inventory.Inventory; import sh.okx.rankup.RankupPlugin; +import sh.okx.rankup.util.folia.FoliaScheduler; @RequiredArgsConstructor public class GuiListener implements Listener { @@ -27,19 +28,22 @@ public void on(InventoryClickEvent e) { Gui gui = (Gui) inventory.getHolder(); if (gui.getRankup().isSimilar(e.getCurrentItem())) { - Bukkit.getScheduler().runTask(plugin, player::closeInventory); + if (FoliaScheduler.isFolia()) FoliaScheduler.getEntityScheduler().run(player, plugin, $ -> player.closeInventory(), null); + else Bukkit.getScheduler().runTask(plugin, () -> player.closeInventory()); if (gui.isPrestige()) { plugin.getHelper().prestige(player); } else { plugin.getHelper().rankup(player); } } else if (gui.getCancel().isSimilar(e.getCurrentItem())) { - Bukkit.getScheduler().runTask(plugin, () -> { + final Runnable runnable = () -> { player.closeInventory(); if (gui.isReturnToRanksGui()) { Bukkit.dispatchCommand(player, "ranks"); } - }); + }; + if (FoliaScheduler.isFolia()) FoliaScheduler.getEntityScheduler().run(player, plugin, $ -> runnable.run(), null); + else Bukkit.getScheduler().runTask(plugin, runnable); } } } diff --git a/src/main/java/sh/okx/rankup/ranks/Rank.java b/src/main/java/sh/okx/rankup/ranks/Rank.java index 9fed31e..efb3ccd 100644 --- a/src/main/java/sh/okx/rankup/ranks/Rank.java +++ b/src/main/java/sh/okx/rankup/ranks/Rank.java @@ -11,6 +11,7 @@ import sh.okx.rankup.RankupPlugin; import sh.okx.rankup.ranks.requirements.RankRequirements; import sh.okx.rankup.requirements.Requirement; +import sh.okx.rankup.util.folia.FoliaScheduler; @EqualsAndHashCode @RequiredArgsConstructor(access = AccessLevel.PROTECTED) @@ -46,10 +47,15 @@ public void applyRequirements(Player player) { } public void runCommands(Player player, Rank next) { - for (String command : commands) { - String string = plugin.newMessageBuilder(command).replacePlayer(player).replaceOldRank(this).replaceRank(next).toString(player); - Bukkit.dispatchCommand(Bukkit.getConsoleSender(), string); - } + final Runnable runnable = () -> { + for (String command : commands) { + String string = plugin.newMessageBuilder(command).replacePlayer(player).replaceOldRank(this).replaceRank(next).toString(player); + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), string); + } + }; + + if (FoliaScheduler.isFolia()) FoliaScheduler.getGlobalRegionScheduler().run(plugin, $ -> runnable.run()); + else runnable.run(); } @Override diff --git a/src/main/java/sh/okx/rankup/ranksgui/RanksGui.java b/src/main/java/sh/okx/rankup/ranksgui/RanksGui.java index c792865..2e73ac5 100644 --- a/src/main/java/sh/okx/rankup/ranksgui/RanksGui.java +++ b/src/main/java/sh/okx/rankup/ranksgui/RanksGui.java @@ -13,6 +13,7 @@ import sh.okx.rankup.ranks.Rank; import sh.okx.rankup.ranks.RankElement; import sh.okx.rankup.util.Colour; +import sh.okx.rankup.util.folia.FoliaScheduler; public class RanksGui { private final RankupPlugin plugin; @@ -111,10 +112,12 @@ public void click(InventoryClickEvent event) { } int slot = event.getRawSlot(); if (slot == rankupSlot) { - Bukkit.getScheduler().runTask(plugin, () -> { + final Runnable runnable = () -> { player.closeInventory(); Bukkit.dispatchCommand(player, "rankup gui"); - }); + }; + if (FoliaScheduler.isFolia()) FoliaScheduler.getEntityScheduler().run(player, plugin, $ -> runnable.run(), null); + else Bukkit.getScheduler().runTask(plugin, runnable); } } diff --git a/src/main/java/sh/okx/rankup/util/VersionChecker.java b/src/main/java/sh/okx/rankup/util/VersionChecker.java index e4d5692..520d18a 100644 --- a/src/main/java/sh/okx/rankup/util/VersionChecker.java +++ b/src/main/java/sh/okx/rankup/util/VersionChecker.java @@ -5,12 +5,15 @@ import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; +import lombok.Getter; import org.bukkit.Bukkit; import org.bukkit.plugin.Plugin; +import sh.okx.rankup.util.folia.FoliaScheduler; public class VersionChecker { public static final int RESOURCE_ID = 76964; + @Getter private final Plugin plugin; private final String currentVersion; private String latestVersion; @@ -21,10 +24,6 @@ public VersionChecker(Plugin plugin) { this.plugin = plugin; } - public Plugin getPlugin() { - return plugin; - } - /** * Checks if the version checker has already made an asynchronous call to the web server to check * the version, so future checks will run instantly. @@ -48,7 +47,11 @@ public void checkVersion(VersionCheckerCallback callback) { checked = true; callback.onPreReleaseVersion(currentVersion); } else { - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> checkVersionAsync(callback)); + if (FoliaScheduler.isFolia()) { + FoliaScheduler.getAsyncScheduler().runNow(plugin, ignored -> checkVersionAsync(callback)); + } else { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> checkVersionAsync(callback)); + } } } @@ -144,7 +147,11 @@ public void onFailure() { } private void doSync(Runnable r) { - Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, r); + if (FoliaScheduler.isFolia()) { + FoliaScheduler.getGlobalRegionScheduler().execute(plugin, r); + } else { + Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, r); + } } } } diff --git a/src/main/java/sh/okx/rankup/util/folia/AsyncScheduler.java b/src/main/java/sh/okx/rankup/util/folia/AsyncScheduler.java new file mode 100644 index 0000000..f291c1f --- /dev/null +++ b/src/main/java/sh/okx/rankup/util/folia/AsyncScheduler.java @@ -0,0 +1,140 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package sh.okx.rankup.util.folia; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitScheduler; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * Represents a scheduler for executing tasks asynchronously. + */ +public class AsyncScheduler { + + private BukkitScheduler bukkitScheduler; + private io.papermc.paper.threadedregions.scheduler.AsyncScheduler asyncScheduler; + + protected AsyncScheduler() { + if (FoliaScheduler.isFolia) { + asyncScheduler = Bukkit.getAsyncScheduler(); + } else { + bukkitScheduler = Bukkit.getScheduler(); + } + } + + /** + * Schedules the specified task to be executed asynchronously immediately. + * + * @param plugin Plugin which owns the specified task. + * @param task Specified task. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runNow(@NotNull Plugin plugin, @NotNull Consumer task) { + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTaskAsynchronously(plugin, () -> task.accept(null))); + } + + return new TaskWrapper(asyncScheduler.runNow(plugin, (o) -> task.accept(null))); + } + + /** + * Schedules the specified task to be executed asynchronously after the specified delay. + * + * @param plugin Plugin which owns the specified task. + * @param task Specified task. + * @param delay The time delay to pass before the task should be executed. + * @param timeUnit The time unit for the time delay. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runDelayed(@NotNull Plugin plugin, @NotNull Consumer task, long delay, @NotNull TimeUnit timeUnit) { + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTaskLaterAsynchronously(plugin, () -> task.accept(null), convertTimeToTicks(delay, timeUnit))); + } + + return new TaskWrapper(asyncScheduler.runDelayed(plugin, (o) -> task.accept(null), delay, timeUnit)); + } + + /** + * Schedules the specified task to be executed asynchronously after the initial delay has passed, and then periodically executed with the specified period. + * + * @param plugin Plugin which owns the specified task. + * @param task Specified task. + * @param delay The time delay to pass before the task should be executed. + * @param period The time period between each task execution. Any value less-than 1 is treated as 1. + * @param timeUnit The time unit for the initial delay and period. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runAtFixedRate(@NotNull Plugin plugin, @NotNull Consumer task, long delay, long period, @NotNull TimeUnit timeUnit) { + if (period < 1) period = 1; + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTaskTimerAsynchronously(plugin, () -> task.accept(null), convertTimeToTicks(delay, timeUnit), convertTimeToTicks(period, timeUnit))); + } + + return new TaskWrapper(asyncScheduler.runAtFixedRate(plugin, (o) -> task.accept(null), delay, period, timeUnit)); + } + + /** + * Schedules the specified task to be executed asynchronously after the initial delay has passed, and then periodically executed. + * + * @param plugin Plugin which owns the specified task. + * @param task Specified task. + * @param initialDelayTicks The time delay in ticks to pass before the task should be executed. + * @param periodTicks The time period in ticks between each task execution. Any value less-than 1 is treated as 1. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runAtFixedRate(@NotNull Plugin plugin, @NotNull Consumer task, long initialDelayTicks, long periodTicks) { + if (periodTicks < 1) periodTicks = 1; + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTaskTimerAsynchronously(plugin, () -> task.accept(null), initialDelayTicks, periodTicks)); + } + + return new TaskWrapper(asyncScheduler.runAtFixedRate(plugin, (o) -> task.accept(null), initialDelayTicks * 50, periodTicks * 50, TimeUnit.MILLISECONDS)); + } + + /** + * Attempts to cancel all tasks scheduled by the specified plugin. + * + * @param plugin Specified plugin. + */ + public void cancel(@NotNull Plugin plugin) { + if (!FoliaScheduler.isFolia) { + bukkitScheduler.cancelTasks(plugin); + return; + } + + asyncScheduler.cancelTasks(plugin); + } + + /** + * Converts the specified time to ticks. + * + * @param time The time to convert. + * @param timeUnit The time unit of the time. + * @return The time converted to ticks. + */ + private long convertTimeToTicks(long time, TimeUnit timeUnit) { + return timeUnit.toMillis(time) / 50; + } +} diff --git a/src/main/java/sh/okx/rankup/util/folia/EntityScheduler.java b/src/main/java/sh/okx/rankup/util/folia/EntityScheduler.java new file mode 100644 index 0000000..3d3ae38 --- /dev/null +++ b/src/main/java/sh/okx/rankup/util/folia/EntityScheduler.java @@ -0,0 +1,135 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package sh.okx.rankup.util.folia; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitScheduler; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +/** + * Represents a scheduler for executing entity tasks. + */ +public class EntityScheduler { + private BukkitScheduler bukkitScheduler; + + protected EntityScheduler() { + if (!FoliaScheduler.isFolia) { + bukkitScheduler = Bukkit.getScheduler(); + } + } + + /** + * Schedules a task with the given delay. If the task failed to schedule because the scheduler is retired (entity removed), then returns false. + * Otherwise, either the run callback will be invoked after the specified delay, or the retired callback will be invoked if the scheduler is retired. + * Note that the retired callback is invoked in critical code, so it should not attempt to remove the entity, + * remove other entities, load chunks, load worlds, modify ticket levels, etc. + *

+ * It is guaranteed that the run and retired callback are invoked on the region which owns the entity. + * + * @param plugin Plugin which owns the specified task. + * @param run The callback to run after the specified delay, may not be null. + * @param retired Retire callback to run if the entity is retired before the run callback can be invoked, may be null. + * @param delay The delay in ticks before the run callback is invoked. + */ + public void execute(@NotNull Entity entity, @NotNull Plugin plugin, @NotNull Runnable run, @Nullable Runnable retired, long delay) { + if (!FoliaScheduler.isFolia) { + bukkitScheduler.runTaskLater(plugin, run, delay); + return; + } + + entity.getScheduler().execute(plugin, run, retired, delay); + } + + /** + * Schedules a task to execute on the next tick. If the task failed to schedule because the scheduler is retired (entity removed), + * then returns null. Otherwise, either the task callback will be invoked after the specified delay, + * or the retired callback will be invoked if the scheduler is retired. + * Note that the retired callback is invoked in critical code, so it should not attempt to remove the entity, + * remove other entities, load chunks, load worlds, modify ticket levels, etc. + *

+ * It is guaranteed that the task and retired callback are invoked on the region which owns the entity. + * + * @param plugin The plugin that owns the task + * @param task The task to execute + * @param retired Retire callback to run if the entity is retired before the run callback can be invoked, may be null. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper run(@NotNull Entity entity, @NotNull Plugin plugin, @NotNull Consumer task, @Nullable Runnable retired) { + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTask(plugin, () -> task.accept(null))); + } + + return new TaskWrapper(entity.getScheduler().run(plugin, (o) -> task.accept(null), retired)); + } + + /** + * Schedules a task with the given delay. If the task failed to schedule because the scheduler is retired (entity removed), + * then returns null. Otherwise, either the task callback will be invoked after the specified delay, or the retired callback will be invoked if the scheduler is retired. + * Note that the retired callback is invoked in critical code, so it should not attempt to remove the entity, + * remove other entities, load chunks, load worlds, modify ticket levels, etc. + *

+ * It is guaranteed that the task and retired callback are invoked on the region which owns the entity. + * + * @param plugin The plugin that owns the task + * @param task The task to execute + * @param retired Retire callback to run if the entity is retired before the run callback can be invoked, may be null. + * @param delayTicks The delay in ticks before the run callback is invoked. Any value less-than 1 is treated as 1. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runDelayed(@NotNull Entity entity, @NotNull Plugin plugin, @NotNull Consumer task, @Nullable Runnable retired, long delayTicks) { + if (delayTicks < 1) delayTicks = 1; + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTaskLater(plugin, () -> task.accept(null), delayTicks)); + } + + return new TaskWrapper(entity.getScheduler().runDelayed(plugin, (o) -> task.accept(null), retired, delayTicks)); + } + + /** + * Schedules a repeating task with the given delay and period. If the task failed to schedule because the scheduler is retired (entity removed), + * then returns null. Otherwise, either the task callback will be invoked after the specified delay, or the retired callback will be invoked if the scheduler is retired. + * Note that the retired callback is invoked in critical code, so it should not attempt to remove the entity, + * remove other entities, load chunks, load worlds, modify ticket levels, etc. + *

+ * It is guaranteed that the task and retired callback are invoked on the region which owns the entity. + * + * @param plugin The plugin that owns the task + * @param task The task to execute + * @param retired Retire callback to run if the entity is retired before the run callback can be invoked, may be null. + * @param initialDelayTicks The initial delay, in ticks before the method is invoked. Any value less-than 1 is treated as 1. + * @param periodTicks The period, in ticks. Any value less-than 1 is treated as 1. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runAtFixedRate(@NotNull Entity entity, @NotNull Plugin plugin, @NotNull Consumer task, @Nullable Runnable retired, long initialDelayTicks, long periodTicks) { + if (initialDelayTicks < 1) initialDelayTicks = 1; + if (periodTicks < 1) periodTicks = 1; + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTaskTimer(plugin, () -> task.accept(null), initialDelayTicks, periodTicks)); + } + + return new TaskWrapper(entity.getScheduler().runAtFixedRate(plugin, (o) -> task.accept(null), retired, initialDelayTicks, periodTicks)); + } +} diff --git a/src/main/java/sh/okx/rankup/util/folia/FoliaScheduler.java b/src/main/java/sh/okx/rankup/util/folia/FoliaScheduler.java new file mode 100644 index 0000000..6bef27a --- /dev/null +++ b/src/main/java/sh/okx/rankup/util/folia/FoliaScheduler.java @@ -0,0 +1,130 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package sh.okx.rankup.util.folia; + +import org.bukkit.Bukkit; +import org.bukkit.event.Event; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.plugin.Plugin; + +/** + * Utility class to handle scheduling tasks. + * It uses Paper's threaded-regions schedulers if Folia is used, + * otherwise it falls back to the default Bukkit scheduler. + */ +public class FoliaScheduler { + static final boolean isFolia; + private static Class regionizedServerInitEventClass; + + private static AsyncScheduler asyncScheduler; + private static EntityScheduler entityScheduler; + private static GlobalRegionScheduler globalRegionScheduler; + private static RegionScheduler regionScheduler; + + static { + boolean folia; + try { + Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); + folia = true; + + // Thanks for this code ViaVersion + // The class is only part of the Folia API, so we need to use reflections to get it + regionizedServerInitEventClass = (Class) Class.forName("io.papermc.paper.threadedregions.RegionizedServerInitEvent"); + } catch (ClassNotFoundException e) { + folia = false; + } + + isFolia = folia; + } + + /** + * @return Whether the server is running Folia + */ + public static boolean isFolia() { + return isFolia; + } + + /** + * Returns the async scheduler. + * + * @return async scheduler instance of {@link AsyncScheduler} + */ + public static AsyncScheduler getAsyncScheduler() { + if (asyncScheduler == null) { + asyncScheduler = new AsyncScheduler(); + } + return asyncScheduler; + } + + /** + * Returns the entity scheduler. + * + * @return entity scheduler instance of {@link EntityScheduler} + */ + public static EntityScheduler getEntityScheduler() { + if (entityScheduler == null) { + entityScheduler = new EntityScheduler(); + } + return entityScheduler; + } + + /** + * Returns the global region scheduler. + * + * @return global region scheduler instance of {@link GlobalRegionScheduler} + */ + public static GlobalRegionScheduler getGlobalRegionScheduler() { + if (globalRegionScheduler == null) { + globalRegionScheduler = new GlobalRegionScheduler(); + } + return globalRegionScheduler; + } + + /** + * Returns the region scheduler. + * + * @return region scheduler instance of {@link RegionScheduler} + */ + public static RegionScheduler getRegionScheduler() { + if (regionScheduler == null) { + regionScheduler = new RegionScheduler(); + } + return regionScheduler; + } + + /** + * Run a task after the server has finished initializing. + * Undefined behavior if called after the server has finished initializing. + *

+ * We still need to use reflections to get the server init event class, as this is only part of the Folia API. + * + * @param plugin Your plugin or PacketEvents + * @param run The task to run + */ + public static void runTaskOnInit(Plugin plugin, Runnable run) { + if (!isFolia) { + Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, run); + return; + } + + Bukkit.getServer().getPluginManager().registerEvent(regionizedServerInitEventClass, new Listener() { + }, EventPriority.HIGHEST, (listener, event) -> run.run(), plugin); + } +} diff --git a/src/main/java/sh/okx/rankup/util/folia/GlobalRegionScheduler.java b/src/main/java/sh/okx/rankup/util/folia/GlobalRegionScheduler.java new file mode 100644 index 0000000..9e49651 --- /dev/null +++ b/src/main/java/sh/okx/rankup/util/folia/GlobalRegionScheduler.java @@ -0,0 +1,125 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package sh.okx.rankup.util.folia; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitScheduler; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; + +/** + * Represents a scheduler for executing global region tasks. + */ +public class GlobalRegionScheduler { + + private BukkitScheduler bukkitScheduler; + private io.papermc.paper.threadedregions.scheduler.GlobalRegionScheduler globalRegionScheduler; + + protected GlobalRegionScheduler() { + if (FoliaScheduler.isFolia) { + globalRegionScheduler = Bukkit.getGlobalRegionScheduler(); + } else { + bukkitScheduler = Bukkit.getScheduler(); + } + } + + /** + * Schedules a task to be executed on the global region. + * + * @param plugin The plugin that owns the task + * @param run The task to execute + */ + public void execute(@NotNull Plugin plugin, @NotNull Runnable run) { + if (!FoliaScheduler.isFolia) { + bukkitScheduler.runTask(plugin, run); + return; + } + + globalRegionScheduler.execute(plugin, run); + } + + /** + * Schedules a task to be executed on the global region. + * + * @param plugin The plugin that owns the task + * @param task The task to execute + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper run(@NotNull Plugin plugin, @NotNull Consumer task) { + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTask(plugin, () -> task.accept(null))); + } + + return new TaskWrapper(globalRegionScheduler.run(plugin, (o) -> task.accept(null))); + } + + /** + * Schedules a task to be executed on the global region after the specified delay in ticks. + * + * @param plugin The plugin that owns the task + * @param task The task to execute + * @param delay The delay, in ticks before the method is invoked. Any value less-than 1 is treated as 1. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runDelayed(@NotNull Plugin plugin, @NotNull Consumer task, long delay) { + if (delay < 1) delay = 1; + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTaskLater(plugin, () -> task.accept(null), delay)); + } + + return new TaskWrapper(globalRegionScheduler.runDelayed(plugin, (o) -> task.accept(null), delay)); + } + + /** + * Schedules a repeating task to be executed on the global region after the initial delay with the specified period. + * + * @param plugin The plugin that owns the task + * @param task The task to execute + * @param initialDelayTicks The initial delay, in ticks before the method is invoked. Any value less-than 1 is treated as 1. + * @param periodTicks The period, in ticks. Any value less-than 1 is treated as 1. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runAtFixedRate(@NotNull Plugin plugin, @NotNull Consumer task, long initialDelayTicks, long periodTicks) { + if (initialDelayTicks < 1) initialDelayTicks = 1; + if (periodTicks < 1) periodTicks = 1; + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(bukkitScheduler.runTaskTimer(plugin, () -> task.accept(null), initialDelayTicks, periodTicks)); + } + + return new TaskWrapper(globalRegionScheduler.runAtFixedRate(plugin, (o) -> task.accept(null), initialDelayTicks, periodTicks)); + } + + /** + * Attempts to cancel all tasks scheduled by the specified plugin. + * + * @param plugin Specified plugin. + */ + public void cancel(@NotNull Plugin plugin) { + if (!FoliaScheduler.isFolia) { + Bukkit.getScheduler().cancelTasks(plugin); + return; + } + + globalRegionScheduler.cancelTasks(plugin); + } +} diff --git a/src/main/java/sh/okx/rankup/util/folia/RegionScheduler.java b/src/main/java/sh/okx/rankup/util/folia/RegionScheduler.java new file mode 100644 index 0000000..a010186 --- /dev/null +++ b/src/main/java/sh/okx/rankup/util/folia/RegionScheduler.java @@ -0,0 +1,197 @@ +/* + * This file is part of packetevents - https://github.com/retrooper/packetevents + * Copyright (C) 2024 retrooper and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package sh.okx.rankup.util.folia; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitScheduler; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; + +/** + * Represents a scheduler for executing region tasks + */ +public class RegionScheduler { + + private BukkitScheduler bukkitScheduler; + private io.papermc.paper.threadedregions.scheduler.RegionScheduler regionScheduler; + + protected RegionScheduler() { + if (FoliaScheduler.isFolia) { + regionScheduler = Bukkit.getRegionScheduler(); + } else { + bukkitScheduler = Bukkit.getScheduler(); + } + } + + /** + * Schedules a task to be executed on the region which owns the location. + * + * @param plugin The plugin that owns the task + * @param world The world of the region that owns the task + * @param chunkX The chunk X coordinate of the region that owns the task + * @param chunkZ The chunk Z coordinate of the region that owns the task + * @param run The task to execute + */ + public void execute(@NotNull Plugin plugin, @NotNull World world, int chunkX, int chunkZ, @NotNull Runnable run) { + if (!FoliaScheduler.isFolia) { + bukkitScheduler.runTask(plugin, run); + return; + } + + regionScheduler.execute(plugin, world, chunkX, chunkZ, run); + } + + /** + * Schedules a task to be executed on the region which owns the location. + * + * @param plugin The plugin that owns the task + * @param location The location at which the region executing should own + * @param run The task to execute + */ + public void execute(@NotNull Plugin plugin, @NotNull Location location, @NotNull Runnable run) { + if (!FoliaScheduler.isFolia) { + Bukkit.getScheduler().runTask(plugin, run); + return; + } + + regionScheduler.execute(plugin, location, run); + } + + /** + * Schedules a task to be executed on the region which owns the location on the next tick. + * + * @param plugin The plugin that owns the task + * @param world The world of the region that owns the task + * @param chunkX The chunk X coordinate of the region that owns the task + * @param chunkZ The chunk Z coordinate of the region that owns the task + * @param task The task to execute + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper run(@NotNull Plugin plugin, @NotNull World world, int chunkX, int chunkZ, @NotNull Consumer task) { + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(Bukkit.getScheduler().runTask(plugin, () -> task.accept(null))); + } + + return new TaskWrapper(regionScheduler.run(plugin, world, chunkX, chunkZ, (o) -> task.accept(null))); + } + + /** + * Schedules a task to be executed on the region which owns the location on the next tick. + * + * @param plugin The plugin that owns the task + * @param location The location at which the region executing should own + * @param task The task to execute + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper run(@NotNull Plugin plugin, @NotNull Location location, @NotNull Consumer task) { + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(Bukkit.getScheduler().runTask(plugin, () -> task.accept(null))); + } + + return new TaskWrapper(regionScheduler.run(plugin, location, (o) -> task.accept(null))); + } + + /** + * Schedules a task to be executed on the region which owns the location after the specified delay in ticks. + * + * @param plugin The plugin that owns the task + * @param world The world of the region that owns the task + * @param chunkX The chunk X coordinate of the region that owns the task + * @param chunkZ The chunk Z coordinate of the region that owns the task + * @param task The task to execute + * @param delayTicks The delay, in ticks before the method is invoked. Any value less-than 1 is treated as 1. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runDelayed(@NotNull Plugin plugin, @NotNull World world, int chunkX, int chunkZ, @NotNull Consumer task, long delayTicks) { + if (delayTicks < 1) delayTicks = 1; + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(Bukkit.getScheduler().runTaskLater(plugin, () -> task.accept(null), delayTicks)); + } + + return new TaskWrapper(regionScheduler.runDelayed(plugin, world, chunkX, chunkZ, (o) -> task.accept(null), delayTicks)); + } + + /** + * Schedules a task to be executed on the region which owns the location after the specified delay in ticks. + * + * @param plugin The plugin that owns the task + * @param location The location at which the region executing should own + * @param task The task to execute + * @param delayTicks The delay, in ticks before the method is invoked. Any value less-than 1 is treated as 1. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runDelayed(@NotNull Plugin plugin, @NotNull Location location, @NotNull Consumer task, long delayTicks) { + if (delayTicks < 1) delayTicks = 1; + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(Bukkit.getScheduler().runTaskLater(plugin, () -> task.accept(null), delayTicks)); + } + + return new TaskWrapper(regionScheduler.runDelayed(plugin, location, (o) -> task.accept(null), delayTicks)); + } + + /** + * Schedules a repeating task to be executed on the region which owns the location after the initial delay with the specified period. + * + * @param plugin The plugin that owns the task + * @param world The world of the region that owns the task + * @param chunkX The chunk X coordinate of the region that owns the task + * @param chunkZ The chunk Z coordinate of the region that owns the task + * @param task The task to execute + * @param initialDelayTicks The initial delay, in ticks before the method is invoked. Any value less-than 1 is treated as 1. + * @param periodTicks The period, in ticks. Any value less-than 1 is treated as 1. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runAtFixedRate(@NotNull Plugin plugin, @NotNull World world, int chunkX, int chunkZ, @NotNull Consumer task, long initialDelayTicks, long periodTicks) { + if (initialDelayTicks < 1) initialDelayTicks = 1; + if (periodTicks < 1) periodTicks = 1; + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(Bukkit.getScheduler().runTaskTimer(plugin, () -> task.accept(null), initialDelayTicks, periodTicks)); + } + + return new TaskWrapper(regionScheduler.runAtFixedRate(plugin, world, chunkX, chunkZ, (o) -> task.accept(null), initialDelayTicks, periodTicks)); + } + + /** + * Schedules a repeating task to be executed on the region which owns the location after the initial delay with the specified period. + * + * @param plugin The plugin that owns the task + * @param location The location at which the region executing should own + * @param task The task to execute + * @param initialDelayTicks The initial delay, in ticks before the method is invoked. Any value less-than 1 is treated as 1. + * @param periodTicks The period, in ticks. Any value less-than 1 is treated as 1. + * @return {@link TaskWrapper} instance representing a wrapped task + */ + public TaskWrapper runAtFixedRate(@NotNull Plugin plugin, @NotNull Location location, @NotNull Consumer task, long initialDelayTicks, long periodTicks) { + if (initialDelayTicks < 1) initialDelayTicks = 1; + if (periodTicks < 1) periodTicks = 1; + + if (!FoliaScheduler.isFolia) { + return new TaskWrapper(Bukkit.getScheduler().runTaskTimer(plugin, () -> task.accept(null), initialDelayTicks, periodTicks)); + } + + return new TaskWrapper(regionScheduler.runAtFixedRate(plugin, location, (o) -> task.accept(null), initialDelayTicks, periodTicks)); + } +} diff --git a/src/main/java/sh/okx/rankup/util/folia/TaskWrapper.java b/src/main/java/sh/okx/rankup/util/folia/TaskWrapper.java new file mode 100644 index 0000000..a339ddf --- /dev/null +++ b/src/main/java/sh/okx/rankup/util/folia/TaskWrapper.java @@ -0,0 +1,64 @@ +package sh.okx.rankup.util.folia; + +import io.papermc.paper.threadedregions.scheduler.ScheduledTask; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitTask; +import org.jetbrains.annotations.NotNull; + +/** + * Represents a wrapper around {@code BukkitTask} and Paper's {@code ScheduledTask}. + * This class provides a unified interface for interacting with both Bukkit's task scheduler + * and Paper's task scheduler. + */ +public class TaskWrapper { + + private BukkitTask bukkitTask; + private ScheduledTask scheduledTask; + + /** + * Constructs a new TaskWrapper around a BukkitTask. + * + * @param bukkitTask the BukkitTask to wrap + */ + public TaskWrapper(@NotNull BukkitTask bukkitTask) { + this.bukkitTask = bukkitTask; + } + + /** + * Constructs a new TaskWrapper around Paper's ScheduledTask. + * + * @param scheduledTask the ScheduledTask to wrap + */ + public TaskWrapper(@NotNull ScheduledTask scheduledTask) { + this.scheduledTask = scheduledTask; + } + + /** + * Retrieves the Plugin that owns this task. + * + * @return the owning {@link Plugin} + */ + public Plugin getOwner() { + return bukkitTask != null ? bukkitTask.getOwner() : scheduledTask.getOwningPlugin(); + } + + /** + * Checks if the task is canceled. + * + * @return true if the task is canceled, false otherwise + */ + public boolean isCancelled() { + return bukkitTask != null ? bukkitTask.isCancelled() : scheduledTask.isCancelled(); + } + + /** + * Cancels the task. If the task is running, it will be canceled. + */ + public void cancel() { + if (bukkitTask != null) { + bukkitTask.cancel(); + } else { + scheduledTask.cancel(); + } + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index d16c456..6bc766a 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -5,6 +5,7 @@ author: Okx depend: [Vault] softdepend: [PlaceholderAPI, mcMMO, AdvancedAchievements, Towny, SuperbVote, VotingPlugin, LuckPerms] api-version: 1.13 +folia-supported: true commands: rankup: