diff --git a/build.gradle.kts b/build.gradle.kts index 01bfbc75..2dc59f90 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,7 +19,6 @@ plugins { } repositories { - maven("https://libraries.minecraft.net") mavenCentral() mavenLocal() // to get the locally available binaries of spigot (use the BuildTools) } @@ -31,8 +30,6 @@ dependencies { api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2") api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1") api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.5.1") - - api("me.lucko:commodore:1.10") } tasks { diff --git a/src/main/kotlin/net/axay/kspigot/commands/Arguments.kt b/src/main/kotlin/net/axay/kspigot/commands/Arguments.kt new file mode 100644 index 00000000..eb2cec24 --- /dev/null +++ b/src/main/kotlin/net/axay/kspigot/commands/Arguments.kt @@ -0,0 +1,40 @@ +package net.axay.kspigot.commands + +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.builder.ArgumentBuilder +import com.mojang.brigadier.builder.RequiredArgumentBuilder +import com.mojang.brigadier.context.CommandContext +import net.axay.kspigot.commands.internal.ArgumentTypeUtils +import net.axay.kspigot.commands.internal.ServerCommandSource + +/** + * Adds an argument. + * + * @param name the name of the argument + * @param type the type of the argument - e.g. IntegerArgumentType.integer() or StringArgumentType.string() + */ +inline fun ArgumentBuilder.argument( + name: String, + type: ArgumentType, + builder: RequiredArgumentBuilder.() -> Unit = {} +): RequiredArgumentBuilder = + RequiredArgumentBuilder.argument(name, type).apply(builder).also { then(it) } + +/** + * Adds an argument. The argument type will be resolved via the reified + * type [T]. + * + * @param name the name of the argument + */ +@Suppress("UNCHECKED_CAST") +inline fun ArgumentBuilder.argument( + name: String, + builder: RequiredArgumentBuilder.() -> Unit = {} +): RequiredArgumentBuilder = + RequiredArgumentBuilder.argument(name, ArgumentTypeUtils.fromReifiedType()).apply(builder).also { then(it) } + +/** + * Get the value of this argument. + */ +inline fun CommandContext.getArgument(name: String): T = + getArgument(name, T::class.java) diff --git a/src/main/kotlin/net/axay/kspigot/commands/BrigardierSupport.kt b/src/main/kotlin/net/axay/kspigot/commands/BrigardierSupport.kt deleted file mode 100644 index 90ef005c..00000000 --- a/src/main/kotlin/net/axay/kspigot/commands/BrigardierSupport.kt +++ /dev/null @@ -1,28 +0,0 @@ -@file:Suppress("MemberVisibilityCanBePrivate") - -package net.axay.kspigot.commands - -import com.mojang.brigadier.tree.LiteralCommandNode -import me.lucko.commodore.CommodoreProvider -import net.axay.kspigot.main.PluginInstance - -/** - * This class provides Brigardier support. It does that - * by using reflection once. Additionally, this class is - * using some obfuscated functions. - */ -object BrigardierSupport { - private val provider = if (CommodoreProvider.isSupported()) CommodoreProvider.getCommodore(PluginInstance) else kotlin.run { - PluginInstance.logger.severe("Could not initialize Brigardier support on the current Minecraft version! (Requested by ${PluginInstance.name})") - null - } - - fun register(name: String, brigardierCommand: LiteralCommandNode<*>) { - val command = PluginInstance.getCommand(name) - if (command == null) { - PluginInstance.logger.severe("Could not register command '$name' of plugin ${PluginInstance.name}! Maybe it is missing from the plugin.yml?") - return - } - provider?.register(command, brigardierCommand) - } -} diff --git a/src/main/kotlin/net/axay/kspigot/commands/BrigardierWrapper.kt b/src/main/kotlin/net/axay/kspigot/commands/BrigardierWrapper.kt deleted file mode 100644 index 32ee6988..00000000 --- a/src/main/kotlin/net/axay/kspigot/commands/BrigardierWrapper.kt +++ /dev/null @@ -1,97 +0,0 @@ -package net.axay.kspigot.commands - -import com.mojang.brigadier.arguments.ArgumentType -import com.mojang.brigadier.builder.ArgumentBuilder -import com.mojang.brigadier.builder.LiteralArgumentBuilder -import com.mojang.brigadier.builder.RequiredArgumentBuilder -import com.mojang.brigadier.tree.LiteralCommandNode -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.async -import kotlinx.coroutines.future.asCompletableFuture -import net.minecraft.commands.CommandListenerWrapper - -/** - * Create a new command. - * - * @param name the name of the root command - * @param register if true, the command will be automatically registered - * when the plugin is fully enabled - */ -inline fun command( - name: String, - register: Boolean = true, - builder: LiteralArgumentBuilder.() -> Unit, -): LiteralCommandNode = - LiteralArgumentBuilder.literal(name).apply(builder).build().apply { - if (register) - BrigardierSupport.register(name, this) - } - -/** - * Add custom execution logic for this command. - */ -inline fun ArgumentBuilder.simpleExecutes( - crossinline executor: CommandContext.() -> Unit, -) { - executes wrapped@{ - executor.invoke(CommandContext(it)) - return@wrapped 1 - } -} - -/** - * Add a new literal to this command. - * - * @param name the name of the literal - */ -inline fun ArgumentBuilder.literal( - name: String, - builder: LiteralArgumentBuilder.() -> Unit, -) = command(name, false, builder).also { then(it) } - -/** - * Add an argument. - * - * @param name the name of the argument - * @param type the type of the argument - e.g. IntegerArgumentType.integer() or StringArgumentType.string() - */ -inline fun ArgumentBuilder.argument( - name: String, - type: ArgumentType, - builder: RequiredArgumentBuilder.() -> Unit, -): RequiredArgumentBuilder = - RequiredArgumentBuilder.argument(name, type).apply(builder).also { then(it) } - -/** - * Add an argument. - * - * @param name the name of the argument - */ -inline fun ArgumentBuilder.argument( - name: String, - builder: RequiredArgumentBuilder.() -> Unit, -): RequiredArgumentBuilder = - RequiredArgumentBuilder.argument(name, ArgumentTypeUtils.fromReifiedType()).apply(builder).also { then(it) } - -private val argumentCoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - -/** - * Add custom suspending suggestion logic for an argument. - */ -fun RequiredArgumentBuilder.simpleSuggests( - suggestionBuilder: suspend CommandContext.() -> Iterable?, -) { - suggests { context, builder -> - argumentCoroutineScope.async { - suggestionBuilder.invoke(CommandContext(context))?.forEach { - if (it is Int) - builder.suggest(it) - else - builder.suggest(it.toString()) - } - builder.build() - }.asCompletableFuture() - } -} diff --git a/src/main/kotlin/net/axay/kspigot/commands/CommandContext.kt b/src/main/kotlin/net/axay/kspigot/commands/CommandContext.kt index d8a0da70..322ec3f0 100644 --- a/src/main/kotlin/net/axay/kspigot/commands/CommandContext.kt +++ b/src/main/kotlin/net/axay/kspigot/commands/CommandContext.kt @@ -2,14 +2,14 @@ package net.axay.kspigot.commands import com.mojang.brigadier.context.CommandContext import com.mojang.brigadier.exceptions.SimpleCommandExceptionType -import net.minecraft.commands.CommandListenerWrapper +import net.axay.kspigot.commands.internal.ServerCommandSource import net.minecraft.network.chat.ChatMessage import org.bukkit.Location import org.bukkit.Server import org.bukkit.World import org.bukkit.entity.Player -class CommandContext(val nmsContext: CommandContext) { +class CommandContext(val nmsContext: CommandContext) { companion object { private val REQUIRES_PLAYER_EXCEPTION = SimpleCommandExceptionType(ChatMessage("permissions.requires.player")) } diff --git a/src/main/kotlin/net/axay/kspigot/commands/Creation.kt b/src/main/kotlin/net/axay/kspigot/commands/Creation.kt new file mode 100644 index 00000000..2cd8cb63 --- /dev/null +++ b/src/main/kotlin/net/axay/kspigot/commands/Creation.kt @@ -0,0 +1,32 @@ +package net.axay.kspigot.commands + +import com.mojang.brigadier.builder.ArgumentBuilder +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import net.axay.kspigot.commands.internal.BrigardierSupport +import net.axay.kspigot.commands.internal.ServerCommandSource + +/** + * Creates a new command. + * + * @param name the name of the root command + * @param register if true, the command will automatically be registered + */ +inline fun command( + name: String, + register: Boolean = true, + builder: LiteralArgumentBuilder.() -> Unit +): LiteralArgumentBuilder = + LiteralArgumentBuilder.literal(name).apply(builder).apply { + if (register) + BrigardierSupport.commands += this + } + +/** + * Adds a new literal to this command. + * + * @param name the name of the literal + */ +inline fun ArgumentBuilder.literal( + name: String, + builder: LiteralArgumentBuilder.() -> Unit = {} +) = command(name, false, builder).also { then(it) } diff --git a/src/main/kotlin/net/axay/kspigot/commands/Execution.kt b/src/main/kotlin/net/axay/kspigot/commands/Execution.kt new file mode 100644 index 00000000..a3c6ac11 --- /dev/null +++ b/src/main/kotlin/net/axay/kspigot/commands/Execution.kt @@ -0,0 +1,44 @@ +package net.axay.kspigot.commands + +import com.mojang.brigadier.Command +import com.mojang.brigadier.builder.ArgumentBuilder +import net.axay.kspigot.commands.internal.ServerCommandSource + +/** + * Adds execution logic to this command. The place where this function + * is called matters, as this defines for which path in the command tree + * this executor should be called. + * + * @see com.mojang.brigadier.builder.ArgumentBuilder.executes + */ +inline infix fun ArgumentBuilder.runs( + crossinline executor: CommandContext.() -> Unit, +) = this.apply { + executes wrapped@{ + executor.invoke(CommandContext(it)) + return@wrapped 1 + } +} + +/** + * Adds execution logic to this command. The place where this function + * is called matters, as this defines for which path in the command tree + * this executor should be called. + * + * @see com.mojang.brigadier.builder.ArgumentBuilder.executes + */ +infix fun ArgumentBuilder.runs(executor: Command) = + this.apply { + executes(executor) + } + +/** + * Add custom execution logic for this command. + */ +@Deprecated( + "The name 'simpleExecutes' has been superseded by 'runs'.", + ReplaceWith("runs { executor.invoke() }") +) +inline infix fun ArgumentBuilder.simpleExecutes( + crossinline executor: CommandContext.() -> Unit, +) = runs(executor) diff --git a/src/main/kotlin/net/axay/kspigot/commands/Registration.kt b/src/main/kotlin/net/axay/kspigot/commands/Registration.kt new file mode 100644 index 00000000..36c78d80 --- /dev/null +++ b/src/main/kotlin/net/axay/kspigot/commands/Registration.kt @@ -0,0 +1,25 @@ +package net.axay.kspigot.commands + +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import net.axay.kspigot.annotations.NMS_General +import net.axay.kspigot.commands.internal.BrigardierSupport +import net.minecraft.commands.CommandListenerWrapper + +/** + * Registers this command at the [CommandDispatcher] of the server. + * + * @param sendToPlayers whether the new command tree should be send to + * all players, this is true by default, but you can disable it if you are + * calling this function as the server is starting + */ +@NMS_General +fun LiteralArgumentBuilder.register(sendToPlayers: Boolean = true) { + if (!BrigardierSupport.executedDefaultRegistration) + BrigardierSupport.commands += this + else { + BrigardierSupport.commandDispatcher.register(this) + if (sendToPlayers) + BrigardierSupport.updateCommandTree() + } +} diff --git a/src/main/kotlin/net/axay/kspigot/commands/Requires.kt b/src/main/kotlin/net/axay/kspigot/commands/Requires.kt new file mode 100644 index 00000000..f190cd78 --- /dev/null +++ b/src/main/kotlin/net/axay/kspigot/commands/Requires.kt @@ -0,0 +1,25 @@ +package net.axay.kspigot.commands + +import com.mojang.brigadier.builder.ArgumentBuilder +import net.axay.kspigot.commands.internal.ServerCommandSource +import org.bukkit.permissions.Permission + +/** + * Defines that the given [permission] is required to interact with this + * path of the command. + */ +fun ArgumentBuilder.requiresPermission(permission: String) { + requires { + it.bukkitSender.hasPermission(permission) + } +} + +/** + * Defines that the given [permission] is required to interact with this + * path of the command. + */ +fun ArgumentBuilder.requiresPermission(permission: Permission) { + requires { + it.bukkitSender.hasPermission(permission) + } +} diff --git a/src/main/kotlin/net/axay/kspigot/commands/Suggestions.kt b/src/main/kotlin/net/axay/kspigot/commands/Suggestions.kt new file mode 100644 index 00000000..dba76a2d --- /dev/null +++ b/src/main/kotlin/net/axay/kspigot/commands/Suggestions.kt @@ -0,0 +1,185 @@ +package net.axay.kspigot.commands + +import com.mojang.brigadier.Message +import com.mojang.brigadier.builder.RequiredArgumentBuilder +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.future.asCompletableFuture + +@PublishedApi +internal val argumentCoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + +/** + * Suggest the value which is the result of the [suggestionBuilder]. + */ +inline fun RequiredArgumentBuilder.suggestSingle( + crossinline suggestionBuilder: (CommandContext) -> Any? +) { + suggests { context, builder -> + builder.applyAny(suggestionBuilder(context)) + builder.buildFuture() + } +} + +/** + * Suggest the value which is the result of the [suggestionBuilder]. + * Additionaly, a separate tooltip associated with the suggestion + * will be shown as well. + */ +inline fun RequiredArgumentBuilder.suggestSingleWithTooltip( + crossinline suggestionBuilder: (CommandContext) -> Pair? +) { + suggests { context, builder -> + builder.applyAnyWithTooltip(suggestionBuilder(context)) + builder.buildFuture() + } +} + +/** + * Suggest the value which is the result of the [suggestionBuilder]. + * + * @param coroutineScope the [CoroutineScope] where the suggestion should be built in - an async scope by default + */ +inline fun RequiredArgumentBuilder.suggestSingleSuspending( + coroutineScope: CoroutineScope = argumentCoroutineScope, + crossinline suggestionBuilder: suspend (CommandContext) -> Any? +) { + suggests { context, builder -> + coroutineScope.async { + builder.applyAny(suggestionBuilder(context)) + builder.build() + }.asCompletableFuture() + } +} + +/** + * Suggest the value which is the result of the [suggestionBuilder]. + * Additionaly, a separate tooltip associated with the suggestion + * will be shown as well. + * + * @param coroutineScope the [CoroutineScope] where the suggestion should be built in - an async scope by default + */ +inline fun RequiredArgumentBuilder.suggestSingleWithTooltipSuspending( + coroutineScope: CoroutineScope = argumentCoroutineScope, + crossinline suggestionBuilder: suspend (CommandContext) -> Pair? +) { + suggests { context, builder -> + coroutineScope.async { + builder.applyAnyWithTooltip(suggestionBuilder(context)) + builder.build() + }.asCompletableFuture() + } +} + +/** + * Suggest the entries of the iterable which is the result of the + * [suggestionsBuilder]. + */ +inline fun RequiredArgumentBuilder.suggestList( + crossinline suggestionsBuilder: (CommandContext) -> Iterable? +) { + suggests { context, builder -> + builder.applyIterable(suggestionsBuilder(context)) + builder.buildFuture() + } +} + +/** + * Suggest the entries of the iterable which is the result of the + * [suggestionsBuilder]. + * Additionaly, a separate tooltip associated with each suggestion + * will be shown as well. + */ +inline fun RequiredArgumentBuilder.suggestListWithTooltips( + crossinline suggestionsBuilder: (CommandContext) -> Iterable?>? +) { + suggests { context, builder -> + builder.applyIterableWithTooltips(suggestionsBuilder(context)) + builder.buildFuture() + } +} + +/** + * Suggest the entries of the iterable which is the result of the + * [suggestionsBuilder]. + * + * @param coroutineScope the [CoroutineScope] where the suggestions should be built in - an async scope by default + */ +inline fun RequiredArgumentBuilder.suggestListSuspending( + coroutineScope: CoroutineScope = argumentCoroutineScope, + crossinline suggestionsBuilder: suspend (CommandContext) -> Iterable? +) { + suggests { context, builder -> + coroutineScope.async { + builder.applyIterable(suggestionsBuilder(context)) + builder.build() + }.asCompletableFuture() + } +} + +/** + * Suggest the entries of the iterable which is the result of the + * [suggestionsBuilder]. + * Additionaly, a separate tooltip associated with each suggestion + * will be shown as well. + * + * @param coroutineScope the [CoroutineScope] where the suggestions should be built in - an async scope by default + */ +inline fun RequiredArgumentBuilder.suggestListWithTooltipsSuspending( + coroutineScope: CoroutineScope = argumentCoroutineScope, + crossinline suggestionsBuilder: (CommandContext) -> Iterable?>? +) { + suggests { context, builder -> + coroutineScope.async { + builder.applyIterableWithTooltips(suggestionsBuilder(context)) + builder.build() + }.asCompletableFuture() + } +} + +@PublishedApi +internal fun SuggestionsBuilder.applyAny(any: Any?) { + when (any) { + is Int -> suggest(any) + is String -> suggest(any) + else -> suggest(any.toString()) + } +} + + +@PublishedApi +internal fun SuggestionsBuilder.applyAnyWithTooltip(pair: Pair?) { + if (pair == null) return + val (any, message) = pair + when (any) { + is Int -> suggest(any, message) + is String -> suggest(any, message) + else -> suggest(any.toString(), message) + } +} + +@PublishedApi +internal fun SuggestionsBuilder.applyIterable(iterable: Iterable?) = + iterable?.forEach(::applyAny) + +@PublishedApi +internal fun SuggestionsBuilder.applyIterableWithTooltips(iterable: Iterable?>?) = + iterable?.forEach(::applyAnyWithTooltip) + +/** + * Adds custom suspending suggestion logic for an argument. + * + * @param coroutineScope the [CoroutineScope] where the suggestions should be built in - an async scope by default + */ +@Deprecated( + "The name 'simpleSuggests' has been superseded by 'suggestListSuspending'", + ReplaceWith("suggestListSuspending(coroutineScope, suggestionBuilder)") +) +fun RequiredArgumentBuilder.simpleSuggests( + coroutineScope: CoroutineScope = argumentCoroutineScope, + suggestionBuilder: suspend (CommandContext) -> Iterable? +) = suggestListSuspending(coroutineScope, suggestionBuilder) diff --git a/src/main/kotlin/net/axay/kspigot/commands/ArgumentTypeUtils.kt b/src/main/kotlin/net/axay/kspigot/commands/internal/ArgumentTypeUtils.kt similarity index 93% rename from src/main/kotlin/net/axay/kspigot/commands/ArgumentTypeUtils.kt rename to src/main/kotlin/net/axay/kspigot/commands/internal/ArgumentTypeUtils.kt index 164ab472..199eaf0e 100644 --- a/src/main/kotlin/net/axay/kspigot/commands/ArgumentTypeUtils.kt +++ b/src/main/kotlin/net/axay/kspigot/commands/internal/ArgumentTypeUtils.kt @@ -1,4 +1,4 @@ -package net.axay.kspigot.commands +package net.axay.kspigot.commands.internal import com.mojang.brigadier.arguments.* diff --git a/src/main/kotlin/net/axay/kspigot/commands/internal/BrigardierSupport.kt b/src/main/kotlin/net/axay/kspigot/commands/internal/BrigardierSupport.kt new file mode 100644 index 00000000..f8f93348 --- /dev/null +++ b/src/main/kotlin/net/axay/kspigot/commands/internal/BrigardierSupport.kt @@ -0,0 +1,82 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + +package net.axay.kspigot.commands.internal + +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import net.axay.kspigot.annotations.NMS_1_17 +import net.axay.kspigot.annotations.NMS_General +import net.axay.kspigot.event.listen +import net.axay.kspigot.extensions.onlinePlayers +import net.axay.kspigot.extensions.server +import net.axay.kspigot.main.KSpigotMainInstance +import net.axay.kspigot.utils.reflectField +import net.minecraft.commands.CommandListenerWrapper +import org.bukkit.event.player.PlayerJoinEvent + +typealias ServerCommandSource = CommandListenerWrapper + +/** + * This class provides Brigardier support. It does that + * by using reflection once. Additionally, this class is + * using some obfuscated functions. + */ +object BrigardierSupport { + @PublishedApi + internal val commands = LinkedHashSet>() + + internal var executedDefaultRegistration = false + private set + + init { + listen { event -> + val player = event.player + val permAttachment = player.addAttachment(KSpigotMainInstance) + commands.forEach { + permAttachment.setPermission("minecraft.command.${it.literal}", true) + } + } + } + + /** + * The command manager is used to hold the command dispatcher, + * and to manage and dispatch the brigardier commands for + * all players on the server. + */ + @Suppress("HasPlatformType") // do not refer non-lazily to the type in this class + @NMS_General + val commandManager by lazy { + (server as org.bukkit.craftbukkit.v1_17_R1.CraftServer).server.vanillaCommandDispatcher + } + + /** + * The command dispatcher is used to register brigardier commands. + */ + @NMS_1_17 + val commandDispatcher by lazy { + // g = the command dispatcher + commandManager.reflectField>("g") + } + + @NMS_General + internal fun registerAll() { + executedDefaultRegistration = true + + // TODO unregister commands which are now missing due to a possible reload + if (commands.isNotEmpty()) { + commands.forEach { + commandDispatcher.register(it) + } + if (onlinePlayers.isNotEmpty()) + updateCommandTree() + } + } + + @NMS_General + fun updateCommandTree() { + onlinePlayers.forEach { + // send the command tree + commandManager.a((it as org.bukkit.craftbukkit.v1_17_R1.entity.CraftPlayer).handle) + } + } +} diff --git a/src/main/kotlin/net/axay/kspigot/main/KSpigot.kt b/src/main/kotlin/net/axay/kspigot/main/KSpigot.kt index 124a3da7..fa370f8c 100644 --- a/src/main/kotlin/net/axay/kspigot/main/KSpigot.kt +++ b/src/main/kotlin/net/axay/kspigot/main/KSpigot.kt @@ -1,5 +1,6 @@ package net.axay.kspigot.main +import net.axay.kspigot.commands.internal.BrigardierSupport import net.axay.kspigot.extensions.bukkit.warn import net.axay.kspigot.extensions.console import net.axay.kspigot.gui.GUIHolder @@ -64,6 +65,8 @@ abstract class KSpigot : JavaPlugin() { final override fun onEnable() { startup() + + BrigardierSupport.registerAll() } final override fun onDisable() {