diff --git a/gradle.properties b/gradle.properties index f92e3683..deeda5d4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled javaVersion=25 mcVersion=26.1.2 group=dev.slne.surf.api -version=3.15.0 +version=3.16.0 relocationPrefix=dev.slne.surf.api.libs snapshot=false diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-common/src/main/kotlin/dev/slne/surf/api/paper/nms/common/CommandSendPacketBlockerListener.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-common/src/main/kotlin/dev/slne/surf/api/paper/nms/common/CommandSendPacketBlockerListener.kt new file mode 100644 index 00000000..c1bab758 --- /dev/null +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-common/src/main/kotlin/dev/slne/surf/api/paper/nms/common/CommandSendPacketBlockerListener.kt @@ -0,0 +1,15 @@ +package dev.slne.surf.api.paper.nms.common + +import dev.slne.surf.api.paper.nms.NmsUseWithCaution +import dev.slne.surf.api.paper.packet.listener.listener.PacketListener +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +@OptIn(NmsUseWithCaution::class) +abstract class CommandSendPacketBlockerListener(protected val blockedPlayers: Set) : PacketListener { + protected val receivedFirstCommandPacket: MutableSet = ConcurrentHashMap.newKeySet() + + fun removeReceivedFirstCommandPacket(uuid: UUID) { + receivedFirstCommandPacket.remove(uuid) + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-common/src/main/kotlin/dev/slne/surf/api/paper/nms/common/NmsProvider.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-common/src/main/kotlin/dev/slne/surf/api/paper/nms/common/NmsProvider.kt index e38e41e6..639dbd8f 100644 --- a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-common/src/main/kotlin/dev/slne/surf/api/paper/nms/common/NmsProvider.kt +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-common/src/main/kotlin/dev/slne/surf/api/paper/nms/common/NmsProvider.kt @@ -18,6 +18,7 @@ import dev.slne.surf.api.shared.internal.nms.NmsVersion import org.bukkit.plugin.java.JavaPlugin import java.io.File import java.net.URI +import java.util.* import java.util.jar.JarFile /** @@ -84,6 +85,8 @@ interface NmsProvider { fun createChannelInjector(): AbstractChannelInjector<*> fun createPacketListenerApi(): InternalPacketListenerApiBridge + fun createCommandSendPacketBlockerListener(blockedPlayers: Set): CommandSendPacketBlockerListener + /** * Creates version-specific packet listeners (e.g. lore handler, glowing handler). * diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/V1_21_11NmsProvider.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/V1_21_11NmsProvider.kt index 25b02856..d3ce9753 100644 --- a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/V1_21_11NmsProvider.kt +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/V1_21_11NmsProvider.kt @@ -22,6 +22,7 @@ import dev.slne.surf.api.paper.server.nms.v1_21_11.bridges.packets.player.V1_21_ import dev.slne.surf.api.paper.server.nms.v1_21_11.glow.V1_21_11GlowingLifecycleHandler import dev.slne.surf.api.paper.server.nms.v1_21_11.glow.V1_21_11SurfGlowingApiImpl import dev.slne.surf.api.paper.server.nms.v1_21_11.packet.listener.V1_21_11ChannelInjector +import dev.slne.surf.api.paper.server.nms.v1_21_11.packet.listener.V1_21_11CommandSendPacketBlockerListenerImpl import dev.slne.surf.api.paper.server.nms.v1_21_11.packet.listener.V1_21_11GlowingPacketListener import dev.slne.surf.api.paper.server.nms.v1_21_11.packet.lore.V1_21_11PacketLoreListener import dev.slne.surf.api.paper.server.nms.v1_21_11.packet.lore.V1_21_11PacketLoreRegistry @@ -30,6 +31,7 @@ import dev.slne.surf.api.paper.server.nms.v1_21_11.region.V1_21_11TickThreadGuar import dev.slne.surf.api.shared.internal.nms.NmsProviderMarker import dev.slne.surf.api.shared.internal.nms.NmsVersion import org.bukkit.plugin.java.JavaPlugin +import java.util.* @Suppress("ClassName") @OptIn(NmsUseWithCaution::class) @@ -89,6 +91,10 @@ class V1_21_11NmsProvider(override val plugin: JavaPlugin) : NmsProvider { override fun createChannelInjector(): AbstractChannelInjector<*> = V1_21_11ChannelInjector override fun createPacketListenerApi(): InternalPacketListenerApiBridge = V1_21_11PacketListenerApiImpl() + override fun createCommandSendPacketBlockerListener(blockedPlayers: Set): CommandSendPacketBlockerListener { + return V1_21_11CommandSendPacketBlockerListenerImpl(blockedPlayers) + } + override fun createPacketListeners(): List = listOf( V1_21_11PacketLoreListener, V1_21_11GlowingPacketListener, diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/packet/listener/V1_21_11CommandSendPacketBlockerListenerImpl.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/packet/listener/V1_21_11CommandSendPacketBlockerListenerImpl.kt new file mode 100644 index 00000000..04f0587e --- /dev/null +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/packet/listener/V1_21_11CommandSendPacketBlockerListenerImpl.kt @@ -0,0 +1,55 @@ +package dev.slne.surf.api.paper.server.nms.v1_21_11.packet.listener + +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import com.mojang.brigadier.tree.ArgumentCommandNode +import com.mojang.brigadier.tree.CommandNode +import dev.slne.surf.api.paper.nms.NmsUseWithCaution +import dev.slne.surf.api.paper.nms.common.CommandSendPacketBlockerListener +import dev.slne.surf.api.paper.packet.listener.listener.annotation.ClientboundListener +import net.minecraft.commands.CommandSourceStack +import net.minecraft.network.protocol.game.ClientboundCommandsPacket +import net.minecraft.resources.Identifier +import net.minecraft.server.level.ServerPlayer +import java.util.* + +@OptIn(NmsUseWithCaution::class) +@Suppress("ClassName") +class V1_21_11CommandSendPacketBlockerListenerImpl(blockedPlayers: Set) : + CommandSendPacketBlockerListener(blockedPlayers) { + + private val loadingCommandsDispatcher = CommandDispatcher() + private val commandNodeInspector = object : ClientboundCommandsPacket.NodeInspector { + override fun suggestionId(p0: ArgumentCommandNode): Identifier? { + return null + } + + override fun isExecutable(p0: CommandNode): Boolean { + return false + } + + override fun isRestricted(p0: CommandNode): Boolean { + return false + } + } + + init { + loadingCommandsDispatcher.register(LiteralArgumentBuilder.literal("commands-are-loading")) + } + + @ClientboundListener + fun onClientboundCommandsPacket( + packet: ClientboundCommandsPacket, + player: ServerPlayer + ): ClientboundCommandsPacket? { + if (blockedPlayers.contains(player.uuid)) { + return if (receivedFirstCommandPacket.add(player.uuid)) { + ClientboundCommandsPacket(loadingCommandsDispatcher.root, commandNodeInspector) + } else { + null + } + } + + return packet + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-1/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_1/V26_1NmsProvider.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-1/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_1/V26_1NmsProvider.kt index 77480d57..357e99dd 100644 --- a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-1/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_1/V26_1NmsProvider.kt +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-1/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_1/V26_1NmsProvider.kt @@ -22,6 +22,7 @@ import dev.slne.surf.api.paper.server.nms.v26_1.bridges.packets.player.V26_1Surf import dev.slne.surf.api.paper.server.nms.v26_1.glow.V26_1GlowingLifecycleHandler import dev.slne.surf.api.paper.server.nms.v26_1.glow.V26_1SurfGlowingApiImpl import dev.slne.surf.api.paper.server.nms.v26_1.packet.listener.V26_1ChannelInjector +import dev.slne.surf.api.paper.server.nms.v26_1.packet.listener.V26_1CommandSendPacketBlockerListenerImpl import dev.slne.surf.api.paper.server.nms.v26_1.packet.listener.V26_1GlowingPacketListener import dev.slne.surf.api.paper.server.nms.v26_1.packet.lore.V26_1PacketLoreListener import dev.slne.surf.api.paper.server.nms.v26_1.packet.lore.V26_1PacketLoreRegistry @@ -30,6 +31,7 @@ import dev.slne.surf.api.paper.server.nms.v26_1.region.V26_1TickThreadGuard import dev.slne.surf.api.shared.internal.nms.NmsProviderMarker import dev.slne.surf.api.shared.internal.nms.NmsVersion import org.bukkit.plugin.java.JavaPlugin +import java.util.* @Suppress("ClassName") @OptIn(NmsUseWithCaution::class) @@ -68,6 +70,10 @@ class V26_1NmsProvider(override val plugin: JavaPlugin) : NmsProvider { override fun createChannelInjector(): AbstractChannelInjector<*> = V26_1ChannelInjector override fun createPacketListenerApi(): InternalPacketListenerApiBridge = V26_1PacketListenerApiImpl() + override fun createCommandSendPacketBlockerListener(blockedPlayers: Set): CommandSendPacketBlockerListener { + return V26_1CommandSendPacketBlockerListenerImpl(blockedPlayers) + } + override fun createPacketListeners(): List = listOf( V26_1PacketLoreListener, V26_1GlowingPacketListener, diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-1/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_1/packet/listener/V26_1CommandSendPacketBlockerListenerImpl.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-1/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_1/packet/listener/V26_1CommandSendPacketBlockerListenerImpl.kt new file mode 100644 index 00000000..085b5105 --- /dev/null +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-1/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_1/packet/listener/V26_1CommandSendPacketBlockerListenerImpl.kt @@ -0,0 +1,56 @@ +package dev.slne.surf.api.paper.server.nms.v26_1.packet.listener + +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import com.mojang.brigadier.tree.ArgumentCommandNode +import com.mojang.brigadier.tree.CommandNode +import dev.slne.surf.api.paper.nms.NmsUseWithCaution +import dev.slne.surf.api.paper.nms.common.CommandSendPacketBlockerListener +import dev.slne.surf.api.paper.packet.listener.listener.annotation.ClientboundListener +import net.minecraft.commands.CommandSourceStack +import net.minecraft.network.protocol.game.ClientboundCommandsPacket +import net.minecraft.resources.Identifier +import net.minecraft.server.level.ServerPlayer +import java.util.* + + +@Suppress("ClassName") +@OptIn(NmsUseWithCaution::class) +class V26_1CommandSendPacketBlockerListenerImpl(blockedPlayers: Set) : + CommandSendPacketBlockerListener(blockedPlayers) { + + private val loadingCommandsDispatcher = CommandDispatcher() + private val commandNodeInspector = object : ClientboundCommandsPacket.NodeInspector { + override fun suggestionId(p0: ArgumentCommandNode): Identifier? { + return null + } + + override fun isExecutable(p0: CommandNode): Boolean { + return false + } + + override fun isRestricted(p0: CommandNode): Boolean { + return false + } + } + + init { + loadingCommandsDispatcher.register(LiteralArgumentBuilder.literal("commands-are-loading")) + } + + @ClientboundListener + fun onClientboundCommandsPacket( + packet: ClientboundCommandsPacket, + player: ServerPlayer + ): ClientboundCommandsPacket? { + if (blockedPlayers.contains(player.uuid)) { + return if (receivedFirstCommandPacket.add(player.uuid)) { + ClientboundCommandsPacket(loadingCommandsDispatcher.root, commandNodeInspector) + } else { + null + } + } + + return packet + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/SurfApiTestCommand.java b/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/SurfApiTestCommand.java index c4dcc920..b454e4da 100644 --- a/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/SurfApiTestCommand.java +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/SurfApiTestCommand.java @@ -33,7 +33,8 @@ public SurfApiTestCommand() { new SignedMessageArgumentTest("signedmessage"), new BlockPdcContainerTest("blockpdc"), new OfflineInventoryEditTest("editOfflineInventory"), - new ModernSerializerTestConfigCommand("modernSerializerTestConfig") + new ModernSerializerTestConfigCommand("modernSerializerTestConfig"), + new SuspendRequirementTestCommand("suspendRequirement") ); } } diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/SuspendRequirementTestCommand.kt b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/SuspendRequirementTestCommand.kt new file mode 100644 index 00000000..babe8c6c --- /dev/null +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/SuspendRequirementTestCommand.kt @@ -0,0 +1,45 @@ +package dev.slne.surf.surfapi.bukkit.test.command.subcommands + +import dev.jorel.commandapi.CommandAPICommand +import dev.jorel.commandapi.kotlindsl.* +import dev.slne.surf.api.paper.command.requirement.withSuspendPlayerRequirement +import kotlinx.coroutines.delay +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import kotlin.time.Duration.Companion.seconds + +class SuspendRequirementTestCommand(name: String) : CommandAPICommand(name) { + private val shown = ConcurrentHashMap.newKeySet() + + init { + subcommand("updateCommands") { + playerExecutor { player, _ -> + player.updateCommands() + } + } + + subcommand("showCommand") { + booleanArgument("show") + + playerExecutor { player, arguments -> + val show: Boolean by arguments + if (show) { + shown.add(player.uniqueId) + } else { + shown.remove(player.uniqueId) + } + } + } + + subcommand("conditionalCommand") { + withSuspendPlayerRequirement { player -> + delay(10.seconds) + player.uniqueId in shown + } + + anyExecutor { sender, _ -> + sender.sendMessage("This command is only available to players who have shown it") + } + } + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/command/SuspendRequirementServiceImpl.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/command/SuspendRequirementServiceImpl.kt new file mode 100644 index 00000000..8a54c366 --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/command/SuspendRequirementServiceImpl.kt @@ -0,0 +1,167 @@ +package dev.slne.surf.api.paper.server.command + +import com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent +import com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.shynixn.mccoroutine.folia.launch +import com.google.auto.service.AutoService +import dev.jorel.commandapi.ExecutableCommand +import dev.slne.surf.api.core.util.checkInstantiationByServiceLoader +import dev.slne.surf.api.core.util.logger +import dev.slne.surf.api.paper.command.executors.CoroutineScopeProvider +import dev.slne.surf.api.paper.command.requirement.SuspendRequirementService +import dev.slne.surf.api.paper.nms.NmsUseWithCaution +import dev.slne.surf.api.paper.nms.common.CommandSendPacketBlockerListener +import dev.slne.surf.api.paper.nms.common.NmsProvider +import dev.slne.surf.api.paper.server.plugin +import io.papermc.paper.command.brigadier.CommandSourceStack +import kotlinx.coroutines.* +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import kotlin.coroutines.cancellation.CancellationException + +@AutoService(SuspendRequirementService::class) +class SuspendRequirementServiceImpl : SuspendRequirementService { + init { + checkInstantiationByServiceLoader() + } + + companion object { + private val log = logger() + + fun get() = SuspendRequirementService.instance as SuspendRequirementServiceImpl + } + + private val requirements = Caffeine.newBuilder() + .build() + + private val blockedCommandPackets = ConcurrentHashMap.newKeySet() + private val ready = ConcurrentHashMap.newKeySet() + + private val listener = EventListener() + + @OptIn(NmsUseWithCaution::class) + private val commandSendPacketBlockerListener = + NmsProvider.current.createCommandSendPacketBlockerListener(blockedCommandPackets) + + fun getCommandSendPacketBlockerListener(): CommandSendPacketBlockerListener { + return commandSendPacketBlockerListener + } + + fun getEventListener(): Listener { + return listener + } + + override fun > ExecutableCommand.withSuspendRequirement( + scope: CoroutineScopeProvider, + requirement: suspend CoroutineScope.(Player) -> Boolean, + allowIfNonPlayer: Boolean + ) { + val id = UUID.randomUUID() + register(id, scope, requirement) + withRequirement { + if (it !is Player) { + allowIfNonPlayer + } else { + testCached(id, it) + } + } + } + + private fun register( + id: UUID, + scope: CoroutineScopeProvider, + requirement: suspend CoroutineScope.(Player) -> Boolean + ) { + requirements.put(id, Requirement(scope, requirement)) + } + + private fun testCached(id: UUID, sender: Player): Boolean { + val requirement = requirements.getIfPresent(id) ?: return false + return requirement.testCached(sender) + } + + suspend fun refreshForSender(sender: Player) { + coroutineScope { + for (requirement in requirements.asMap().values) { + launch { + requirement.refreshForSender(sender) + } + } + } + + ready.add(sender.uniqueId) + sender.updateCommands() + } + + fun triggerRefreshForSender(sender: Player) { + plugin.launch { + refreshForSender(sender) + } + } + + inner class EventListener : Listener { + + @EventHandler + @Suppress("UnstableApiUsage") + fun onAsyncPlayerSendCommand(event: AsyncPlayerSendCommandsEvent) { + if (requirements.asMap().isEmpty()) return + + if (event.isAsynchronous || !event.hasFiredAsync()) { + val uuid = event.player.uniqueId + + if (ready.remove(uuid)) { + blockedCommandPackets.remove(uuid) + } else { + if (blockedCommandPackets.add(uuid)) { + triggerRefreshForSender(event.player) + } + } + } + } + + @EventHandler + fun onConnectionClosed(event: PlayerConnectionCloseEvent) { + blockedCommandPackets.remove(event.playerUniqueId) + ready.remove(event.playerUniqueId) + commandSendPacketBlockerListener.removeReceivedFirstCommandPacket(event.playerUniqueId) + requirements.asMap().values.forEach { it.invalidate(event.playerUniqueId) } + } + } + + data class Requirement( + val scope: CoroutineScopeProvider, + val requirement: suspend CoroutineScope.(Player) -> Boolean + ) { + private val cached = Caffeine.newBuilder() + .build() + + fun testCached(sender: Player): Boolean { + return cached.getIfPresent(sender.uniqueId) ?: false + } + + fun invalidate(uuid: UUID) { + cached.invalidate(uuid) + } + + suspend fun refreshForSender(sender: Player) { + withContext(scope().coroutineContext.minusKey(Job)) { + try { + val isRequirementMet = requirement(sender) + if (sender.isConnected) { // check if this player instance is still connected + cached.put(sender.uniqueId, isRequirementMet) + } + } catch (e: Throwable) { + if (e is CancellationException) throw e + log.atWarning() + .withCause(e) + .log("Failed to check requirement for sender $sender! Not updating cache.") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/listener/ListenerManager.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/listener/ListenerManager.kt index 1680262f..fadc9f6d 100644 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/listener/ListenerManager.kt +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/listener/ListenerManager.kt @@ -1,6 +1,7 @@ package dev.slne.surf.api.paper.server.listener import dev.slne.surf.api.paper.event.register +import dev.slne.surf.api.paper.server.command.SuspendRequirementServiceImpl import dev.slne.surf.api.paper.server.impl.glow.GlowingListener import dev.slne.surf.api.paper.server.impl.pdc.block.BlockDataListener import dev.slne.surf.api.paper.server.impl.visualizer.visualizer.VisualizerListener @@ -17,6 +18,8 @@ object ListenerManager { GlowingListener.register() BlockDataListener.register() + + SuspendRequirementServiceImpl.get().getEventListener().register() } /** diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/packet/PacketApiLoader.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/packet/PacketApiLoader.kt index 85569bfd..bd8ac416 100644 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/packet/PacketApiLoader.kt +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/packet/PacketApiLoader.kt @@ -9,6 +9,7 @@ import dev.slne.surf.api.paper.nms.common.AbstractChannelInjector import dev.slne.surf.api.paper.nms.common.NmsProvider import dev.slne.surf.api.paper.packet.listener.SurfPaperPacketListenerApi import dev.slne.surf.api.paper.packet.listener.listener.PacketListener +import dev.slne.surf.api.paper.server.command.SuspendRequirementServiceImpl import dev.slne.surf.api.paper.server.packet.lore.PluginDisablePacketLoreListener import dev.slne.surf.api.paper.server.plugin import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder @@ -35,6 +36,8 @@ object PacketApiLoader { SurfPaperPacketListenerApi.registerListeners(listener) } + SurfPaperPacketListenerApi.registerListeners(SuspendRequirementServiceImpl.get().getCommandSendPacketBlockerListener()) + AbstractChannelInjector.instance.register() PluginDisablePacketLoreListener.register() } @@ -49,6 +52,8 @@ object PacketApiLoader { } versionPacketListeners = emptyList() + SurfPaperPacketListenerApi.unregisterListeners(SuspendRequirementServiceImpl.get().getCommandSendPacketBlockerListener()) + PluginDisablePacketLoreListener.unregister() AbstractChannelInjector.instance.unregister() provider.shutdown() diff --git a/surf-api-paper/surf-api-paper/api/surf-api-paper.api b/surf-api-paper/surf-api-paper/api/surf-api-paper.api index dafefb6e..49296999 100644 --- a/surf-api-paper/surf-api-paper/api/surf-api-paper.api +++ b/surf-api-paper/surf-api-paper/api/surf-api-paper.api @@ -227,6 +227,20 @@ public final class dev/slne/surf/api/paper/command/executors/SuspendCommandExecu public static final fun sendSyntaxMessageOrRethrow (Lorg/bukkit/command/CommandSender;Ljava/lang/String;Ljava/lang/Throwable;)V } +public final class dev/slne/surf/api/paper/command/requirement/SuspendCommandRequirementsKt { + public static final fun withSuspendPlayerRequirement (Ldev/jorel/commandapi/ExecutableCommand;Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function3;)V + public static synthetic fun withSuspendPlayerRequirement$default (Ldev/jorel/commandapi/ExecutableCommand;Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function3;ILjava/lang/Object;)V +} + +public abstract interface class dev/slne/surf/api/paper/command/requirement/SuspendRequirementService { + public static final field Companion Ldev/slne/surf/api/paper/command/requirement/SuspendRequirementService$Companion; + public abstract fun withSuspendRequirement (Ldev/jorel/commandapi/ExecutableCommand;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;Z)V +} + +public final class dev/slne/surf/api/paper/command/requirement/SuspendRequirementService$Companion { + public final fun getInstance ()Ldev/slne/surf/api/paper/command/requirement/SuspendRequirementService; +} + public final class dev/slne/surf/api/paper/command/util/AsyncPlayerProfileArgumentUtilsKt { public static final fun awaitAsyncPlayerProfile (Ldev/jorel/commandapi/executors/CommandArguments;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun awaitAsyncPlayerProfileOptional (Ldev/jorel/commandapi/executors/CommandArguments;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/command/requirement/SuspendCommandRequirements.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/command/requirement/SuspendCommandRequirements.kt new file mode 100644 index 00000000..0fc4d55d --- /dev/null +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/command/requirement/SuspendCommandRequirements.kt @@ -0,0 +1,41 @@ +package dev.slne.surf.api.paper.command.requirement + +import dev.jorel.commandapi.ExecutableCommand +import dev.slne.surf.api.paper.command.executors.CoroutineScopeProvider +import dev.slne.surf.api.paper.command.executors.extractCallingPluginScopeOrThrow +import kotlinx.coroutines.CoroutineScope +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +/** + * Adds a suspendable player-only requirement to this command. + * + * The suspendable [requirement] is refreshed asynchronously when the command tree is sent + * to a player. Its result is cached and used by the underlying CommandAPI requirement. + * + * This means the cached result is still checked when the command is executed, but the + * suspendable requirement itself is not refreshed at execution time. The command may + * therefore be executed based on the last cached value until the command tree is sent + * again and the cache is updated. + * + * The cached result can be refreshed manually by calling [Player.updateCommands], which + * causes the command tree to be resent to the player. + * + * Because of that, this should mainly be used for command visibility or other visual + * command-list behavior. Any condition that must be enforced with up-to-date data during + * execution should also be checked explicitly inside the command executor. + * + * @param scope the coroutine scope provider used to refresh the requirement + * @param allowIfNonPlayer whether non-player command senders should bypass this player-only requirement + * @param requirement the suspendable predicate used to refresh the cached requirement + * result for a player + */ +fun > ExecutableCommand.withSuspendPlayerRequirement( + scope: CoroutineScopeProvider = extractCallingPluginScopeOrThrow(), + allowIfNonPlayer: Boolean = false, + requirement: suspend CoroutineScope.(Player) -> Boolean +) { + SuspendRequirementService.instance.apply { + withSuspendRequirement(scope, requirement, allowIfNonPlayer) + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/command/requirement/SuspendRequirementService.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/command/requirement/SuspendRequirementService.kt new file mode 100644 index 00000000..212c6479 --- /dev/null +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/command/requirement/SuspendRequirementService.kt @@ -0,0 +1,23 @@ +package dev.slne.surf.api.paper.command.requirement + +import dev.jorel.commandapi.ExecutableCommand +import dev.slne.surf.api.core.util.requiredService +import dev.slne.surf.api.paper.command.executors.CoroutineScopeProvider +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import kotlinx.coroutines.CoroutineScope +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +@InternalSurfApi +interface SuspendRequirementService { + + fun > ExecutableCommand.withSuspendRequirement( + scope: CoroutineScopeProvider, + requirement: suspend CoroutineScope.(Player) -> Boolean, + allowIfNonPlayer: Boolean + ) + + companion object { + val instance = requiredService() + } +} \ No newline at end of file