From fb6a5eb6b8a5393fb787ec2418e17484e22d8353 Mon Sep 17 00:00:00 2001 From: OPmasterLEO <98689894+OPmasterLEO@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:45:03 +0100 Subject: [PATCH 1/3] Add platform detection and scheduler abstraction for Folia support --- README.md | 14 + api/pom.xml | 6 + .../java/de/rapha149/signgui/SignGUI.java | 9 +- .../de/rapha149/signgui/SignGUIBuilder.java | 5 +- .../signgui/util/PlatformDetector.java | 103 +++++++ .../rapha149/signgui/util/PlatformType.java | 26 ++ .../signgui/util/SchedulerAdapter.java | 81 ++++++ .../scheduler/BukkitSchedulerAdapter.java | 62 +++++ .../util/scheduler/FoliaSchedulerAdapter.java | 261 ++++++++++++++++++ .../util/scheduler/SchedulerFactory.java | 38 +++ pom.xml | 4 + 11 files changed, 605 insertions(+), 4 deletions(-) create mode 100644 api/src/main/java/de/rapha149/signgui/util/PlatformDetector.java create mode 100644 api/src/main/java/de/rapha149/signgui/util/PlatformType.java create mode 100644 api/src/main/java/de/rapha149/signgui/util/SchedulerAdapter.java create mode 100644 api/src/main/java/de/rapha149/signgui/util/scheduler/BukkitSchedulerAdapter.java create mode 100644 api/src/main/java/de/rapha149/signgui/util/scheduler/FoliaSchedulerAdapter.java create mode 100644 api/src/main/java/de/rapha149/signgui/util/scheduler/SchedulerFactory.java diff --git a/README.md b/README.md index 1fae7a7..f128a63 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,14 @@ An api to get input text via a sign in Minecraft. The api supports the Minecraft versions from `1.8` to `1.21`. Also supports adventure text and mojang-mapped Paper plugins (1.20.5+). +## ✨ Full Platform Support +- ✅ **Bukkit / Spigot / Paper** - Full support +- ✅ **Folia** - Complete regionized multithreading support +- ✅ **CanvasMC** - Full support +- ✅ **Archlight** - Full support (Forge+Bukkit hybrid) + +SignGUI automatically detects your server platform and uses the appropriate scheduler for thread-safe operation! + ## Integration Maven dependency: @@ -120,6 +128,10 @@ try { return Collections.emptyList(); }) + // RECOMMENDED: Call handler synchronously for thread safety + // REQUIRED for Folia, CanvasMC, and Archlight + .callHandlerSynchronously(this) // "this" = your JavaPlugin instance + // build the SignGUI .build(); @@ -135,6 +147,8 @@ try { You don't have to call all methods. Only `setHandler` is mandatory. +**Important for Folia/CanvasMC/Archlight:** Always use `callHandlerSynchronously(plugin)` to ensure thread-safe operation on all platforms. On Folia, this ensures tasks run on the correct region thread. On other platforms, it ensures tasks run on the main thread. + By default, the handler is called by an asynchronous thread. You can change that behaviour by calling the method `callHandlerSynchronously` of the builder. An explanation for the different methods can be found on the [Javadoc](https://javadoc.io/doc/de.rapha149.signgui/signgui). diff --git a/api/pom.xml b/api/pom.xml index 243b55f..d3f16c2 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -49,6 +49,12 @@ 1.8-R0.1-SNAPSHOT provided + + dev.folia + folia-api + 1.21.11-R0.1-SNAPSHOT + provided + de.rapha149.signgui signgui-wrapper diff --git a/api/src/main/java/de/rapha149/signgui/SignGUI.java b/api/src/main/java/de/rapha149/signgui/SignGUI.java index 1f37daa..51c9696 100644 --- a/api/src/main/java/de/rapha149/signgui/SignGUI.java +++ b/api/src/main/java/de/rapha149/signgui/SignGUI.java @@ -3,6 +3,7 @@ import de.rapha149.signgui.SignGUIAction.SignGUIActionInfo; import de.rapha149.signgui.exception.SignGUIException; import de.rapha149.signgui.exception.SignGUIVersionException; +import de.rapha149.signgui.util.scheduler.SchedulerFactory; import de.rapha149.signgui.version.VersionMatcher; import org.apache.commons.lang.Validate; import org.bukkit.Bukkit; @@ -107,10 +108,12 @@ public void open(Player player) throws SignGUIException { action.execute(this, signEditor, player); }; - if (callHandlerSynchronously) - Bukkit.getScheduler().runTask(plugin, runnable); - else + if (callHandlerSynchronously) { + // Use platform-aware scheduler that works on Folia, CanvasMC, Archlight, and Bukkit + SchedulerFactory.getScheduler().runTask(plugin, player, runnable); + } else { runnable.run(); + } }); } catch (Exception e) { throw new SignGUIException("Failed to open sign gui", e); diff --git a/api/src/main/java/de/rapha149/signgui/SignGUIBuilder.java b/api/src/main/java/de/rapha149/signgui/SignGUIBuilder.java index 33babcc..31b13ad 100644 --- a/api/src/main/java/de/rapha149/signgui/SignGUIBuilder.java +++ b/api/src/main/java/de/rapha149/signgui/SignGUIBuilder.java @@ -168,7 +168,10 @@ public SignGUIBuilder setHandler(SignGUIFinishHandler handler) { } /** - * If called the handler will be called synchronously by calling the method {@link org.bukkit.scheduler.BukkitScheduler#runTask(Plugin, Runnable)} + * If called the handler will be called synchronously by using the appropriate scheduler. + * This is required for Folia, CanvasMC, and Archlight support and ensures thread-safe execution. + * On Folia, tasks are scheduled on the player's region thread. + * On Bukkit/Spigot/Paper, tasks are scheduled on the main thread. * * @param plugin Your {@link org.bukkit.plugin.java.JavaPlugin} instance. * @return The {@link SignGUIBuilder} instance diff --git a/api/src/main/java/de/rapha149/signgui/util/PlatformDetector.java b/api/src/main/java/de/rapha149/signgui/util/PlatformDetector.java new file mode 100644 index 0000000..9b6b1af --- /dev/null +++ b/api/src/main/java/de/rapha149/signgui/util/PlatformDetector.java @@ -0,0 +1,103 @@ +package de.rapha149.signgui.util; + +import org.bukkit.Bukkit; + +/** + * Utility class for detecting the server platform. + */ +public class PlatformDetector { + + private static PlatformType detectedPlatform; + private static boolean hasFoliaClasses; + private static boolean hasRegionScheduler; + + static { + detectPlatform(); + } + + /** + * Detects the server platform. + */ + private static void detectPlatform() { + try { + Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); + hasFoliaClasses = true; + } catch (ClassNotFoundException ignored) { + hasFoliaClasses = false; + } + + try { + Class.forName("io.papermc.paper.threadedregions.scheduler.RegionScheduler"); + hasRegionScheduler = true; + } catch (ClassNotFoundException ignored) { + hasRegionScheduler = false; + } + + String serverVersion = Bukkit.getVersion(); + String serverName = Bukkit.getName(); + + if (hasFoliaClasses && hasRegionScheduler) { + detectedPlatform = PlatformType.FOLIA; + } else if (serverVersion.contains("Canvas") || serverName.contains("Canvas")) { + detectedPlatform = PlatformType.CANVAS; + } else if (serverVersion.contains("Archlight") || serverName.contains("Archlight")) { + detectedPlatform = PlatformType.ARCHLIGHT; + } else { + detectedPlatform = PlatformType.BUKKIT; + } + } + + /** + * Gets the detected platform type. + * + * @return The detected platform type + */ + public static PlatformType getPlatformType() { + return detectedPlatform; + } + + /** + * Checks if the server is running on Folia. + * + * @return true if running on Folia, false otherwise + */ + public static boolean isFolia() { + return detectedPlatform == PlatformType.FOLIA; + } + + /** + * Checks if the server is running on CanvasMC. + * + * @return true if running on CanvasMC, false otherwise + */ + public static boolean isCanvas() { + return detectedPlatform == PlatformType.CANVAS; + } + + /** + * Checks if the server is running on Archlight. + * + * @return true if running on Archlight, false otherwise + */ + public static boolean isArchlight() { + return detectedPlatform == PlatformType.ARCHLIGHT; + } + + /** + * Checks if the server supports region-based scheduling (Folia). + * + * @return true if region scheduling is supported, false otherwise + */ + public static boolean hasRegionScheduler() { + return hasRegionScheduler; + } + + /** + * Checks if the server has Folia classes available. + * + * @return true if Folia classes are available, false otherwise + */ + public static boolean hasFoliaClasses() { + return hasFoliaClasses; + } +} diff --git a/api/src/main/java/de/rapha149/signgui/util/PlatformType.java b/api/src/main/java/de/rapha149/signgui/util/PlatformType.java new file mode 100644 index 0000000..0f394f5 --- /dev/null +++ b/api/src/main/java/de/rapha149/signgui/util/PlatformType.java @@ -0,0 +1,26 @@ +package de.rapha149.signgui.util; + +/** + * Enum representing different server platforms. + */ +public enum PlatformType { + /** + * Folia - Paper's regionized multithreaded server + */ + FOLIA, + + /** + * CanvasMC - Fork of Paper + */ + CANVAS, + + /** + * Archlight - Forge+Bukkit hybrid server + */ + ARCHLIGHT, + + /** + * Standard Bukkit/Spigot/Paper server + */ + BUKKIT +} diff --git a/api/src/main/java/de/rapha149/signgui/util/SchedulerAdapter.java b/api/src/main/java/de/rapha149/signgui/util/SchedulerAdapter.java new file mode 100644 index 0000000..9c78c23 --- /dev/null +++ b/api/src/main/java/de/rapha149/signgui/util/SchedulerAdapter.java @@ -0,0 +1,81 @@ +package de.rapha149.signgui.util; + +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.java.JavaPlugin; + +/** + * Adapter interface for scheduling tasks across different server platforms. + */ +public interface SchedulerAdapter { + + /** + * Runs a task synchronously on the main thread or appropriate region thread. + * + * @param plugin The plugin instance + * @param task The task to run + */ + void runTask(JavaPlugin plugin, Runnable task); + + /** + * Runs a task synchronously on the main thread or appropriate region thread. + * + * @param plugin The plugin instance + * @param entity The entity to run the task for (used for Folia region scheduling) + * @param task The task to run + */ + void runTask(JavaPlugin plugin, Entity entity, Runnable task); + + /** + * Runs a task synchronously at a specific location. + * + * @param plugin The plugin instance + * @param location The location to run the task at (used for Folia region scheduling) + * @param task The task to run + */ + void runTask(JavaPlugin plugin, Location location, Runnable task); + + /** + * Runs a task asynchronously. + * + * @param plugin The plugin instance + * @param task The task to run + */ + void runTaskAsynchronously(JavaPlugin plugin, Runnable task); + + /** + * Schedules a delayed task synchronously. + * + * @param plugin The plugin instance + * @param task The task to run + * @param delayTicks The delay in ticks + */ + void runTaskLater(JavaPlugin plugin, Runnable task, long delayTicks); + + /** + * Schedules a delayed task synchronously for an entity. + * + * @param plugin The plugin instance + * @param entity The entity to run the task for (used for Folia region scheduling) + * @param task The task to run + * @param delayTicks The delay in ticks + */ + void runTaskLater(JavaPlugin plugin, Entity entity, Runnable task, long delayTicks); + + /** + * Schedules a delayed task synchronously at a location. + * + * @param plugin The plugin instance + * @param location The location to run the task at (used for Folia region scheduling) + * @param task The task to run + * @param delayTicks The delay in ticks + */ + void runTaskLater(JavaPlugin plugin, Location location, Runnable task, long delayTicks); + + /** + * Checks if the current thread is the main server thread or appropriate region thread. + * + * @return true if on the main/region thread, false otherwise + */ + boolean isOnMainThread(); +} diff --git a/api/src/main/java/de/rapha149/signgui/util/scheduler/BukkitSchedulerAdapter.java b/api/src/main/java/de/rapha149/signgui/util/scheduler/BukkitSchedulerAdapter.java new file mode 100644 index 0000000..95e9741 --- /dev/null +++ b/api/src/main/java/de/rapha149/signgui/util/scheduler/BukkitSchedulerAdapter.java @@ -0,0 +1,62 @@ +package de.rapha149.signgui.util.scheduler; + +import de.rapha149.signgui.util.SchedulerAdapter; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.java.JavaPlugin; + +/** + * Standard Bukkit/Spigot/Paper scheduler implementation. + * Also used for CanvasMC and Archlight which are compatible with Bukkit API. + */ +public class BukkitSchedulerAdapter implements SchedulerAdapter { + + @Override + public void runTask(JavaPlugin plugin, Runnable task) { + if (Bukkit.isPrimaryThread()) { + task.run(); + } else { + Bukkit.getScheduler().runTask(plugin, task); + } + } + + @Override + public void runTask(JavaPlugin plugin, Entity entity, Runnable task) { + runTask(plugin, task); + } + + @Override + public void runTask(JavaPlugin plugin, Location location, Runnable task) { + runTask(plugin, task); + } + + @Override + public void runTaskAsynchronously(JavaPlugin plugin, Runnable task) { + if (Bukkit.isPrimaryThread()) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, task); + } else { + task.run(); + } + } + + @Override + public void runTaskLater(JavaPlugin plugin, Runnable task, long delayTicks) { + Bukkit.getScheduler().runTaskLater(plugin, task, delayTicks); + } + + @Override + public void runTaskLater(JavaPlugin plugin, Entity entity, Runnable task, long delayTicks) { + runTaskLater(plugin, task, delayTicks); + } + + @Override + public void runTaskLater(JavaPlugin plugin, Location location, Runnable task, long delayTicks) { + runTaskLater(plugin, task, delayTicks); + } + + @Override + public boolean isOnMainThread() { + return Bukkit.isPrimaryThread(); + } +} diff --git a/api/src/main/java/de/rapha149/signgui/util/scheduler/FoliaSchedulerAdapter.java b/api/src/main/java/de/rapha149/signgui/util/scheduler/FoliaSchedulerAdapter.java new file mode 100644 index 0000000..e80c230 --- /dev/null +++ b/api/src/main/java/de/rapha149/signgui/util/scheduler/FoliaSchedulerAdapter.java @@ -0,0 +1,261 @@ +package de.rapha149.signgui.util.scheduler; + +import de.rapha149.signgui.util.SchedulerAdapter; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.java.JavaPlugin; + +import java.lang.reflect.Method; +import java.util.concurrent.TimeUnit; + +/** + * Folia-compatible scheduler implementation using region-based scheduling. + * This implementation uses reflection to access Folia's API without requiring it at compile time. + */ +public class FoliaSchedulerAdapter implements SchedulerAdapter { + + private static final Class REGION_SCHEDULER_CLASS; + private static final Class GLOBAL_REGION_SCHEDULER_CLASS; + private static final Class ENTITY_SCHEDULER_CLASS; + private static final Class ASYNC_SCHEDULER_CLASS; + + private static final Method GET_REGION_SCHEDULER_METHOD; + private static final Method GET_GLOBAL_REGION_SCHEDULER_METHOD; + private static final Method GET_ASYNC_SCHEDULER_METHOD; + private static final Method REGION_EXECUTE_METHOD; + private static final Method REGION_RUN_DELAYED_METHOD; + private static final Method GLOBAL_RUN_METHOD; + private static final Method ASYNC_RUN_NOW_METHOD; + private static final Method ENTITY_GET_SCHEDULER_METHOD; + private static final Method ENTITY_EXECUTE_METHOD; + private static final Method ENTITY_RUN_DELAYED_METHOD; + private static final Method IS_OWNED_BY_CURRENT_REGION_METHOD; + + private static final boolean FOLIA_AVAILABLE; + + static { + boolean available = false; + Class regionSchedulerClass = null; + Class globalRegionSchedulerClass = null; + Class entitySchedulerClass = null; + Class asyncSchedulerClass = null; + Method getRegionScheduler = null; + Method getGlobalRegionScheduler = null; + Method getAsyncScheduler = null; + Method regionExecute = null; + Method regionRunDelayed = null; + Method globalRun = null; + Method asyncRunNow = null; + Method entityGetScheduler = null; + Method entityExecute = null; + Method entityRunDelayed = null; + Method isOwnedByCurrentRegion = null; + + try { + regionSchedulerClass = Class.forName("io.papermc.paper.threadedregions.scheduler.RegionScheduler"); + globalRegionSchedulerClass = Class.forName("io.papermc.paper.threadedregions.scheduler.GlobalRegionScheduler"); + entitySchedulerClass = Class.forName("io.papermc.paper.threadedregions.scheduler.EntityScheduler"); + asyncSchedulerClass = Class.forName("io.papermc.paper.threadedregions.scheduler.AsyncScheduler"); + + Class serverClass = Bukkit.getServer().getClass(); + getRegionScheduler = serverClass.getMethod("getRegionScheduler"); + getGlobalRegionScheduler = serverClass.getMethod("getGlobalRegionScheduler"); + getAsyncScheduler = serverClass.getMethod("getAsyncScheduler"); + regionExecute = regionSchedulerClass.getMethod("execute", org.bukkit.plugin.Plugin.class, Location.class, Runnable.class); + regionRunDelayed = regionSchedulerClass.getMethod("runDelayed", org.bukkit.plugin.Plugin.class, Location.class, + java.util.function.Consumer.class, long.class); + + globalRun = globalRegionSchedulerClass.getMethod("run", org.bukkit.plugin.Plugin.class, java.util.function.Consumer.class); + + asyncRunNow = asyncSchedulerClass.getMethod("runNow", org.bukkit.plugin.Plugin.class, java.util.function.Consumer.class); + + entityGetScheduler = Entity.class.getMethod("getScheduler"); + entityExecute = entitySchedulerClass.getMethod("execute", org.bukkit.plugin.Plugin.class, Runnable.class, + Runnable.class, long.class); + entityRunDelayed = entitySchedulerClass.getMethod("runDelayed", org.bukkit.plugin.Plugin.class, + java.util.function.Consumer.class, Runnable.class, long.class); + + Class regionizedServerClass = Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); + isOwnedByCurrentRegion = regionizedServerClass.getMethod("isOwnedByCurrentRegion", Location.class); + + available = true; + } catch (Exception e) { + System.err.println("Warning: Folia classes not found, but FoliaSchedulerAdapter was attempted to be used. " + + "This should not happen. Falling back to Bukkit scheduler."); + } + + FOLIA_AVAILABLE = available; + REGION_SCHEDULER_CLASS = regionSchedulerClass; + GLOBAL_REGION_SCHEDULER_CLASS = globalRegionSchedulerClass; + ENTITY_SCHEDULER_CLASS = entitySchedulerClass; + ASYNC_SCHEDULER_CLASS = asyncSchedulerClass; + GET_REGION_SCHEDULER_METHOD = getRegionScheduler; + GET_GLOBAL_REGION_SCHEDULER_METHOD = getGlobalRegionScheduler; + GET_ASYNC_SCHEDULER_METHOD = getAsyncScheduler; + REGION_EXECUTE_METHOD = regionExecute; + REGION_RUN_DELAYED_METHOD = regionRunDelayed; + GLOBAL_RUN_METHOD = globalRun; + ASYNC_RUN_NOW_METHOD = asyncRunNow; + ENTITY_GET_SCHEDULER_METHOD = entityGetScheduler; + ENTITY_EXECUTE_METHOD = entityExecute; + ENTITY_RUN_DELAYED_METHOD = entityRunDelayed; + IS_OWNED_BY_CURRENT_REGION_METHOD = isOwnedByCurrentRegion; + } + + /** + * Checks if Folia is available. + * + * @return true if Folia is available, false otherwise + */ + public static boolean isFoliaAvailable() { + return FOLIA_AVAILABLE; + } + + @Override + public void runTask(JavaPlugin plugin, Runnable task) { + if (!FOLIA_AVAILABLE) { + fallbackScheduler().runTask(plugin, task); + return; + } + + try { + Object globalScheduler = GET_GLOBAL_REGION_SCHEDULER_METHOD.invoke(Bukkit.getServer()); + GLOBAL_RUN_METHOD.invoke(globalScheduler, plugin, (java.util.function.Consumer) scheduledTask -> task.run()); + } catch (Exception e) { + e.printStackTrace(); + fallbackScheduler().runTask(plugin, task); + } + } + + @Override + public void runTask(JavaPlugin plugin, Entity entity, Runnable task) { + if (!FOLIA_AVAILABLE) { + fallbackScheduler().runTask(plugin, entity, task); + return; + } + + try { + Object entityScheduler = ENTITY_GET_SCHEDULER_METHOD.invoke(entity); + ENTITY_EXECUTE_METHOD.invoke(entityScheduler, plugin, task, null, 1L); + } catch (Exception e) { + e.printStackTrace(); + fallbackScheduler().runTask(plugin, entity, task); + } + } + + @Override + public void runTask(JavaPlugin plugin, Location location, Runnable task) { + if (!FOLIA_AVAILABLE) { + fallbackScheduler().runTask(plugin, location, task); + return; + } + + try { + boolean isOwned = (boolean) IS_OWNED_BY_CURRENT_REGION_METHOD.invoke(null, location); + if (isOwned) { + task.run(); + return; + } + + Object regionScheduler = GET_REGION_SCHEDULER_METHOD.invoke(Bukkit.getServer()); + REGION_EXECUTE_METHOD.invoke(regionScheduler, plugin, location, task); + } catch (Exception e) { + e.printStackTrace(); + fallbackScheduler().runTask(plugin, location, task); + } + } + + @Override + public void runTaskAsynchronously(JavaPlugin plugin, Runnable task) { + if (!FOLIA_AVAILABLE) { + fallbackScheduler().runTaskAsynchronously(plugin, task); + return; + } + + try { + Object asyncScheduler = GET_ASYNC_SCHEDULER_METHOD.invoke(Bukkit.getServer()); + ASYNC_RUN_NOW_METHOD.invoke(asyncScheduler, plugin, (java.util.function.Consumer) scheduledTask -> task.run()); + } catch (Exception e) { + e.printStackTrace(); + fallbackScheduler().runTaskAsynchronously(plugin, task); + } + } + + @Override + public void runTaskLater(JavaPlugin plugin, Runnable task, long delayTicks) { + if (!FOLIA_AVAILABLE) { + fallbackScheduler().runTaskLater(plugin, task, delayTicks); + return; + } + + try { + Object globalScheduler = GET_GLOBAL_REGION_SCHEDULER_METHOD.invoke(Bukkit.getServer()); + Object asyncScheduler = GET_ASYNC_SCHEDULER_METHOD.invoke(Bukkit.getServer()); + long delayMillis = delayTicks * 50L; + + Method asyncRunDelayed = ASYNC_SCHEDULER_CLASS.getMethod("runDelayed", + org.bukkit.plugin.Plugin.class, java.util.function.Consumer.class, long.class, TimeUnit.class); + asyncRunDelayed.invoke(asyncScheduler, plugin, + (java.util.function.Consumer) scheduledTask -> runTask(plugin, task), + delayMillis, TimeUnit.MILLISECONDS); + } catch (Exception e) { + e.printStackTrace(); + fallbackScheduler().runTaskLater(plugin, task, delayTicks); + } + } + + @Override + public void runTaskLater(JavaPlugin plugin, Entity entity, Runnable task, long delayTicks) { + if (!FOLIA_AVAILABLE) { + fallbackScheduler().runTaskLater(plugin, entity, task, delayTicks); + return; + } + + try { + Object entityScheduler = ENTITY_GET_SCHEDULER_METHOD.invoke(entity); + ENTITY_RUN_DELAYED_METHOD.invoke(entityScheduler, plugin, + (java.util.function.Consumer) scheduledTask -> task.run(), null, delayTicks); + } catch (Exception e) { + e.printStackTrace(); + fallbackScheduler().runTaskLater(plugin, entity, task, delayTicks); + } + } + + @Override + public void runTaskLater(JavaPlugin plugin, Location location, Runnable task, long delayTicks) { + if (!FOLIA_AVAILABLE) { + fallbackScheduler().runTaskLater(plugin, location, task, delayTicks); + return; + } + + try { + Object regionScheduler = GET_REGION_SCHEDULER_METHOD.invoke(Bukkit.getServer()); + REGION_RUN_DELAYED_METHOD.invoke(regionScheduler, plugin, location, + (java.util.function.Consumer) scheduledTask -> task.run(), delayTicks); + } catch (Exception e) { + e.printStackTrace(); + fallbackScheduler().runTaskLater(plugin, location, task, delayTicks); + } + } + + @Override + public boolean isOnMainThread() { + if (!FOLIA_AVAILABLE) { + return fallbackScheduler().isOnMainThread(); + } + + // On Folia, there is no single main thread - we're always on a valid region thread + // if we're executing game logic + return true; + } + + /** + * Gets the fallback scheduler adapter. + * + * @return The fallback scheduler adapter + */ + private SchedulerAdapter fallbackScheduler() { + return new BukkitSchedulerAdapter(); + } +} diff --git a/api/src/main/java/de/rapha149/signgui/util/scheduler/SchedulerFactory.java b/api/src/main/java/de/rapha149/signgui/util/scheduler/SchedulerFactory.java new file mode 100644 index 0000000..cfd6b92 --- /dev/null +++ b/api/src/main/java/de/rapha149/signgui/util/scheduler/SchedulerFactory.java @@ -0,0 +1,38 @@ +package de.rapha149.signgui.util.scheduler; + +import de.rapha149.signgui.util.PlatformDetector; +import de.rapha149.signgui.util.SchedulerAdapter; + +/** + * Factory class for creating appropriate scheduler adapters based on the server platform. + */ +public class SchedulerFactory { + + private static SchedulerAdapter cachedAdapter; + + /** + * Gets the appropriate scheduler adapter for the current platform. + * + * @return The scheduler adapter + */ + public static SchedulerAdapter getScheduler() { + if (cachedAdapter != null) { + return cachedAdapter; + } + + if (PlatformDetector.isFolia()) { + cachedAdapter = new FoliaSchedulerAdapter(); + } else { + cachedAdapter = new BukkitSchedulerAdapter(); + } + + return cachedAdapter; + } + + /** + * Resets the cached scheduler adapter. Useful for testing or reloading. + */ + public static void reset() { + cachedAdapter = null; + } +} diff --git a/pom.xml b/pom.xml index 12a1556..ecbf170 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,10 @@ nms-repo https://repo.codemc.org/repository/nms/ + + papermc + https://repo.papermc.io/repository/maven-public/ + From 827f0c3f2f2c979883c07397aa9865f9325adf46 Mon Sep 17 00:00:00 2001 From: OPmasterLEO <98689894+OPmasterLEO@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:39:16 +0100 Subject: [PATCH 2/3] Update Maven compiler and plugin versions --- 1_17_R1/pom.xml | 2 +- 1_18_R1/pom.xml | 2 +- 1_18_R2/pom.xml | 2 +- 1_19_R1/pom.xml | 2 +- 1_19_R2/pom.xml | 2 +- 1_19_R3/pom.xml | 2 +- 1_20_R1/pom.xml | 2 +- 1_20_R2/pom.xml | 2 +- 1_20_R3/pom.xml | 2 +- 1_20_R4/pom.xml | 2 +- 1_21_R1/pom.xml | 13 +++++++++---- 1_21_R2/pom.xml | 2 +- 1_21_R3/pom.xml | 2 +- 1_21_R4/pom.xml | 2 +- 1_21_R5/pom.xml | 2 +- Mojang1_20_R4/pom.xml | 4 ++-- Mojang1_21_R1/pom.xml | 4 ++-- Mojang1_21_R2/pom.xml | 4 ++-- Mojang1_21_R3/pom.xml | 4 ++-- Mojang1_21_R4/pom.xml | 4 ++-- Mojang1_21_R5/pom.xml | 4 ++-- pom.xml | 2 +- 22 files changed, 36 insertions(+), 31 deletions(-) diff --git a/1_17_R1/pom.xml b/1_17_R1/pom.xml index 665a245..954521a 100644 --- a/1_17_R1/pom.xml +++ b/1_17_R1/pom.xml @@ -35,7 +35,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 16 16 diff --git a/1_18_R1/pom.xml b/1_18_R1/pom.xml index 114afbe..aaf2a97 100644 --- a/1_18_R1/pom.xml +++ b/1_18_R1/pom.xml @@ -35,7 +35,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 17 17 diff --git a/1_18_R2/pom.xml b/1_18_R2/pom.xml index b307410..1deaefe 100644 --- a/1_18_R2/pom.xml +++ b/1_18_R2/pom.xml @@ -35,7 +35,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 17 17 diff --git a/1_19_R1/pom.xml b/1_19_R1/pom.xml index 09fdbfe..8617a6a 100644 --- a/1_19_R1/pom.xml +++ b/1_19_R1/pom.xml @@ -35,7 +35,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 17 17 diff --git a/1_19_R2/pom.xml b/1_19_R2/pom.xml index 6af12bc..d0595f4 100644 --- a/1_19_R2/pom.xml +++ b/1_19_R2/pom.xml @@ -35,7 +35,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 17 17 diff --git a/1_19_R3/pom.xml b/1_19_R3/pom.xml index dde640b..5023cc5 100644 --- a/1_19_R3/pom.xml +++ b/1_19_R3/pom.xml @@ -35,7 +35,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 17 17 diff --git a/1_20_R1/pom.xml b/1_20_R1/pom.xml index 5b5a823..dbaf75b 100644 --- a/1_20_R1/pom.xml +++ b/1_20_R1/pom.xml @@ -35,7 +35,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 17 17 diff --git a/1_20_R2/pom.xml b/1_20_R2/pom.xml index 050a37f..06181d0 100644 --- a/1_20_R2/pom.xml +++ b/1_20_R2/pom.xml @@ -35,7 +35,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 17 17 diff --git a/1_20_R3/pom.xml b/1_20_R3/pom.xml index 83209f5..822307f 100644 --- a/1_20_R3/pom.xml +++ b/1_20_R3/pom.xml @@ -35,7 +35,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 17 17 diff --git a/1_20_R4/pom.xml b/1_20_R4/pom.xml index ea7ba53..710e10b 100644 --- a/1_20_R4/pom.xml +++ b/1_20_R4/pom.xml @@ -35,7 +35,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 21 21 diff --git a/1_21_R1/pom.xml b/1_21_R1/pom.xml index a29ec80..9220004 100644 --- a/1_21_R1/pom.xml +++ b/1_21_R1/pom.xml @@ -17,8 +17,8 @@ - org.spigotmc - spigot + io.papermc.paper + paper-api 1.21.1-R0.1-SNAPSHOT provided @@ -29,13 +29,18 @@ provided - + + + papermc + https://repo.papermc.io/repository/maven-public/ + + org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 21 21 diff --git a/1_21_R2/pom.xml b/1_21_R2/pom.xml index bd44edb..cacda40 100644 --- a/1_21_R2/pom.xml +++ b/1_21_R2/pom.xml @@ -35,7 +35,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 21 21 diff --git a/1_21_R3/pom.xml b/1_21_R3/pom.xml index f8acfdf..f27a50f 100644 --- a/1_21_R3/pom.xml +++ b/1_21_R3/pom.xml @@ -35,7 +35,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 21 21 diff --git a/1_21_R4/pom.xml b/1_21_R4/pom.xml index b07307a..71a9475 100644 --- a/1_21_R4/pom.xml +++ b/1_21_R4/pom.xml @@ -35,7 +35,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 21 21 diff --git a/1_21_R5/pom.xml b/1_21_R5/pom.xml index 90df437..85c2ffd 100644 --- a/1_21_R5/pom.xml +++ b/1_21_R5/pom.xml @@ -35,7 +35,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 21 21 diff --git a/Mojang1_20_R4/pom.xml b/Mojang1_20_R4/pom.xml index f046623..8f68b64 100644 --- a/Mojang1_20_R4/pom.xml +++ b/Mojang1_20_R4/pom.xml @@ -52,8 +52,8 @@ ca.bkaw paper-nms-maven-plugin - 1.4.7 + 1.4.10 - + \ No newline at end of file diff --git a/Mojang1_21_R1/pom.xml b/Mojang1_21_R1/pom.xml index c52f7d2..fbde605 100644 --- a/Mojang1_21_R1/pom.xml +++ b/Mojang1_21_R1/pom.xml @@ -43,7 +43,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 21 21 @@ -52,7 +52,7 @@ ca.bkaw paper-nms-maven-plugin - 1.4.7 + 1.4.10 diff --git a/Mojang1_21_R2/pom.xml b/Mojang1_21_R2/pom.xml index 4668dda..4a8c04e 100644 --- a/Mojang1_21_R2/pom.xml +++ b/Mojang1_21_R2/pom.xml @@ -43,7 +43,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 21 21 @@ -52,7 +52,7 @@ ca.bkaw paper-nms-maven-plugin - 1.4.7 + 1.4.10 diff --git a/Mojang1_21_R3/pom.xml b/Mojang1_21_R3/pom.xml index 3586b6d..d8077c2 100644 --- a/Mojang1_21_R3/pom.xml +++ b/Mojang1_21_R3/pom.xml @@ -43,7 +43,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 21 21 @@ -52,7 +52,7 @@ ca.bkaw paper-nms-maven-plugin - 1.4.7 + 1.4.10 diff --git a/Mojang1_21_R4/pom.xml b/Mojang1_21_R4/pom.xml index c6d3509..e7e6157 100644 --- a/Mojang1_21_R4/pom.xml +++ b/Mojang1_21_R4/pom.xml @@ -43,7 +43,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 21 21 @@ -52,7 +52,7 @@ ca.bkaw paper-nms-maven-plugin - 1.4.7 + 1.4.10 diff --git a/Mojang1_21_R5/pom.xml b/Mojang1_21_R5/pom.xml index d201d02..4d7c54f 100644 --- a/Mojang1_21_R5/pom.xml +++ b/Mojang1_21_R5/pom.xml @@ -43,7 +43,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 21 21 @@ -52,7 +52,7 @@ ca.bkaw paper-nms-maven-plugin - 1.4.7 + 1.4.10 diff --git a/pom.xml b/pom.xml index ecbf170..4cf25ce 100644 --- a/pom.xml +++ b/pom.xml @@ -71,7 +71,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.1 1.8 1.8 From 9a28246a27351938aa74985192643da32d36a86b Mon Sep 17 00:00:00 2001 From: OPmasterLEO <98689894+OPmasterLEO@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:08:54 +0100 Subject: [PATCH 3/3] Improve Folia scheduler error handling and docs --- README.md | 4 +- .../de/rapha149/signgui/SignGUIBuilder.java | 2 +- .../util/scheduler/FoliaSchedulerAdapter.java | 39 ++++++++++++------- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f128a63..d958dc2 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ try { }) // RECOMMENDED: Call handler synchronously for thread safety - // REQUIRED for Folia, CanvasMC, and Archlight + // REQUIRED for Folia; RECOMMENDED for CanvasMC, Archlight, and other platforms .callHandlerSynchronously(this) // "this" = your JavaPlugin instance // build the SignGUI @@ -147,7 +147,7 @@ try { You don't have to call all methods. Only `setHandler` is mandatory. -**Important for Folia/CanvasMC/Archlight:** Always use `callHandlerSynchronously(plugin)` to ensure thread-safe operation on all platforms. On Folia, this ensures tasks run on the correct region thread. On other platforms, it ensures tasks run on the main thread. +**Important:** `callHandlerSynchronously(plugin)` is **REQUIRED for Folia** and **RECOMMENDED for all other platforms for thread safety**. On Folia, this ensures tasks run on the correct region thread. On other platforms, it ensures tasks run on the main thread. By default, the handler is called by an asynchronous thread. You can change that behaviour by calling the method `callHandlerSynchronously` of the builder. An explanation for the different methods can be found on the [Javadoc](https://javadoc.io/doc/de.rapha149.signgui/signgui). diff --git a/api/src/main/java/de/rapha149/signgui/SignGUIBuilder.java b/api/src/main/java/de/rapha149/signgui/SignGUIBuilder.java index 31b13ad..6efdf85 100644 --- a/api/src/main/java/de/rapha149/signgui/SignGUIBuilder.java +++ b/api/src/main/java/de/rapha149/signgui/SignGUIBuilder.java @@ -170,7 +170,7 @@ public SignGUIBuilder setHandler(SignGUIFinishHandler handler) { /** * If called the handler will be called synchronously by using the appropriate scheduler. * This is required for Folia, CanvasMC, and Archlight support and ensures thread-safe execution. - * On Folia, tasks are scheduled on the player's region thread. + * On Folia, tasks are scheduled via the player's entity scheduler (on the thread owning the player's region). * On Bukkit/Spigot/Paper, tasks are scheduled on the main thread. * * @param plugin Your {@link org.bukkit.plugin.java.JavaPlugin} instance. diff --git a/api/src/main/java/de/rapha149/signgui/util/scheduler/FoliaSchedulerAdapter.java b/api/src/main/java/de/rapha149/signgui/util/scheduler/FoliaSchedulerAdapter.java index e80c230..7d1909b 100644 --- a/api/src/main/java/de/rapha149/signgui/util/scheduler/FoliaSchedulerAdapter.java +++ b/api/src/main/java/de/rapha149/signgui/util/scheduler/FoliaSchedulerAdapter.java @@ -80,9 +80,19 @@ public class FoliaSchedulerAdapter implements SchedulerAdapter { isOwnedByCurrentRegion = regionizedServerClass.getMethod("isOwnedByCurrentRegion", Location.class); available = true; + } catch (ClassNotFoundException e) { + System.err.println("Warning: Required Folia/Paper scheduler class not found while initializing FoliaSchedulerAdapter: " + + e.getMessage()); + System.err.println("Falling back to Bukkit scheduler. This usually indicates a Folia/Paper version incompatibility or that Folia is not present."); + e.printStackTrace(System.err); + } catch (NoSuchMethodException e) { + System.err.println("Warning: Required Folia/Paper scheduler method not found while initializing FoliaSchedulerAdapter: " + + e.getMessage()); + System.err.println("Falling back to Bukkit scheduler. This usually indicates a Folia/Paper or Bukkit API version incompatibility."); + e.printStackTrace(System.err); } catch (Exception e) { - System.err.println("Warning: Folia classes not found, but FoliaSchedulerAdapter was attempted to be used. " + - "This should not happen. Falling back to Bukkit scheduler."); + System.err.println("Warning: Unexpected error while initializing FoliaSchedulerAdapter. Falling back to Bukkit scheduler."); + e.printStackTrace(System.err); } FOLIA_AVAILABLE = available; @@ -154,7 +164,11 @@ public void runTask(JavaPlugin plugin, Location location, Runnable task) { try { boolean isOwned = (boolean) IS_OWNED_BY_CURRENT_REGION_METHOD.invoke(null, location); if (isOwned) { - task.run(); + try { + task.run(); + } catch (Exception ex) { + ex.printStackTrace(); + } return; } @@ -191,14 +205,12 @@ public void runTaskLater(JavaPlugin plugin, Runnable task, long delayTicks) { try { Object globalScheduler = GET_GLOBAL_REGION_SCHEDULER_METHOD.invoke(Bukkit.getServer()); - Object asyncScheduler = GET_ASYNC_SCHEDULER_METHOD.invoke(Bukkit.getServer()); - long delayMillis = delayTicks * 50L; - Method asyncRunDelayed = ASYNC_SCHEDULER_CLASS.getMethod("runDelayed", - org.bukkit.plugin.Plugin.class, java.util.function.Consumer.class, long.class, TimeUnit.class); - asyncRunDelayed.invoke(asyncScheduler, plugin, - (java.util.function.Consumer) scheduledTask -> runTask(plugin, task), - delayMillis, TimeUnit.MILLISECONDS); + Method globalRunDelayed = globalScheduler.getClass().getMethod("runDelayed", + org.bukkit.plugin.Plugin.class, java.util.function.Consumer.class, long.class); + globalRunDelayed.invoke(globalScheduler, plugin, + (java.util.function.Consumer) scheduledTask -> task.run(), + delayTicks); } catch (Exception e) { e.printStackTrace(); fallbackScheduler().runTaskLater(plugin, task, delayTicks); @@ -244,10 +256,9 @@ public boolean isOnMainThread() { if (!FOLIA_AVAILABLE) { return fallbackScheduler().isOnMainThread(); } - - // On Folia, there is no single main thread - we're always on a valid region thread - // if we're executing game logic - return true; + // On Folia, there is no single main thread; use Bukkit's primary thread check, + // which returns true on region/global region threads and false on async threads. + return Bukkit.isPrimaryThread(); } /**