Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ Release tags use the `v` prefix (e.g. `v3.0.2`).

---

## [3.4.3] - 2026-07-27

### Fixed

- Fixed `/rtp` intermittently failing (observed on Purpur) with `CompletionException - java.lang.IllegalArgumentException: argument type mismatch` during async location search. The Folia region/global-scheduler reflection bridges in `PaperPlatformScheduler` and `BukkitPlatformScheduler` only caught `ReflectiveOperationException` around `Method.invoke()` calls; they now also catch `RuntimeException` and fall back to the standard Bukkit scheduler instead of letting the exception propagate. This can occur even on non-Folia servers whose classpath carries the Folia regionized-runtime marker class.
- `BukkitPlatformScheduler#executeRegion`/`executeGlobal`/`executeGlobalDelayed`/`executeRegionDelayed` now consistently fall back to the standard Bukkit scheduler when the reflective Folia scheduler call fails, instead of silently dropping the task (matching existing `PaperPlatformScheduler` behavior).

### Added

- Regression test coverage for the scheduler reflection-fallback fix:
- `PaperPlatformSchedulerReflectionFallbackTest` and `BukkitPlatformSchedulerReflectionFallbackTest` simulate a broken region/global-scheduler reflection call and verify the submitted task still runs via the standard scheduler fallback.
- `PlatformRuntimeCapabilities.PURPUR_REGIONIZED` constant documenting the exact capability combination that triggered the bug.

---

## [3.4.2] - 2026-07-07

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions ezrtp-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
<parent>
<groupId>com.skyblockexp</groupId>
<artifactId>ezrtp-parent</artifactId>
<version>3.4.2</version>
<version>3.4.3</version>
<relativePath>../pom.xml</relativePath>
</parent>

<groupId>com.skyblockexp</groupId>
<artifactId>ezrtp-api</artifactId>
<version>3.4.2</version>
<version>3.4.3</version>
<packaging>jar</packaging>
<name>EzRTP API</name>
<description>Lightweight public API for EzRTP intended for third-party plugins.</description>
Expand Down
6 changes: 3 additions & 3 deletions ezrtp-bukkit/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
<parent>
<groupId>com.skyblockexp</groupId>
<artifactId>ezrtp-parent</artifactId>
<version>3.4.2</version>
<version>3.4.3</version>
<relativePath>../pom.xml</relativePath>
</parent>

<groupId>com.skyblockexp</groupId>
<artifactId>ezrtp-bukkit</artifactId>
<version>3.4.2</version>
<version>3.4.3</version>
<packaging>jar</packaging>
<name>EzRTP (Bukkit)</name>
<description>EzRTP Bukkit-compatible plugin module.</description>
Expand Down Expand Up @@ -111,7 +111,7 @@
<dependency>
<groupId>com.skyblockexp</groupId>
<artifactId>ezrtp-common</artifactId>
<version>3.4.2</version>
<version>3.4.3</version>
</dependency>
</dependencies>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import java.lang.reflect.Method;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;

public final class BukkitPlatformScheduler implements PlatformScheduler {

Expand Down Expand Up @@ -35,12 +36,15 @@ public void executeAsync(Runnable task) {
@Override
public void executeRegion(World world, int chunkX, int chunkZ, Runnable task) {
if (capabilities.regionizedRuntime()) {
if (world != null) {
invokeRegionTask(world, chunkX, chunkZ, task);
} else {
invokeGlobalRun(task);
boolean handled = world != null
? invokeRegionTask(world, chunkX, chunkZ, task)
: invokeGlobalRun(task);
if (handled) {
return;
}
return;
// Regionized runtime was detected but the reflective call failed unexpectedly
// (e.g. an API signature mismatch). Falling through to the standard scheduler
// is safer than silently dropping the task.
}
plugin.getServer().getScheduler().runTask(plugin, task);
}
Expand All @@ -63,8 +67,7 @@ public PlatformTask scheduleRepeating(Runnable task, long delayTicks, long perio

@Override
public void executeGlobal(Runnable task) {
if (capabilities.regionizedRuntime()) {
invokeGlobalRun(task);
if (capabilities.regionizedRuntime() && invokeGlobalRun(task)) {
return;
}
plugin.getServer().getScheduler().runTask(plugin, task);
Expand All @@ -77,7 +80,8 @@ public PlatformTask executeGlobalDelayed(Runnable task, long delayTicks) {
if (foliaTask != null) {
return foliaTask;
}
return () -> {};
// Reflective call failed unexpectedly; fall through to the standard scheduler
// rather than returning a no-op that would silently drop the task.
}
org.bukkit.scheduler.BukkitTask bukkit =
plugin.getServer().getScheduler().runTaskLater(plugin, task, delayTicks);
Expand All @@ -88,12 +92,12 @@ public PlatformTask executeGlobalDelayed(Runnable task, long delayTicks) {
public void executeRegionDelayed(
World world, int chunkX, int chunkZ, Runnable task, long delayTicks) {
if (capabilities.regionizedRuntime()) {
if (world != null) {
invokeRegionDelayed(world, chunkX, chunkZ, task, delayTicks);
} else {
invokeGlobalRunDelayed(task, delayTicks);
boolean handled = world != null
? invokeRegionDelayed(world, chunkX, chunkZ, task, delayTicks)
: invokeGlobalRunDelayed(task, delayTicks) != null;
if (handled) {
return;
}
return;
}
plugin.getServer().getScheduler().runTaskLater(plugin, task, delayTicks);
}
Expand All @@ -114,8 +118,11 @@ private CompletableFuture<Boolean> invokeTeleportAsync(Player player, Location d
try {
Method method = player.getClass().getMethod("teleportAsync", Location.class);
return (CompletableFuture<Boolean>) method.invoke(player, destination);
} catch (ReflectiveOperationException ignored) {
// teleportAsync not available (shouldn't happen on Folia); fall back to sync.
} catch (ReflectiveOperationException | RuntimeException ex) {
// teleportAsync not available, or the reflective call failed due to an API
// signature change on a newer/forked server; fall back to sync teleport rather
// than propagating the failure into the async RTP search chain.
logReflectionFallback("teleportAsync", ex);
return CompletableFuture.completedFuture(player.teleport(destination));
}
}
Expand All @@ -128,7 +135,8 @@ private boolean invokeGlobalRun(Runnable task) {
run.invoke(globalScheduler, plugin,
(java.util.function.Consumer<Object>) ignored -> task.run());
return true;
} catch (ReflectiveOperationException ignored) {
} catch (ReflectiveOperationException | RuntimeException ex) {
logReflectionFallback("getGlobalRegionScheduler#run", ex);
return false;
}
}
Expand All @@ -144,10 +152,11 @@ private PlatformTask invokeGlobalRunDelayed(Runnable task, long delayTicks) {
return () -> {
try {
cancel.invoke(scheduledTask);
} catch (ReflectiveOperationException ignored) {
} catch (ReflectiveOperationException | RuntimeException ignored) {
}
};
} catch (ReflectiveOperationException ignored) {
} catch (ReflectiveOperationException | RuntimeException ex) {
logReflectionFallback("getGlobalRegionScheduler#runDelayed", ex);
return null;
}
}
Expand All @@ -161,7 +170,8 @@ private boolean invokeRegionTask(World world, int chunkX, int chunkZ, Runnable t
run.invoke(regionScheduler, plugin, world, chunkX, chunkZ,
(java.util.function.Consumer<Object>) ignored -> task.run());
return true;
} catch (ReflectiveOperationException ignored) {
} catch (ReflectiveOperationException | RuntimeException ex) {
logReflectionFallback("getRegionScheduler#run", ex);
return false;
}
}
Expand All @@ -176,7 +186,8 @@ private boolean invokeRegionDelayed(
runDelayed.invoke(regionScheduler, plugin, world, chunkX, chunkZ,
(java.util.function.Consumer<Object>) ignored -> task.run(), delayTicks);
return true;
} catch (ReflectiveOperationException ignored) {
} catch (ReflectiveOperationException | RuntimeException ex) {
logReflectionFallback("getRegionScheduler#runDelayed", ex);
return false;
}
}
Expand All @@ -198,11 +209,27 @@ private PlatformTask scheduleGlobalRepeating(Runnable task, long delayTicks, lon
return () -> {
try {
cancel.invoke(scheduledTask);
} catch (ReflectiveOperationException ignored) {
} catch (ReflectiveOperationException | RuntimeException ignored) {
}
};
} catch (ReflectiveOperationException ignored) {
} catch (ReflectiveOperationException | RuntimeException ex) {
logReflectionFallback("getGlobalRegionScheduler#runAtFixedRate", ex);
return null;
}
}

/**
* Logs a fine-level diagnostic when a Folia/region-scheduler reflection call fails.
*
* <p>Reflective calls into region-scheduler APIs can fail with more than just
* {@link ReflectiveOperationException} — a signature change on a newer or forked
* server (e.g. Purpur) can surface as {@link IllegalArgumentException} ("argument
* type mismatch") from {@link Method#invoke}. These are caught broadly so a single
* unexpected API drift falls back to the standard scheduler instead of breaking
* the caller (notably the async RTP location search).
*/
private void logReflectionFallback(String operation, Throwable ex) {
plugin.getLogger().log(Level.FINE, "EzRTP: region-scheduler reflection call " + operation
+ " failed, falling back to standard scheduler", ex);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class BukkitPlatformSchedulerFoliaTest {
void setUp() {
when(plugin.getServer()).thenReturn(server);
when(server.getScheduler()).thenReturn(bukkitScheduler);
when(plugin.getLogger()).thenReturn(java.util.logging.Logger.getLogger("test"));
}

// --- scheduleRepeating ---
Expand Down Expand Up @@ -79,7 +80,9 @@ void scheduleRepeating_withoutFoliaAwareness_propagatesUnsupportedOperationExcep
*/
@Test
void scheduleRepeating_withFoliaCapabilities_doesNotThrow() {
// No stub needed: the fixed implementation never reaches the Bukkit scheduler on Folia.
// scheduleRepeating intentionally stays a no-op on reflection failure (rather than
// falling back to the standard scheduler) since Folia forbids synchronous scheduling
// outright; see the comment in BukkitPlatformScheduler#scheduleRepeating.
BukkitPlatformScheduler scheduler =
new BukkitPlatformScheduler(plugin, PlatformRuntimeCapabilities.PAPER_FOLIA);

Expand Down Expand Up @@ -110,11 +113,16 @@ void scheduleRepeating_withBukkitCapabilities_delegatesToBukkitScheduler() {

@Test
void executeGlobal_withFoliaCapabilities_doesNotThrow() {
// No stub: the fix never reaches the Bukkit scheduler when regionizedRuntime=true.
// Reflection naturally fails in the test JVM (no Folia classes present), so this
// now falls through to the standard Bukkit scheduler, matching PaperPlatformScheduler.
BukkitTask mockTask = mock(BukkitTask.class);
when(bukkitScheduler.runTask(any(Plugin.class), any(Runnable.class))).thenReturn(mockTask);

BukkitPlatformScheduler scheduler =
new BukkitPlatformScheduler(plugin, PlatformRuntimeCapabilities.PAPER_FOLIA);

assertDoesNotThrow(() -> scheduler.executeGlobal(() -> {}));
verify(bukkitScheduler).runTask(eq(plugin), any(Runnable.class));
}

@Test
Expand All @@ -134,7 +142,12 @@ void executeGlobal_withBukkitCapabilities_delegatesToBukkitScheduler() {

@Test
void executeGlobalDelayed_withFoliaCapabilities_doesNotThrow() {
// No stub: the fix never reaches the Bukkit scheduler when regionizedRuntime=true.
// Reflection naturally fails in the test JVM (no Folia classes present), so this
// now falls through to the standard Bukkit scheduler, matching PaperPlatformScheduler.
BukkitTask mockTask = mock(BukkitTask.class);
when(bukkitScheduler.runTaskLater(any(Plugin.class), any(Runnable.class), anyLong()))
.thenReturn(mockTask);

BukkitPlatformScheduler scheduler =
new BukkitPlatformScheduler(plugin, PlatformRuntimeCapabilities.PAPER_FOLIA);

Expand All @@ -161,22 +174,33 @@ void executeGlobalDelayed_withBukkitCapabilities_delegatesToBukkitScheduler() {

@Test
void executeRegion_withFoliaCapabilities_doesNotThrow() {
// No stub: the fix never reaches the Bukkit scheduler when regionizedRuntime=true.
// Reflection naturally fails in the test JVM (no Folia classes present), so this
// now falls through to the standard Bukkit scheduler, matching PaperPlatformScheduler.
BukkitTask mockTask = mock(BukkitTask.class);
when(bukkitScheduler.runTask(any(Plugin.class), any(Runnable.class))).thenReturn(mockTask);

BukkitPlatformScheduler scheduler =
new BukkitPlatformScheduler(plugin, PlatformRuntimeCapabilities.PAPER_FOLIA);

assertDoesNotThrow(() -> scheduler.executeRegion(null, 0, 0, () -> {}));
verify(bukkitScheduler).runTask(eq(plugin), any(Runnable.class));
}

// --- executeRegionDelayed ---

@Test
void executeRegionDelayed_withFoliaCapabilities_doesNotThrow() {
// No stub: the fix never reaches the Bukkit scheduler when regionizedRuntime=true.
// Reflection naturally fails in the test JVM (no Folia classes present), so this
// now falls through to the standard Bukkit scheduler, matching PaperPlatformScheduler.
BukkitTask mockTask = mock(BukkitTask.class);
when(bukkitScheduler.runTaskLater(any(Plugin.class), any(Runnable.class), anyLong()))
.thenReturn(mockTask);

BukkitPlatformScheduler scheduler =
new BukkitPlatformScheduler(plugin, PlatformRuntimeCapabilities.PAPER_FOLIA);

assertDoesNotThrow(() -> scheduler.executeRegionDelayed(null, 0, 0, () -> {}, 20L));
verify(bukkitScheduler).runTaskLater(eq(plugin), any(Runnable.class), eq(20L));
}

// --- teleportAsync ---
Expand Down
Loading
Loading