Skip to content

Commit 7d6385f

Browse files
authored
Merge pull request #100 from ez-plugins/fix/folia-error
fix: Folia runtime RegionScheduler
2 parents 8e8f2cf + e8e3a92 commit 7d6385f

11 files changed

Lines changed: 347 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@ Release tags use the `v` prefix (e.g. `v3.0.2`).
2020

2121
---
2222

23+
## [3.2.2] - 2026-05-12
24+
25+
### Fixed
26+
27+
- `BukkitPlatformScheduler` now handles Folia servers correctly. Previously, running EzRTP on
28+
Folia without the Paper runtime module caused an `UnsupportedOperationException` during plugin
29+
enable because the Bukkit `CraftScheduler` rejects all synchronous task scheduling on Folia.
30+
The scheduler now detects `regionizedRuntime` capabilities and routes all scheduling calls
31+
through Folia's `GlobalRegionScheduler` / `RegionScheduler` via reflection, matching the
32+
behaviour already present in `PaperPlatformScheduler`.
33+
34+
---
35+
2336
## [3.2.1] - 2026-05-11
2437

2538
### Fixed

ezrtp-api/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
<parent>
66
<groupId>com.skyblockexp</groupId>
77
<artifactId>ezrtp-parent</artifactId>
8-
<version>3.2.1</version>
8+
<version>3.2.2</version>
99
<relativePath>../pom.xml</relativePath>
1010
</parent>
1111

1212
<groupId>com.skyblockexp</groupId>
1313
<artifactId>ezrtp-api</artifactId>
14-
<version>3.2.1</version>
14+
<version>3.2.2</version>
1515
<packaging>jar</packaging>
1616
<name>EzRTP API</name>
1717
<description>Lightweight public API for EzRTP intended for third-party plugins.</description>

ezrtp-bukkit/pom.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
<parent>
66
<groupId>com.skyblockexp</groupId>
77
<artifactId>ezrtp-parent</artifactId>
8-
<version>3.2.1</version>
8+
<version>3.2.2</version>
99
<relativePath>../pom.xml</relativePath>
1010
</parent>
1111

1212
<groupId>com.skyblockexp</groupId>
1313
<artifactId>ezrtp-bukkit</artifactId>
14-
<version>3.2.1</version>
14+
<version>3.2.2</version>
1515
<packaging>jar</packaging>
1616
<name>EzRTP (Bukkit)</name>
1717
<description>EzRTP Bukkit-compatible plugin module.</description>
@@ -111,7 +111,7 @@
111111
<dependency>
112112
<groupId>com.skyblockexp</groupId>
113113
<artifactId>ezrtp-common</artifactId>
114-
<version>3.2.1</version>
114+
<version>3.2.2</version>
115115
</dependency>
116116
</dependencies>
117117

ezrtp-bukkit/src/main/java/com/skyblockexp/ezrtp/platform/BukkitPlatformRuntime.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public final class BukkitPlatformRuntime implements PlatformRuntime {
1010

1111
public BukkitPlatformRuntime(PlatformRuntimeCapabilities capabilities, Plugin plugin) {
1212
this.capabilities = capabilities;
13-
this.scheduler = new BukkitPlatformScheduler(plugin);
13+
this.scheduler = new BukkitPlatformScheduler(plugin, capabilities);
1414
}
1515

1616
@Override
Lines changed: 138 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
11
package com.skyblockexp.ezrtp.platform;
22

3+
import org.bukkit.Bukkit;
34
import org.bukkit.World;
45
import org.bukkit.plugin.Plugin;
56

7+
import java.lang.reflect.Method;
8+
69
public final class BukkitPlatformScheduler implements PlatformScheduler {
710

811
private final Plugin plugin;
12+
private final PlatformRuntimeCapabilities capabilities;
913

10-
public BukkitPlatformScheduler(Plugin plugin) {
14+
/**
15+
* Creates a scheduler with explicit capability information.
16+
*
17+
* <p>When {@code capabilities.regionizedRuntime()} is {@code true} (Folia), sync Bukkit
18+
* scheduler calls are replaced with reflection-based Folia scheduler calls. This prevents
19+
* the {@link UnsupportedOperationException} that Folia's {@code CraftScheduler} throws for
20+
* all synchronous tasks.
21+
*/
22+
public BukkitPlatformScheduler(Plugin plugin, PlatformRuntimeCapabilities capabilities) {
1123
this.plugin = plugin;
24+
this.capabilities = capabilities;
1225
}
1326

1427
@Override
@@ -18,31 +31,153 @@ public void executeAsync(Runnable task) {
1831

1932
@Override
2033
public void executeRegion(World world, int chunkX, int chunkZ, Runnable task) {
34+
if (capabilities.regionizedRuntime()) {
35+
if (world != null) {
36+
invokeRegionTask(world, chunkX, chunkZ, task);
37+
} else {
38+
invokeGlobalRun(task);
39+
}
40+
return;
41+
}
2142
plugin.getServer().getScheduler().runTask(plugin, task);
2243
}
2344

2445
@Override
2546
public PlatformTask scheduleRepeating(Runnable task, long delayTicks, long periodTicks) {
47+
if (capabilities.regionizedRuntime()) {
48+
PlatformTask foliaTask = scheduleGlobalRepeating(task, delayTicks, periodTicks);
49+
if (foliaTask != null) {
50+
return foliaTask;
51+
}
52+
// Folia detected but reflection unavailable — return a no-op rather than calling the
53+
// Bukkit scheduler which Folia forbids.
54+
return () -> {};
55+
}
2656
org.bukkit.scheduler.BukkitTask bukkitTask = plugin.getServer().getScheduler()
2757
.runTaskTimer(plugin, task, delayTicks, periodTicks);
2858
return bukkitTask::cancel;
2959
}
3060

3161
@Override
3262
public void executeGlobal(Runnable task) {
63+
if (capabilities.regionizedRuntime()) {
64+
invokeGlobalRun(task);
65+
return;
66+
}
3367
plugin.getServer().getScheduler().runTask(plugin, task);
3468
}
3569

3670
@Override
3771
public PlatformTask executeGlobalDelayed(Runnable task, long delayTicks) {
38-
org.bukkit.scheduler.BukkitTask bukkitTask =
72+
if (capabilities.regionizedRuntime()) {
73+
PlatformTask foliaTask = invokeGlobalRunDelayed(task, delayTicks);
74+
if (foliaTask != null) {
75+
return foliaTask;
76+
}
77+
return () -> {};
78+
}
79+
org.bukkit.scheduler.BukkitTask bukkit =
3980
plugin.getServer().getScheduler().runTaskLater(plugin, task, delayTicks);
40-
return bukkitTask::cancel;
81+
return bukkit::cancel;
4182
}
4283

4384
@Override
4485
public void executeRegionDelayed(
4586
World world, int chunkX, int chunkZ, Runnable task, long delayTicks) {
87+
if (capabilities.regionizedRuntime()) {
88+
if (world != null) {
89+
invokeRegionDelayed(world, chunkX, chunkZ, task, delayTicks);
90+
} else {
91+
invokeGlobalRunDelayed(task, delayTicks);
92+
}
93+
return;
94+
}
4695
plugin.getServer().getScheduler().runTaskLater(plugin, task, delayTicks);
4796
}
97+
98+
private boolean invokeGlobalRun(Runnable task) {
99+
try {
100+
Object globalScheduler = Bukkit.class.getMethod("getGlobalRegionScheduler").invoke(null);
101+
Method run = globalScheduler.getClass().getMethod(
102+
"run", Plugin.class, java.util.function.Consumer.class);
103+
run.invoke(globalScheduler, plugin,
104+
(java.util.function.Consumer<Object>) ignored -> task.run());
105+
return true;
106+
} catch (ReflectiveOperationException ignored) {
107+
return false;
108+
}
109+
}
110+
111+
private PlatformTask invokeGlobalRunDelayed(Runnable task, long delayTicks) {
112+
try {
113+
Object globalScheduler = Bukkit.class.getMethod("getGlobalRegionScheduler").invoke(null);
114+
Method runDelayed = globalScheduler.getClass().getMethod(
115+
"runDelayed", Plugin.class, java.util.function.Consumer.class, long.class);
116+
Object scheduledTask = runDelayed.invoke(globalScheduler, plugin,
117+
(java.util.function.Consumer<Object>) ignored -> task.run(), delayTicks);
118+
Method cancel = scheduledTask.getClass().getMethod("cancel");
119+
return () -> {
120+
try {
121+
cancel.invoke(scheduledTask);
122+
} catch (ReflectiveOperationException ignored) {
123+
}
124+
};
125+
} catch (ReflectiveOperationException ignored) {
126+
return null;
127+
}
128+
}
129+
130+
private boolean invokeRegionTask(World world, int chunkX, int chunkZ, Runnable task) {
131+
try {
132+
Object regionScheduler = Bukkit.class.getMethod("getRegionScheduler").invoke(null);
133+
Method run = regionScheduler.getClass().getMethod(
134+
"run", Plugin.class, World.class, int.class, int.class,
135+
java.util.function.Consumer.class);
136+
run.invoke(regionScheduler, plugin, world, chunkX, chunkZ,
137+
(java.util.function.Consumer<Object>) ignored -> task.run());
138+
return true;
139+
} catch (ReflectiveOperationException ignored) {
140+
return false;
141+
}
142+
}
143+
144+
private boolean invokeRegionDelayed(
145+
World world, int chunkX, int chunkZ, Runnable task, long delayTicks) {
146+
try {
147+
Object regionScheduler = Bukkit.class.getMethod("getRegionScheduler").invoke(null);
148+
Method runDelayed = regionScheduler.getClass().getMethod(
149+
"runDelayed", Plugin.class, World.class, int.class, int.class,
150+
java.util.function.Consumer.class, long.class);
151+
runDelayed.invoke(regionScheduler, plugin, world, chunkX, chunkZ,
152+
(java.util.function.Consumer<Object>) ignored -> task.run(), delayTicks);
153+
return true;
154+
} catch (ReflectiveOperationException ignored) {
155+
return false;
156+
}
157+
}
158+
159+
private PlatformTask scheduleGlobalRepeating(Runnable task, long delayTicks, long periodTicks) {
160+
try {
161+
Object globalRegionScheduler = Bukkit.class.getMethod("getGlobalRegionScheduler").invoke(null);
162+
Method runAtFixedRate = globalRegionScheduler.getClass().getMethod(
163+
"runAtFixedRate",
164+
Plugin.class,
165+
java.util.function.Consumer.class,
166+
long.class,
167+
long.class);
168+
Object scheduledTask = runAtFixedRate.invoke(globalRegionScheduler, plugin,
169+
(java.util.function.Consumer<Object>) ignored -> task.run(),
170+
delayTicks,
171+
periodTicks);
172+
Method cancel = scheduledTask.getClass().getMethod("cancel");
173+
return () -> {
174+
try {
175+
cancel.invoke(scheduledTask);
176+
} catch (ReflectiveOperationException ignored) {
177+
}
178+
};
179+
} catch (ReflectiveOperationException ignored) {
180+
return null;
181+
}
182+
}
48183
}

0 commit comments

Comments
 (0)