feat: 構文ヒント
This commit is contained in:
parent
6c62d3306e
commit
9af293122b
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,3 +2,4 @@
|
|||
.kotlin
|
||||
.gradle
|
||||
build
|
||||
din
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ Paper/Bukkit サーバー向けのコマンド定義を DSL で記述するた
|
|||
- 1 つの定義から実行とタブ補完の両方を生成
|
||||
- パーミッションや条件をノード単位で宣言し、子ノードへ自動伝播
|
||||
- `suggests {}` で引数ごとの補完候補を柔軟に制御
|
||||
- Brigadier (Paper 1.21 Lifecycle API) 対応により、クライアント側で `<speed> <count>` のような構文ヒントや、数値範囲の検証エラー(赤文字)が表示されます
|
||||
- `permits-lib` との連携により、コマンドツリーから Bukkit パーミッションを自動生成し、`compileOnly` 依存として参照可能
|
||||
|
||||
## 依存関係
|
||||
|
|
@ -162,6 +163,14 @@ commands = kommand(this) {
|
|||
|
||||
`Coordinates3` は `coordinates("pos") { ... }` 直後のコンテキストで `argument<Coordinates3>("pos")` として取得でき、`resolve(baseLocation)` で基準座標に対して実座標を求められます。
|
||||
|
||||
## クライアント側構文ヒント (Brigadier)
|
||||
|
||||
Paper 1.21 以降の環境では、`LifecycleEventManager` を通じてコマンドが登録されるため、クライアントにコマンドの構造が送信されます。これにより以下のメリットがあります:
|
||||
|
||||
- **構文の可視化**: 入力中に `<speed> <amount>` のような引数名が表示されます。
|
||||
- **クライアント側検証**: `integer("val", min=1, max=10)` などの範囲指定がクライアント側でも判定され、範囲外の値を入力すると赤字になります。
|
||||
- **互換性**: 内部的には `Brigadier` のノードに変換されますが、実際のコマンド実行は `kommand-lib` の既存ロジック(`KommandContext`)を使用するため、古いコードの修正は不要です。
|
||||
|
||||
## ビルドとテスト
|
||||
|
||||
```bash
|
||||
|
|
|
|||
135
src/main/kotlin/net/hareworks/kommand-lib/BrigadierMapper.kt
Normal file
135
src/main/kotlin/net/hareworks/kommand-lib/BrigadierMapper.kt
Normal file
|
|
@ -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<CommandSourceStack> {
|
||||
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<CommandSourceStack>? {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ object WordArgumentType : KommandArgumentType<String> {
|
|||
}
|
||||
|
||||
class IntegerArgumentType(
|
||||
private val min: Int? = null,
|
||||
private val max: Int? = null
|
||||
val min: Int? = null,
|
||||
val max: Int? = null
|
||||
) : KommandArgumentType<Int> {
|
||||
override fun parse(input: String, context: KommandContext): ArgumentParseResult<Int> {
|
||||
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<Double> {
|
||||
override fun parse(input: String, context: KommandContext): ArgumentParseResult<Double> {
|
||||
val value = input.toDoubleOrNull()
|
||||
|
|
|
|||
|
|
@ -42,12 +42,12 @@ class LiteralNode internal constructor(private val literal: String) : KommandNod
|
|||
|
||||
open class ValueNode<T> internal constructor(
|
||||
private val name: String,
|
||||
private val type: KommandArgumentType<T>
|
||||
val argumentType: KommandArgumentType<T>
|
||||
) : KommandNode() {
|
||||
var suggestionProvider: ((KommandContext, String) -> List<String>)? = 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<T> internal constructor(
|
|||
override fun suggestions(prefix: String, context: KommandContext): List<String> {
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user