From 359db5e887d073c4d0ee542dfb9a2909dff37673 Mon Sep 17 00:00:00 2001 From: Ally Piechowski Date: Sun, 31 May 2026 01:11:37 +0700 Subject: [PATCH 1/3] Add CivScheduler facade and migrate own sites civmodcore had no scheduling abstraction: 13 call sites used the Bukkit scheduler directly, which throws under Folia's regionized threading. This blocks Folia readiness for every dependent plugin. Add a CivScheduler facade that delegates to the region-aware schedulers Paper exposes on every build, so a single jar runs on both Paper and Folia with no reflection or server-type detection. Migrate civmodcore's own sites onto it, routing block and entity work to the region and entity schedulers so it stays correct when regionized. Add CivTask as a cancellable handle, with a test. Two pre-existing Folia concerns are preserved unchanged and left for follow-up: global chunk-meta iteration in GlobalChunkMetaManager and a cumulative location offset in DelayedItemDrop. --- .../mc/civmodcore/chat/dialog/Dialog.java | 4 +- .../inventory/gui/AnimatedClickable.java | 12 +- .../inventory/gui/ClickableInventory.java | 14 +- .../mc/civmodcore/players/PlayerNames.java | 5 +- .../players/scoreboard/bottom/BottomLine.java | 43 +++--- .../scoreboard/bottom/BottomLineAPI.java | 12 +- .../scoreboard/side/CivScoreBoard.java | 43 +++--- .../civmodcore/scheduling/CivScheduler.java | 146 ++++++++++++++++++ .../mc/civmodcore/scheduling/CivTask.java | 33 ++++ .../civmodcore/utilities/DelayedItemDrop.java | 7 +- .../utilities/DoubleInteractFixer.java | 4 +- .../mc/civmodcore/utilities/SkinCache.java | 9 +- .../cooldowns/TickCoolDownHandler.java | 4 +- .../chunkmeta/GlobalChunkMetaManager.java | 3 +- .../chunkmeta/api/SingleBlockAPIView.java | 4 +- .../civmodcore/scheduling/CivTaskTests.java | 64 ++++++++ 16 files changed, 314 insertions(+), 93 deletions(-) create mode 100644 plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/scheduling/CivScheduler.java create mode 100644 plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/scheduling/CivTask.java create mode 100644 plugins/civmodcore-paper/src/test/java/vg/civcraft/mc/civmodcore/scheduling/CivTaskTests.java diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/chat/dialog/Dialog.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/chat/dialog/Dialog.java index faf8e3a7cf..07e3ccf90b 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/chat/dialog/Dialog.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/chat/dialog/Dialog.java @@ -2,7 +2,6 @@ import com.google.common.base.Preconditions; import java.util.List; -import org.bukkit.Bukkit; import org.bukkit.conversations.Conversation; import org.bukkit.conversations.ConversationContext; import org.bukkit.conversations.ConversationFactory; @@ -11,6 +10,7 @@ import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import vg.civcraft.mc.civmodcore.CivModCorePlugin; +import vg.civcraft.mc.civmodcore.scheduling.CivScheduler; public abstract class Dialog { @@ -33,7 +33,7 @@ public Dialog(final Player player, final Plugin plugin, final String prompt) { Preconditions.checkNotNull(player, "Player cannot be null!"); Preconditions.checkNotNull(plugin, "Plugin cannot be null!"); this.player = player; - Bukkit.getScheduler().runTask(plugin, (Runnable) player::closeInventory); + CivScheduler.runEntity(player, player::closeInventory, () -> {}); this.conversation = new ConversationFactory(plugin) .withModality(false) .withLocalEcho(false) diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/AnimatedClickable.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/AnimatedClickable.java index 274a6e7b81..8cde2f3844 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/AnimatedClickable.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/AnimatedClickable.java @@ -3,9 +3,8 @@ import java.util.List; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import org.bukkit.scheduler.BukkitRunnable; -import org.bukkit.scheduler.BukkitTask; -import vg.civcraft.mc.civmodcore.CivModCorePlugin; +import vg.civcraft.mc.civmodcore.scheduling.CivScheduler; +import vg.civcraft.mc.civmodcore.scheduling.CivTask; public class AnimatedClickable extends IClickable { @@ -41,12 +40,7 @@ public ItemStack getItemStack() { @Override public void addedToInventory(final ClickableInventory inv, final int slot) { // Schedule swapping out of item - BukkitTask task = new BukkitRunnable() { - @Override - public void run() { - inv.setItem(getNext(), slot); - } - }.runTaskTimer(CivModCorePlugin.getInstance(), timing, timing); + CivTask task = CivScheduler.runGlobalTimer(() -> inv.setItem(getNext(), slot), timing, timing); inv.registerTask(task); } diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/ClickableInventory.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/ClickableInventory.java index 3256894df3..581c964bbe 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/ClickableInventory.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/ClickableInventory.java @@ -14,8 +14,8 @@ import org.bukkit.event.inventory.InventoryType; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; -import org.bukkit.scheduler.BukkitTask; -import vg.civcraft.mc.civmodcore.CivModCorePlugin; +import vg.civcraft.mc.civmodcore.scheduling.CivScheduler; +import vg.civcraft.mc.civmodcore.scheduling.CivTask; /** * Represents an inventory filled with Clickables. Whenever one of those is @@ -43,7 +43,7 @@ public class ClickableInventory { private IClickable[] clickables; - private List runnables; + private List runnables; private String name; private Runnable onClose; @@ -193,10 +193,10 @@ public void showInventory(Player p) { public void showInventory(Player p, boolean eventSafe) { if (p != null) { if (eventSafe) { - Bukkit.getScheduler().runTask(CivModCorePlugin.getInstance(), () -> { + CivScheduler.runEntity(p, () -> { p.openInventory(inventory); openInventories.put(p.getUniqueId(), this); - }); + }, () -> {}); } else { p.openInventory(inventory); openInventories.put(p.getUniqueId(), this); @@ -245,7 +245,7 @@ public void setItem(ItemStack is, int slot) { inventory.setItem(slot, is); } - public void registerTask(BukkitTask runnable) { + public void registerTask(CivTask runnable) { this.runnables.add(runnable); } @@ -310,7 +310,7 @@ private static void inventoryClosed(Player p, boolean force) { private static void stopRunnables(ClickableInventory ci) { if (ci.inventory.getViewers().size() == 1) { // last one is closing - for (BukkitTask runnable : ci.runnables) { + for (CivTask runnable : ci.runnables) { runnable.cancel(); } } diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/PlayerNames.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/PlayerNames.java index dafd8073cb..4782020327 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/PlayerNames.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/PlayerNames.java @@ -16,6 +16,7 @@ import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import vg.civcraft.mc.civmodcore.scheduling.CivScheduler; public final class PlayerNames implements Listener { @@ -23,13 +24,13 @@ public final class PlayerNames implements Listener { public PlayerNames(Plugin plugin) { names.clear(); - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + CivScheduler.runAsync(plugin, () -> { OfflinePlayer[] players = Bukkit.getOfflinePlayers(); List namesList = Stream.of(players) .map(OfflinePlayer::getName) .filter(StringUtils::isNotBlank) .toList(); - Bukkit.getScheduler().runTask(plugin, () -> { + CivScheduler.runGlobal(plugin, () -> { names.addAll(namesList); }); }); diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/bottom/BottomLine.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/bottom/BottomLine.java index be2b6d01c6..3d1a689dcd 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/bottom/BottomLine.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/bottom/BottomLine.java @@ -8,14 +8,14 @@ import java.util.function.BiFunction; import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; -import vg.civcraft.mc.civmodcore.CivModCorePlugin; +import vg.civcraft.mc.civmodcore.scheduling.CivScheduler; +import vg.civcraft.mc.civmodcore.scheduling.CivTask; public class BottomLine implements Comparable { private Map texts; private String identifier; - private BukkitRunnable updater; + private CivTask updater; private int priority; BottomLine(String identifier, int priority) { @@ -44,30 +44,25 @@ public void updatePeriodically(BiFunction updateFunction if (updater != null) { updater.cancel(); } - updater = new BukkitRunnable() { - - @Override - public void run() { - Iterator> iter = texts.entrySet().iterator(); - while (iter.hasNext()) { - Entry entry = iter.next(); - Player player = Bukkit.getPlayer(entry.getKey()); - if (player != null) { - String newText = updateFunction.apply(player, entry.getValue()); - if (newText == null) { - iter.remove(); - BottomLineAPI.refreshIndividually(player.getUniqueId()); - continue; - } - if (!newText.equals(entry.getValue())) { - entry.setValue(newText); - BottomLineAPI.refreshIndividually(player.getUniqueId()); - } + updater = CivScheduler.runGlobalTimer(() -> { + Iterator> iter = texts.entrySet().iterator(); + while (iter.hasNext()) { + Entry entry = iter.next(); + Player player = Bukkit.getPlayer(entry.getKey()); + if (player != null) { + String newText = updateFunction.apply(player, entry.getValue()); + if (newText == null) { + iter.remove(); + BottomLineAPI.refreshIndividually(player.getUniqueId()); + continue; + } + if (!newText.equals(entry.getValue())) { + entry.setValue(newText); + BottomLineAPI.refreshIndividually(player.getUniqueId()); } } } - }; - updater.runTaskTimer(CivModCorePlugin.getInstance(), delay, delay); + }, delay, delay); } public void removePlayer(Player player) { diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/bottom/BottomLineAPI.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/bottom/BottomLineAPI.java index 5fee3ccbbf..935e34b359 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/bottom/BottomLineAPI.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/bottom/BottomLineAPI.java @@ -9,8 +9,7 @@ import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; -import vg.civcraft.mc.civmodcore.CivModCorePlugin; +import vg.civcraft.mc.civmodcore.scheduling.CivScheduler; public final class BottomLineAPI { @@ -18,14 +17,7 @@ public final class BottomLineAPI { private static final String SEPARATOR = ChatColor.BOLD + " " + ChatColor.BLACK + "|| " + ChatColor.RESET; public static void init() { - BukkitRunnable run = new BukkitRunnable() { - - @Override - public void run() { - refreshAll(); - } - }; - run.runTaskTimer(CivModCorePlugin.getInstance(), 15, 15); + CivScheduler.runGlobalTimer(BottomLineAPI::refreshAll, 15, 15); } public static BottomLine createBottomLine(String identifier, int priority) { diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/side/CivScoreBoard.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/side/CivScoreBoard.java index b073b489a9..221d5382d3 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/side/CivScoreBoard.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/side/CivScoreBoard.java @@ -8,18 +8,18 @@ import java.util.function.BiFunction; import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scoreboard.DisplaySlot; import org.bukkit.scoreboard.Objective; import org.bukkit.scoreboard.Score; import org.bukkit.scoreboard.Scoreboard; -import vg.civcraft.mc.civmodcore.CivModCorePlugin; +import vg.civcraft.mc.civmodcore.scheduling.CivScheduler; +import vg.civcraft.mc.civmodcore.scheduling.CivTask; public class CivScoreBoard { private String scoreName; private Map currentScoreText; - private BukkitRunnable updater; + private CivTask updater; CivScoreBoard(String scoreName) { this.scoreName = scoreName; @@ -34,30 +34,25 @@ public void updatePeriodically(BiFunction updateFunction if (updater != null) { updater.cancel(); } - updater = new BukkitRunnable() { - - @Override - public void run() { - Iterator> iter = currentScoreText.entrySet().iterator(); - while (iter.hasNext()) { - Entry entry = iter.next(); - Player player = Bukkit.getPlayer(entry.getKey()); - if (player != null) { - String newText = updateFunction.apply(player, entry.getValue()); - if (newText == null) { - hideForPlayer(player); - iter.remove(); - continue; - } - if (!newText.equals(entry.getValue())) { - internalUpdate(player, entry.getValue(), newText); - entry.setValue(newText); - } + updater = CivScheduler.runGlobalTimer(() -> { + Iterator> iter = currentScoreText.entrySet().iterator(); + while (iter.hasNext()) { + Entry entry = iter.next(); + Player player = Bukkit.getPlayer(entry.getKey()); + if (player != null) { + String newText = updateFunction.apply(player, entry.getValue()); + if (newText == null) { + hideForPlayer(player); + iter.remove(); + continue; + } + if (!newText.equals(entry.getValue())) { + internalUpdate(player, entry.getValue(), newText); + entry.setValue(newText); } } } - }; - updater.runTaskTimer(CivModCorePlugin.getInstance(), delay, delay); + }, delay, delay); } public void set(Player p, String newText) { diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/scheduling/CivScheduler.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/scheduling/CivScheduler.java new file mode 100644 index 0000000000..61720bd7bc --- /dev/null +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/scheduling/CivScheduler.java @@ -0,0 +1,146 @@ +package vg.civcraft.mc.civmodcore.scheduling; + +import io.papermc.paper.threadedregions.scheduler.ScheduledTask; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; +import vg.civcraft.mc.civmodcore.CivModCorePlugin; + +/** + * Scheduling facade over Paper's region-aware schedulers. + *

+ * Paper ships the Folia scheduler API on every build: on regular Paper these schedulers run on the main + * thread and never regionize, while on Folia they regionize. Delegating to them directly means one shadow + * jar runs on both with no reflection or runtime server-type detection. + */ +public final class CivScheduler { + + private static final long MILLIS_PER_TICK = 50L; + + private CivScheduler() { + } + + private static Plugin plugin() { + return CivModCorePlugin.getInstance(); + } + + // The Folia schedulers hand the task its own ScheduledTask handle; our callers don't need it. + private static Consumer ignoreHandle(final Runnable task) { + return ignored -> task.run(); + } + + // Global/region/entity scheduler delays and periods are in ticks and throw below 1. + private static long atLeastOneTick(final long ticks) { + return Math.max(1L, ticks); + } + + // --- GLOBAL (main-thread on Paper, global region on Folia) --- + + public static void runGlobal(final Runnable task) { + runGlobal(plugin(), task); + } + + public static void runGlobal(final Plugin plugin, final Runnable task) { + Bukkit.getGlobalRegionScheduler().run(plugin, ignoreHandle(task)); + } + + public static CivTask runGlobalLater(final Runnable task, final long delayTicks) { + return runGlobalLater(plugin(), task, delayTicks); + } + + public static CivTask runGlobalLater(final Plugin plugin, final Runnable task, final long delayTicks) { + return CivTask.wrap(Bukkit.getGlobalRegionScheduler() + .runDelayed(plugin, ignoreHandle(task), atLeastOneTick(delayTicks))); + } + + public static CivTask runGlobalTimer(final Runnable task, final long delayTicks, final long periodTicks) { + return runGlobalTimer(plugin(), task, delayTicks, periodTicks); + } + + public static CivTask runGlobalTimer(final Plugin plugin, final Runnable task, final long delayTicks, + final long periodTicks) { + return CivTask.wrap(Bukkit.getGlobalRegionScheduler() + .runAtFixedRate(plugin, ignoreHandle(task), atLeastOneTick(delayTicks), atLeastOneTick(periodTicks))); + } + + // --- REGION (by location / block) --- + + public static void runRegion(final Location loc, final Runnable task) { + Bukkit.getRegionScheduler().run(plugin(), loc, ignoreHandle(task)); + } + + public static void runRegion(final Block block, final Runnable task) { + runRegion(block.getLocation(), task); + } + + public static CivTask runRegionLater(final Location loc, final Runnable task, final long delayTicks) { + return CivTask.wrap(Bukkit.getRegionScheduler() + .runDelayed(plugin(), loc, ignoreHandle(task), atLeastOneTick(delayTicks))); + } + + public static CivTask runRegionLater(final Block block, final Runnable task, final long delayTicks) { + return runRegionLater(block.getLocation(), task, delayTicks); + } + + public static CivTask runRegionTimer(final Location loc, final Runnable task, final long delayTicks, + final long periodTicks) { + return CivTask.wrap(Bukkit.getRegionScheduler() + .runAtFixedRate(plugin(), loc, ignoreHandle(task), atLeastOneTick(delayTicks), + atLeastOneTick(periodTicks))); + } + + public static CivTask runRegionTimer(final Block block, final Runnable task, final long delayTicks, + final long periodTicks) { + return runRegionTimer(block.getLocation(), task, delayTicks, periodTicks); + } + + // --- ENTITY (retiredFallback runs if entity removed before task) --- + + public static void runEntity(final Entity entity, final Runnable task, final Runnable retiredFallback) { + entity.getScheduler().run(plugin(), ignoreHandle(task), retiredFallback); + } + + public static CivTask runEntityLater(final Entity entity, final Runnable task, final Runnable retiredFallback, + final long delayTicks) { + return CivTask.wrap(entity.getScheduler() + .runDelayed(plugin(), ignoreHandle(task), retiredFallback, atLeastOneTick(delayTicks))); + } + + public static CivTask runEntityTimer(final Entity entity, final Runnable task, final Runnable retiredFallback, + final long delayTicks, final long periodTicks) { + return CivTask.wrap(entity.getScheduler() + .runAtFixedRate(plugin(), ignoreHandle(task), retiredFallback, atLeastOneTick(delayTicks), + atLeastOneTick(periodTicks))); + } + + // --- ASYNC (off main thread) --- + + public static CivTask runAsync(final Runnable task) { + return runAsync(plugin(), task); + } + + public static CivTask runAsync(final Plugin plugin, final Runnable task) { + return CivTask.wrap(Bukkit.getAsyncScheduler().runNow(plugin, ignoreHandle(task))); + } + + public static CivTask runAsyncLater(final Runnable task, final long delayTicks) { + return CivTask.wrap(Bukkit.getAsyncScheduler() + .runDelayed(plugin(), ignoreHandle(task), ticksToMillis(delayTicks), TimeUnit.MILLISECONDS)); + } + + public static CivTask runAsyncTimer(final Runnable task, final long delayTicks, final long periodTicks) { + return CivTask.wrap(Bukkit.getAsyncScheduler() + .runAtFixedRate(plugin(), ignoreHandle(task), ticksToMillis(delayTicks), ticksToMillis(periodTicks), + TimeUnit.MILLISECONDS)); + } + + // The async scheduler works in real time, so its delay/period are expressed as ticks-equivalent millis. + private static long ticksToMillis(final long ticks) { + return atLeastOneTick(ticks) * MILLIS_PER_TICK; + } + +} diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/scheduling/CivTask.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/scheduling/CivTask.java new file mode 100644 index 0000000000..61a3e0484d --- /dev/null +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/scheduling/CivTask.java @@ -0,0 +1,33 @@ +package vg.civcraft.mc.civmodcore.scheduling; + +import io.papermc.paper.threadedregions.scheduler.ScheduledTask; + +/** + * A cancellable handle to a scheduled task. Wraps Paper's {@link ScheduledTask} so call sites do not + * need to import the Folia scheduler types directly. + */ +public interface CivTask { + + void cancel(); + + boolean isCancelled(); + + static CivTask wrap(final ScheduledTask task) { + return new ScheduledCivTask(task); + } + + record ScheduledCivTask(ScheduledTask task) implements CivTask { + + @Override + public void cancel() { + this.task.cancel(); + } + + @Override + public boolean isCancelled() { + return this.task.isCancelled(); + } + + } + +} diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/DelayedItemDrop.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/DelayedItemDrop.java index 4786e4d564..32ec2d3f1f 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/DelayedItemDrop.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/DelayedItemDrop.java @@ -2,12 +2,11 @@ import java.util.Collections; import java.util.List; -import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.block.Block; import org.bukkit.inventory.ItemStack; import org.bukkit.util.Vector; -import vg.civcraft.mc.civmodcore.CivModCorePlugin; +import vg.civcraft.mc.civmodcore.scheduling.CivScheduler; public final class DelayedItemDrop { @@ -39,11 +38,11 @@ public static void dropAt(final Location l, final ItemStack is) { public static void dropAt(final Location l, final List stacks) { // Schedule the item to drop 1 tick later - Bukkit.getScheduler().scheduleSyncDelayedTask(CivModCorePlugin.getInstance(), () -> { + CivScheduler.runRegionLater(l, () -> { for (ItemStack is : stacks) { l.getWorld().dropItem(l.add(0.5, 0.5, 0.5), is).setVelocity(new Vector(0, 0.05, 0)); } - }, 1); + }, 1L); } } diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/DoubleInteractFixer.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/DoubleInteractFixer.java index 8bdb221913..bf49cb1776 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/DoubleInteractFixer.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/DoubleInteractFixer.java @@ -5,11 +5,11 @@ import java.util.Map; import java.util.TreeMap; import java.util.UUID; -import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.block.Block; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; +import vg.civcraft.mc.civmodcore.scheduling.CivScheduler; public class DoubleInteractFixer { @@ -17,7 +17,7 @@ public class DoubleInteractFixer { public DoubleInteractFixer(Plugin plugin) { locations = new TreeMap<>(); - Bukkit.getScheduler().runTaskTimer(plugin, () -> locations.clear(), 1L, 1L); + CivScheduler.runGlobalTimer(plugin, () -> locations.clear(), 1L, 1L); } /** diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/SkinCache.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/SkinCache.java index 24d5ade4c9..2c809f8aa1 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/SkinCache.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/SkinCache.java @@ -17,7 +17,8 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.plugin.Plugin; -import org.bukkit.scheduler.BukkitTask; +import vg.civcraft.mc.civmodcore.scheduling.CivScheduler; +import vg.civcraft.mc.civmodcore.scheduling.CivTask; /** * @author caucow ( https://github.com/caucow ) @@ -30,7 +31,7 @@ public class SkinCache { // TODO: If an easier way to limit the number of threads spawned by Bukkit's scheduler exists, use it. // This is dirty. It feels bad. private final ExecutorService executor; - private final BukkitTask watchdog; + private final CivTask watchdog; private Thread watchdogThread; /** * Caching of PlayerProfiles, should only be accessed directly through @@ -88,7 +89,7 @@ public CompletableFuture load(UUID uuid) throws Exception { // because I don't trust plugins to clean up on disable so might as well // have something screeching in the logs. not that many plugins try to // be reloadable - this.watchdog = Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + this.watchdog = CivScheduler.runAsync(plugin, () -> { watchdogThread = Thread.currentThread(); do { try { @@ -145,7 +146,7 @@ public ItemStack getHeadItem(final UUID playerId, Supplier placeholde metaFuture.thenAccept((asyncMeta) -> { if (plugin.isEnabled()) { ItemStack headItem = createHeadItem(asyncMeta); - Bukkit.getScheduler().runTask(plugin, () -> notifyAvailable.accept(headItem)); + CivScheduler.runGlobal(plugin, () -> notifyAvailable.accept(headItem)); } }); return placeholderSupplier.get(); diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/cooldowns/TickCoolDownHandler.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/cooldowns/TickCoolDownHandler.java index 1312f6a4b7..70950a863c 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/cooldowns/TickCoolDownHandler.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/cooldowns/TickCoolDownHandler.java @@ -2,8 +2,8 @@ import java.util.HashMap; import java.util.Map; -import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; +import vg.civcraft.mc.civmodcore.scheduling.CivScheduler; /** * Cooldown implementation that keeps track of objects in ticks. The value given in the constructor is assumed to be in @@ -24,7 +24,7 @@ public class TickCoolDownHandler implements ICoolDownHandler { public TickCoolDownHandler(JavaPlugin executingPlugin, long cooldown) { this.cooldown = cooldown; cds = new HashMap<>(); - Bukkit.getScheduler().scheduleSyncRepeatingTask(executingPlugin, () -> { + CivScheduler.runGlobalTimer(executingPlugin, () -> { tickCounter++; // increment every tick }, 1L, 1L); } diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/world/locations/chunkmeta/GlobalChunkMetaManager.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/world/locations/chunkmeta/GlobalChunkMetaManager.java index 895865fa72..13146cbcd6 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/world/locations/chunkmeta/GlobalChunkMetaManager.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/world/locations/chunkmeta/GlobalChunkMetaManager.java @@ -10,6 +10,7 @@ import org.bukkit.Chunk; import org.bukkit.World; import vg.civcraft.mc.civmodcore.CivModCorePlugin; +import vg.civcraft.mc.civmodcore.scheduling.CivScheduler; import vg.civcraft.mc.civmodcore.world.locations.chunkmeta.api.ChunkMetaViewTracker; import vg.civcraft.mc.civmodcore.world.locations.global.CMCWorldDAO; import vg.civcraft.mc.civmodcore.world.locations.global.WorldIDManager; @@ -35,7 +36,7 @@ public GlobalChunkMetaManager(CMCWorldDAO chunkDao, WorldIDManager idManager, in registerWorld(idManager.getInternalWorldId(world), world); } Bukkit.getPluginManager().registerEvents(new ChunkMetaListener(this, ChunkMetaViewTracker.getInstance()), CivModCorePlugin.getInstance()); - Bukkit.getScheduler().scheduleSyncDelayedTask(CivModCorePlugin.getInstance(), () -> { + CivScheduler.runGlobalLater(() -> { for (World world : Bukkit.getWorlds()) { for (Chunk chunk : world.getLoadedChunks()) { loadChunkData(chunk); diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/world/locations/chunkmeta/api/SingleBlockAPIView.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/world/locations/chunkmeta/api/SingleBlockAPIView.java index db90a6a56a..575d07729b 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/world/locations/chunkmeta/api/SingleBlockAPIView.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/world/locations/chunkmeta/api/SingleBlockAPIView.java @@ -1,9 +1,9 @@ package vg.civcraft.mc.civmodcore.world.locations.chunkmeta.api; -import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.plugin.java.JavaPlugin; +import vg.civcraft.mc.civmodcore.scheduling.CivScheduler; import vg.civcraft.mc.civmodcore.world.locations.global.GlobalLocationTracker; import vg.civcraft.mc.civmodcore.world.locations.global.LocationTrackable; @@ -23,7 +23,7 @@ public class SingleBlockAPIView extends APIView { SingleBlockAPIView(JavaPlugin plugin, short pluginID, GlobalLocationTracker tracker) { super(plugin, pluginID); this.tracker = tracker; - Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, tracker::initFromDB); + CivScheduler.runGlobal(plugin, tracker::initFromDB); registerRegularSaveRunnable(); } diff --git a/plugins/civmodcore-paper/src/test/java/vg/civcraft/mc/civmodcore/scheduling/CivTaskTests.java b/plugins/civmodcore-paper/src/test/java/vg/civcraft/mc/civmodcore/scheduling/CivTaskTests.java new file mode 100644 index 0000000000..1d44d1d5b7 --- /dev/null +++ b/plugins/civmodcore-paper/src/test/java/vg/civcraft/mc/civmodcore/scheduling/CivTaskTests.java @@ -0,0 +1,64 @@ +package vg.civcraft.mc.civmodcore.scheduling; + +import io.papermc.paper.threadedregions.scheduler.ScheduledTask; +import org.bukkit.plugin.Plugin; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class CivTaskTests { + + @Test + public void cancelForwardsToScheduledTask() { + FakeScheduledTask fake = new FakeScheduledTask(); + CivTask task = CivTask.wrap(fake); + + Assertions.assertFalse(task.isCancelled()); + + task.cancel(); + + Assertions.assertTrue(fake.cancelled); + Assertions.assertTrue(task.isCancelled()); + } + + @Test + public void isCancelledReflectsUnderlyingState() { + FakeScheduledTask fake = new FakeScheduledTask(); + CivTask task = CivTask.wrap(fake); + + Assertions.assertFalse(task.isCancelled()); + fake.cancelled = true; + Assertions.assertTrue(task.isCancelled()); + } + + private static final class FakeScheduledTask implements ScheduledTask { + + private boolean cancelled; + + @Override + public Plugin getOwningPlugin() { + return null; + } + + @Override + public boolean isRepeatingTask() { + return false; + } + + @Override + public CancelledState cancel() { + this.cancelled = true; + return CancelledState.CANCELLED_BY_CALLER; + } + + @Override + public ExecutionState getExecutionState() { + return this.cancelled ? ExecutionState.CANCELLED : ExecutionState.IDLE; + } + + @Override + public boolean isCancelled() { + return this.cancelled; + } + } + +} From 60333439bfb9bb5ca01f67e3c3dd10d5e61c030a Mon Sep 17 00:00:00 2001 From: Ally Piechowski Date: Sun, 31 May 2026 01:22:11 +0700 Subject: [PATCH 2/3] Make migrated sites thread-safe; fix two bugs Moving these call sites onto the region-aware schedulers means their state is now reached from more than one thread under Folia: the global-thread timers iterate or mutate collections that event handlers on region and entity threads also write. Make those structures concurrent so they no longer race: - DoubleInteractFixer, BottomLine, CivScoreBoard: TreeMap to ConcurrentHashMap (UUID keys, no ordering relied upon). - PlayerNames: HashSet to ConcurrentHashMap.newKeySet(). - TickCoolDownHandler: tickCounter to AtomicLong for visibility. Also fix two latent bugs the migration exposed: - DelayedItemDrop mutated the caller's Location cumulatively in the drop loop, offsetting each stack further; clone once. - CivTask.wrap wrapped a null handle when the entity scheduler declined to schedule a retired entity, NPEing on cancel; return a no-op task instead. --- .../mc/civmodcore/players/PlayerNames.java | 6 ++++-- .../players/scoreboard/bottom/BottomLine.java | 6 ++++-- .../players/scoreboard/side/CivScoreBoard.java | 5 +++-- .../mc/civmodcore/scheduling/CivTask.java | 18 +++++++++++++++++- .../civmodcore/utilities/DelayedItemDrop.java | 5 ++++- .../utilities/DoubleInteractFixer.java | 5 +++-- .../cooldowns/TickCoolDownHandler.java | 14 +++++++------- 7 files changed, 42 insertions(+), 17 deletions(-) diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/PlayerNames.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/PlayerNames.java index 4782020327..d356c91fbb 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/PlayerNames.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/PlayerNames.java @@ -2,9 +2,9 @@ import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; import org.bukkit.Bukkit; @@ -20,7 +20,9 @@ public final class PlayerNames implements Listener { - private static final Set names = new HashSet<>(); + // Concurrent: the async-seed task writes on the global region thread while the login handler and external + // getPlayerNames() callers touch it from connection/region threads under Folia. + private static final Set names = ConcurrentHashMap.newKeySet(); public PlayerNames(Plugin plugin) { names.clear(); diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/bottom/BottomLine.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/bottom/BottomLine.java index 3d1a689dcd..c9c103f4a3 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/bottom/BottomLine.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/bottom/BottomLine.java @@ -3,8 +3,8 @@ import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; -import java.util.TreeMap; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -21,7 +21,9 @@ public class BottomLine implements Comparable { BottomLine(String identifier, int priority) { this.identifier = identifier; this.priority = priority; - this.texts = new TreeMap<>(); + // Mutated by event handlers on region/entity threads while the updatePeriodically task iterates it + // on the global thread under Folia; UUID keys are never sorted, so a hash map is enough. + this.texts = new ConcurrentHashMap<>(); } public String getIdentifier() { diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/side/CivScoreBoard.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/side/CivScoreBoard.java index 221d5382d3..d6ff83a2ca 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/side/CivScoreBoard.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/players/scoreboard/side/CivScoreBoard.java @@ -3,8 +3,8 @@ import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; -import java.util.TreeMap; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -23,7 +23,8 @@ public class CivScoreBoard { CivScoreBoard(String scoreName) { this.scoreName = scoreName; - this.currentScoreText = new TreeMap<>(); + // Updater runs on the global region thread while set/hide/purge mutate from player region threads on Folia. + this.currentScoreText = new ConcurrentHashMap<>(); } public String getName() { diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/scheduling/CivTask.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/scheduling/CivTask.java index 61a3e0484d..1e5e034567 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/scheduling/CivTask.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/scheduling/CivTask.java @@ -13,7 +13,8 @@ public interface CivTask { boolean isCancelled(); static CivTask wrap(final ScheduledTask task) { - return new ScheduledCivTask(task); + // EntityScheduler.run*/ returns null when the entity was retired before the task could be scheduled. + return task == null ? NoOpCivTask.INSTANCE : new ScheduledCivTask(task); } record ScheduledCivTask(ScheduledTask task) implements CivTask { @@ -30,4 +31,19 @@ public boolean isCancelled() { } + record NoOpCivTask() implements CivTask { + + static final NoOpCivTask INSTANCE = new NoOpCivTask(); + + @Override + public void cancel() { + } + + @Override + public boolean isCancelled() { + return true; + } + + } + } diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/DelayedItemDrop.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/DelayedItemDrop.java index 32ec2d3f1f..a14da17b7d 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/DelayedItemDrop.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/DelayedItemDrop.java @@ -39,8 +39,11 @@ public static void dropAt(final Location l, final ItemStack is) { public static void dropAt(final Location l, final List stacks) { // Schedule the item to drop 1 tick later CivScheduler.runRegionLater(l, () -> { + // Location.add mutates in place, so clone once to center the drop without + // accumulating the offset per stack or mutating the caller's Location. + final Location dropLoc = l.clone().add(0.5, 0.5, 0.5); for (ItemStack is : stacks) { - l.getWorld().dropItem(l.add(0.5, 0.5, 0.5), is).setVelocity(new Vector(0, 0.05, 0)); + dropLoc.getWorld().dropItem(dropLoc, is).setVelocity(new Vector(0, 0.05, 0)); } }, 1L); } diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/DoubleInteractFixer.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/DoubleInteractFixer.java index bf49cb1776..bcda3fdffb 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/DoubleInteractFixer.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/DoubleInteractFixer.java @@ -3,8 +3,8 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.TreeMap; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import org.bukkit.Location; import org.bukkit.block.Block; import org.bukkit.entity.Player; @@ -16,7 +16,8 @@ public class DoubleInteractFixer { private Map> locations; public DoubleInteractFixer(Plugin plugin) { - locations = new TreeMap<>(); + // Folia: interact events run on per-region threads, the clear timer on the global thread. + locations = new ConcurrentHashMap<>(); CivScheduler.runGlobalTimer(plugin, () -> locations.clear(), 1L, 1L); } diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/cooldowns/TickCoolDownHandler.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/cooldowns/TickCoolDownHandler.java index 70950a863c..29cba52c3c 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/cooldowns/TickCoolDownHandler.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/utilities/cooldowns/TickCoolDownHandler.java @@ -2,6 +2,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; import org.bukkit.plugin.java.JavaPlugin; import vg.civcraft.mc.civmodcore.scheduling.CivScheduler; @@ -19,25 +20,24 @@ public class TickCoolDownHandler implements ICoolDownHandler { private long cooldown; - private long tickCounter; + // Incremented on the global region thread but read by cooldown checks on region/entity threads + private final AtomicLong tickCounter = new AtomicLong(); public TickCoolDownHandler(JavaPlugin executingPlugin, long cooldown) { this.cooldown = cooldown; cds = new HashMap<>(); - CivScheduler.runGlobalTimer(executingPlugin, () -> { - tickCounter++; // increment every tick - }, 1L, 1L); + CivScheduler.runGlobalTimer(executingPlugin, tickCounter::incrementAndGet, 1L, 1L); } @Override public void putOnCoolDown(E e) { - cds.put(e, tickCounter); + cds.put(e, tickCounter.get()); } @Override public boolean onCoolDown(E e) { Long lastUsed = cds.get(e); - if (lastUsed == null || (tickCounter - lastUsed) > cooldown) { + if (lastUsed == null || (tickCounter.get() - lastUsed) > cooldown) { return false; } return true; @@ -49,7 +49,7 @@ public long getRemainingCoolDown(E e) { if (lastUsed == null) { return 0L; } - long leftOver = tickCounter - lastUsed; + long leftOver = tickCounter.get() - lastUsed; if (leftOver < cooldown) { return cooldown - leftOver; } From 4ef2f9906a96ef3358758c291c420bcc8b51aed4 Mon Sep 17 00:00:00 2001 From: Ally Piechowski Date: Sun, 31 May 2026 01:43:56 +0700 Subject: [PATCH 3/3] Add runEntity convenience overload Both entity call sites passed an empty retired-fallback. Add a two-arg runEntity that supplies a no-op fallback so the common fire-and-forget case drops the () -> {} noise. --- .../java/vg/civcraft/mc/civmodcore/chat/dialog/Dialog.java | 2 +- .../mc/civmodcore/inventory/gui/ClickableInventory.java | 2 +- .../vg/civcraft/mc/civmodcore/scheduling/CivScheduler.java | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/chat/dialog/Dialog.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/chat/dialog/Dialog.java index 07e3ccf90b..920f5ec98e 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/chat/dialog/Dialog.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/chat/dialog/Dialog.java @@ -33,7 +33,7 @@ public Dialog(final Player player, final Plugin plugin, final String prompt) { Preconditions.checkNotNull(player, "Player cannot be null!"); Preconditions.checkNotNull(plugin, "Plugin cannot be null!"); this.player = player; - CivScheduler.runEntity(player, player::closeInventory, () -> {}); + CivScheduler.runEntity(player, player::closeInventory); this.conversation = new ConversationFactory(plugin) .withModality(false) .withLocalEcho(false) diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/ClickableInventory.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/ClickableInventory.java index 581c964bbe..85bf3887b2 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/ClickableInventory.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/inventory/gui/ClickableInventory.java @@ -196,7 +196,7 @@ public void showInventory(Player p, boolean eventSafe) { CivScheduler.runEntity(p, () -> { p.openInventory(inventory); openInventories.put(p.getUniqueId(), this); - }, () -> {}); + }); } else { p.openInventory(inventory); openInventories.put(p.getUniqueId(), this); diff --git a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/scheduling/CivScheduler.java b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/scheduling/CivScheduler.java index 61720bd7bc..a860f64081 100644 --- a/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/scheduling/CivScheduler.java +++ b/plugins/civmodcore-paper/src/main/java/vg/civcraft/mc/civmodcore/scheduling/CivScheduler.java @@ -21,6 +21,9 @@ public final class CivScheduler { private static final long MILLIS_PER_TICK = 50L; + private static final Runnable NO_OP = () -> { + }; + private CivScheduler() { } @@ -100,6 +103,10 @@ public static CivTask runRegionTimer(final Block block, final Runnable task, fin // --- ENTITY (retiredFallback runs if entity removed before task) --- + public static void runEntity(final Entity entity, final Runnable task) { + runEntity(entity, task, NO_OP); + } + public static void runEntity(final Entity entity, final Runnable task, final Runnable retiredFallback) { entity.getScheduler().run(plugin(), ignoreHandle(task), retiredFallback); }