diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 526639b..02067f4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ kmongo = "5.1.0" kotest = "5.9.1" # @pin kotlin = "2.0.21" +kotlinxIO = "0.5.4" ktor = "2.3.11" logback = "1.5.9" mccoroutine = "2.20.0" @@ -64,6 +65,7 @@ kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } +kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinxIO" } kotlinx-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "serialization" } kotlinx-serialization-hocon = { module = "org.jetbrains.kotlinx:kotlinx-serialization-hocon", version = "serialization" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } @@ -115,6 +117,7 @@ platform = [ "kotlin-reflect", "kotlin-stdlib", "kotlinx-coroutines", + "kotlinx-io", "kotlinx-serialization-cbor", "kotlinx-serialization-hocon", "kotlinx-serialization-json", diff --git a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/ArgumentExecutes.kt b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/ArgumentExecutes.kt new file mode 100644 index 0000000..ec92c1a --- /dev/null +++ b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/ArgumentExecutes.kt @@ -0,0 +1,62 @@ +package com.mineinabyss.idofront.commands.brigadier + +import com.mineinabyss.idofront.commands.brigadier.context.IdoCommandContext +import com.mojang.brigadier.arguments.ArgumentType + + +inline fun IdoCommand.executes( + a: ArgumentType, + crossinline run: IdoCommandContext.(A) -> Unit +) { + executesDefaulting(a) { (a) -> run(arg(a)) } +} + +inline fun IdoCommand.executes( + a: ArgumentType, + b: ArgumentType, + crossinline run: IdoCommandContext.(A, B) -> Unit +) { + executesDefaulting(a, b) { (a, b) -> run(arg(a), arg(b)) } +} + +inline fun IdoCommand.executes( + a: ArgumentType, + b: ArgumentType, + c: ArgumentType, + crossinline run: IdoCommandContext.(A, B, C) -> Unit +) { + executesDefaulting(a, b, c) { (a, b, c) -> run(arg(a), arg(b), arg(c)) } +} + +inline fun IdoCommand.executes( + a: ArgumentType, + b: ArgumentType, + c: ArgumentType, + d: ArgumentType, + crossinline run: IdoCommandContext.(A, B, C, D) -> Unit +) { + executesDefaulting(a, b, c, d) { (a, b, c, d) -> run(arg(a), arg(b), arg(c), arg(d)) } +} + +inline fun IdoCommand.executes( + a: ArgumentType, + b: ArgumentType, + c: ArgumentType, + d: ArgumentType, + e: ArgumentType, + crossinline run: IdoCommandContext.(A, B, C, D, E) -> Unit +) { + executesDefaulting(a, b, c, d, e) { (a, b, c, d, e) -> run(arg(a), arg(b), arg(c), arg(d), arg(e)) } +} + +inline fun IdoCommand.executes( + a: ArgumentType, + b: ArgumentType, + c: ArgumentType, + d: ArgumentType, + e: ArgumentType, + f: ArgumentType, + crossinline run: IdoCommandContext.(A, B, C, D, E, F) -> Unit +) { + executesDefaulting(a, b, c, d, e, f) { run(arg(it[0]), arg(it[1]), arg(it[2]), arg(it[3]), arg(it[4]), arg(it[5])) } +} diff --git a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/ArgumentPlayerExecutes.kt b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/ArgumentPlayerExecutes.kt new file mode 100644 index 0000000..ddb46e7 --- /dev/null +++ b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/ArgumentPlayerExecutes.kt @@ -0,0 +1,62 @@ +package com.mineinabyss.idofront.commands.brigadier + +import com.mineinabyss.idofront.commands.brigadier.context.IdoPlayerCommandContext +import com.mojang.brigadier.arguments.ArgumentType + + +inline fun IdoCommand.playerExecutes( + a: ArgumentType, + crossinline run: IdoPlayerCommandContext.(A) -> Unit +) { + playerExecutesDefaulting(a) { (a) -> run(arg(a)) } +} + +inline fun IdoCommand.playerExecutes( + a: ArgumentType, + b: ArgumentType, + crossinline run: IdoPlayerCommandContext.(A, B) -> Unit +) { + playerExecutesDefaulting(a, b) { (a, b) -> run(arg(a), arg(b)) } +} + +inline fun IdoCommand.playerExecutes( + a: ArgumentType, + b: ArgumentType, + c: ArgumentType, + crossinline run: IdoPlayerCommandContext.(A, B, C) -> Unit +) { + playerExecutesDefaulting(a, b, c) { (a, b, c) -> run(arg(a), arg(b), arg(c)) } +} + +inline fun IdoCommand.playerExecutes( + a: ArgumentType, + b: ArgumentType, + c: ArgumentType, + d: ArgumentType, + crossinline run: IdoPlayerCommandContext.(A, B, C, D) -> Unit +) { + playerExecutesDefaulting(a, b, c, d) { (a, b, c, d) -> run(arg(a), arg(b), arg(c), arg(d)) } +} + +inline fun IdoCommand.playerExecutes( + a: ArgumentType, + b: ArgumentType, + c: ArgumentType, + d: ArgumentType, + e: ArgumentType, + crossinline run: IdoPlayerCommandContext.(A, B, C, D, E) -> Unit +) { + playerExecutesDefaulting(a, b, c, d, e) { (a, b, c, d, e) -> run(arg(a), arg(b), arg(c), arg(d), arg(e)) } +} + +inline fun IdoCommand.playerExecutes( + a: ArgumentType, + b: ArgumentType, + c: ArgumentType, + d: ArgumentType, + e: ArgumentType, + f: ArgumentType, + crossinline run: IdoPlayerCommandContext.(A, B, C, D, E, F) -> Unit +) { + playerExecutesDefaulting(a, b, c, d, e, f) { run(arg(it[0]), arg(it[1]), arg(it[2]), arg(it[3]), arg(it[4]), arg(it[5])) } +} diff --git a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoArgument.kt b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoArgument.kt index e079285..7fcd83e 100644 --- a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoArgument.kt +++ b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoArgument.kt @@ -1,9 +1,12 @@ package com.mineinabyss.idofront.commands.brigadier +import com.mineinabyss.idofront.commands.brigadier.context.IdoCommandContext import kotlin.reflect.KProperty class IdoArgument( val name: String, + val resolve: ((IdoCommandContext, Any) -> T)? = null, + val default: ((IdoCommandContext) -> T)? = null, ) { operator fun getValue(thisRef: Any?, property: KProperty<*>): IdoArgument { return this diff --git a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoArgumentBuilder.kt b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoArgumentBuilder.kt index 5313832..a500e2e 100644 --- a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoArgumentBuilder.kt +++ b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoArgumentBuilder.kt @@ -1,8 +1,68 @@ package com.mineinabyss.idofront.commands.brigadier +import com.github.shynixn.mccoroutine.bukkit.asyncDispatcher +import com.github.shynixn.mccoroutine.bukkit.scope +import com.mineinabyss.idofront.commands.brigadier.context.IdoCommandContext +import com.mineinabyss.idofront.commands.brigadier.context.IdoSuggestionsContext +import com.mojang.brigadier.StringReader import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.suggestion.SuggestionProvider +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import io.papermc.paper.command.brigadier.CommandSourceStack +import kotlinx.coroutines.future.future +import org.bukkit.Bukkit +import java.util.concurrent.CompletableFuture -data class IdoArgumentBuilder( - val type: ArgumentType, - val suggestions: (suspend IdoSuggestionsContext.() -> Unit)? = null, -) +data class IdoArgumentType( + val nativeType: ArgumentType, + val name: String? = null, + val resolve: ((IdoCommandContext, Any) -> T)? = null, + val suggestions: ((CommandContext, SuggestionsBuilder) -> CompletableFuture)? = null, + val commandExamples: MutableCollection, + val default: (IdoCommandContext.() -> T)? = null, +) : ArgumentType { + fun createType() = nativeType + + override fun parse(reader: StringReader?) = + error("IdoArgumentType should not be parsed directly, call createType() instead.") + + inline fun suggests(crossinline suggestions: suspend IdoSuggestionsContext.() -> Unit): IdoArgumentType = + copy( + suggestions = { context, builder -> + val plugin = Bukkit.getPluginManager().getPlugin("Idofront")!! + plugin.scope.future(plugin.asyncDispatcher) { + suggestions(IdoSuggestionsContext(context as CommandContext, builder)) + builder.build() + } + } + ) + + fun suggests(provider: SuggestionProvider): IdoArgumentType = copy( + suggestions = { context, suggestions -> + provider.getSuggestions( + context as CommandContext, + suggestions + ) + }, + ) + + fun default(default: IdoCommandContext.() -> T): IdoArgumentType = + copy(default = default) + + inline fun map(crossinline transform: IdoCommandContext.(T) -> R): IdoArgumentType = + IdoArgumentType( + nativeType = nativeType, + name = name, + resolve = { context, value -> + resolve + ?.let { transform(context, it(context, value)) } + ?: transform(context, value as T) + }, + suggestions = suggestions, + commandExamples = commandExamples, + ) + + fun named(name: String) = copy(name = name) +} diff --git a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoCommand.kt b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoCommand.kt index 689a81f..635abd3 100644 --- a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoCommand.kt +++ b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoCommand.kt @@ -1,70 +1,72 @@ package com.mineinabyss.idofront.commands.brigadier -import com.github.shynixn.mccoroutine.bukkit.asyncDispatcher +import com.mineinabyss.idofront.commands.brigadier.context.IdoCommandContext +import com.mineinabyss.idofront.commands.brigadier.context.IdoPlayerCommandContext +import com.mineinabyss.idofront.commands.brigadier.context.IdoSuggestionsContext import com.mineinabyss.idofront.commands.execution.CommandExecutionFailedException import com.mineinabyss.idofront.textcomponents.miniMsg -import com.mojang.brigadier.arguments.* +import com.mojang.brigadier.arguments.ArgumentType import com.mojang.brigadier.builder.ArgumentBuilder import com.mojang.brigadier.builder.LiteralArgumentBuilder +import com.mojang.brigadier.context.CommandContext import com.mojang.brigadier.suggestion.SuggestionProvider import com.mojang.brigadier.tree.LiteralCommandNode import io.papermc.paper.command.brigadier.CommandSourceStack import io.papermc.paper.command.brigadier.Commands -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.async -import kotlinx.coroutines.future.asCompletableFuture +import io.papermc.paper.command.brigadier.argument.CustomArgumentType +import io.papermc.paper.command.brigadier.argument.resolvers.ArgumentResolver +import org.bukkit.command.CommandSender import org.bukkit.entity.Player import org.bukkit.plugin.Plugin import kotlin.reflect.KProperty +/** + * @property initial The initial literal argument builder for the command, the dsl adds render steps to it, + * which will get applied when [build] gets called. + * @property name The name of the command. + * @property plugin The plugin associated with the command. + * @property parentPermission The permission used by the parent of this command, if any. + */ @Suppress("UnstableApiUsage") @Annotations open class IdoCommand( internal val initial: LiteralArgumentBuilder, val name: String, val plugin: Plugin, + val parentPermission: String?, ) { private val renderSteps = mutableListOf() + var permission: String? = defaultPermission() - fun ArgumentType.suggests(suggestions: suspend IdoSuggestionsContext.() -> Unit): IdoArgumentBuilder { - return IdoArgumentBuilder(this, suggestions) - } - - fun ArgumentType.suggests(provider: SuggestionProvider): IdoArgumentBuilder { - return IdoArgumentBuilder(this) { provider.getSuggestions(context, suggestions) } - } - - operator fun ArgumentType.provideDelegate(thisRef: Any?, property: KProperty<*>): IdoArgument { + fun registerArgument(argument: ArgumentType, defaultName: String): IdoArgument { + val type = if (argument is IdoArgumentType) argument.createType() else argument + val name = if (argument is IdoArgumentType) argument.name ?: defaultName else defaultName // If no suggestions are provided, use the default listSuggestions method - add(RenderStep.Builder(Commands.argument(property.name, this).apply { - suggests { context, builder -> - // Call the default listSuggestions method on the ArgumentType - this@provideDelegate.listSuggestions(context, builder) + add(RenderStep.Builder(Commands.argument(name, type).apply { + (argument as? IdoArgumentType)?.suggestions?.let { + suggests { context, builder -> + it(context as CommandContext, builder) + } } })) // Return an IdoArgument object with the argument's name - return IdoArgument(property.name) + return createArgumentRef(argument, name) } - operator fun IdoArgumentBuilder.provideDelegate(thisRef: Any?, property: KProperty<*>): IdoArgument { - add(RenderStep.Builder(Commands.argument(property.name, type).apply { - if (this@provideDelegate.suggestions != null) - suggests { context, builder -> - CoroutineScope(plugin.asyncDispatcher).async { - this@provideDelegate.suggestions.invoke(IdoSuggestionsContext(context, builder)) - builder.build() - }.asCompletableFuture() - } - })) - return IdoArgument(property.name) + fun createArgumentRef(argument: ArgumentType, defaultName: String): IdoArgument { + val resolve = (argument as? IdoArgumentType)?.resolve + val default = (argument as? IdoArgumentType)?.default + val name = if (argument is IdoArgumentType<*>) argument.name ?: defaultName else defaultName + return IdoArgument(name, resolve, default) } /** Creates a subcommand using [Commands.literal]. */ inline operator fun String.invoke(init: IdoCommand.() -> Unit) { - add(RenderStep.Command(IdoCommand(Commands.literal(this), this, plugin).apply(init))) + add(RenderStep.Command(IdoCommand(Commands.literal(this), this, plugin, permission).apply(init))) } + /** Creates a subcommand with aliases using [Commands.literal]. */ inline operator fun List.invoke(init: IdoCommand.() -> Unit) { forEach { it.invoke { init() } } } @@ -78,7 +80,6 @@ open class IdoCommand( } /** The permission to use for this command. If null, use default of plugin.commandname. If it is blank, require no permission */ - var permission: String? = null fun requiresPermission(permission: String) { this.permission = permission } @@ -90,19 +91,69 @@ open class IdoCommand( run(IdoCommandContext(context)) } catch (e: CommandExecutionFailedException) { e.replyWith?.let { context.source.sender.sendMessage(it) } + } catch (e: Exception) { + e.printStackTrace() + context.source.sender.sendMessage("An error occurred while executing this command.".miniMsg()) } com.mojang.brigadier.Command.SINGLE_SUCCESS } } + inline fun executesDefaulting( + vararg arguments: ArgumentType<*>, + crossinline + run: IdoCommandContext.(arguments: List>) -> Unit, + ): List> { + val trailingDefaultIndex = + arguments.lastIndex - arguments.takeLastWhile { (it as? IdoArgumentType<*>)?.default != null }.size + val refs = arguments.mapIndexed { index, it -> createArgumentRef(it, index.toString()) } + + if (trailingDefaultIndex == -1) executes { run(refs) } + + return arguments.foldIndexed(listOf>()) { index, acc, arg -> + val registered = acc + registerArgument(arg, index.toString()) + if (index >= trailingDefaultIndex) executes { run(registered + refs.drop(registered.size)) } + registered + } + } + + fun playerExecutesDefaulting( + vararg arguments: ArgumentType<*>, + run: IdoPlayerCommandContext.(arguments: List>) -> Unit, + ) { + executesDefaulting( + *arguments.toList().plus( + ArgsMinecraft + .player() + .resolve() + .named("target-player") + .default { listOf(sender as? Player ?: fail("Sender needs to be a player")) } + ).toTypedArray() + ) { + if (executor !is Player) fail("This command can only be run by a player.".miniMsg()) + run.invoke(IdoPlayerCommandContext(context, arg>(it.last()).single()), it.dropLast(1)) + } + } + /** [executes], ensuring the executor is a player. */ inline fun playerExecutes(crossinline run: IdoPlayerCommandContext.() -> Unit) { executes { - if (executor !is Player) commandException("This command can only be run by a player.".miniMsg()) + if (executor !is Player) fail("This command can only be run by a player.".miniMsg()) run(IdoPlayerCommandContext(context)) } } + /** Gets the assumed permission for this command based on its [plugin], [parentPermission], and [name] */ + fun defaultPermission(): String { + val safeName = name.replace('.', '_') + val pluginName = plugin.name.lowercase() + return when { + parentPermission != null -> "$parentPermission.$safeName" + pluginName == safeName -> pluginName + else -> "$pluginName.$safeName" + } + } + @PublishedApi internal fun add(step: RenderStep) { renderSteps += step @@ -120,9 +171,64 @@ open class IdoCommand( } internal fun build(): LiteralCommandNode { + // Apply default command permission + permission?.takeIf { it.isNotEmpty() } + ?.let { perm -> initial.requires { it.sender.hasPermissionRecursive(perm) } } + + // Apply render steps to command sequentially render().fold(initial as IdoArgBuilder) { acc, curr -> curr.foldLeft(acc) } + + // Get a final built command from Brigadier return initial.build() } + + // ArgumentType extensions + + fun ArgumentType.toIdo(): IdoArgumentType = IdoArgumentType( + nativeType = (when (this) { + is CustomArgumentType<*, *> -> nativeType + is IdoArgumentType -> nativeType + else -> this + }) as ArgumentType, + suggestions = null, + commandExamples = mutableListOf() + ) + + fun , T> ArgumentType.resolve(): IdoArgumentType = toIdo().let { + IdoArgumentType( + nativeType = it.nativeType, + resolve = { context, value -> (value as R).resolve(context.context.source) }, + suggestions = it.suggestions, + commandExamples = it.commandExamples + ) + } + + inline fun ArgumentType.suggests(crossinline suggestions: suspend IdoSuggestionsContext.() -> Unit) = + toIdo().suggests(suggestions) + + fun ArgumentType.default(default: IdoCommandContext.() -> T): IdoArgumentType = + toIdo().copy(default = default) + + fun ArgumentType.suggests(provider: SuggestionProvider) = + toIdo().suggests(provider) + + inline fun ArgumentType.map(crossinline transform: IdoCommandContext.(T) -> R) = + toIdo().map(transform) + + fun ArgumentType.named(name: String) = toIdo().copy(name = name) + + operator fun ArgumentType.provideDelegate(thisRef: Any?, property: KProperty<*>): IdoArgument { + return registerArgument(this, property.name) + } + + + companion object { + fun CommandSender.hasPermissionRecursive(permission: String): Boolean { + val parts = permission.split(".") + if (hasPermission(permission)) return true + return (1..parts.size).any { hasPermission(parts.take(it).joinToString(".") + ".*") } + } + } } diff --git a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoCommandContext.kt b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoCommandContext.kt deleted file mode 100644 index 5207049..0000000 --- a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoCommandContext.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.mineinabyss.idofront.commands.brigadier - -import com.mineinabyss.idofront.commands.execution.CommandExecutionFailedException -import com.mineinabyss.idofront.textcomponents.miniMsg -import com.mojang.brigadier.context.CommandContext -import io.papermc.paper.command.brigadier.CommandSourceStack -import io.papermc.paper.command.brigadier.argument.resolvers.ArgumentResolver -import net.kyori.adventure.text.Component -import org.bukkit.Location -import org.bukkit.command.CommandSender -import org.bukkit.entity.Entity - -@Annotations -@Suppress("UnstableApiUsage") -open class IdoCommandContext( - val context: CommandContext, -) { - /** Stops the command, sending a [message] formatted with MiniMessage to its [sender]. */ - fun commandException(message: String): Nothing = throw CommandExecutionFailedException(message.miniMsg()) - - /** Stops the command, sending a [message] to its [sender]. */ - fun commandException(message: Component): Nothing = throw CommandExecutionFailedException(message) - - /** The sender that ran this command. */ - val sender: CommandSender = context.source.sender - - /** An entity representing the [sender] on the server. */ - val executor: Entity? = context.source.executor - - val location: Location = context.source.location - - @JvmName("invoke1") - inline operator fun IdoArgument>.invoke(): T { - @Suppress("UNCHECKED_CAST") // getArgument logic ensures this cast always succeeds if the argument was registered - return ((this as IdoArgument).invoke() as ArgumentResolver) - .resolve(context.source) - } - - @JvmName("invoke2") - inline operator fun IdoArgument.invoke(): T { - return context.getArgumentOrNull(name) - ?: commandException("Argument $name not found".miniMsg()) - } - - @PublishedApi - internal inline fun CommandContext.getArgumentOrNull(name: String): T? = runCatching { - context.getArgument(name, T::class.java) - }.getOrNull() -} diff --git a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoRootCommand.kt b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoRootCommand.kt index e7d812c..e441397 100644 --- a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoRootCommand.kt +++ b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoRootCommand.kt @@ -12,4 +12,4 @@ class IdoRootCommand( val description: String?, val aliases: List, plugin: Plugin, -) : IdoCommand(initial, name, plugin) +) : IdoCommand(initial, name, plugin, parentPermission = null) diff --git a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/RootIdoCommands.kt b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/RootIdoCommands.kt index 0608251..e2d753b 100644 --- a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/RootIdoCommands.kt +++ b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/RootIdoCommands.kt @@ -1,5 +1,6 @@ package com.mineinabyss.idofront.commands.brigadier +import com.mojang.brigadier.tree.LiteralCommandNode import io.papermc.paper.command.brigadier.Commands import org.bukkit.plugin.Plugin @@ -37,10 +38,7 @@ class RootIdoCommands( internal fun buildEach() { rootCommands.forEach { command -> commands.register( - command.apply { - val permission = permission ?: "${plugin.name}.$name" - if (permission.isNotEmpty()) requires { sender.hasPermission("$permission.*") || sender.hasPermission(permission) } - }.build(), + command.build(), command.description, command.aliases ) diff --git a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/context/IdoCommandContext.kt b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/context/IdoCommandContext.kt new file mode 100644 index 0000000..8d9f6b6 --- /dev/null +++ b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/context/IdoCommandContext.kt @@ -0,0 +1,49 @@ +package com.mineinabyss.idofront.commands.brigadier.context + +import com.mineinabyss.idofront.commands.brigadier.Annotations +import com.mineinabyss.idofront.commands.brigadier.IdoArgument +import com.mineinabyss.idofront.commands.execution.CommandExecutionFailedException +import com.mineinabyss.idofront.textcomponents.miniMsg +import com.mojang.brigadier.context.CommandContext +import io.papermc.paper.command.brigadier.CommandSourceStack +import net.kyori.adventure.text.Component +import org.bukkit.Location +import org.bukkit.command.CommandSender +import org.bukkit.entity.Entity + +@Annotations +@Suppress("UnstableApiUsage") +open class IdoCommandContext( + val context: CommandContext, +) { + /** Stops the command, sending a [message] formatted with MiniMessage to its [sender]. */ + fun fail(message: String): Nothing = throw CommandExecutionFailedException("$message".miniMsg()) + + /** Stops the command, sending a [message] to its [sender]. */ + fun fail(message: Component): Nothing = throw CommandExecutionFailedException(message) + + /** The sender that ran this command. */ + val sender: CommandSender = context.source.sender + + val source get() = context.source + + /** An entity representing the [sender] on the server. */ + val executor: Entity? = source.executor + + val location: Location = source.location + + @JvmName("invoke2") + inline operator fun IdoArgument.invoke(): T { + return getArgumentOrNull(this) ?: fail("Argument $name not found".miniMsg()) + } + + @PublishedApi + internal inline fun getArgumentOrNull(argument: IdoArgument): T? { + val arg: Any = runCatching { context.getArgument(argument.name, Any::class.java) }.getOrNull() + ?: return argument.default?.invoke(this) + return (argument.resolve?.invoke(this, arg) ?: arg) as T + } + + @PublishedApi + internal inline fun arg(argument: IdoArgument<*>): T = (argument as IdoArgument).invoke() +} diff --git a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoPlayerCommandContext.kt b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/context/IdoPlayerCommandContext.kt similarity index 55% rename from idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoPlayerCommandContext.kt rename to idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/context/IdoPlayerCommandContext.kt index 68d6c60..0002ba4 100644 --- a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoPlayerCommandContext.kt +++ b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/context/IdoPlayerCommandContext.kt @@ -1,5 +1,6 @@ -package com.mineinabyss.idofront.commands.brigadier +package com.mineinabyss.idofront.commands.brigadier.context +import com.mineinabyss.idofront.commands.brigadier.Annotations import com.mojang.brigadier.context.CommandContext import io.papermc.paper.command.brigadier.CommandSourceStack import org.bukkit.entity.Player @@ -8,6 +9,5 @@ import org.bukkit.entity.Player @Suppress("UnstableApiUsage") class IdoPlayerCommandContext( context: CommandContext, -): IdoCommandContext(context) { - val player = executor as Player -} + val player: Player = context.source.executor as Player +): IdoCommandContext(context) diff --git a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoSuggestionsContext.kt b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/context/IdoSuggestionsContext.kt similarity index 90% rename from idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoSuggestionsContext.kt rename to idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/context/IdoSuggestionsContext.kt index 72d28e0..819eb71 100644 --- a/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/IdoSuggestionsContext.kt +++ b/idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/context/IdoSuggestionsContext.kt @@ -1,15 +1,17 @@ -package com.mineinabyss.idofront.commands.brigadier +package com.mineinabyss.idofront.commands.brigadier.context +import com.mojang.brigadier.LiteralMessage import com.mojang.brigadier.Message import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType import com.mojang.brigadier.suggestion.SuggestionsBuilder import io.papermc.paper.command.brigadier.CommandSourceStack @Suppress("UnstableApiUsage") -data class IdoSuggestionsContext( - val context: CommandContext, +class IdoSuggestionsContext( + context: CommandContext, val suggestions: SuggestionsBuilder, -) { +): IdoCommandContext(context) { /** The argument currently being typed*/ val argument get() = suggestions.remaining diff --git a/idofront-serializers/src/main/kotlin/com/mineinabyss/idofront/serialization/FoodComponentSerializer.kt b/idofront-serializers/src/main/kotlin/com/mineinabyss/idofront/serialization/FoodComponentSerializer.kt index 3819e56..a362966 100644 --- a/idofront-serializers/src/main/kotlin/com/mineinabyss/idofront/serialization/FoodComponentSerializer.kt +++ b/idofront-serializers/src/main/kotlin/com/mineinabyss/idofront/serialization/FoodComponentSerializer.kt @@ -20,7 +20,7 @@ import kotlin.time.DurationUnit @Serializable @SerialName("FoodComponent") -private class FoodComponentSurrogate( +class FoodComponentSurrogate( val nutrition: Int, val saturation: Float, val eatSeconds: Float = 1.6f, @@ -28,37 +28,29 @@ private class FoodComponentSurrogate( val usingConvertsTo: SerializableItemStack? = null, val effects: List = emptyList() ) { + + constructor(food: FoodComponent) : this( + food.nutrition, + food.saturation, + food.eatSeconds, + food.canAlwaysEat(), + food.usingConvertsTo?.toSerializable(), + food.effects.map { FoodEffectWrapper(it.effect, it.probability) }) + init { require(nutrition >= 0) { "FoodComponent must have a positive nutrition" } require(saturation >= 0) { "FoodComponent must have a positive saturation" } require(eatSeconds >= 0) { "FoodComponent must have a positive eatSeconds" } require(effects.all { it.probability in 0.0..1.0 }) { "FoodEffect-probability must be between 0.0..1.0" } } -} -object FoodComponentSerializer : KSerializer { - override val descriptor: SerialDescriptor = FoodComponentSurrogate.serializer().descriptor - override fun serialize(encoder: Encoder, value: FoodComponent) { - val surrogate = FoodComponentSurrogate( - value.nutrition, - value.saturation, - value.eatSeconds, - value.canAlwaysEat(), - value.usingConvertsTo?.toSerializable(), - value.effects.map { FoodEffectWrapper(it.effect, it.probability) } - ) - encoder.encodeSerializableValue(FoodComponentSurrogate.serializer(), surrogate) - } - - override fun deserialize(decoder: Decoder): FoodComponent { - return ItemStack(Material.PAPER).itemMeta.food.apply { - val surrogate = decoder.decodeSerializableValue(FoodComponentSurrogate.serializer()) - nutrition = surrogate.nutrition - saturation = surrogate.saturation - setCanAlwaysEat(surrogate.canAlwaysEat) - eatSeconds = surrogate.eatSeconds - usingConvertsTo = surrogate.usingConvertsTo?.toItemStackOrNull() - surrogate.effects.forEach { addEffect(it.effect, it.probability) } + val foodComponent: FoodComponent + get() = ItemStack.of(Material.PAPER).itemMeta.food.also { + it.nutrition = nutrition + it.saturation = saturation + it.eatSeconds = eatSeconds + it.setCanAlwaysEat(canAlwaysEat) + it.usingConvertsTo = usingConvertsTo?.toItemStackOrNull() + effects.forEach { e -> it.addEffect(e.effect, e.probability) } } - } -} \ No newline at end of file +} diff --git a/idofront-serializers/src/main/kotlin/com/mineinabyss/idofront/serialization/SerializableItemStack.kt b/idofront-serializers/src/main/kotlin/com/mineinabyss/idofront/serialization/SerializableItemStack.kt index 9a44b94..41ca897 100644 --- a/idofront-serializers/src/main/kotlin/com/mineinabyss/idofront/serialization/SerializableItemStack.kt +++ b/idofront-serializers/src/main/kotlin/com/mineinabyss/idofront/serialization/SerializableItemStack.kt @@ -61,7 +61,7 @@ data class BaseSerializableItemStack( @EncodeDefault(NEVER) val customPotionEffects: List<@Serializable(with = PotionEffectSerializer::class) PotionEffect> = listOf(), @EncodeDefault(NEVER) val knowledgeBookRecipes: List? = null, @EncodeDefault(NEVER) val color: @Serializable(with = ColorSerializer::class) Color? = null, - @EncodeDefault(NEVER) val food: @Serializable(with = FoodComponentSerializer::class) FoodComponent? = null, + @EncodeDefault(NEVER) val food: FoodComponentSurrogate? = null, @EncodeDefault(NEVER) val tool: @Serializable(with = ToolComponentSerializer::class) ToolComponent? = null, @EncodeDefault(NEVER) val jukeboxPlayable: @Serializable(with = JukeboxPlayableSerializer::class) JukeboxPlayableComponent? = null, @EncodeDefault(NEVER) val hideTooltip: Boolean? = null, @@ -161,7 +161,7 @@ data class BaseSerializableItemStack( } enchantmentGlintOverride?.let(meta::setEnchantmentGlintOverride) - food?.let(meta::setFood) + food?.foodComponent?.let(meta::setFood) tool?.let(meta::setTool) jukeboxPlayable?.let(meta::setJukeboxPlayable) maxStackSize?.let(meta::setMaxStackSize) @@ -213,7 +213,7 @@ fun ItemStack.toSerializable(): SerializableItemStack = with(itemMeta) { attributeModifiers = attributeList.takeIf { it.isNotEmpty() }, basePotionType = (this as? PotionMeta)?.basePotionType, color = (this as? PotionMeta)?.color ?: (this as? LeatherArmorMeta)?.color, - food = if (hasFood()) food else null, + food = if (hasFood()) FoodComponentSurrogate(food) else null, tool = if (hasTool()) tool else null, jukeboxPlayable = if (hasJukeboxPlayable()) jukeboxPlayable else null, enchantmentGlintOverride = if (hasEnchantmentGlintOverride()) enchantmentGlintOverride else null,