From 08f1f71430ad77cb06953f0e00ad83c64a00cf68 Mon Sep 17 00:00:00 2001 From: Lewis Gibson <12851394+lewisgibson@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:45:00 +0100 Subject: [PATCH] Use PaperLib's asynchronous teleports and chunk loads on 26.x PaperLib 1.0.6 reads the server version with a regex that only accepts a single digit major version, so on 26.x it sees version 0 and falls back to its synchronous handlers. Every PaperLib.teleportAsync, getChunkAtAsync and getBedSpawnLocationAsync call in EssentialsX then ran a blocking teleport or chunk load instead of the asynchronous Paper API. When PaperLib cannot read the version of a server that is newer than 1.13, install an environment that selects the handlers PaperLib would have chosen for a modern Paper. Co-authored-by: Raw2d <255907405+Raw2d@users.noreply.github.com> --- .../com/earth2me/essentials/Essentials.java | 5 ++ .../utils/ModernPaperEnvironment.java | 23 +++++++ .../utils/ModernPaperEnvironmentTest.java | 62 +++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 Essentials/src/main/java/com/earth2me/essentials/utils/ModernPaperEnvironment.java create mode 100644 Essentials/src/test/java/com/earth2me/essentials/utils/ModernPaperEnvironmentTest.java diff --git a/Essentials/src/main/java/com/earth2me/essentials/Essentials.java b/Essentials/src/main/java/com/earth2me/essentials/Essentials.java index 1629f8e432..d6ad1f93e5 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/Essentials.java +++ b/Essentials/src/main/java/com/earth2me/essentials/Essentials.java @@ -46,6 +46,7 @@ import com.earth2me.essentials.updatecheck.UpdateChecker; import com.earth2me.essentials.userstorage.ModernUserMap; import com.earth2me.essentials.utils.FormatUtil; +import com.earth2me.essentials.utils.ModernPaperEnvironment; import com.earth2me.essentials.utils.PasteUtil; import com.earth2me.essentials.utils.VersionUtil; import io.papermc.lib.PaperLib; @@ -261,6 +262,10 @@ public void onEnable() { getLogger().severe(getAdventureFacet().miniToLegacy(tlLiteral("serverSnapshot"))); } + if (PaperLib.isPaper() && !PaperLib.isVersion(13) && VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_13_0_R01)) { + PaperLib.setCustomEnvironment(new ModernPaperEnvironment()); + } + final PluginManager pm = getServer().getPluginManager(); for (final Plugin plugin : pm.getPlugins()) { if (plugin.getDescription().getName().startsWith("Essentials") && !plugin.getDescription().getVersion().equals(this.getDescription().getVersion()) && !plugin.getDescription().getName().equals("EssentialsAntiCheat")) { diff --git a/Essentials/src/main/java/com/earth2me/essentials/utils/ModernPaperEnvironment.java b/Essentials/src/main/java/com/earth2me/essentials/utils/ModernPaperEnvironment.java new file mode 100644 index 0000000000..1311f223d9 --- /dev/null +++ b/Essentials/src/main/java/com/earth2me/essentials/utils/ModernPaperEnvironment.java @@ -0,0 +1,23 @@ +package com.earth2me.essentials.utils; + +import io.papermc.lib.environments.PaperEnvironment; +import io.papermc.lib.features.asyncchunks.AsyncChunksPaper_15; +import io.papermc.lib.features.asyncteleport.AsyncTeleportPaper_13; +import io.papermc.lib.features.bedspawnlocation.BedSpawnLocationPaper; +import io.papermc.lib.features.chunkisgenerated.ChunkIsGeneratedApiExists; + +/** + * A PaperLib environment for Paper servers whose version PaperLib cannot read. + *

+ * PaperLib 1.0.6 expects a single digit major version in {@code Bukkit#getVersion()}, so it reads 26.x as version 0 and + * selects its synchronous fallbacks for chunk loading, teleporting and bed lookups. All of those exist on every Paper + * version that reports a year based version. + */ +public class ModernPaperEnvironment extends PaperEnvironment { + public ModernPaperEnvironment() { + asyncChunksHandler = new AsyncChunksPaper_15(); + asyncTeleportHandler = new AsyncTeleportPaper_13(); + isGeneratedHandler = new ChunkIsGeneratedApiExists(); + bedSpawnLocationHandler = new BedSpawnLocationPaper(); + } +} diff --git a/Essentials/src/test/java/com/earth2me/essentials/utils/ModernPaperEnvironmentTest.java b/Essentials/src/test/java/com/earth2me/essentials/utils/ModernPaperEnvironmentTest.java new file mode 100644 index 0000000000..7bb08cfb76 --- /dev/null +++ b/Essentials/src/test/java/com/earth2me/essentials/utils/ModernPaperEnvironmentTest.java @@ -0,0 +1,62 @@ +package com.earth2me.essentials.utils; + +import io.papermc.lib.environments.PaperEnvironment; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class ModernPaperEnvironmentTest { + private static final String YEAR_VERSION = "26.3-140-main@a1b2c3d (2026-09-30T10:00:00Z) (MC: 26.3)"; + + @Test + public void testPaperLibFallsBackToSynchronousTeleportOnYearVersions() { + // Documents why ModernPaperEnvironment exists: PaperLib itself picks the synchronous handlers here. + withVersion(YEAR_VERSION, PaperEnvironment::new, env -> { + final Entity entity = mock(Entity.class); + final Location location = new Location(mock(World.class), 0, 64, 0); + env.teleport(entity, location, TeleportCause.PLUGIN); + verify(entity).teleport(location, TeleportCause.PLUGIN); + verify(entity, never()).teleportAsync(any(Location.class), any(TeleportCause.class)); + }); + } + + @Test + public void testTeleportsAsynchronouslyOnYearVersions() { + withVersion(YEAR_VERSION, ModernPaperEnvironment::new, env -> { + final Entity entity = mock(Entity.class); + final Location location = new Location(mock(World.class), 0, 64, 0); + env.teleport(entity, location, TeleportCause.PLUGIN); + verify(entity).teleportAsync(location, TeleportCause.PLUGIN); + verify(entity, never()).teleport(any(Location.class), any(TeleportCause.class)); + }); + } + + @Test + public void testLoadsChunksAsynchronouslyOnYearVersions() { + withVersion(YEAR_VERSION, ModernPaperEnvironment::new, env -> { + final World world = mock(World.class); + env.getChunkAtAsync(world, 1, 2, true); + verify(world).getChunkAtAsync(1, 2, true, false); + }); + } + + private static void withVersion(final String version, final Supplier create, final Consumer test) { + try (MockedStatic bukkit = mockStatic(Bukkit.class)) { + bukkit.when(Bukkit::getVersion).thenReturn(version); + test.accept(create.get()); + } + } +}