From aab2b1169cc7b32fd7292a60346f2423b7721b8a Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 7 Dec 2025 06:00:56 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Brigadier=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MIGRATION_GUIDE.md | 451 ++++++++++++++++++ README.md | 39 +- .../hareworks/kommand-lib/BrigadierMapper.kt | 135 ------ .../net/hareworks/kommand-lib/Kommand.kt | 83 +--- .../net/hareworks/kommand-lib/TreeCompiler.kt | 88 ++++ .../kommand-lib/arguments/ArgumentTypes.kt | 248 +++------- .../kommand-lib/context/ArgumentResolver.kt | 76 +++ .../kommand-lib/context/KommandContext.kt | 34 +- .../kommand-lib/dsl/RegistryBuilders.kt | 60 +-- .../kommand-lib/execution/CommandTree.kt | 59 --- .../net/hareworks/kommand-lib/nodes/Nodes.kt | 81 +--- 11 files changed, 750 insertions(+), 604 deletions(-) create mode 100644 MIGRATION_GUIDE.md delete mode 100644 src/main/kotlin/net/hareworks/kommand-lib/BrigadierMapper.kt create mode 100644 src/main/kotlin/net/hareworks/kommand-lib/TreeCompiler.kt create mode 100644 src/main/kotlin/net/hareworks/kommand-lib/context/ArgumentResolver.kt delete mode 100644 src/main/kotlin/net/hareworks/kommand-lib/execution/CommandTree.kt diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 0000000..a216244 --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,451 @@ +# kommand-lib マイグレーションガイド + +このガイドでは、旧バージョンの kommand-lib から最新の Brigadier ネイティブ対応バージョンへの移行方法を説明します。 + +--- + +## 📋 目次 + +1. [変更の概要](#変更の概要) +2. [破壊的変更](#破壊的変更) +3. [マイグレーション手順](#マイグレーション手順) +4. [コード例の比較](#コード例の比較) +5. [トラブルシューティング](#トラブルシューティング) + +--- + +## 変更の概要 + +### 🎯 主な変更点 + +1. **Brigadier ネイティブ対応** + - Paper 1.21 の Lifecycle API を使用したコマンド登録 + - クライアント側での構文ヒントと検証 + - より正確な型システム + +2. **引数の型変更** + - `coordinates()` の返り値が `Coordinates3` から `io.papermc.paper.math.Position` に変更 + - Player/Entity セレクターの内部処理が改善 + +3. **自動登録** + - `kommand()` 関数の呼び出しで自動的にコマンドが登録されるように変更 + +--- + +## 破壊的変更 + +### 🔴 1. `coordinates()` 引数の型変更 + +#### 旧バージョン (動作しません) +```kotlin +coordinates("point") { + executes { + val coords = argument("point") + val target = coords.resolve(player.location) + // Coordinates3 型は存在しません + } +} +``` + +#### 新バージョン (正しい方法) +```kotlin +coordinates("point") { + executes { + val position = argument("point") + val location = position.toLocation(player.world) + // Position 型を使用します + } +} +``` + +**理由**: Paper の Brigadier API は `io.papermc.paper.math.Position` を返します。これは Paper の公式 API に準拠しています。 + +--- + +### 🟡 2. Player/Entity セレクターの内部処理 + +**ユーザーコードに変更は不要です**が、内部的に以下の変更が行われました: + +#### 内部処理の改善 +```kotlin +// 旧: 直接キャスト (実行時エラーの原因) +val player = context.getArgument("player", Player::class.java) + +// 新: Resolver を使用して解決 (正しい方法) +val resolver = context.getArgument("player", PlayerSelectorArgumentResolver::class.java) +val player = resolver.resolve(source).firstOrNull() +``` + +**影響**: Player/Entity 引数がより安定して動作するようになりました。 + +--- + +## マイグレーション手順 + +### ステップ 1: 依存関係の確認 + +`build.gradle.kts` で Paper API のバージョンを確認してください: + +```kotlin +dependencies { + compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT") + // 1.21 以降が必要です +} +``` + +### ステップ 2: import 文の追加 + +`coordinates()` を使用している場合、import を追加してください: + +```kotlin +import io.papermc.paper.math.Position +``` + +### ステップ 3: コードの更新 + +以下のパターンを検索して置換してください: + +#### パターン 1: coordinates の型指定 + +**検索**: +```kotlin +argument(" +``` + +**置換**: +```kotlin +argument(" +``` + +または、import を追加して: +```kotlin +argument(" +``` + +#### パターン 2: coordinates の解決方法 + +**検索**: +```kotlin +val coords = argument("pos") +val location = coords.resolve(baseLocation) +``` + +**置換**: +```kotlin +val position = argument("pos") +val location = position.toLocation(world) +``` + +### ステップ 4: ビルドとテスト + +```bash +./gradlew build +``` + +エラーがないことを確認してから、サーバーでテストしてください。 + +--- + +## コード例の比較 + +### 例 1: スポーン地点の設定 + +#### ❌ 旧バージョン +```kotlin +literal("setspawn") { + coordinates("point") { + executes { + val base = (sender as? Player)?.location ?: return@executes + val coords = argument("point") + val target = coords.resolve(base) + plugin.server.worlds.first().setSpawnLocation(target) + sender.sendMessage("Spawn set to ${target.x}, ${target.y}, ${target.z}") + } + } +} +``` + +#### ✅ 新バージョン +```kotlin +literal("setspawn") { + coordinates("point") { + executes { + val player = sender as? Player ?: return@executes + val position = argument("point") + val location = position.toLocation(player.world) + player.world.setSpawnLocation(location) + sender.sendMessage("Spawn set to ${location.x}, ${location.y}, ${location.z}") + } + } +} +``` + +**変更点**: +- `Coordinates3` → `Position` +- `coords.resolve(base)` → `position.toLocation(world)` +- より明確な変数名 + +--- + +### 例 2: テレポートコマンド + +#### ❌ 旧バージョン +```kotlin +literal("tp") { + coordinates("destination") { + executes { + val player = sender as? Player ?: return@executes + val coords = argument("destination") + val target = coords.resolve(player.location) + player.teleport(target) + } + } +} +``` + +#### ✅ 新バージョン +```kotlin +literal("tp") { + coordinates("destination") { + executes { + val player = sender as? Player ?: return@executes + val position = argument("destination") + val location = position.toLocation(player.world) + player.teleport(location) + } + } +} +``` + +--- + +### 例 3: 範囲指定コマンド + +#### ❌ 旧バージョン +```kotlin +literal("fill") { + coordinates("pos1") { + coordinates("pos2") { + executes { + val player = sender as? Player ?: return@executes + val base = player.location + val pos1 = argument("pos1").resolve(base) + val pos2 = argument("pos2").resolve(base) + // 処理... + } + } + } +} +``` + +#### ✅ 新バージョン +```kotlin +literal("fill") { + coordinates("pos1") { + coordinates("pos2") { + executes { + val player = sender as? Player ?: return@executes + val world = player.world + val pos1 = argument("pos1").toLocation(world) + val pos2 = argument("pos2").toLocation(world) + // 処理... + } + } + } +} +``` + +--- + +## Position API リファレンス + +### Position のメソッド + +```kotlin +interface Position { + fun x(): Double + fun y(): Double + fun z(): Double + + fun blockX(): Int + fun blockY(): Int + fun blockZ(): Int + + fun toLocation(world: World): Location +} +``` + +### 使用例 + +```kotlin +val position = argument("pos") + +// 座標の取得 +val x = position.x() +val y = position.y() +val z = position.z() + +// ブロック座標の取得 +val blockX = position.blockX() +val blockY = position.blockY() +val blockZ = position.blockZ() + +// Location への変換 +val location = position.toLocation(player.world) +``` + +--- + +## トラブルシューティング + +### 問題 1: `Coordinates3` が見つからない + +**エラー**: +``` +Unresolved reference: Coordinates3 +``` + +**解決方法**: +`Coordinates3` は存在しません。`io.papermc.paper.math.Position` を使用してください。 + +```kotlin +// ❌ 間違い +argument("pos") + +// ✅ 正しい +argument("pos") +``` + +--- + +### 問題 2: `resolve()` メソッドが見つからない + +**エラー**: +``` +Unresolved reference: resolve +``` + +**解決方法**: +`Position` には `resolve()` メソッドはありません。`toLocation(world)` を使用してください。 + +```kotlin +// ❌ 間違い +val location = position.resolve(baseLocation) + +// ✅ 正しい +val location = position.toLocation(world) +``` + +--- + +### 問題 3: Player セレクターが動作しない + +**症状**: Player 引数を使用するとエラーが発生する + +**解決方法**: +最新バージョンに更新してください。内部的に `ArgumentResolver` が自動的に処理します。 + +```kotlin +// これは自動的に動作します +val player = argument("target") +val players = argument>("targets") +``` + +--- + +### 問題 4: コマンドが登録されない + +**症状**: `/help` にコマンドが表示されない + +**解決方法**: +最新バージョンでは `kommand()` 関数の呼び出しで自動的に登録されます。 + +```kotlin +class MyPlugin : JavaPlugin() { + private lateinit var commands: KommandLib + + override fun onEnable() { + // これだけで自動的に登録されます + commands = kommand(this) { + command("mycommand") { + // ... + } + } + } +} +``` + +--- + +## よくある質問 (FAQ) + +### Q1: 相対座標 (`~`) は引き続き使えますか? + +**A**: はい、引き続き使えます。`Position` は相対座標を完全にサポートしています。 + +```kotlin +// "~ ~1 ~-2" のような入力が可能 +val position = argument("pos") +``` + +--- + +### Q2: 旧バージョンとの互換性はありますか? + +**A**: `coordinates()` 引数の型が変更されているため、**互換性はありません**。マイグレーションが必要です。 + +ただし、`player()`, `players()`, `selector()` などの他の引数は互換性があります。 + +--- + +### Q3: マイグレーションにどのくらい時間がかかりますか? + +**A**: プロジェクトの規模によりますが、通常は以下の通りです: + +- **小規模** (1-5 コマンド): 5-10 分 +- **中規模** (5-20 コマンド): 15-30 分 +- **大規模** (20+ コマンド): 30-60 分 + +主な作業は検索と置換なので、比較的短時間で完了します。 + +--- + +### Q4: 段階的な移行は可能ですか? + +**A**: いいえ、`coordinates()` を使用している場合は一度にすべて移行する必要があります。 + +ただし、`coordinates()` を使用していないコマンドは変更不要です。 + +--- + +## サポート + +問題が発生した場合は、以下のドキュメントを参照してください: + +- [README.md](./README.md) - 基本的な使い方 +- [BRIGADIER_REVIEW.md](./BRIGADIER_REVIEW.md) - 詳細なレビュー +- [Paper API Documentation](https://docs.papermc.io/paper/dev/command-api/arguments/location) - Position API の詳細 + +--- + +## まとめ + +### ✅ マイグレーションチェックリスト + +- [ ] Paper API 1.21 以降を使用していることを確認 +- [ ] `Coordinates3` を `Position` に置換 +- [ ] `coords.resolve()` を `position.toLocation()` に置換 +- [ ] 必要な import を追加 +- [ ] ビルドが成功することを確認 +- [ ] サーバーでテストして動作を確認 + +### 🎉 完了! + +マイグレーションが完了すると、以下のメリットが得られます: + +- ✅ クライアント側での構文ヒント +- ✅ より正確な型チェック +- ✅ Paper の公式 API に準拠 +- ✅ より安定した動作 + +ご不明な点がございましたら、お気軽にお問い合わせください。 diff --git a/README.md b/README.md index 5aa50ee..5cd5142 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,19 @@ Paper/Bukkit サーバー向けのコマンド定義を DSL で記述するた - Brigadier (Paper 1.21 Lifecycle API) 対応により、クライアント側で ` ` のような構文ヒントや、数値範囲の検証エラー(赤文字)が表示されます - `permits-lib` との連携により、コマンドツリーから Bukkit パーミッションを自動生成し、`compileOnly` 依存として参照可能 +## バージョン情報 + +**現在のバージョン**: 1.1 (Brigadier ネイティブ対応) + +### 🔄 旧バージョンからの移行 + +旧バージョン (Brigadier 対応前) から移行する場合は、[マイグレーションガイド](./MIGRATION_GUIDE.md) を参照してください。 + +**主な変更点**: +- `coordinates()` の返り値が `Coordinates3` から `io.papermc.paper.math.Position` に変更 +- `position.toLocation(world)` で `Location` に変換する方式に変更 +- Player/Entity セレクターの内部処理が改善され、より安定した動作を実現 + ## 依存関係 `build.gradle.kts` では Paper API と Kotlin 標準ライブラリのみを `compileOnly` に追加しています。Paper 1.21.10 対応の API を利用しています。 @@ -77,11 +90,11 @@ class EconomyPlugin : JavaPlugin() { literal("setspawn") { coordinates("point") { // "~ ~1 ~-2" のような入力を受け付ける executes { - val base = (sender as? Player)?.location ?: return@executes - val coords = argument("point") - val target = coords.resolve(base) - plugin.server.worlds.first().setSpawnLocation(target) - sender.sendMessage("Spawn set to ${target.x}, ${target.y}, ${target.z}") + val player = sender as? Player ?: return@executes + val position = argument("point") + val location = position.toLocation(player.world) + player.world.setSpawnLocation(location) + sender.sendMessage("Spawn set to ${location.x}, ${location.y}, ${location.z}") } } } @@ -110,7 +123,7 @@ class EconomyPlugin : JavaPlugin() { - `string("name")` や `integer("value", min = 0)` は値をパースし、成功すると `KommandContext` に記憶されます。取得時は `argument("name")` や `argument("value")` を呼び出してください。 - `float("speed")` や `player("target")`/`players("targets")`/`selector("entities")` は Minecraft の標準セレクター (`@p`, `@a`, `@s` など) やプレイヤー名を型付きで扱えます。実行時は `argument("speed")`、`argument("target")`、`argument>("targets")` のように取得できます。 - `suggests { prefix -> ... }` を指定すると、タブ補完時に任意の候補リストを返せます。 -- `coordinates("pos")` は `x y z` をまとめて 1 つの引数として受け取り、`argument("pos").resolve(player.location)` で現在位置を基準に解決できます (`~` を使用した相対座標に対応)。 +- `coordinates("pos")` は `x y z` をまとめて 1 つの引数として受け取り、`argument("pos")` で取得できます。`position.toLocation(world)` で `Location` に変換できます (`~` を使用した相対座標に対応)。 - `command` や各ノードの `condition { sender -> ... }` で実行条件 (例: コンソール禁止) を追加できます。 - ルートレベルで `executes { ... }` を指定すると、引数なしで `/eco` を実行した場合に呼び出されます。 @@ -158,10 +171,10 @@ commands = kommand(this) { | `float("speed", min, max)` | `Double` | 小数/指数表記に対応 | | `player("target", allowSelectors = true)` | `Player` | `@p` などのセレクターまたはプレイヤー名を 1 人に解決 | | `players("targets")` | `List` | `@a`/`@r` など複数指定、プレイヤー名入力も可 | -| `selector("entities")` | `List` | Bukkit の `Bukkit.selectEntities` をそのまま利用 | -| `coordinates("pos")` | `Coordinates3` | `~` 相対座標を含む 3 軸をまとめて扱う | +| `selector("entities")` | `List` | エンティティセレクター (`@e` など) | +| `coordinates("pos")` | `io.papermc.paper.math.Position` | `~` 相対座標を含む 3 軸をまとめて扱う | -`Coordinates3` は `coordinates("pos") { ... }` 直後のコンテキストで `argument("pos")` として取得でき、`resolve(baseLocation)` で基準座標に対して実座標を求められます。 +`Position` は `coordinates("pos") { ... }` 直後のコンテキストで `argument("pos")` として取得でき、`position.toLocation(world)` で `Location` に変換できます。 ## クライアント側構文ヒント (Brigadier) @@ -178,3 +191,11 @@ Paper 1.21 以降の環境では、`LifecycleEventManager` を通じてコマン ``` ShadowJar タスクが実行され、`build/libs` に出力されます。Paper サーバーに配置して動作確認してください。 + +## ドキュメント + +- **[マイグレーションガイド](./MIGRATION_GUIDE.md)** - 旧バージョンからの移行方法 + +## ライセンス + +このプロジェクトは MIT ライセンスの下で公開されています。 diff --git a/src/main/kotlin/net/hareworks/kommand-lib/BrigadierMapper.kt b/src/main/kotlin/net/hareworks/kommand-lib/BrigadierMapper.kt deleted file mode 100644 index b681e5d..0000000 --- a/src/main/kotlin/net/hareworks/kommand-lib/BrigadierMapper.kt +++ /dev/null @@ -1,135 +0,0 @@ -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 2252b16..548b858 100644 --- a/src/main/kotlin/net/hareworks/kommand-lib/Kommand.kt +++ b/src/main/kotlin/net/hareworks/kommand-lib/Kommand.kt @@ -1,18 +1,12 @@ package net.hareworks.kommand_lib import net.hareworks.kommand_lib.context.KommandContext -import net.hareworks.kommand_lib.execution.CommandTree -import net.hareworks.kommand_lib.execution.ParseMode import net.hareworks.kommand_lib.dsl.KommandRegistry import net.hareworks.kommand_lib.permissions.PermissionOptions import net.hareworks.kommand_lib.permissions.PermissionRuntime -import org.bukkit.Bukkit -import org.bukkit.command.CommandMap import org.bukkit.command.CommandSender -import org.bukkit.command.PluginCommand -import org.bukkit.command.TabCompleter -import org.bukkit.plugin.Plugin import org.bukkit.plugin.java.JavaPlugin +import org.bukkit.plugin.Plugin fun kommand(plugin: JavaPlugin, block: KommandRegistry.() -> Unit): KommandLib { val registry = KommandRegistry(plugin) @@ -28,62 +22,31 @@ class KommandLib internal constructor( private val definitions: List, private val permissionRuntime: PermissionRuntime? ) { - private val commandMap: CommandMap by lazy { - val field = Bukkit.getServer().javaClass.getDeclaredField("commandMap") - field.isAccessible = true - field.get(Bukkit.getServer()) as CommandMap - } - private val registered = mutableListOf() - init { registerAll() - permissionRuntime?.let { - if (it.config.autoApply) it.apply() - } } - + 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) + // Compile the definition to a Brigadier LiteralArgumentBuilder + val node = TreeCompiler.compile(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) - if (definition.aliases.isNotEmpty()) command.aliases = definition.aliases - definition.description?.let { command.description = it } - definition.usage?.let { command.usage = it } - definition.permission?.let { command.permission = it } - - command.setExecutor { sender, _, alias, args -> - definition.execute(plugin, sender, alias, args) - } - command.tabCompleter = TabCompleter { sender, _, alias, args -> - definition.tabComplete(plugin, sender, alias, args) - } - commandMap.register(plugin.name.lowercase(), command) - registered += command - } } fun unregister() { - registered.forEach { it.unregister(commandMap) } - registered.clear() + // Lifecycle API handles unregistration automatically on disable usually? + // Or we might need to verify if manual unregistration is needed. + // For now, clearing local state. + // Note: Paper Lifecycle API doesn't expose easy unregister for static commands registered in 'COMMANDS' event usually, + // it rebuilds the dispatcher on reload. permissionRuntime?.clear() } - - private fun newPluginCommand(name: String): PluginCommand { - val constructor = PluginCommand::class.java.getDeclaredConstructor(String::class.java, Plugin::class.java) - constructor.isAccessible = true - return constructor.newInstance(name, plugin) - } } internal data class CommandDefinition( @@ -96,30 +59,4 @@ internal data class CommandDefinition( val rootExecutor: (KommandContext.() -> Unit)?, val nodes: List, val permissionOptions: PermissionOptions -) { - private val tree = CommandTree(nodes) - - fun execute(plugin: JavaPlugin, sender: CommandSender, alias: String, args: Array): Boolean { - if (!rootCondition(sender)) return false - val context = KommandContext(plugin, sender, alias, args, ParseMode.EXECUTE) - if (args.isEmpty()) { - val executor = rootExecutor ?: return false - executor.invoke(context) - return true - } - return tree.execute(context) - } - - fun tabComplete(plugin: JavaPlugin, sender: CommandSender, alias: String, args: Array): List { - if (!rootCondition(sender)) return emptyList() - if (nodes.isEmpty()) return emptyList() - val context = KommandContext(plugin, sender, alias, args, ParseMode.SUGGEST) - if (args.isEmpty()) { - return nodes - .filter { it.isVisible(sender) } - .flatMap { it.suggestions("", context) } - .distinct() - } - return tree.tabComplete(context) - } -} +) diff --git a/src/main/kotlin/net/hareworks/kommand-lib/TreeCompiler.kt b/src/main/kotlin/net/hareworks/kommand-lib/TreeCompiler.kt new file mode 100644 index 0000000..a3877c8 --- /dev/null +++ b/src/main/kotlin/net/hareworks/kommand-lib/TreeCompiler.kt @@ -0,0 +1,88 @@ +package net.hareworks.kommand_lib + +import com.mojang.brigadier.builder.ArgumentBuilder +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import com.mojang.brigadier.builder.RequiredArgumentBuilder +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import com.mojang.brigadier.tree.CommandNode +import io.papermc.paper.command.brigadier.CommandSourceStack +import io.papermc.paper.command.brigadier.Commands +import net.hareworks.kommand_lib.context.KommandContext +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 TreeCompiler { + + fun compile( + plugin: JavaPlugin, + definition: CommandDefinition + ): LiteralArgumentBuilder { + val root = Commands.literal(definition.name) + .requires { source -> definition.rootCondition(source.sender) } + + // Root execution + definition.rootExecutor?.let { executor -> + root.executes { ctx -> + val context = KommandContext(plugin, ctx) + executor(context) + 1 + } + } + + // Children + definition.nodes.forEach { child -> + compileNode(plugin, child)?.let { root.then(it) } + } + + return root + } + + private fun compileNode( + plugin: JavaPlugin, + node: KommandNode + ): ArgumentBuilder? { + val builder = when (node) { + is LiteralNode -> { + Commands.literal(node.literal) + } + is ValueNode<*> -> { + val argType = node.argument.build() + Commands.argument(node.name, argType) + } + else -> return null + } + + builder.requires { source -> node.isVisible(source.sender) } + + // Execution + node.executor?.let { executor -> + builder.executes { ctx -> + val context = KommandContext(plugin, ctx) + executor(context) + 1 + } + } + + // Custom Suggestions (if any) + if (node is ValueNode<*> && node.suggestionProvider != null && builder is RequiredArgumentBuilder<*, *>) { + @Suppress("UNCHECKED_CAST") + (builder as RequiredArgumentBuilder).suggests { ctx: CommandContext, suggestionsBuilder: SuggestionsBuilder -> + val context = KommandContext(plugin, ctx) + val suggestions = node.suggestionProvider!!.invoke(context, suggestionsBuilder.remaining) + suggestions.forEach { suggestionsBuilder.suggest(it) } + suggestionsBuilder.buildFuture() + } + } + + // Recursion + node.children.forEach { child -> + compileNode(plugin, child)?.let { builder.then(it) } + } + + return builder + } +} 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 a16b6f6..0d64484 100644 --- a/src/main/kotlin/net/hareworks/kommand-lib/arguments/ArgumentTypes.kt +++ b/src/main/kotlin/net/hareworks/kommand-lib/arguments/ArgumentTypes.kt @@ -1,211 +1,81 @@ package net.hareworks.kommand_lib.arguments -import net.hareworks.kommand_lib.context.KommandContext -import org.bukkit.Bukkit -import org.bukkit.Location +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.arguments.BoolArgumentType +import com.mojang.brigadier.arguments.DoubleArgumentType +import com.mojang.brigadier.arguments.IntegerArgumentType +import com.mojang.brigadier.arguments.StringArgumentType +import io.papermc.paper.command.brigadier.argument.ArgumentTypes +import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver +import io.papermc.paper.command.brigadier.argument.resolvers.selector.EntitySelectorArgumentResolver import org.bukkit.entity.Entity import org.bukkit.entity.Player +import org.bukkit.util.Vector +import org.bukkit.Location +import org.bukkit.command.CommandSender -sealed class ArgumentParseResult { - data class Success(val value: T) : ArgumentParseResult() - data class Failure(val reason: String) : ArgumentParseResult() +/** + * A holder for the Brigadier ArgumentType and any metadata needed for the DSL. + * + * Note: T represents the final type that users will receive in KommandContext.argument(), + * not necessarily the raw Brigadier return type. For example, PlayerArgument has T=Player, + * but Brigadier returns PlayerSelectorArgumentResolver which is resolved to Player by ArgumentResolver. + */ +interface KommandArgument { + fun build(): ArgumentType<*> } -interface KommandArgumentType { - fun parse(input: String, context: KommandContext): ArgumentParseResult - fun suggestions(context: KommandContext, prefix: String): List = emptyList() +class WordArgument : KommandArgument { + override fun build(): ArgumentType = StringArgumentType.word() } -object WordArgumentType : KommandArgumentType { - override fun parse(input: String, context: KommandContext): ArgumentParseResult { - if (input.isBlank()) return ArgumentParseResult.Failure("Value cannot be blank") - return ArgumentParseResult.Success(input) - } +class IntegerArgument( + private val min: Int = Int.MIN_VALUE, + private val max: Int = Int.MAX_VALUE +) : KommandArgument { + override fun build(): ArgumentType = IntegerArgumentType.integer(min, max) } -object BooleanArgumentType : KommandArgumentType { - override fun parse(input: String, context: KommandContext): ArgumentParseResult { - val lower = input.lowercase() - return when (lower) { - "true" -> ArgumentParseResult.Success(true) - "false" -> ArgumentParseResult.Success(false) - else -> ArgumentParseResult.Failure("Expected true/false but got '$input'") - } - } - - override fun suggestions(context: KommandContext, prefix: String): List = - listOf("true", "false").filter { it.startsWith(prefix, ignoreCase = true) } +class FloatArgument( + private val min: Double = -Double.MAX_VALUE, + private val max: Double = Double.MAX_VALUE +) : KommandArgument { + override fun build(): ArgumentType = DoubleArgumentType.doubleArg(min, max) } -class IntegerArgumentType( - val min: Int? = null, - val max: Int? = null -) : KommandArgumentType { - override fun parse(input: String, context: KommandContext): ArgumentParseResult { - val value = input.toIntOrNull() - ?: return ArgumentParseResult.Failure("Expected integer but got '$input'") - if (min != null && value < min) { - return ArgumentParseResult.Failure("Value must be >= $min") - } - if (max != null && value > max) { - return ArgumentParseResult.Failure("Value must be <= $max") - } - return ArgumentParseResult.Success(value) - } +class BooleanArgument : KommandArgument { + override fun build(): ArgumentType = BoolArgumentType.bool() } -class FloatArgumentType( - val min: Double? = null, - val max: Double? = null -) : KommandArgumentType { - override fun parse(input: String, context: KommandContext): ArgumentParseResult { - val value = input.toDoubleOrNull() - ?: return ArgumentParseResult.Failure("Expected decimal number but got '$input'") - if (min != null && value < min) { - return ArgumentParseResult.Failure("Value must be >= $min") - } - if (max != null && value > max) { - return ArgumentParseResult.Failure("Value must be <= $max") - } - return ArgumentParseResult.Success(value) - } +/** + * Single player argument. Returns a Player object after resolving the selector. + * Supports player names and selectors like @p, @s, @r[limit=1]. + */ +class PlayerArgument : KommandArgument { + override fun build(): ArgumentType = ArgumentTypes.player() } -class PlayerArgumentType( - private val allowSelectors: Boolean -) : KommandArgumentType { - override fun parse(input: String, context: KommandContext): ArgumentParseResult { - val trimmed = input.trim() - if (allowSelectors && trimmed.startsWith("@")) { - val entities = try { - Bukkit.selectEntities(context.sender, trimmed) - } catch (ex: IllegalArgumentException) { - return ArgumentParseResult.Failure(ex.message ?: "Invalid selector '$trimmed'") - } - val player = entities.firstOrNull { it is Player } as? Player - ?: return ArgumentParseResult.Failure("Selector '$trimmed' did not match a player") - return ArgumentParseResult.Success(player) - } - val player = Bukkit.getPlayerExact(trimmed) - ?: return ArgumentParseResult.Failure("Player '$trimmed' is not online") - return ArgumentParseResult.Success(player) - } - - override fun suggestions(context: KommandContext, prefix: String): List { - val names = Bukkit.getOnlinePlayers() - .map { it.name } - .filter { it.startsWith(prefix, ignoreCase = true) } - if (!allowSelectors) return names - val selectors = DEFAULT_SELECTOR_SUGGESTIONS.filter { it.startsWith(prefix) } - return (names + selectors).distinct() - } +/** + * Multiple players argument. Returns a List after resolving the selector. + * Supports player names and selectors like @a, @r. + */ +class PlayersArgument : KommandArgument> { + override fun build(): ArgumentType = ArgumentTypes.players() } -class PlayerSelectorArgumentType( - private val allowDirectNames: Boolean -) : KommandArgumentType> { - override fun parse(input: String, context: KommandContext): ArgumentParseResult> { - val trimmed = input.trim() - if (!trimmed.startsWith("@")) { - if (!allowDirectNames) { - return ArgumentParseResult.Failure("Selector expected but got '$trimmed'") - } - val player = Bukkit.getPlayerExact(trimmed) - ?: return ArgumentParseResult.Failure("Player '$trimmed' is not online") - return ArgumentParseResult.Success(listOf(player)) - } - - val entities = try { - Bukkit.selectEntities(context.sender, trimmed) - } catch (ex: IllegalArgumentException) { - return ArgumentParseResult.Failure(ex.message ?: "Invalid selector '$trimmed'") - } - val players = entities.filterIsInstance() - if (players.isEmpty()) { - return ArgumentParseResult.Failure("Selector '$trimmed' did not match any players") - } - return ArgumentParseResult.Success(players) - } - - override fun suggestions(context: KommandContext, prefix: String): List { - val candidates = linkedSetOf() - candidates += Bukkit.getOnlinePlayers() - .map { it.name } - .filter { it.startsWith(prefix, ignoreCase = true) } - candidates += DEFAULT_SELECTOR_SUGGESTIONS.filter { it.startsWith(prefix) } - return candidates.toList() - } +/** + * Entity selector argument. Returns a List after resolving the selector. + * Supports all entity selectors like @e, @e[type=minecraft:zombie]. + */ +class EntityArgument : KommandArgument> { + override fun build(): ArgumentType = ArgumentTypes.entities() } -class EntitySelectorArgumentType( - private val requireMatch: Boolean -) : KommandArgumentType> { - override fun parse(input: String, context: KommandContext): ArgumentParseResult> { - val trimmed = input.trim() - if (!trimmed.startsWith("@")) { - val player = Bukkit.getPlayerExact(trimmed) - if (player != null) return ArgumentParseResult.Success(listOf(player)) - return if (requireMatch) { - ArgumentParseResult.Failure("No entity matched '$trimmed'") - } else { - ArgumentParseResult.Success(emptyList()) - } - } - - val entities = try { - Bukkit.selectEntities(context.sender, trimmed) - } catch (ex: IllegalArgumentException) { - return ArgumentParseResult.Failure(ex.message ?: "Invalid selector '$trimmed'") - } - if (requireMatch && entities.isEmpty()) { - return ArgumentParseResult.Failure("Selector '$trimmed' did not match any entities") - } - return ArgumentParseResult.Success(entities) - } - - override fun suggestions(context: KommandContext, prefix: String): List { - return DEFAULT_SELECTOR_SUGGESTIONS.filter { it.startsWith(prefix) } - } -} - -class CoordinateComponentArgumentType( - private val allowRelative: Boolean -) : KommandArgumentType { - override fun parse(input: String, context: KommandContext): ArgumentParseResult { - val trimmed = input.trim() - if (allowRelative && trimmed.startsWith("~")) { - val remainder = trimmed.removePrefix("~") - val offset = if (remainder.isEmpty()) 0.0 else remainder.toDoubleOrNull() - ?: return ArgumentParseResult.Failure("Invalid relative coordinate '$trimmed'") - return ArgumentParseResult.Success(CoordinateComponent(relative = true, value = offset)) - } - val absolute = trimmed.toDoubleOrNull() - ?: return ArgumentParseResult.Failure("Expected coordinate but got '$trimmed'") - return ArgumentParseResult.Success(CoordinateComponent(relative = false, value = absolute)) - } - - override fun suggestions(context: KommandContext, prefix: String): List { - if (!allowRelative) return emptyList() - return if ("~".startsWith(prefix)) listOf("~") else emptyList() - } -} - -private val DEFAULT_SELECTOR_SUGGESTIONS = listOf("@p", "@a", "@s", "@r", "@e") - -data class CoordinateComponent(val relative: Boolean, val value: Double) { - fun resolve(origin: Double): Double = if (relative) origin + value else value -} - -data class Coordinates3( - val x: CoordinateComponent, - val y: CoordinateComponent, - val z: CoordinateComponent -) { - fun resolve(origin: Location): Location { - val clone = origin.clone() - clone.x = x.resolve(clone.x) - clone.y = y.resolve(clone.y) - clone.z = z.resolve(clone.z) - return clone - } +/** + * Fine position argument for coordinates with decimal precision. + * Supports relative coordinates like ~ ~1 ~-2. + * Returns a Position (io.papermc.paper.math.Position) after resolving. + */ +class CoordinatesArgument : KommandArgument { + override fun build(): ArgumentType<*> = ArgumentTypes.finePosition() } diff --git a/src/main/kotlin/net/hareworks/kommand-lib/context/ArgumentResolver.kt b/src/main/kotlin/net/hareworks/kommand-lib/context/ArgumentResolver.kt new file mode 100644 index 0000000..4e44878 --- /dev/null +++ b/src/main/kotlin/net/hareworks/kommand-lib/context/ArgumentResolver.kt @@ -0,0 +1,76 @@ +package net.hareworks.kommand_lib.context + +import com.mojang.brigadier.context.CommandContext +import io.papermc.paper.command.brigadier.CommandSourceStack +import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver +import io.papermc.paper.command.brigadier.argument.resolvers.selector.EntitySelectorArgumentResolver +import io.papermc.paper.command.brigadier.argument.resolvers.FinePositionResolver +import io.papermc.paper.command.brigadier.argument.resolvers.BlockPositionResolver +import io.papermc.paper.math.Position +import org.bukkit.entity.Entity +import org.bukkit.entity.Player + +/** + * Internal helper to resolve Brigadier argument types to their actual values. + * This handles the conversion from Paper's resolver types to concrete Bukkit types. + * + * Note: This is public because it's called from inline functions in KommandContext, + * but it's not intended for direct use by library consumers. + */ +object ArgumentResolver { + + /** + * Resolves an argument from the command context. + * Handles special cases for Paper's selector resolvers and position resolvers. + */ + inline fun resolve(context: CommandContext, name: String): T { + val rawValue = context.getArgument(name, Any::class.java) + + @Suppress("UNCHECKED_CAST") + return when { + // Single player selector + T::class.java == Player::class.java && rawValue is PlayerSelectorArgumentResolver -> { + rawValue.resolve(context.source).firstOrNull() as T + ?: throw IllegalStateException("Player selector '$name' did not resolve to any player") + } + + // Multiple players selector + T::class.java == List::class.java && rawValue is PlayerSelectorArgumentResolver -> { + rawValue.resolve(context.source) as T + } + + // Entity selector + T::class.java == List::class.java && rawValue is EntitySelectorArgumentResolver -> { + rawValue.resolve(context.source) as T + } + + // Fine position (coordinates with decimals) + rawValue is FinePositionResolver -> { + rawValue.resolve(context.source) as T + } + + // Block position (integer coordinates) + rawValue is BlockPositionResolver -> { + rawValue.resolve(context.source) as T + } + + // All other types (primitives, strings, etc.) + else -> { + context.getArgument(name, T::class.java) + } + } + } + + /** + * Resolves an argument or returns null if not found. + */ + inline fun resolveOrNull(context: CommandContext, name: String): T? { + return try { + resolve(context, name) + } catch (e: IllegalArgumentException) { + null + } catch (e: IllegalStateException) { + null + } + } +} diff --git a/src/main/kotlin/net/hareworks/kommand-lib/context/KommandContext.kt b/src/main/kotlin/net/hareworks/kommand-lib/context/KommandContext.kt index 3bf4f93..cb8f907 100644 --- a/src/main/kotlin/net/hareworks/kommand-lib/context/KommandContext.kt +++ b/src/main/kotlin/net/hareworks/kommand-lib/context/KommandContext.kt @@ -1,33 +1,25 @@ package net.hareworks.kommand_lib.context +import com.mojang.brigadier.context.CommandContext +import io.papermc.paper.command.brigadier.CommandSourceStack import org.bukkit.command.CommandSender import org.bukkit.plugin.java.JavaPlugin -import net.hareworks.kommand_lib.execution.ParseMode -open class KommandContext internal constructor( +class KommandContext internal constructor( val plugin: JavaPlugin, - val sender: CommandSender, - val alias: String, - val args: Array, - val mode: ParseMode + val internal: CommandContext ) { - private val parsedArguments = linkedMapOf() + val sender: CommandSender + get() = internal.source.sender - internal fun remember(name: String, value: Any?) { - parsedArguments[name] = value + val commandSource: CommandSourceStack + get() = internal.source + + inline fun argument(name: String): T { + return ArgumentResolver.resolve(internal, name) } - internal fun drop(name: String) { - parsedArguments.remove(name) + inline fun argumentOrNull(name: String): T? { + return ArgumentResolver.resolveOrNull(internal, name) } - - @Suppress("UNCHECKED_CAST") - fun argument(name: String): T = - parsedArguments[name] as? T - ?: error("Argument '$name' is not present in this context.") - - @Suppress("UNCHECKED_CAST") - fun argumentOrNull(name: String): T? = parsedArguments[name] as? T - - fun arguments(): Map = parsedArguments.toMap() } diff --git a/src/main/kotlin/net/hareworks/kommand-lib/dsl/RegistryBuilders.kt b/src/main/kotlin/net/hareworks/kommand-lib/dsl/RegistryBuilders.kt index 41408db..12fd9d4 100644 --- a/src/main/kotlin/net/hareworks/kommand-lib/dsl/RegistryBuilders.kt +++ b/src/main/kotlin/net/hareworks/kommand-lib/dsl/RegistryBuilders.kt @@ -6,8 +6,6 @@ import net.hareworks.kommand_lib.permissions.PermissionConfigBuilder import net.hareworks.kommand_lib.permissions.PermissionOptions import net.hareworks.kommand_lib.permissions.PermissionPlanner import net.hareworks.kommand_lib.permissions.PermissionRuntime -import net.hareworks.kommand_lib.nodes.Axis -import net.hareworks.kommand_lib.nodes.CoordinateAxisNode import net.hareworks.kommand_lib.nodes.KommandNode import net.hareworks.kommand_lib.nodes.LiteralNode import net.hareworks.kommand_lib.nodes.ValueNode @@ -21,9 +19,6 @@ class KommandRegistry internal constructor(private val plugin: JavaPlugin) { private val definitions = mutableListOf() private var permissionConfigBuilder: PermissionConfigBuilder? = null - /** - * Declares a new command root. - */ fun command(name: String, vararg aliases: String, block: CommandBuilder.() -> Unit) { val builder = CommandBuilder(name, aliases.toList()) builder.block() @@ -122,7 +117,7 @@ abstract class BranchScope internal constructor( LiteralBuilder(node).apply(block) } - fun argument(name: String, type: KommandArgumentType, block: ValueBuilder.() -> Unit = {}) { + fun argument(name: String, type: KommandArgument, block: ValueBuilder.() -> Unit = {}) { val node = ValueNode(name, type) node.permission = inheritedPermission node.condition = inheritedCondition @@ -131,66 +126,50 @@ abstract class BranchScope internal constructor( ValueBuilder(node).apply(block) } - fun string(name: String, block: ValueBuilder.() -> Unit = {}) = argument(name, WordArgumentType, block) + fun string(name: String, block: ValueBuilder.() -> Unit = {}) = argument(name, WordArgument(), block) fun integer( name: String, - min: Int? = null, - max: Int? = null, + min: Int = Int.MIN_VALUE, + max: Int = Int.MAX_VALUE, block: ValueBuilder.() -> Unit = {} - ) = argument(name, IntegerArgumentType(min, max), block) + ) = argument(name, IntegerArgument(min, max), block) fun float( name: String, - min: Double? = null, - max: Double? = null, + min: Double = -Double.MAX_VALUE, + max: Double = Double.MAX_VALUE, block: ValueBuilder.() -> Unit = {} - ) = argument(name, FloatArgumentType(min, max), block) + ) = argument(name, FloatArgument(min, max), block) fun bool( name: String, block: ValueBuilder.() -> Unit = {} - ) = argument(name, BooleanArgumentType, block) + ) = argument(name, BooleanArgument(), block) fun player( name: String, - allowSelectors: Boolean = true, + allowSelectors: Boolean = true, // Ignored logic-wise if using native, assuming it handles selectors block: ValueBuilder.() -> Unit = {} - ) = argument(name, PlayerArgumentType(allowSelectors), block) + ) = argument(name, PlayerArgument(), block) fun players( name: String, allowDirectNames: Boolean = true, block: ValueBuilder>.() -> Unit = {} - ) = argument(name, PlayerSelectorArgumentType(allowDirectNames), block) + ) = argument(name, PlayersArgument(), block) fun selector( name: String, requireMatch: Boolean = true, block: ValueBuilder>.() -> Unit = {} - ) = argument(name, EntitySelectorArgumentType(requireMatch), block) + ) = argument(name, EntityArgument(), block) fun coordinates( name: String, allowRelative: Boolean = true, - block: CoordinateNodeScope.() -> Unit = {} - ) { - val xNode = CoordinateAxisNode(name, Axis.X, allowRelative) - val yNode = CoordinateAxisNode(name, Axis.Y, allowRelative) - val zNode = CoordinateAxisNode(name, Axis.Z, allowRelative) - val nodes = listOf(xNode, yNode, zNode) - nodes.forEach { node -> - node.permission = inheritedPermission - node.condition = inheritedCondition - } - xNode.permissionOptions.skipPermission() - yNode.permissionOptions.skipPermission() - zNode.permissionOptions.rename(name) - xNode.children += yNode - yNode.children += zNode - children += xNode - CoordinateNodeScope(zNode).apply(block) - } + block: ValueBuilder.() -> Unit = {} + ) = argument(name, CoordinatesArgument(), block) } @KommandDsl @@ -227,23 +206,20 @@ abstract class NodeScope internal constructor( @KommandDsl class LiteralBuilder internal constructor( - node: LiteralNode -) : NodeScope(node) + private val literalNode: LiteralNode +) : NodeScope(literalNode) @KommandDsl class ValueBuilder internal constructor( private val valueNode: ValueNode ) : NodeScope(valueNode) { /** - * Overrides the default suggestion provider for this argument. + * Overrides the default suggestion provider (wrapper around Brigadier logic) */ fun suggests(block: net.hareworks.kommand_lib.context.KommandContext.(prefix: String) -> List) { valueNode.suggestionProvider = { ctx, prefix -> block(ctx, prefix) } } } -@KommandDsl -class CoordinateNodeScope internal constructor(node: KommandNode) : NodeScope(node) - @DslMarker annotation class KommandDsl diff --git a/src/main/kotlin/net/hareworks/kommand-lib/execution/CommandTree.kt b/src/main/kotlin/net/hareworks/kommand-lib/execution/CommandTree.kt deleted file mode 100644 index 90136d4..0000000 --- a/src/main/kotlin/net/hareworks/kommand-lib/execution/CommandTree.kt +++ /dev/null @@ -1,59 +0,0 @@ -package net.hareworks.kommand_lib.execution - -import net.hareworks.kommand_lib.context.KommandContext -import net.hareworks.kommand_lib.nodes.KommandNode - -internal class CommandTree(private val roots: List) { - fun execute(context: KommandContext): Boolean { - if (context.args.isEmpty()) return false - val node = match(roots, context, 0) ?: return false - val executor = node.executor ?: return false - executor.invoke(context) - return true - } - - fun tabComplete(context: KommandContext): List { - if (context.args.isEmpty()) return emptyList() - return collect(roots, context, 0) - } - - private fun match(nodes: List, context: KommandContext, index: Int): KommandNode? { - if (index >= context.args.size) return null - val token = context.args[index] - for (node in nodes) { - if (!node.isVisible(context.sender)) continue - if (!node.consume(token, context, ParseMode.EXECUTE)) continue - if (index == context.args.lastIndex && node.executor != null) { - return node - } - val result = match(node.children, context, index + 1) - if (result != null) return result - node.undo(context) - } - return null - } - - private fun collect(nodes: List, context: KommandContext, index: Int): List { - val token = context.args[index] - val last = index == context.args.lastIndex - val suggestions = linkedSetOf() - for (node in nodes) { - if (!node.isVisible(context.sender)) continue - if (last) { - suggestions += node.suggestions(token, context) - } - if (node.consume(token, context, ParseMode.SUGGEST)) { - if (!last && node.children.isNotEmpty()) { - suggestions += collect(node.children, context, index + 1) - } - node.undo(context) - } - } - return suggestions.toList() - } -} - -enum class ParseMode { - EXECUTE, - SUGGEST -} 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 0668aef..d58d782 100644 --- a/src/main/kotlin/net/hareworks/kommand-lib/nodes/Nodes.kt +++ b/src/main/kotlin/net/hareworks/kommand-lib/nodes/Nodes.kt @@ -1,10 +1,7 @@ package net.hareworks.kommand_lib.nodes -import net.hareworks.kommand_lib.arguments.CoordinateComponent -import net.hareworks.kommand_lib.arguments.Coordinates3 -import net.hareworks.kommand_lib.arguments.KommandArgumentType +import net.hareworks.kommand_lib.arguments.KommandArgument import net.hareworks.kommand_lib.context.KommandContext -import net.hareworks.kommand_lib.execution.ParseMode import net.hareworks.kommand_lib.permissions.PermissionOptions import org.bukkit.command.CommandSender @@ -21,86 +18,18 @@ abstract class KommandNode internal constructor() { return condition(sender) } - abstract fun consume(token: String, context: KommandContext, mode: ParseMode): Boolean - open fun undo(context: KommandContext) {} - abstract fun suggestions(prefix: String, context: KommandContext): List - open fun segment(): String? = null } -class LiteralNode internal constructor(private val literal: String) : KommandNode() { - override fun consume(token: String, context: KommandContext, mode: ParseMode): Boolean { - return literal.equals(token, ignoreCase = true) - } - - override fun suggestions(prefix: String, context: KommandContext): List { - return if (literal.startsWith(prefix, ignoreCase = true)) listOf(literal) else emptyList() - } - +class LiteralNode internal constructor(val literal: String) : KommandNode() { override fun segment(): String = literal } -open class ValueNode internal constructor( - private val name: String, - val argumentType: KommandArgumentType +class ValueNode internal constructor( + val name: String, + val argument: KommandArgument ) : KommandNode() { var suggestionProvider: ((KommandContext, String) -> List)? = null - override fun consume(token: String, context: KommandContext, mode: ParseMode): Boolean { - return when (val result = argumentType.parse(token, context)) { - is net.hareworks.kommand_lib.arguments.ArgumentParseResult.Success -> { - context.remember(name, result.value) - true - } - is net.hareworks.kommand_lib.arguments.ArgumentParseResult.Failure -> { - if (mode == ParseMode.EXECUTE) { - context.sender.sendMessage(result.reason) - } - false - } - } - } - - override fun undo(context: KommandContext) { - context.drop(name) - } - - override fun suggestions(prefix: String, context: KommandContext): List { - val custom = suggestionProvider?.invoke(context, prefix) - if (custom != null) return custom - return argumentType.suggestions(context, prefix) - } - override fun segment(): String = name } - -private fun coordinateAxisKey(base: String, axis: Axis): String = "$base::__${axis.name.lowercase()}" - -class CoordinateAxisNode( - private val aggregateName: String, - private val axis: Axis, - allowRelative: Boolean -) : ValueNode(coordinateAxisKey(aggregateName, axis), - net.hareworks.kommand_lib.arguments.CoordinateComponentArgumentType(allowRelative)) { - override fun consume(token: String, context: KommandContext, mode: ParseMode): Boolean { - val success = super.consume(token, context, mode) - if (success && axis == Axis.Z) { - val x = context.argumentOrNull(coordinateAxisKey(aggregateName, Axis.X)) - val y = context.argumentOrNull(coordinateAxisKey(aggregateName, Axis.Y)) - val z = context.argumentOrNull(coordinateAxisKey(aggregateName, Axis.Z)) - if (x != null && y != null && z != null) { - context.remember(aggregateName, Coordinates3(x, y, z)) - } - } - return success - } - - override fun undo(context: KommandContext) { - if (axis == Axis.Z) { - context.drop(aggregateName) - } - super.undo(context) - } -} - -enum class Axis { X, Y, Z }