From a2654da0a9e0c4414777e6785ed1a39c9d97922c Mon Sep 17 00:00:00 2001 From: EanglyTe Date: Wed, 18 Mar 2026 19:43:31 +0700 Subject: [PATCH] Add Folia support to Bukkit module --- .../serverapi/bukkit/FeatherBukkitPlugin.java | 12 ++ .../serverapi/bukkit/PlayerTaskScheduler.java | 156 ++++++++++++++++++ .../messaging/BukkitMessagingService.java | 29 ++-- .../bukkit/player/BukkitFeatherPlayer.java | 8 +- .../bukkit/player/BukkitPlayerService.java | 4 +- .../bukkit/player/PlayerMessageHandler.java | 5 +- .../serverapi/bukkit/ui/BukkitUIService.java | 9 +- .../bukkit/ui/rpc/BukkitRpcResponse.java | 18 +- .../serverapi/bukkit/ui/rpc/RpcService.java | 21 ++- .../bukkit/update/UpdateNotifier.java | 15 +- bukkit/src/main/resources/plugin.yml | 1 + 11 files changed, 225 insertions(+), 53 deletions(-) create mode 100644 bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/PlayerTaskScheduler.java diff --git a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/FeatherBukkitPlugin.java b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/FeatherBukkitPlugin.java index 9a0cd33..c008a49 100644 --- a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/FeatherBukkitPlugin.java +++ b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/FeatherBukkitPlugin.java @@ -9,9 +9,12 @@ import net.digitalingot.feather.serverapi.bukkit.ui.rpc.RpcService; import net.digitalingot.feather.serverapi.bukkit.update.UpdateNotifier; import net.digitalingot.feather.serverapi.bukkit.waypoint.BukkitWaypointService; +import org.jetbrains.annotations.NotNull; import org.bukkit.plugin.java.JavaPlugin; public class FeatherBukkitPlugin extends JavaPlugin { + private PlayerTaskScheduler playerTaskScheduler; + @Override public void onDisable() { super.onDisable(); @@ -19,6 +22,8 @@ public void onDisable() { @Override public void onEnable() { + this.playerTaskScheduler = new PlayerTaskScheduler(this); + BukkitEventService eventService = new BukkitEventService(this); BukkitPlayerService playerService = new BukkitPlayerService(this); @@ -37,4 +42,11 @@ public void onEnable() { super.onEnable(); } + + public @NotNull PlayerTaskScheduler getPlayerTaskScheduler() { + if (this.playerTaskScheduler == null) { + throw new IllegalStateException("Player task scheduler not initialized"); + } + return this.playerTaskScheduler; + } } diff --git a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/PlayerTaskScheduler.java b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/PlayerTaskScheduler.java new file mode 100644 index 0000000..f4a56c3 --- /dev/null +++ b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/PlayerTaskScheduler.java @@ -0,0 +1,156 @@ +package net.digitalingot.feather.serverapi.bukkit; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Objects; +import java.util.function.Consumer; +import org.bukkit.Bukkit; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class PlayerTaskScheduler { + private static final Runnable NO_OP = () -> {}; + + @NotNull private final FeatherBukkitPlugin plugin; + private final boolean regionSchedulerSupported; + @Nullable private final Method isOwnedByCurrentRegionMethod; + @Nullable private final Method getSchedulerMethod; + @Nullable private final Method runMethod; + @Nullable private final Method runDelayedMethod; + + public PlayerTaskScheduler(@NotNull FeatherBukkitPlugin plugin) { + this.plugin = plugin; + + this.isOwnedByCurrentRegionMethod = + findMethod(Bukkit.getServer().getClass(), "isOwnedByCurrentRegion", Entity.class); + this.getSchedulerMethod = findMethod(Player.class, "getScheduler"); + + Class schedulerType = + this.getSchedulerMethod != null ? this.getSchedulerMethod.getReturnType() : null; + + this.runMethod = findMethod(schedulerType, "run", 3); + this.runDelayedMethod = findMethod(schedulerType, "runDelayed", 4); + this.regionSchedulerSupported = + this.getSchedulerMethod != null && this.runMethod != null && this.runDelayedMethod != null; + } + + public void execute(@NotNull Player player, @NotNull Runnable task) { + Objects.requireNonNull(player, "player"); + Objects.requireNonNull(task, "task"); + + if (shouldRunInline(player)) { + task.run(); + return; + } + + if (!player.isOnline()) { + return; + } + + if (this.regionSchedulerSupported) { + invoke( + this.runMethod, + invoke(this.getSchedulerMethod, player), + this.plugin, + consumer(player, task), + NO_OP); + return; + } + + Bukkit.getScheduler().runTask(this.plugin, () -> runIfOnline(player, task)); + } + + public void executeLater(@NotNull Player player, long delayTicks, @NotNull Runnable task) { + Objects.requireNonNull(player, "player"); + Objects.requireNonNull(task, "task"); + + if (delayTicks <= 0L) { + execute(player, task); + return; + } + + if (!player.isOnline()) { + return; + } + + if (this.regionSchedulerSupported) { + invoke( + this.runDelayedMethod, + invoke(this.getSchedulerMethod, player), + this.plugin, + consumer(player, task), + NO_OP, + delayTicks); + return; + } + + Bukkit.getScheduler().runTaskLater(this.plugin, () -> runIfOnline(player, task), delayTicks); + } + + private boolean shouldRunInline(@NotNull Player player) { + if (!player.isOnline()) { + return false; + } + + if (this.regionSchedulerSupported) { + return this.isOwnedByCurrentRegionMethod != null + && Boolean.TRUE.equals(invoke(this.isOwnedByCurrentRegionMethod, Bukkit.getServer(), player)); + } + + return Bukkit.isPrimaryThread(); + } + + private static void runIfOnline(@NotNull Player player, @NotNull Runnable task) { + if (player.isOnline()) { + task.run(); + } + } + + private static Consumer consumer(@NotNull Player player, @NotNull Runnable task) { + return ignored -> runIfOnline(player, task); + } + + @Nullable + private static Method findMethod(@Nullable Class type, @NotNull String name, int parameterCount) { + if (type == null) { + return null; + } + + for (Method method : type.getMethods()) { + if (method.getName().equals(name) && method.getParameterTypes().length == parameterCount) { + return method; + } + } + + return null; + } + + @Nullable + private static Method findMethod( + @Nullable Class type, @NotNull String name, @NotNull Class... parameterTypes) { + if (type == null) { + return null; + } + + try { + return type.getMethod(name, parameterTypes); + } catch (NoSuchMethodException ignored) { + return null; + } + } + + private static Object invoke(@Nullable Method method, @Nullable Object instance, Object... args) { + if (method == null) { + throw new IllegalStateException("Required scheduler method is not available"); + } + + try { + return method.invoke(instance, args); + } catch (IllegalAccessException | InvocationTargetException exception) { + throw new IllegalStateException( + "Failed to invoke scheduler method '" + method.getName() + "'", exception); + } + } +} diff --git a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/messaging/BukkitMessagingService.java b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/messaging/BukkitMessagingService.java index b45756b..48e8405 100644 --- a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/messaging/BukkitMessagingService.java +++ b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/messaging/BukkitMessagingService.java @@ -1,10 +1,10 @@ package net.digitalingot.feather.serverapi.bukkit.messaging; import java.util.Collection; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import net.digitalingot.feather.serverapi.api.model.FeatherMod; import net.digitalingot.feather.serverapi.api.model.Platform; @@ -135,10 +135,8 @@ public void sendMessage(Collection recipients, Message message List fragments = MessageFragmenter.CLIENT_BOUND.fragment(message); for (FeatherPlayer recipient : recipients) { - for (byte[] data : fragments) { - sendPluginMessage( - ((BukkitFeatherPlayer) recipient).getPlayer(), CHANNEL_FRAGMENTED, data); - } + sendPluginMessages( + ((BukkitFeatherPlayer) recipient).getPlayer(), CHANNEL_FRAGMENTED, fragments); } } else { for (FeatherPlayer recipient : recipients) { @@ -151,21 +149,32 @@ public void sendMessage(Player player, Message message) { byte[] encoded = MessageEncoder.CLIENT_BOUND.encode(message); if (encoded.length > Messenger.MAX_MESSAGE_SIZE) { - for (byte[] data : MessageFragmenter.CLIENT_BOUND.fragment(message)) { - sendPluginMessage(player, CHANNEL_FRAGMENTED, data); - } + sendPluginMessages(player, CHANNEL_FRAGMENTED, MessageFragmenter.CLIENT_BOUND.fragment(message)); } else { sendPluginMessage(player, CHANNEL, encoded); } } private void sendPluginMessage(@NotNull Player player, @NotNull String channel, byte[] data) { - player.sendPluginMessage(this.plugin, channel, data); + sendPluginMessages(player, channel, java.util.Collections.singletonList(data)); + } + + private void sendPluginMessages( + @NotNull Player player, @NotNull String channel, @NotNull List dataMessages) { + this.plugin + .getPlayerTaskScheduler() + .execute( + player, + () -> { + for (byte[] data : dataMessages) { + player.sendPluginMessage(this.plugin, channel, data); + } + }); } private static class Handshaking implements Listener { private final BukkitMessagingService messagingService; - private final Map handshakes = new HashMap<>(); + private final Map handshakes = new ConcurrentHashMap<>(); private final UpdateNotifier updateNotifier; public Handshaking(BukkitMessagingService messagingService, UpdateNotifier updateNotifier) { diff --git a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/player/BukkitFeatherPlayer.java b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/player/BukkitFeatherPlayer.java index 6a474cd..ab99ffa 100644 --- a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/player/BukkitFeatherPlayer.java +++ b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/player/BukkitFeatherPlayer.java @@ -1,11 +1,12 @@ package net.digitalingot.feather.serverapi.bukkit.player; -import com.google.common.collect.Sets; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import net.digitalingot.feather.serverapi.api.model.FeatherMod; import net.digitalingot.feather.serverapi.api.player.FeatherPlayer; @@ -24,7 +25,7 @@ public class BukkitFeatherPlayer implements FeatherPlayer { @NotNull private final Player player; @NotNull private final BukkitMessagingService messagingService; @NotNull private final PlayerMessageHandler messageHandler; - private final Set blockedMods = Sets.newHashSet(); + private final Set blockedMods = ConcurrentHashMap.newKeySet(); public BukkitFeatherPlayer( @NotNull Player player, @@ -63,7 +64,8 @@ public void unblockMods(@NotNull Collection<@NotNull FeatherMod> mods) { @Override public CompletableFuture<@NotNull Collection<@NotNull FeatherMod>> getBlockedMods() { - return CompletableFuture.completedFuture(Collections.unmodifiableCollection(this.blockedMods)); + return CompletableFuture.completedFuture( + Collections.unmodifiableSet(new HashSet<>(this.blockedMods))); } @Override diff --git a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/player/BukkitPlayerService.java b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/player/BukkitPlayerService.java index 157357b..bd01eed 100644 --- a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/player/BukkitPlayerService.java +++ b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/player/BukkitPlayerService.java @@ -2,9 +2,9 @@ import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import net.digitalingot.feather.serverapi.api.player.FeatherPlayer; import net.digitalingot.feather.serverapi.api.player.PlayerService; import net.digitalingot.feather.serverapi.bukkit.FeatherBukkitPlugin; @@ -19,7 +19,7 @@ public class BukkitPlayerService implements PlayerService, Listener { private final PluginManager pluginManager; - private final Map players = new HashMap<>(); + private final Map players = new ConcurrentHashMap<>(); public BukkitPlayerService(FeatherBukkitPlugin plugin) { this.pluginManager = plugin.getServer().getPluginManager(); diff --git a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/player/PlayerMessageHandler.java b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/player/PlayerMessageHandler.java index 8703a78..9846563 100644 --- a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/player/PlayerMessageHandler.java +++ b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/player/PlayerMessageHandler.java @@ -7,6 +7,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import net.digitalingot.feather.serverapi.api.FeatherAPI; import net.digitalingot.feather.serverapi.api.meta.ServerListBackground; @@ -47,7 +48,7 @@ class PlayerMessageHandler implements ServerMessageHandler { }) .build(); - private int requestId = 0; + private final AtomicInteger requestId = new AtomicInteger(); public PlayerMessageHandler(BukkitFeatherPlayer player, RpcService rpcService) { this.player = player; @@ -135,7 +136,7 @@ public void handle(C2SRequestServerBackground serverBackground) { } public @NotNull CompletableFuture<@NotNull Collection<@NotNull FeatherMod>> requestEnabledMods() { - int id = this.requestId++; + int id = this.requestId.getAndIncrement(); CompletableFuture<@NotNull Collection<@NotNull FeatherMod>> future = new CompletableFuture<>(); //noinspection UnstableApiUsage this.pendingModsRequests.put(id, future); diff --git a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/ui/BukkitUIService.java b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/ui/BukkitUIService.java index dc29de2..5f5641a 100644 --- a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/ui/BukkitUIService.java +++ b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/ui/BukkitUIService.java @@ -1,7 +1,7 @@ package net.digitalingot.feather.serverapi.bukkit.ui; -import com.google.common.collect.Maps; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import net.digitalingot.feather.serverapi.api.player.FeatherPlayer; import net.digitalingot.feather.serverapi.api.ui.UIPage; import net.digitalingot.feather.serverapi.api.ui.UIService; @@ -23,7 +23,7 @@ public class BukkitUIService implements UIService { @NotNull private final BukkitMessagingService messagingService; @NotNull private final RpcService rpcService; - private final Map registeredPages = Maps.newHashMap(); + private final Map registeredPages = new ConcurrentHashMap<>(); public BukkitUIService( @NotNull BukkitMessagingService messagingService, @NotNull RpcService rpcService) { @@ -42,11 +42,10 @@ private static Plugin validatePlugin(Object plugin) { public UIPage registerPage(@NotNull Object pluginObject, @NotNull String url) { Plugin plugin = validatePlugin(pluginObject); String pluginName = plugin.getName(); - if (this.registeredPages.containsKey(pluginName)) { + BukkitUIPage page = new BukkitUIPage(plugin, url); + if (this.registeredPages.putIfAbsent(pluginName, page) != null) { throw new IllegalArgumentException("Page already exists"); } - BukkitUIPage page = new BukkitUIPage(plugin, url); - this.registeredPages.put(pluginName, page); return page; } diff --git a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/ui/rpc/BukkitRpcResponse.java b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/ui/rpc/BukkitRpcResponse.java index 8ef59f5..122ff49 100644 --- a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/ui/rpc/BukkitRpcResponse.java +++ b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/ui/rpc/BukkitRpcResponse.java @@ -1,35 +1,21 @@ package net.digitalingot.feather.serverapi.bukkit.ui.rpc; import net.digitalingot.feather.serverapi.api.ui.rpc.RpcResponse; -import net.digitalingot.feather.serverapi.bukkit.FeatherBukkitPlugin; import net.digitalingot.feather.serverapi.bukkit.player.BukkitFeatherPlayer; import net.digitalingot.feather.serverapi.messaging.messages.client.S2CFUIResponse; -import org.bukkit.Bukkit; import org.jetbrains.annotations.NotNull; public class BukkitRpcResponse implements RpcResponse { - private final FeatherBukkitPlugin plugin; public final int id; @NotNull public final BukkitFeatherPlayer player; - public BukkitRpcResponse( - int id, @NotNull BukkitFeatherPlayer player, @NotNull FeatherBukkitPlugin plugin) { + public BukkitRpcResponse(int id, @NotNull BukkitFeatherPlayer player) { this.id = id; this.player = player; - this.plugin = plugin; } @Override public void respond(final @NotNull String jsonResponse) { - sendOnMainThread( - () -> this.player.sendMessage(new S2CFUIResponse(this.id, true, jsonResponse))); - } - - private void sendOnMainThread(Runnable task) { - if (!Bukkit.getServer().isPrimaryThread()) { - Bukkit.getScheduler().runTask(this.plugin, task); - } else { - task.run(); - } + this.player.sendMessage(new S2CFUIResponse(this.id, true, jsonResponse)); } } diff --git a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/ui/rpc/RpcService.java b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/ui/rpc/RpcService.java index 8483413..efaa7cc 100644 --- a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/ui/rpc/RpcService.java +++ b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/ui/rpc/RpcService.java @@ -7,6 +7,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; import net.digitalingot.feather.serverapi.api.ui.rpc.RpcController; import net.digitalingot.feather.serverapi.api.ui.rpc.RpcHandler; @@ -25,14 +26,12 @@ public class RpcService { - private final FeatherBukkitPlugin plugin; private final Logger logger; private final Map rpcHosts; public RpcService(FeatherBukkitPlugin plugin) { - this.plugin = plugin; this.logger = plugin.getLogger(); - this.rpcHosts = new HashMap<>(); + this.rpcHosts = new ConcurrentHashMap<>(); plugin.getServer().getPluginManager().registerEvents(new HandlerReaper(), plugin); } @@ -121,8 +120,9 @@ private void register(@NotNull String rpcHostName, @NotNull RpcController contro } if (rpcHost == null) { - rpcHost = new RpcHost(); - this.rpcHosts.put(rpcHostName, rpcHost); + RpcHost newRpcHost = new RpcHost(); + RpcHost previous = this.rpcHosts.putIfAbsent(rpcHostName, newRpcHost); + rpcHost = previous != null ? previous : newRpcHost; } rpcHost.registerAll(handlers); @@ -147,7 +147,7 @@ public void handle( try { handler.invoke( new BukkitRpcRequest(player, body), - new BukkitRpcResponse(requestId, player, this.plugin)); + new BukkitRpcResponse(requestId, player)); } catch (Throwable throwable) { this.logger.warning("Error occurred handling RPC request"); throwable.printStackTrace(); @@ -181,7 +181,7 @@ private static class RpcHost { private final Map handlers; public RpcHost() { - this.handlers = new HashMap<>(); + this.handlers = new ConcurrentHashMap<>(); } public void registerAll(@NotNull Map handlers) { @@ -189,7 +189,12 @@ public void registerAll(@NotNull Map handlers) { } public void unregisterByController(@NotNull RpcController controller) { - this.handlers.values().removeIf(handler -> handler.getController() == controller); + for (Entry entry : this.handlers.entrySet()) { + RegisteredRpcHandler handler = entry.getValue(); + if (handler.getController() == controller) { + this.handlers.remove(entry.getKey(), handler); + } + } } public RegisteredRpcHandler getHandlerByName(@NotNull String name) { diff --git a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/update/UpdateNotifier.java b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/update/UpdateNotifier.java index e04ad9a..54b4f1c 100644 --- a/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/update/UpdateNotifier.java +++ b/bukkit/src/main/java/net/digitalingot/feather/serverapi/bukkit/update/UpdateNotifier.java @@ -25,8 +25,8 @@ public class UpdateNotifier implements Listener { + "%d"; private final FeatherBukkitPlugin plugin; - private boolean potentiallyOutOfDate = false; - private String outOfDateMessage; + private volatile boolean potentiallyOutOfDate = false; + private volatile String outOfDateMessage; public UpdateNotifier(FeatherBukkitPlugin plugin) { this.plugin = plugin; @@ -47,16 +47,17 @@ public void onPlayerJoin(PlayerJoinEvent event) { Player joiningPlayer = event.getPlayer(); if (joiningPlayer.hasPermission(NOTIFY_PERMISSION)) { final UUID joiningPlayerId = joiningPlayer.getUniqueId(); - Bukkit.getScheduler() - .runTaskLater( - this.plugin, + this.plugin + .getPlayerTaskScheduler() + .executeLater( + joiningPlayer, + NOTIFICATION_DELAY_IN_TICKS, () -> { Player player = Bukkit.getPlayer(joiningPlayerId); if (player != null) { player.sendMessage(this.outOfDateMessage); } - }, - NOTIFICATION_DELAY_IN_TICKS); + }); } } } diff --git a/bukkit/src/main/resources/plugin.yml b/bukkit/src/main/resources/plugin.yml index 8313c07..cfa1236 100644 --- a/bukkit/src/main/resources/plugin.yml +++ b/bukkit/src/main/resources/plugin.yml @@ -5,6 +5,7 @@ website: https://feathermc.com main: net.digitalingot.feather.serverapi.bukkit.FeatherBukkitPlugin load: STARTUP api-version: "1.13" +folia-supported: true permissions: feather-server-api.notify: