diff --git a/src/main/java/eu/decentsoftware/holograms/api/DecentHolograms.java b/src/main/java/eu/decentsoftware/holograms/api/DecentHolograms.java index e4e50d5b..bf3d3481 100644 --- a/src/main/java/eu/decentsoftware/holograms/api/DecentHolograms.java +++ b/src/main/java/eu/decentsoftware/holograms/api/DecentHolograms.java @@ -9,6 +9,9 @@ import eu.decentsoftware.holograms.api.listeners.WorldListener; import eu.decentsoftware.holograms.api.nms.NMS; import eu.decentsoftware.holograms.api.nms.PacketListener; +import eu.decentsoftware.holograms.api.utils.scheduler.SchedulerAdapter; +import eu.decentsoftware.holograms.api.utils.scheduler.adapters.BukkitSchedulerAdapter; +import eu.decentsoftware.holograms.api.utils.scheduler.adapters.FoliaSchedulerAdapter; import eu.decentsoftware.holograms.api.utils.BungeeUtils; import eu.decentsoftware.holograms.api.utils.Common; import eu.decentsoftware.holograms.api.utils.DExecutor; @@ -42,6 +45,7 @@ public final class DecentHolograms { private final JavaPlugin plugin; + private final SchedulerAdapter scheduler; private HologramManager hologramManager; private CommandManager commandManager; private FeatureManager featureManager; @@ -56,6 +60,7 @@ public final class DecentHolograms { DecentHolograms(@NonNull JavaPlugin plugin) { this.plugin = plugin; + this.scheduler = FoliaSchedulerAdapter.isSupported() ? new FoliaSchedulerAdapter(plugin) : new BukkitSchedulerAdapter(plugin); } /* diff --git a/src/main/java/eu/decentsoftware/holograms/api/actions/ActionType.java b/src/main/java/eu/decentsoftware/holograms/api/actions/ActionType.java index cddad99a..3540818b 100644 --- a/src/main/java/eu/decentsoftware/holograms/api/actions/ActionType.java +++ b/src/main/java/eu/decentsoftware/holograms/api/actions/ActionType.java @@ -66,7 +66,7 @@ public boolean execute(Player player, String... args) { Validate.notNull(player); String string = String.join(" ", args); - Bukkit.getScheduler().runTask(DECENT_HOLOGRAMS.getPlugin(), () -> { + DECENT_HOLOGRAMS.getScheduler().executeAtEntity(player, () -> { // player.chat(PAPI.setPlaceholders(player, string.replace("{player}", player.getName()))); }); @@ -80,7 +80,7 @@ public boolean execute(Player player, String... args) { Validate.notNull(player); String string = String.join(" ", args); - Bukkit.getScheduler().runTask(DECENT_HOLOGRAMS.getPlugin(), () -> { + DECENT_HOLOGRAMS.getScheduler().executeAtEntity(player, () -> { // Bukkit.dispatchCommand(Bukkit.getConsoleSender(), PAPI.setPlaceholders(player, string.replace("{player}", player.getName()))); }); @@ -113,7 +113,7 @@ public boolean execute(Player player, String... args) { if (location == null) { return false; } - Bukkit.getScheduler().runTask(DECENT_HOLOGRAMS.getPlugin(), () -> player.teleport(location)); + DECENT_HOLOGRAMS.getScheduler().executeAtEntity(player, () -> player.teleport(location)); return true; } }; diff --git a/src/main/java/eu/decentsoftware/holograms/api/holograms/Hologram.java b/src/main/java/eu/decentsoftware/holograms/api/holograms/Hologram.java index 2617ca5a..8f49fb54 100644 --- a/src/main/java/eu/decentsoftware/holograms/api/holograms/Hologram.java +++ b/src/main/java/eu/decentsoftware/holograms/api/holograms/Hologram.java @@ -642,7 +642,7 @@ public boolean show(@NonNull Player player, int pageIndex) { } else { // We need to run the task later on older versions as, if we don't, it causes issues with some holograms *randomly* becoming invisible. // I *think* this is from despawning and spawning the entities (with the same ID) in the same tick. - S.sync(() -> showPageTo(player, page, pageIndex), 0L); + S.sync(player, () -> showPageTo(player, page, pageIndex), 0L); } return true; } diff --git a/src/main/java/eu/decentsoftware/holograms/api/listeners/PlayerListener.java b/src/main/java/eu/decentsoftware/holograms/api/listeners/PlayerListener.java index 0532efb6..4b8258e2 100644 --- a/src/main/java/eu/decentsoftware/holograms/api/listeners/PlayerListener.java +++ b/src/main/java/eu/decentsoftware/holograms/api/listeners/PlayerListener.java @@ -24,7 +24,7 @@ public PlayerListener(DecentHolograms decentHolograms) { public void onJoin(PlayerJoinEvent e) { Player player = e.getPlayer(); S.async(() -> decentHolograms.getHologramManager().updateVisibility(player)); - S.sync(() -> decentHolograms.getPacketListener().hook(player), 20L); + S.sync(player, () -> decentHolograms.getPacketListener().hook(player), 20L); if (decentHolograms.isUpdateAvailable() && player.hasPermission("dh.admin")) { Lang.sendUpdateMessage(player); } @@ -34,7 +34,7 @@ public void onJoin(PlayerJoinEvent e) { public void onQuit(PlayerQuitEvent e) { Player player = e.getPlayer(); S.async(() -> decentHolograms.getHologramManager().onQuit(player)); - S.sync(() -> decentHolograms.getPacketListener().unhook(player)); + S.sync(player, () -> decentHolograms.getPacketListener().unhook(player)); } @EventHandler diff --git a/src/main/java/eu/decentsoftware/holograms/api/utils/UpdateChecker.java b/src/main/java/eu/decentsoftware/holograms/api/utils/UpdateChecker.java index de0276ca..129c1e5e 100644 --- a/src/main/java/eu/decentsoftware/holograms/api/utils/UpdateChecker.java +++ b/src/main/java/eu/decentsoftware/holograms/api/utils/UpdateChecker.java @@ -1,7 +1,7 @@ package eu.decentsoftware.holograms.api.utils; +import eu.decentsoftware.holograms.api.DecentHologramsAPI; import org.apache.commons.lang.Validate; -import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; import java.io.IOException; @@ -23,7 +23,7 @@ public UpdateChecker(JavaPlugin plugin, int resourceId) { } public void getVersion(Consumer consumer) { - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + DecentHologramsAPI.get().getScheduler().runAsync(() -> { try (InputStream inputStream = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + resourceId).openStream(); Scanner scanner = new Scanner(inputStream)) { if (scanner.hasNext() && consumer != null) { diff --git a/src/main/java/eu/decentsoftware/holograms/api/utils/scheduler/S.java b/src/main/java/eu/decentsoftware/holograms/api/utils/scheduler/S.java index a67df287..5218c58f 100644 --- a/src/main/java/eu/decentsoftware/holograms/api/utils/scheduler/S.java +++ b/src/main/java/eu/decentsoftware/holograms/api/utils/scheduler/S.java @@ -3,33 +3,27 @@ import eu.decentsoftware.holograms.api.DecentHolograms; import eu.decentsoftware.holograms.api.DecentHologramsAPI; import eu.decentsoftware.holograms.api.utils.DExecutor; -import org.bukkit.Bukkit; +import org.bukkit.entity.Entity; import org.bukkit.plugin.IllegalPluginAccessException; import org.bukkit.scheduler.BukkitTask; +import java.util.concurrent.TimeUnit; + public class S { private static final DecentHolograms DECENT_HOLOGRAMS = DecentHologramsAPI.get(); - public static void stopTask(int id) { - Bukkit.getScheduler().cancelTask(id); - } - - public static void sync(Runnable runnable) { - Bukkit.getScheduler().runTask(DECENT_HOLOGRAMS.getPlugin(), runnable); + public static void sync(Entity entity, Runnable runnable, long delay) { + DECENT_HOLOGRAMS.getScheduler().runAtEntityDelayed(entity, runnable, delay); } - public static BukkitTask sync(Runnable runnable, long delay) { - return Bukkit.getScheduler().runTaskLater(DECENT_HOLOGRAMS.getPlugin(), runnable, delay); - } - - public static BukkitTask syncTask(Runnable runnable, long interval) { - return Bukkit.getScheduler().runTaskTimer(DECENT_HOLOGRAMS.getPlugin(), runnable, 0, interval); + public static void sync(Entity entity, Runnable runnable) { + DECENT_HOLOGRAMS.getScheduler().executeAtEntity(entity, runnable); } public static void async(Runnable runnable) { try { - Bukkit.getScheduler().runTaskAsynchronously(DECENT_HOLOGRAMS.getPlugin(), runnable); + DECENT_HOLOGRAMS.getScheduler().runAsync(runnable); } catch (IllegalPluginAccessException e) { DExecutor.execute(runnable); } @@ -37,18 +31,14 @@ public static void async(Runnable runnable) { public static void async(Runnable runnable, long delay) { try { - Bukkit.getScheduler().runTaskLaterAsynchronously(DECENT_HOLOGRAMS.getPlugin(), runnable, delay); + DECENT_HOLOGRAMS.getScheduler().runAsyncDelayed(runnable, delay * 50, TimeUnit.MILLISECONDS); } catch (IllegalPluginAccessException e) { DExecutor.execute(runnable); } } - public static BukkitTask asyncTask(Runnable runnable, long interval) { - return Bukkit.getScheduler().runTaskTimerAsynchronously(DECENT_HOLOGRAMS.getPlugin(), runnable, 0, interval); - } - public static BukkitTask asyncTask(Runnable runnable, long interval, long delay) { - return Bukkit.getScheduler().runTaskTimerAsynchronously(DECENT_HOLOGRAMS.getPlugin(), runnable, delay, interval); + return DECENT_HOLOGRAMS.getScheduler().runAsyncRate(runnable, 0, interval * 50, TimeUnit.MILLISECONDS); } } diff --git a/src/main/java/eu/decentsoftware/holograms/api/utils/scheduler/SchedulerAdapter.java b/src/main/java/eu/decentsoftware/holograms/api/utils/scheduler/SchedulerAdapter.java new file mode 100644 index 00000000..a0825024 --- /dev/null +++ b/src/main/java/eu/decentsoftware/holograms/api/utils/scheduler/SchedulerAdapter.java @@ -0,0 +1,70 @@ +package eu.decentsoftware.holograms.api.utils.scheduler; + +import org.bukkit.entity.Entity; +import org.bukkit.scheduler.BukkitTask; + +import java.util.concurrent.TimeUnit; + +public interface SchedulerAdapter { + + /** + * Schedules the specified task to be executed asynchronously immediately. + * + * @param runnable The task to execute. + */ + void runAsync(Runnable runnable); + + /** + * Schedules the specified task to be executed asynchronously after the time delay has passed. + * + * @param runnable The task to execute. + * @param delay The time delay to pass before the task should be executed. + * @param unit The time unit for the initial delay and period. + */ + void runAsyncDelayed(Runnable runnable, long delay, TimeUnit unit); + + /** + * Schedules the specified task to be executed asynchronously after the delay has passed, + * and then periodically executed with the specified period. + * + * @param runnable The task to execute. + * @param delay The time delay to pass before the task should be executed. + * @param period The time between task executions after the first execution of the task. + * @param unit The time unit for the initial delay and period. + * @return The BukkitTask that represents the scheduled task. + */ + BukkitTask runAsyncRate(Runnable runnable, long delay, long period, TimeUnit unit); + + /** + * Schedules a task. If the task failed to schedule because the scheduler is retired (entity removed), + * then returns {@code 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 task and retired callback are invoked on the region which owns the entity. + *

+ * + * @param entity The entity relative to which the scheduler is obtained. + * @param runnable The task to execute. + */ + void executeAtEntity(Entity entity, Runnable runnable); + + /** + * Schedules a task with the given delay. If the task failed to schedule because the scheduler is retired (entity removed), + * then returns {@code 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 task and retired callback are invoked on the region which owns the entity. + *

+ * + * @param entity The entity relative to which the scheduler is obtained. + * @param runnable The task to execute. + * @param delay The time delay to pass before the task should be executed, in ticks. + */ + void runAtEntityDelayed(Entity entity, Runnable runnable, long delay); +} diff --git a/src/main/java/eu/decentsoftware/holograms/api/utils/scheduler/adapters/BukkitSchedulerAdapter.java b/src/main/java/eu/decentsoftware/holograms/api/utils/scheduler/adapters/BukkitSchedulerAdapter.java new file mode 100644 index 00000000..82affaae --- /dev/null +++ b/src/main/java/eu/decentsoftware/holograms/api/utils/scheduler/adapters/BukkitSchedulerAdapter.java @@ -0,0 +1,42 @@ +package eu.decentsoftware.holograms.api.utils.scheduler.adapters; + +import eu.decentsoftware.holograms.api.utils.scheduler.SchedulerAdapter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.bukkit.Bukkit; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitTask; + +import java.util.concurrent.TimeUnit; + +@RequiredArgsConstructor +public class BukkitSchedulerAdapter implements SchedulerAdapter { + + private @NonNull Plugin plugin; + + @Override + public void runAsync(Runnable runnable) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, runnable); + } + + @Override + public void runAsyncDelayed(Runnable runnable, long delay, TimeUnit unit) { + Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, runnable, unit.toMillis(delay) / 50); + } + + @Override + public BukkitTask runAsyncRate(Runnable runnable, long delay, long period, TimeUnit unit) { + return Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, runnable, unit.toMillis(delay) / 50, unit.toMillis(period) / 50); + } + + @Override + public void executeAtEntity(Entity entity, Runnable runnable) { + Bukkit.getScheduler().runTask(plugin, runnable); + } + + @Override + public void runAtEntityDelayed(Entity entity, Runnable runnable, long delay) { + Bukkit.getScheduler().runTaskLater(plugin, runnable, delay); + } +} diff --git a/src/main/java/eu/decentsoftware/holograms/api/utils/scheduler/adapters/FoliaSchedulerAdapter.java b/src/main/java/eu/decentsoftware/holograms/api/utils/scheduler/adapters/FoliaSchedulerAdapter.java new file mode 100644 index 00000000..517200d4 --- /dev/null +++ b/src/main/java/eu/decentsoftware/holograms/api/utils/scheduler/adapters/FoliaSchedulerAdapter.java @@ -0,0 +1,183 @@ +package eu.decentsoftware.holograms.api.utils.scheduler.adapters; + +import eu.decentsoftware.holograms.api.utils.scheduler.SchedulerAdapter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitTask; +import org.jetbrains.annotations.Nullable; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +@SuppressWarnings("JavaLangInvokeHandleSignature") +@RequiredArgsConstructor +public class FoliaSchedulerAdapter implements SchedulerAdapter { + private static final boolean SUPPORTED; + + private static @Nullable MethodHandle ASYNC_SCHEDULER_RUN_NOW; + private static @Nullable MethodHandle ASYNC_SCHEDULER_RUN_DELAYED; + private static @Nullable MethodHandle ASYNC_SCHEDULER_RUN_RATE; + + private static @Nullable MethodHandle ENTITY_SCHEDULER_EXECUTE; + private static @Nullable MethodHandle ENTITY_SCHEDULER_RUN_DELAYED; + + private static @Nullable MethodHandle SCHEDULED_TASK_CANCEL; + + static { + boolean supporting = true; + try { + val lookup = MethodHandles.publicLookup(); + + val scheduledTaskType = Class.forName("io.papermc.paper.threadedregions.scheduler.ScheduledTask"); + SCHEDULED_TASK_CANCEL = lookup.findVirtual(scheduledTaskType, "cancel", MethodType.methodType( + Class.forName("io.papermc.paper.threadedregions.scheduler.ScheduledTask$CancelledState") + )); + + val asyncSchedulerType = Class.forName("io.papermc.paper.threadedregions.scheduler.AsyncScheduler"); + + val getAsyncScheduler = lookup.findVirtual(Server.class, "getAsyncScheduler", MethodType.methodType(asyncSchedulerType)); + val asyncScheduler = getAsyncScheduler.invoke(Bukkit.getServer()); + + ASYNC_SCHEDULER_RUN_NOW = lookup.findVirtual(asyncSchedulerType, "runNow", MethodType.methodType( + scheduledTaskType, Plugin.class, Consumer.class + )).bindTo(asyncScheduler); + ASYNC_SCHEDULER_RUN_DELAYED = lookup.findVirtual(asyncSchedulerType, "runDelayed", MethodType.methodType( + scheduledTaskType, Plugin.class, Consumer.class, long.class, TimeUnit.class + )).bindTo(asyncScheduler); + ASYNC_SCHEDULER_RUN_RATE = lookup.findVirtual(asyncSchedulerType, "runAtFixedRate", MethodType.methodType( + scheduledTaskType, Plugin.class, Consumer.class, long.class, long.class, TimeUnit.class + )).bindTo(asyncScheduler); + + val entitySchedulerType = Class.forName("io.papermc.paper.threadedregions.scheduler.EntityScheduler"); + + val getEntityScheduler = lookup.findVirtual(Entity.class, "getScheduler", MethodType.methodType(entitySchedulerType)); + ENTITY_SCHEDULER_EXECUTE = MethodHandles.filterArguments( + lookup.findVirtual(entitySchedulerType, "execute", MethodType.methodType( + boolean.class, Plugin.class, Runnable.class, Runnable.class, long.class + )), + 0, + getEntityScheduler + ); + ENTITY_SCHEDULER_RUN_DELAYED = MethodHandles.filterArguments( + lookup.findVirtual(entitySchedulerType, "runDelayed", MethodType.methodType( + scheduledTaskType, Plugin.class, Consumer.class, Runnable.class, long.class + )), + 0, + getEntityScheduler + ); + + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { + supporting = false; + } catch (Throwable throwable) { + Logger.getLogger(FoliaSchedulerAdapter.class.getName()).log(Level.WARNING, "Error in Folia scheduler adapter initialization", throwable); + } + SUPPORTED = supporting; + } + + private final @NonNull Plugin plugin; + + public static boolean isSupported() { + return SUPPORTED; + } + + @Override + public void runAsync(Runnable runnable) { + try { + final Consumer consumer = task -> runnable.run(); + Objects.requireNonNull(ASYNC_SCHEDULER_RUN_NOW).invoke(plugin, consumer); + } catch (Throwable e) { + plugin.getLogger().log(Level.SEVERE, "Error in task scheduling by the Folia scheduler adapter", e); + } + } + + @Override + public void runAsyncDelayed(Runnable runnable, long delay, TimeUnit unit) { + try { + final Consumer consumer = task -> runnable.run(); + Objects.requireNonNull(ASYNC_SCHEDULER_RUN_DELAYED).invoke(plugin, consumer, delay, unit); + } catch (Throwable e) { + plugin.getLogger().log(Level.SEVERE, "Error in task scheduling by the Folia scheduler adapter", e); + } + } + + @Override + public BukkitTask runAsyncRate(Runnable runnable, long delay, long period, TimeUnit unit) { + try { + final Consumer consumer = task -> runnable.run(); + return new ScheduledTask(Objects.requireNonNull(ASYNC_SCHEDULER_RUN_RATE).invoke(plugin, consumer, delay, period, unit)); + } catch (Throwable e) { + plugin.getLogger().log(Level.SEVERE, "Error in task scheduling by the Folia scheduler adapter", e); + } + + return new ScheduledTask(null); + } + + @Override + public void executeAtEntity(Entity entity, Runnable runnable) { + try { + final Runnable retried = () -> { + }; + Objects.requireNonNull(ENTITY_SCHEDULER_EXECUTE).invoke(entity, plugin, runnable, retried, 0L); + } catch (Throwable e) { + plugin.getLogger().log(Level.SEVERE, "Error in task scheduling by the Folia scheduler adapter", e); + } + } + + @Override + public void runAtEntityDelayed(Entity entity, Runnable runnable, long delay) { + try { + final Consumer consumer = task -> runnable.run(); + final Runnable retried = () -> { + }; + Objects.requireNonNull(ENTITY_SCHEDULER_RUN_DELAYED).invoke(entity, plugin, consumer, retried, delay); + } catch (Throwable e) { + plugin.getLogger().log(Level.SEVERE, "Error in task scheduling by the Folia scheduler adapter", e); + } + } + + @RequiredArgsConstructor + private class ScheduledTask implements BukkitTask { + + private final Object task; + + @Override + public int getTaskId() { + return 0; + } + + @Override + public Plugin getOwner() { + return null; + } + + @Override + public boolean isSync() { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void cancel() { + try { + Objects.requireNonNull(SCHEDULED_TASK_CANCEL).invoke(task); + } catch (Throwable e) { + plugin.getLogger().log(Level.SEVERE, "Error in task canceling by the Folia scheduler adapter", e); + } + } + } +} diff --git a/src/main/java/eu/decentsoftware/holograms/api/utils/tick/Ticker.java b/src/main/java/eu/decentsoftware/holograms/api/utils/tick/Ticker.java index 7c7eb73c..12791c8a 100644 --- a/src/main/java/eu/decentsoftware/holograms/api/utils/tick/Ticker.java +++ b/src/main/java/eu/decentsoftware/holograms/api/utils/tick/Ticker.java @@ -3,12 +3,13 @@ import eu.decentsoftware.holograms.api.utils.DExecutor; import eu.decentsoftware.holograms.api.utils.collection.DList; import eu.decentsoftware.holograms.api.utils.scheduler.S; +import org.bukkit.scheduler.BukkitTask; import java.util.concurrent.atomic.AtomicLong; public class Ticker { - private final int taskId; + private final BukkitTask task; private final AtomicLong ticks; private final DList tickedObjects; private final DList newTickedObjects; @@ -24,16 +25,16 @@ public Ticker() { this.newTickedObjects = new DList<>(64); this.removeTickedObjects = new DList<>(64); this.performingTick = false; - this.taskId = S.asyncTask(() -> { + this.task = S.asyncTask(() -> { if (!performingTick) tick(); - }, 1L, 5L).getTaskId(); + }, 1L, 5L); } /** * Stop the ticker and unregister all ticked objects. */ public void destroy() { - S.stopTask(taskId); + task.cancel(); tickedObjects.clear(); newTickedObjects.clear(); removeTickedObjects.clear(); diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 132da0f7..daabd4fd 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,6 @@ main: "eu.decentsoftware.holograms.plugin.DecentHologramsPlugin" api-version: 1.13 +folia-supported: true softdepend: - "PlaceholderAPI" - "HeadDatabase"