From 9af293122b860d8c65d68a2bdc02a9c2c5e206cc Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 7 Dec 2025 03:16:54 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=A7=8B=E6=96=87=E3=83=92=E3=83=B3?= =?UTF-8?q?=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 9 ++ .../hareworks/kommand-lib/BrigadierMapper.kt | 135 ++++++++++++++++++ .../net/hareworks/kommand-lib/Kommand.kt | 11 ++ .../kommand-lib/arguments/ArgumentTypes.kt | 8 +- .../net/hareworks/kommand-lib/nodes/Nodes.kt | 6 +- 6 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 src/main/kotlin/net/hareworks/kommand-lib/BrigadierMapper.kt diff --git a/.gitignore b/.gitignore index b85e188..8c5bdf8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .kotlin .gradle build +din diff --git a/README.md b/README.md index 4c0518f..5aa50ee 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Paper/Bukkit サーバー向けのコマンド定義を DSL で記述するた - 1 つの定義から実行とタブ補完の両方を生成 - パーミッションや条件をノード単位で宣言し、子ノードへ自動伝播 - `suggests {}` で引数ごとの補完候補を柔軟に制御 +- Brigadier (Paper 1.21 Lifecycle API) 対応により、クライアント側で ` ` のような構文ヒントや、数値範囲の検証エラー(赤文字)が表示されます - `permits-lib` との連携により、コマンドツリーから Bukkit パーミッションを自動生成し、`compileOnly` 依存として参照可能 ## 依存関係 @@ -162,6 +163,14 @@ commands = kommand(this) { `Coordinates3` は `coordinates("pos") { ... }` 直後のコンテキストで `argument("pos")` として取得でき、`resolve(baseLocation)` で基準座標に対して実座標を求められます。 +## クライアント側構文ヒント (Brigadier) + +Paper 1.21 以降の環境では、`LifecycleEventManager` を通じてコマンドが登録されるため、クライアントにコマンドの構造が送信されます。これにより以下のメリットがあります: + +- **構文の可視化**: 入力中に ` ` のような引数名が表示されます。 +- **クライアント側検証**: `integer("val", min=1, max=10)` などの範囲指定がクライアント側でも判定され、範囲外の値を入力すると赤字になります。 +- **互換性**: 内部的には `Brigadier` のノードに変換されますが、実際のコマンド実行は `kommand-lib` の既存ロジック(`KommandContext`)を使用するため、古いコードの修正は不要です。 + ## ビルドとテスト ```bash diff --git a/src/main/kotlin/net/hareworks/kommand-lib/BrigadierMapper.kt b/src/main/kotlin/net/hareworks/kommand-lib/BrigadierMapper.kt new file mode 100644 index 0000000..b681e5d --- /dev/null +++ b/src/main/kotlin/net/hareworks/kommand-lib/BrigadierMapper.kt @@ -0,0 +1,135 @@ +package net.hareworks.kommand_lib + +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.arguments.DoubleArgumentType +import com.mojang.brigadier.arguments.IntegerArgumentType +import com.mojang.brigadier.arguments.StringArgumentType +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import com.mojang.brigadier.builder.RequiredArgumentBuilder +import com.mojang.brigadier.tree.CommandNode +import io.papermc.paper.command.brigadier.CommandSourceStack +import io.papermc.paper.command.brigadier.Commands +import io.papermc.paper.command.brigadier.argument.ArgumentTypes +import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver +import net.hareworks.kommand_lib.arguments.* +import net.hareworks.kommand_lib.context.KommandContext +import net.hareworks.kommand_lib.execution.ParseMode +import net.hareworks.kommand_lib.nodes.KommandNode +import net.hareworks.kommand_lib.nodes.LiteralNode +import net.hareworks.kommand_lib.nodes.ValueNode +import org.bukkit.plugin.java.JavaPlugin + +@Suppress("UnstableApiUsage") +internal object BrigadierMapper { + + fun map( + plugin: JavaPlugin, + definition: CommandDefinition + ): LiteralArgumentBuilder { + val root = Commands.literal(definition.name) + .requires { source -> definition.rootCondition(source.sender) } + + // Mapped execution for root if args empty + root.executes { ctx -> + definition.execute(plugin, ctx.source.sender, definition.name, emptyArray()) + 1 // Command.SINGLE_SUCCESS + } + + // Map children + definition.nodes.forEach { child -> + mapNode(plugin, definition, child)?.let { root.then(it) } + } + + return root + } + + private fun mapNode( + plugin: JavaPlugin, + definition: CommandDefinition, + node: KommandNode + ): CommandNode? { + val builder = when (node) { + is LiteralNode -> { + Commands.literal(node.segment()) + } + is ValueNode<*> -> { + val argType = mapArgumentType(node) + Commands.argument(node.segment(), argType) + .suggests { ctx, builder -> + // Delegate suggestions back to Kommand + // We need to reconstruct the context vaguely or use existing helpers + // Ideally we grab the full input and pass it to a specialized suggestion handler + // For now we can try to use the node's simple suggestion logic + + // Brigadier pass partial string as builder.remaining + // But Kommand expects a KommandContext. + // Constructing a dummy context might be hard without full chain. + // Simplification: Use standard suggestions from type if available. + + val input = ctx.input + // TODO: more complex suggestion delegation + // For now let Brigadier handle types that it knows (Integer, etc) + // For custom types, we might need a custom SuggestionProvider + + // Simple fallback for custom suggestions from node + val suggestions = node.suggestions(builder.remaining, + KommandContext(plugin, ctx.source.sender, "", emptyArray(), ParseMode.SUGGEST) + // Note: empty args is wrong here, but we can't easily reconstruction full stack + // without reimplementing the parser. + // However, Kommand's `suggestions` method usually only looks at the prefix for simple nodes. + ) + suggestions.forEach { builder.suggest(it) } + builder.buildFuture() + } + } + else -> return null + } + + builder.requires { source -> node.isVisible(source.sender) } + + // Execute wrapper + // Since we want to preserve Kommand's execution logic which relies on parsing the WHOLE string, + // we can just make every node executable and pass the raw input to the existing execute method. + builder.executes { ctx -> + // Reconstruct args from input string + // ctx.input is the full command line e.g. "/cmd arg1 arg2" + val input = ctx.input + val parts = input.trim().split("\\s+".toRegex()) + // Implementation detail: parts[0] is command name normally. + val args = if (parts.size > 1) parts.drop(1).toTypedArray() else emptyArray() + + // We use the alias from the input if possible, or fallback to main name + val alias = parts.firstOrNull()?.removePrefix("/") ?: definition.name + + definition.execute(plugin, ctx.source.sender, alias, args) + 1 + } + + // Recursively map children + node.children.forEach { child -> + mapNode(plugin, definition, child)?.let { builder.then(it) } + } + + return builder.build() + } + + private fun mapArgumentType(node: ValueNode<*>): ArgumentType<*> { + return when (val type = node.argumentType) { + is net.hareworks.kommand_lib.arguments.IntegerArgumentType -> { + val min = type.min ?: Int.MIN_VALUE + val max = type.max ?: Int.MAX_VALUE + com.mojang.brigadier.arguments.IntegerArgumentType.integer(min, max) + } + is net.hareworks.kommand_lib.arguments.FloatArgumentType -> { + val min = type.min ?: -Double.MAX_VALUE + val max = type.max ?: Double.MAX_VALUE + DoubleArgumentType.doubleArg(min, max) + } + is WordArgumentType -> StringArgumentType.word() + is CoordinateComponentArgumentType -> StringArgumentType.string() + is PlayerArgumentType -> StringArgumentType.word() + is PlayerSelectorArgumentType -> StringArgumentType.greedyString() + else -> StringArgumentType.string() + } + } +} diff --git a/src/main/kotlin/net/hareworks/kommand-lib/Kommand.kt b/src/main/kotlin/net/hareworks/kommand-lib/Kommand.kt index 836ae74..2252b16 100644 --- a/src/main/kotlin/net/hareworks/kommand-lib/Kommand.kt +++ b/src/main/kotlin/net/hareworks/kommand-lib/Kommand.kt @@ -43,6 +43,17 @@ class KommandLib internal constructor( } private fun registerAll() { + // Register via Paper Lifecycle API for 1.21+ + val manager = plugin.lifecycleManager + @Suppress("UnstableApiUsage") + manager.registerEventHandler(io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents.COMMANDS) { event -> + val registrar = event.registrar() + for (definition in definitions) { + val node = BrigadierMapper.map(plugin, definition) + registrar.register(node.build(), definition.description, definition.aliases) + } + } + for (definition in definitions) { commandMap.getCommand(definition.name)?.unregister(commandMap) val command = newPluginCommand(definition.name) diff --git a/src/main/kotlin/net/hareworks/kommand-lib/arguments/ArgumentTypes.kt b/src/main/kotlin/net/hareworks/kommand-lib/arguments/ArgumentTypes.kt index ee6393b..192aa84 100644 --- a/src/main/kotlin/net/hareworks/kommand-lib/arguments/ArgumentTypes.kt +++ b/src/main/kotlin/net/hareworks/kommand-lib/arguments/ArgumentTypes.kt @@ -24,8 +24,8 @@ object WordArgumentType : KommandArgumentType { } class IntegerArgumentType( - private val min: Int? = null, - private val max: Int? = null + val min: Int? = null, + val max: Int? = null ) : KommandArgumentType { override fun parse(input: String, context: KommandContext): ArgumentParseResult { val value = input.toIntOrNull() @@ -41,8 +41,8 @@ class IntegerArgumentType( } class FloatArgumentType( - private val min: Double? = null, - private val max: Double? = null + val min: Double? = null, + val max: Double? = null ) : KommandArgumentType { override fun parse(input: String, context: KommandContext): ArgumentParseResult { val value = input.toDoubleOrNull() diff --git a/src/main/kotlin/net/hareworks/kommand-lib/nodes/Nodes.kt b/src/main/kotlin/net/hareworks/kommand-lib/nodes/Nodes.kt index a06e87b..0668aef 100644 --- a/src/main/kotlin/net/hareworks/kommand-lib/nodes/Nodes.kt +++ b/src/main/kotlin/net/hareworks/kommand-lib/nodes/Nodes.kt @@ -42,12 +42,12 @@ class LiteralNode internal constructor(private val literal: String) : KommandNod open class ValueNode internal constructor( private val name: String, - private val type: KommandArgumentType + val argumentType: KommandArgumentType ) : KommandNode() { var suggestionProvider: ((KommandContext, String) -> List)? = null override fun consume(token: String, context: KommandContext, mode: ParseMode): Boolean { - return when (val result = type.parse(token, context)) { + return when (val result = argumentType.parse(token, context)) { is net.hareworks.kommand_lib.arguments.ArgumentParseResult.Success -> { context.remember(name, result.value) true @@ -68,7 +68,7 @@ open class ValueNode internal constructor( override fun suggestions(prefix: String, context: KommandContext): List { val custom = suggestionProvider?.invoke(context, prefix) if (custom != null) return custom - return type.suggestions(context, prefix) + return argumentType.suggestions(context, prefix) } override fun segment(): String = name