+ * Folia distributes world regions across threads, so synchronous tasks + * must target a specific region. Use the appropriate method based on context: + *
+ * On Folia, {@code EntityScheduler#run} returns {@code null} when the entity + * has already been removed from the world — in that case the task is silently + * dropped, which is the correct behaviour (nothing to update for a dead entity). + * + * @param entity the entity whose region thread to target + * @param runnable the task to execute + */ + public static void runTaskForEntity(@NonNull Entity entity, @NonNull Runnable runnable) { + if (FalchusLibMinecraft.getSoftware() == Software.FOLIA) { + // Returns null if the entity is no longer valid — safe to ignore. + entity.getScheduler().run(plugin, task -> runnable.run(), null); + } else { + Bukkit.getScheduler().runTask(plugin, runnable); + } + } + + /** + * Runs a delayed task. + * + * @param runnable the task + * @param delayTicks delay in server ticks (1 tick ≈ 50 ms) + */ + public static void runTaskLater(@NonNull Runnable runnable, long delayTicks) { + if (FalchusLibMinecraft.getSoftware() == Software.FOLIA) { + Bukkit.getGlobalRegionScheduler().runDelayed(plugin, task -> runnable.run(), Math.max(1, delayTicks)); + } else { + Bukkit.getScheduler().runTaskLater(plugin, runnable, Math.max(1, delayTicks)); + } + } +} diff --git a/src/com/falchus/lib/minecraft/spigot/utils/ServerUtils.java b/src/com/falchus/lib/minecraft/spigot/utils/ServerUtils.java index 6f928905..bf8598fb 100644 --- a/src/com/falchus/lib/minecraft/spigot/utils/ServerUtils.java +++ b/src/com/falchus/lib/minecraft/spigot/utils/ServerUtils.java @@ -87,4 +87,4 @@ public static int getMinorVersion() { public static double[] getRecentTps() { return VersionProvider.get().getRecentTps(); } -} \ No newline at end of file +} diff --git a/src/com/falchus/lib/minecraft/spigot/utils/WorldUtils.java b/src/com/falchus/lib/minecraft/spigot/utils/WorldUtils.java index 2d523a8e..6b5eaf8a 100644 --- a/src/com/falchus/lib/minecraft/spigot/utils/WorldUtils.java +++ b/src/com/falchus/lib/minecraft/spigot/utils/WorldUtils.java @@ -1,71 +1,98 @@ package com.falchus.lib.minecraft.spigot.utils; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.block.Biome; - import com.falchus.lib.minecraft.spigot.enums.GameRule; import com.falchus.lib.minecraft.spigot.enums.Version; import com.falchus.lib.minecraft.spigot.utils.version.VersionProvider; - import lombok.NonNull; import lombok.experimental.UtilityClass; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Biome; @UtilityClass public class WorldUtils { - /** - * Sets a game rule for the given world. - */ - public static void setGameRule(@NonNull World world, @NonNull GameRule gameRule, @NonNull String value) { - world.setGameRuleValue(gameRule.getKey(), value); - } - - /** - * @return World Biomes - */ - public static Object[] getWorldBiomes(@NonNull World world) { - return VersionProvider.get().getWorldBiomes(world); - } - - /** - * @return BiomeBase from a Biome - */ - public static Object getNmsBiome(Biome biome) { + /** + * Sets a game rule for the given world. + *
+ * {@code World.setGameRuleValue(String, String)} was deprecated in Paper 1.18
+ * and removed in Paper 26.x. On 1.13+ we resolve the typed
+ * {@link org.bukkit.GameRule} constant by name and use the non-deprecated
+ * {@code World.setGameRule(GameRule
+ * Folia distributes chunk regions across multiple threads. Any task that
+ * touches world state must run on the owning region's thread. This adapter
+ * extends {@link VersionAdapter_v26_1_R1} and overrides every method that
+ * previously relied on {@code Bukkit.getScheduler()}, replacing them with
+ * the appropriate Folia scheduler via {@link SchedulerUtils}.
+ *
+ * The {@link com.falchus.lib.task.Task} class already uses a plain
+ * {@link java.util.concurrent.ScheduledExecutorService}, so it works on
+ * Folia without changes.
+ */
+public class VersionAdapterFolia extends VersionAdapter_v26_1_R1 {
+
+ /**
+ * Spawns a fake {@code EntityPlayer} for {@code player} and schedules the
+ * deferred player-info removal using Folia's global region scheduler so the
+ * task lands on the correct thread.
+ */
+ @Override
+ public void spawnEntityPlayer(@NonNull Player player, @NonNull Object entityPlayer) {
+ try {
+ addEntityPlayer(player, entityPlayer);
+
+ Object spawn = new ClassInstanceBuilder(
+ packageNms + "PacketPlayOutNamedEntitySpawn",
+ packageNm + "network.protocol.game.ClientboundAddPlayerPacket"
+ ).withParams(
+ Map.of(entityHuman, entityPlayer)
+ ).build();
+ sendPacket(player, spawn);
+
+ Object teleport = new ClassInstanceBuilder(
+ packageNms + "PacketPlayOutEntityTeleport",
+ packageNm + "network.protocol.game.ClientboundPlayerPositionPacket"
+ ).withParams(
+ Map.of(entity, entityPlayer)
+ ).build();
+ sendPacket(player, teleport);
+
+ SchedulerUtils.runTask(() -> removeEntityPlayer(player, entityPlayer));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/falchus/lib/minecraft/spigot/utils/version/VersionProvider.java b/src/com/falchus/lib/minecraft/spigot/utils/version/VersionProvider.java
index e1431b6b..5a4ba2b6 100644
--- a/src/com/falchus/lib/minecraft/spigot/utils/version/VersionProvider.java
+++ b/src/com/falchus/lib/minecraft/spigot/utils/version/VersionProvider.java
@@ -2,9 +2,13 @@
import org.bukkit.Bukkit;
+import com.falchus.lib.minecraft.FalchusLibMinecraft;
+import com.falchus.lib.minecraft.enums.Software;
import com.falchus.lib.minecraft.spigot.enums.Version;
import com.falchus.lib.minecraft.spigot.utils.ServerUtils;
+import com.falchus.lib.minecraft.spigot.utils.version.v1_21_R1.VersionAdapter_v1_21_R1;
import com.falchus.lib.minecraft.spigot.utils.version.v1_9_R1.VersionAdapter_v1_9_R1;
+import com.falchus.lib.minecraft.spigot.utils.version.v26_1_R1.VersionAdapter_v26_1_R1;
import com.falchus.lib.minecraft.spigot.utils.version.v_1_13_R1.VersionAdapter_v_1_13_R1;
import com.falchus.lib.utils.builder.ClassInstanceBuilder;
@@ -17,15 +21,21 @@
@UtilityClass
public class VersionProvider {
- private static IVersionAdapter adapter;
-
- private static IVersionAdapter load() {
- Version version = ServerUtils.getVersion();
- if (version.isAfter(Version.v1_16)) return new VersionAdapterModern();
- if (version.isAfter(Version.v1_12)) return new VersionAdapter_v_1_13_R1();
- if (version.isAfter(Version.v1_8)) return new VersionAdapter_v1_9_R1();
- if (version.isBefore(Version.v1_9)) return new VersionAdapter();
-
+ private static IVersionAdapter adapter;
+
+ private static IVersionAdapter load() {
+ if (FalchusLibMinecraft.getSoftware() == Software.FOLIA) {
+ return new VersionAdapterFolia();
+ }
+
+ Version version = ServerUtils.getVersion();
+ if (version.isAfter(Version.v1_21)) return new VersionAdapter_v26_1_R1();
+ if (version.isAfter(Version.v1_20)) return new VersionAdapter_v1_21_R1();
+ if (version.isAfter(Version.v1_16)) return new VersionAdapterModern();
+ if (version.isAfter(Version.v1_12)) return new VersionAdapter_v_1_13_R1();
+ if (version.isAfter(Version.v1_8)) return new VersionAdapter_v1_9_R1();
+ if (version.isBefore(Version.v1_9)) return new VersionAdapter();
+
try {
String[] parts = Bukkit.getServer().getClass().getPackageName().split("\\.");
if (parts.length >= 4) {
@@ -35,13 +45,13 @@ private static IVersionAdapter load() {
).build();
}
} catch (Exception ignored) {}
- return new VersionAdapter();
- }
-
- public static IVersionAdapter get() {
- if (adapter == null) {
- adapter = load();
- }
- return adapter;
- }
+ return new VersionAdapter();
+ }
+
+ public static IVersionAdapter get() {
+ if (adapter == null) {
+ adapter = load();
+ }
+ return adapter;
+ }
}
diff --git a/src/com/falchus/lib/minecraft/spigot/utils/version/v1_21_R1/VersionAdapter_v1_21_R1.java b/src/com/falchus/lib/minecraft/spigot/utils/version/v1_21_R1/VersionAdapter_v1_21_R1.java
new file mode 100644
index 00000000..0aae77d5
--- /dev/null
+++ b/src/com/falchus/lib/minecraft/spigot/utils/version/v1_21_R1/VersionAdapter_v1_21_R1.java
@@ -0,0 +1,114 @@
+package com.falchus.lib.minecraft.spigot.utils.version.v1_21_R1;
+
+import com.falchus.lib.minecraft.spigot.utils.SchedulerUtils;
+import com.falchus.lib.minecraft.spigot.utils.version.VersionAdapterModern;
+import com.falchus.lib.utils.builder.ClassInstanceBuilder;
+import com.falchus.lib.utils.reflection.ReflectionUtils;
+import lombok.NonNull;
+import org.bukkit.entity.Player;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Adapter for Minecraft 1.21.11.
+ *
+ * Changes vs. {@link VersionAdapterModern}:
+ *
+ * Paper 26.1.1 deprecates most legacy {@code String}-based player API in favour
+ * of Adventure {@link Component}s. This adapter replaces the affected methods
+ * with their Adventure equivalents available directly on {@link Player}:
+ *
+ *
+ */
+public class VersionAdapter_v1_21_R1 extends VersionAdapterModern {
+
+ private static final String PACKET_LISTENER_CLASS =
+ "net.minecraft.server.network.ServerGamePacketListenerImpl";
+
+ /**
+ * Returns recent TPS, resolving the field name used in 1.21.x Paper builds
+ * ({@code recentTps} still present; {@code tickTimes} added as alternative).
+ */
+ @Override
+ public double[] getRecentTps() {
+ try {
+ Object server = getMcServer();
+ Field tpsField = ReflectionUtils.getFirstField(
+ server.getClass(),
+ "recentTps",
+ "tickTimes"
+ );
+ Object value = tpsField.get(server);
+ // tickTimes stores nanoseconds per tick; convert to TPS approximation when needed
+ if (value instanceof long[] times) {
+ double[] tps = new double[times.length];
+ for (int i = 0; i < times.length; i++) {
+ double mspt = times[i] / 1_000_000.0;
+ tps[i] = mspt == 0 ? 20.0 : Math.min(20.0, 1000.0 / mspt);
+ }
+ return tps;
+ }
+ return (double[]) value;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Resolves the send-packet method on 1.21.x where the connection class
+ * may be at a Mojang-mapped path.
+ */
+ @Override
+ public void sendPacket(@NonNull Player player, @NonNull Object packet) {
+ try {
+ Object entityPlayer = getEntityPlayer(player);
+ Object connection = entityPlayer_playerConnection.get(entityPlayer);
+ Method send = ReflectionUtils.getFirstMethod(
+ connection.getClass(),
+ List.of(this.packet),
+ "send",
+ "sendPacket"
+ );
+ send.invoke(connection, packet);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Overrides {@link VersionAdapterModern#spawnEntityPlayer} to use
+ * {@link SchedulerUtils#runTask} so the deferred player-info removal
+ * is scheduled correctly on both Bukkit and Folia.
+ */
+ @Override
+ public void spawnEntityPlayer(@NonNull Player player, @NonNull Object entityPlayer) {
+ try {
+ addEntityPlayer(player, entityPlayer);
+
+ Object spawn = new ClassInstanceBuilder(
+ packageNms + "PacketPlayOutNamedEntitySpawn",
+ packageNm + "network.protocol.game.ClientboundAddPlayerPacket"
+ ).withParams(
+ Map.of(entityHuman, entityPlayer)
+ ).build();
+ sendPacket(player, spawn);
+
+ Object teleport = new ClassInstanceBuilder(
+ packageNms + "PacketPlayOutEntityTeleport",
+ packageNm + "network.protocol.game.ClientboundPlayerPositionPacket"
+ ).withParams(
+ Map.of(entity, entityPlayer)
+ ).build();
+ sendPacket(player, teleport);
+
+ SchedulerUtils.runTask(() -> removeEntityPlayer(player, entityPlayer));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/falchus/lib/minecraft/spigot/utils/version/v26_1_R1/VersionAdapter_v26_1_R1.java b/src/com/falchus/lib/minecraft/spigot/utils/version/v26_1_R1/VersionAdapter_v26_1_R1.java
new file mode 100644
index 00000000..6533d1e1
--- /dev/null
+++ b/src/com/falchus/lib/minecraft/spigot/utils/version/v26_1_R1/VersionAdapter_v26_1_R1.java
@@ -0,0 +1,70 @@
+package com.falchus.lib.minecraft.spigot.utils.version.v26_1_R1;
+
+import com.falchus.lib.minecraft.spigot.enums.Sound;
+import com.falchus.lib.minecraft.spigot.utils.version.v1_21_R1.VersionAdapter_v1_21_R1;
+import com.falchus.lib.minecraft.utils.AdventureUtils;
+import lombok.NonNull;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.title.Title;
+import org.bukkit.Location;
+import org.bukkit.entity.Player;
+
+import java.util.List;
+
+/**
+ * Adapter for Paper 26.1.1 (Minecraft 1.21.x, Paper API v5+).
+ *
+ *
+ * NMS packet sending is inherited unchanged from {@link VersionAdapter_v1_21_R1}.
+ */
+public class VersionAdapter_v26_1_R1 extends VersionAdapter_v1_21_R1 {
+
+ /**
+ * Uses the Adventure {@link Player#showTitle(Title)} API.
+ */
+ @Override
+ public void sendTitle(@NonNull Player player, String title, String subtitle) {
+ String t = title != null ? title : "";
+ String st = subtitle != null ? subtitle : "";
+
+ player.showTitle(Title.title(
+ AdventureUtils.legacy(t),
+ AdventureUtils.legacy(st)
+ ));
+ }
+
+ /**
+ * Uses {@link Player#sendPlayerListHeaderAndFooter(Component, Component)} and
+ * {@link Player#playerListName(Component)}, replacing the deprecated
+ * {@code setPlayerListHeaderFooter} / {@code setPlayerListName} String overloads.
+ */
+ @Override
+ public void sendTablist(@NonNull Player player, List