diff --git a/build.gradle.kts b/build.gradle.kts index 5f4808ec..7f3de86e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -76,6 +76,8 @@ paperweight { addServerDependencyTo = configurations.named(JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME).map { setOf(it) } } +runPaper.folia.registerTask() + tasks { runServer { minecraftVersion(minecraftVersion) diff --git a/src/main/java/toutouchien/niveriaapi/menu/component/interactive/Button.java b/src/main/java/toutouchien/niveriaapi/menu/component/interactive/Button.java index 508005b7..d489d56d 100644 --- a/src/main/java/toutouchien/niveriaapi/menu/component/interactive/Button.java +++ b/src/main/java/toutouchien/niveriaapi/menu/component/interactive/Button.java @@ -1,6 +1,7 @@ package toutouchien.niveriaapi.menu.component.interactive; import com.google.common.base.Preconditions; +import io.papermc.paper.threadedregions.scheduler.ScheduledTask; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectMap; @@ -48,12 +49,12 @@ public class Button extends MenuComponent { @Nullable private Function> animationFrames; private int animationInterval; private boolean stopAnimationOnHide; - @Nullable private BukkitTask animationTask; + @Nullable private ScheduledTask animationTask; private int currentFrame; private int updateInterval; private boolean stopUpdatesOnHide; - @Nullable private BukkitTask updateTask; + @Nullable private ScheduledTask updateTask; /** * Constructs a new Button with the specified configuration. @@ -174,7 +175,7 @@ public Int2ObjectMap items(MenuContext context) { * @param context the menu context */ private void startAnimation(MenuContext context) { - this.animationTask = Task.syncRepeat(() -> { + this.animationTask = Task.syncRepeat(ignored -> { if (!enabled() || (this.stopAnimationOnHide && !visible())) { stopAnimation(); return; @@ -213,7 +214,7 @@ private void stopAnimation() { * @param context the menu context */ private void startUpdates(MenuContext context) { - this.updateTask = Task.syncRepeat(() -> { + this.updateTask = Task.syncRepeat(ignored -> { if (!enabled() || (this.stopUpdatesOnHide && !visible())) { stopUpdates(); return; diff --git a/src/main/java/toutouchien/niveriaapi/menu/component/interactive/DoubleDropButton.java b/src/main/java/toutouchien/niveriaapi/menu/component/interactive/DoubleDropButton.java index 42bcd211..282f7fb2 100644 --- a/src/main/java/toutouchien/niveriaapi/menu/component/interactive/DoubleDropButton.java +++ b/src/main/java/toutouchien/niveriaapi/menu/component/interactive/DoubleDropButton.java @@ -1,6 +1,7 @@ package toutouchien.niveriaapi.menu.component.interactive; import com.google.common.base.Preconditions; +import io.papermc.paper.threadedregions.scheduler.ScheduledTask; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectMap; @@ -44,7 +45,7 @@ public class DoubleDropButton extends MenuComponent { @Nullable private Sound sound; - @Nullable private BukkitTask dropTask; + @Nullable private ScheduledTask dropTask; /** * Constructs a new DoubleDropButton with the specified configuration. @@ -149,7 +150,7 @@ private void handleDropClick(NiveriaInventoryClickEvent event, MenuContext conte if (this.onDoubleDrop != null) this.onDoubleDrop.accept(event); } else { - this.dropTask = Task.syncLater(() -> { + this.dropTask = Task.syncLater(ignored -> { this.dropTask = null; render(context); }, NiveriaAPI.instance(), 3L, TimeUnit.SECONDS); diff --git a/src/main/java/toutouchien/niveriaapi/utils/ParticleUtils.java b/src/main/java/toutouchien/niveriaapi/utils/ParticleUtils.java index 02927f76..0d30c977 100644 --- a/src/main/java/toutouchien/niveriaapi/utils/ParticleUtils.java +++ b/src/main/java/toutouchien/niveriaapi/utils/ParticleUtils.java @@ -1,12 +1,12 @@ package toutouchien.niveriaapi.utils; +import io.papermc.paper.threadedregions.scheduler.ScheduledTask; import net.minecraft.network.protocol.game.ClientboundLevelParticlesPacket; import org.bukkit.*; import org.bukkit.craftbukkit.CraftParticle; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; -import org.bukkit.scheduler.BukkitTask; import org.bukkit.util.Vector; import toutouchien.niveriaapi.annotations.Shivery; @@ -509,15 +509,15 @@ public static void drawCube(Location corner1, Location corner2, Particle particl * @param speed particle speed * @param durationTicks total duration in ticks, or -1 to run indefinitely * @param interval interval in ticks between spawns - * @return the scheduled {@link BukkitTask} + * @return the scheduled {@link ScheduledTask} */ - public static BukkitTask followEntity(Entity entity, Plugin plugin, Particle particle, int count, double offsetX, double offsetY, double offsetZ, double speed, long durationTicks, long interval) { - BukkitTask task = Task.syncRepeat(() -> { + public static ScheduledTask followEntity(Entity entity, Plugin plugin, Particle particle, int count, double offsetX, double offsetY, double offsetZ, double speed, long durationTicks, long interval) { + ScheduledTask task = Task.runRepeat(ignored -> { if (!entity.isValid()) return; spawnParticle(entity.getLocation().add(0, 1, 0), particle, count, offsetX, offsetY, offsetZ, speed, null, false); - }, plugin, 0, interval * 50L, TimeUnit.MILLISECONDS); + }, plugin, entity, 0, interval * 50L, TimeUnit.MILLISECONDS); if (durationTicks == -1) return task; @@ -567,15 +567,20 @@ public static void drawParametricCurve(Location center, Particle particle, Doubl public static void animateParticles(Plugin plugin, List frames, Particle particle, int count, double offsetX, double offsetY, double offsetZ, double speed, long ticksPerFrame, long durationTicks, boolean loop) { final int[] currentFrame = {0}; - BukkitTask task = Task.syncRepeat(() -> { + ScheduledTask task = Task.runRepeat(t -> { if (currentFrame[0] >= frames.size() && loop) currentFrame[0] = 0; // Loop animation (optional) + if (currentFrame[0] >= frames.size()) { + t.cancel(); + return; + } + Location location = frames.get(currentFrame[0]); spawnParticle(location, particle, count, offsetX, offsetY, offsetZ, speed, null, false); currentFrame[0]++; - }, plugin, 0, ticksPerFrame * 50L, TimeUnit.MILLISECONDS); + }, plugin, frames.get(currentFrame[0]), 0, ticksPerFrame * 50L, TimeUnit.MILLISECONDS); if (durationTicks == -1) return; @@ -615,4 +620,4 @@ public static void drawGradient(Location start, Location end, int steps, DoubleF spawnParticle(particleLocation, Particle.DUST, 1, offsetX, offsetY, offsetZ, speed, dustOptions, false); } } -} \ No newline at end of file +} diff --git a/src/main/java/toutouchien/niveriaapi/utils/Task.java b/src/main/java/toutouchien/niveriaapi/utils/Task.java index 22910427..5a05f14f 100644 --- a/src/main/java/toutouchien/niveriaapi/utils/Task.java +++ b/src/main/java/toutouchien/niveriaapi/utils/Task.java @@ -3,16 +3,21 @@ import com.google.common.base.Preconditions; import io.papermc.paper.threadedregions.scheduler.ScheduledTask; import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; import org.bukkit.plugin.Plugin; -import org.bukkit.scheduler.BukkitTask; -import org.checkerframework.checker.index.qual.NonNegative; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; /** * Utility class for scheduling synchronous and asynchronous tasks in a server. + *

+ * Delays provided as (value, TimeUnit) are converted to server ticks (1 tick = 50 ms) + * using ceiling division and clamped to a minimum of 1 tick so tasks never schedule + * with zero delay. */ @NullMarked public final class Task { @@ -20,56 +25,71 @@ private Task() { throw new IllegalStateException("Utility class"); } + private static long millisToCeilTicks(long millis) { + long ticks = (millis + 49) / 50; // ceil to next tick + return Math.max(1L, ticks); + } + /** - * Schedules a synchronous task to run immediately. + * Schedules a synchronous task to run immediately (next tick). * - * @param runnable The task to run. + * @param consumer The task to run. * @param plugin The plugin scheduling the task. - * @return The scheduled BukkitTask. + * @return The scheduled ScheduledTask. */ - public static BukkitTask sync(Runnable runnable, Plugin plugin) { - Preconditions.checkNotNull(runnable, "runnable cannot be null"); + public static ScheduledTask sync(Consumer consumer, Plugin plugin) { + Preconditions.checkNotNull(consumer, "consumer cannot be null"); Preconditions.checkNotNull(plugin, "plugin cannot be null"); - return Bukkit.getScheduler().runTask(plugin, runnable); + return Bukkit.getGlobalRegionScheduler().run(plugin, consumer); } /** * Schedules a synchronous task to run after a specified delay. + *

+ * Delay is provided as (delay, timeUnit) and converted to ticks (1 tick = 50ms). + * Conversion uses ceiling and enforces a minimum of 1 tick. * - * @param runnable The task to run. + * @param consumer The task to run. * @param plugin The plugin scheduling the task. * @param delay The delay before the task is executed. * @param timeUnit The time unit of the delay. - * @return The scheduled BukkitTask. + * @return The scheduled ScheduledTask. */ - public static BukkitTask syncLater(Runnable runnable, Plugin plugin, @NonNegative long delay, TimeUnit timeUnit) { - Preconditions.checkNotNull(runnable, "runnable cannot be null"); + public static ScheduledTask syncLater(Consumer consumer, Plugin plugin, long delay, TimeUnit timeUnit) { + Preconditions.checkNotNull(consumer, "consumer cannot be null"); Preconditions.checkNotNull(plugin, "plugin cannot be null"); - Preconditions.checkArgument(delay >= 0, "delay cannot be less than 0: %s", delay); Preconditions.checkNotNull(timeUnit, "timeUnit cannot be null"); - return Bukkit.getScheduler().runTaskLater(plugin, runnable, timeUnit.toMillis(delay) / 50); + long millis = Math.max(1L, timeUnit.toMillis(delay)); + long ticks = millisToCeilTicks(millis); + return Bukkit.getGlobalRegionScheduler().runDelayed(plugin, consumer, ticks); } /** * Schedules a synchronous repeating task. + *

+ * Delays and intervals are provided as (value, timeUnit) and converted to ticks (1 tick = 50ms). + * Conversion uses ceiling and enforces a minimum of 1 tick for both values. * - * @param runnable The task to run. + * @param consumer The task to run. * @param plugin The plugin scheduling the task. * @param delay The delay before the first execution. * @param interval The interval between subsequent executions. * @param timeUnit The time unit of the delay and interval. - * @return The scheduled BukkitTask. + * @return The scheduled ScheduledTask. */ - public static BukkitTask syncRepeat(Runnable runnable, Plugin plugin, @NonNegative long delay, @NonNegative long interval, TimeUnit timeUnit) { - Preconditions.checkNotNull(runnable, "runnable cannot be null"); + public static ScheduledTask syncRepeat(Consumer consumer, Plugin plugin, long delay, long interval, TimeUnit timeUnit) { + Preconditions.checkNotNull(consumer, "consumer cannot be null"); Preconditions.checkNotNull(plugin, "plugin cannot be null"); - Preconditions.checkArgument(delay >= 0, "delay cannot be less than 0: %s", delay); - Preconditions.checkArgument(interval >= 0, "interval cannot be less than 0: %s", interval); Preconditions.checkNotNull(timeUnit, "timeUnit cannot be null"); - return Bukkit.getScheduler().runTaskTimer(plugin, runnable, timeUnit.toMillis(delay) / 50, timeUnit.toMillis(interval) / 50); + long delayMillis = Math.max(1L, timeUnit.toMillis(delay)); + long intervalMillis = Math.max(1L, timeUnit.toMillis(interval)); + long delayTicks = millisToCeilTicks(delayMillis); + long intervalTicks = millisToCeilTicks(intervalMillis); + + return Bukkit.getGlobalRegionScheduler().runAtFixedRate(plugin, consumer, delayTicks, intervalTicks); } /** @@ -88,6 +108,8 @@ public static ScheduledTask async(Consumer consumer, Plugin plugi /** * Schedules an asynchronous task to run after a specified delay. + *

+ * The provided delay is clamped to a minimum of 1 unit in the given timeUnit. * * @param consumer The task to run. * @param plugin The plugin scheduling the task. @@ -95,17 +117,19 @@ public static ScheduledTask async(Consumer consumer, Plugin plugi * @param timeUnit The time unit of the delay. * @return The scheduled ScheduledTask. */ - public static ScheduledTask asyncLater(Consumer consumer, Plugin plugin, @NonNegative long delay, TimeUnit timeUnit) { + public static ScheduledTask asyncLater(Consumer consumer, Plugin plugin, long delay, TimeUnit timeUnit) { Preconditions.checkNotNull(consumer, "consumer cannot be null"); Preconditions.checkNotNull(plugin, "plugin cannot be null"); - Preconditions.checkArgument(delay >= 0, "delay cannot be less than 0: %s", delay); Preconditions.checkNotNull(timeUnit, "timeUnit cannot be null"); - return Bukkit.getAsyncScheduler().runDelayed(plugin, consumer, delay, timeUnit); + long d = Math.max(1L, delay); + return Bukkit.getAsyncScheduler().runDelayed(plugin, consumer, d, timeUnit); } /** * Schedules an asynchronous repeating task. + *

+ * The provided delay and interval are clamped to a minimum of 1 unit in the given timeUnit. * * @param consumer The task to run. * @param plugin The plugin scheduling the task. @@ -114,13 +138,173 @@ public static ScheduledTask asyncLater(Consumer consumer, Plugin * @param timeUnit The time unit of the delay and interval. * @return The scheduled ScheduledTask. */ - public static ScheduledTask asyncRepeat(Consumer consumer, Plugin plugin, @NonNegative long delay, @NonNegative long interval, TimeUnit timeUnit) { + public static ScheduledTask asyncRepeat(Consumer consumer, Plugin plugin, long delay, long interval, TimeUnit timeUnit) { + Preconditions.checkNotNull(consumer, "consumer cannot be null"); + Preconditions.checkNotNull(plugin, "plugin cannot be null"); + Preconditions.checkNotNull(timeUnit, "timeUnit cannot be null"); + + long d = Math.max(1L, delay); + long i = Math.max(1L, interval); + return Bukkit.getAsyncScheduler().runAtFixedRate(plugin, consumer, d, i, timeUnit); + } + + /** + * Schedules a synchronous task to be executed on the region which owns the given location on the next tick. + * + * @param consumer The task to run. + * @param plugin The plugin scheduling the task. + * @param location The location whose region should run the task. + * @return The scheduled ScheduledTask. + */ + public static ScheduledTask run(Consumer consumer, Plugin plugin, Location location) { + Preconditions.checkNotNull(consumer, "consumer cannot be null"); + Preconditions.checkNotNull(plugin, "plugin cannot be null"); + Preconditions.checkNotNull(location, "location cannot be null"); + + return Bukkit.getRegionScheduler().run( + plugin, + location, + consumer + ); + } + + /** + * Schedules a synchronous task to be executed on the region which owns the given location after the specified + * delay. + *

+ * The delay is provided with a TimeUnit and converted to server ticks (1 tick = 50 ms). + * Conversion uses ceiling division and enforces a minimum of 1 tick. + * + * @param consumer The task to run. + * @param plugin The plugin scheduling the task. + * @param location The location whose region should run the task. + * @param delay The delay before execution. + * @param timeUnit The time unit of the delay. + * @return The scheduled ScheduledTask. + */ + public static ScheduledTask runDelayed(Consumer consumer, Plugin plugin, Location location, + long delay, TimeUnit timeUnit) { Preconditions.checkNotNull(consumer, "consumer cannot be null"); Preconditions.checkNotNull(plugin, "plugin cannot be null"); - Preconditions.checkArgument(delay >= 0, "delay cannot be less than 0: %s", delay); - Preconditions.checkArgument(interval >= 0, "interval cannot be less than 0: %s", interval); + Preconditions.checkNotNull(location, "location cannot be null"); Preconditions.checkNotNull(timeUnit, "timeUnit cannot be null"); - return Bukkit.getAsyncScheduler().runAtFixedRate(plugin, consumer, delay, interval, timeUnit); + long millis = Math.max(1L, timeUnit.toMillis(delay)); + long ticks = millisToCeilTicks(millis); + return Bukkit.getRegionScheduler().runDelayed( + plugin, + location, + consumer, + ticks + ); + } + + /** + * Schedules a synchronous repeating task to be executed on the region which owns the given location after the + * initial delay with the specified period. + *

+ * The delays are provided with a TimeUnit and converted to server ticks (1 tick = 50 ms). + * Conversion uses ceiling division and enforces a minimum of 1 tick. + * + * @param consumer The task to run. + * @param plugin The plugin scheduling the task. + * @param location The location whose region should run the task. + * @param initialDelay The initial delay. + * @param period The period between executions. + * @param timeUnit The time unit of the delays. + * @return The scheduled ScheduledTask. + */ + public static ScheduledTask runRepeat(Consumer consumer, Plugin plugin, Location location, + long initialDelay, long period, TimeUnit timeUnit) { + Preconditions.checkNotNull(consumer, "consumer cannot be null"); + Preconditions.checkNotNull(plugin, "plugin cannot be null"); + Preconditions.checkNotNull(location, "location cannot be null"); + Preconditions.checkNotNull(timeUnit, "timeUnit cannot be null"); + + long initialMillis = Math.max(1L, timeUnit.toMillis(initialDelay)); + long periodMillis = Math.max(1L, timeUnit.toMillis(period)); + long initialTicks = millisToCeilTicks(initialMillis); + long periodTicks = millisToCeilTicks(periodMillis); + + return Bukkit.getRegionScheduler().runAtFixedRate( + plugin, + location, + consumer, + initialTicks, + periodTicks + ); + } + + /** + * Schedules a task to execute on the region which owns the given entity on the next tick. + * + * @param consumer The task to run. + * @param plugin The plugin scheduling the task. + * @param entity The entity whose scheduler should run the task. + * @return The scheduled ScheduledTask, or null if the entity scheduler is retired/removed. + */ + @Nullable + public static ScheduledTask run(Consumer consumer, Plugin plugin, Entity entity) { + Preconditions.checkNotNull(consumer, "consumer cannot be null"); + Preconditions.checkNotNull(plugin, "plugin cannot be null"); + Preconditions.checkNotNull(entity, "entity cannot be null"); + + return entity.getScheduler().run(plugin, consumer, null); + } + + /** + * Schedules a task with the given delay on the region which owns the given entity. + *

+ * The delay is supplied together with a {@link TimeUnit} and will be converted to server ticks (1 tick = 50 ms) + * by this helper (ceiling conversion, minimum 1 tick). + * + * @param consumer The task to run. + * @param plugin The plugin scheduling the task. + * @param entity The entity whose scheduler should run the task. + * @param delay The delay before execution. + * @param timeUnit The time unit of the delay. + * @return The scheduled ScheduledTask, or null if the entity scheduler is retired/removed. + */ + @Nullable + public static ScheduledTask runDelayed(Consumer consumer, Plugin plugin, Entity entity, + long delay, TimeUnit timeUnit) { + Preconditions.checkNotNull(consumer, "consumer cannot be null"); + Preconditions.checkNotNull(plugin, "plugin cannot be null"); + Preconditions.checkNotNull(entity, "entity cannot be null"); + Preconditions.checkNotNull(timeUnit, "timeUnit cannot be null"); + + long millis = Math.max(1L, timeUnit.toMillis(delay)); + long ticks = millisToCeilTicks(millis); + return entity.getScheduler().runDelayed(plugin, consumer, null, ticks); + } + + /** + * Schedules a repeating task with the given initial delay and period on the region which owns the given entity. + *

+ * The delays are supplied together with a {@link TimeUnit} and will be converted to server ticks (1 tick = 50 ms) + * by this helper (ceiling conversion, minimum 1 tick). + * + * @param consumer The task to run. + * @param plugin The plugin scheduling the task. + * @param entity The entity whose scheduler should run the task. + * @param initialDelay The initial delay before the first execution. + * @param period The period between subsequent executions. + * @param timeUnit The time unit of the delays. + * @return The scheduled ScheduledTask, or null if the entity scheduler is retired/removed. + */ + @Nullable + public static ScheduledTask runRepeat(Consumer consumer, Plugin plugin, Entity entity, + long initialDelay, long period, TimeUnit timeUnit) { + Preconditions.checkNotNull(consumer, "consumer cannot be null"); + Preconditions.checkNotNull(plugin, "plugin cannot be null"); + Preconditions.checkNotNull(entity, "entity cannot be null"); + Preconditions.checkNotNull(timeUnit, "timeUnit cannot be null"); + + long initialMillis = Math.max(1L, timeUnit.toMillis(initialDelay)); + long periodMillis = Math.max(1L, timeUnit.toMillis(period)); + long initialTicks = millisToCeilTicks(initialMillis); + long periodTicks = millisToCeilTicks(periodMillis); + + return entity.getScheduler().runAtFixedRate(plugin, consumer, null, initialTicks, periodTicks); } } diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml index 7f98e920..68ded3a8 100644 --- a/src/main/resources/paper-plugin.yml +++ b/src/main/resources/paper-plugin.yml @@ -8,6 +8,8 @@ load: STARTUP authors: [ Toutouchien ] website: https://niveria.fr +folia-supported: true + dependencies: server: BlueMap: @@ -44,4 +46,4 @@ dependencies: WorldGuard: load: BEFORE - required: false \ No newline at end of file + required: false diff --git a/src/test/resources/paper-plugin.yml b/src/test/resources/paper-plugin.yml index b8fdfe72..851cc7cc 100644 --- a/src/test/resources/paper-plugin.yml +++ b/src/test/resources/paper-plugin.yml @@ -6,4 +6,6 @@ bootstrapper: toutouchien.niveriaapi.NiveriaAPIBootstrap api-version: '${minMinecraftVersion}' load: STARTUP authors: [ Toutouchien ] -website: https://niveria.fr \ No newline at end of file +website: https://niveria.fr + +folia-supported: true