feat: Brigadier対応
This commit is contained in:
parent
e0613cd052
commit
aab2b1169c
451
MIGRATION_GUIDE.md
Normal file
451
MIGRATION_GUIDE.md
Normal file
|
|
@ -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<Coordinates3>("point")
|
||||
val target = coords.resolve(player.location)
|
||||
// Coordinates3 型は存在しません
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 新バージョン (正しい方法)
|
||||
```kotlin
|
||||
coordinates("point") {
|
||||
executes {
|
||||
val position = argument<io.papermc.paper.math.Position>("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<Coordinates3>("
|
||||
```
|
||||
|
||||
**置換**:
|
||||
```kotlin
|
||||
argument<io.papermc.paper.math.Position>("
|
||||
```
|
||||
|
||||
または、import を追加して:
|
||||
```kotlin
|
||||
argument<Position>("
|
||||
```
|
||||
|
||||
#### パターン 2: coordinates の解決方法
|
||||
|
||||
**検索**:
|
||||
```kotlin
|
||||
val coords = argument<Coordinates3>("pos")
|
||||
val location = coords.resolve(baseLocation)
|
||||
```
|
||||
|
||||
**置換**:
|
||||
```kotlin
|
||||
val position = argument<Position>("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<Coordinates3>("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<Position>("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<Coordinates3>("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<Position>("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<Coordinates3>("pos1").resolve(base)
|
||||
val pos2 = argument<Coordinates3>("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<Position>("pos1").toLocation(world)
|
||||
val pos2 = argument<Position>("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<Position>("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<Coordinates3>("pos")
|
||||
|
||||
// ✅ 正しい
|
||||
argument<Position>("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<Player>("target")
|
||||
val players = argument<List<Player>>("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<Position>("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 に準拠
|
||||
- ✅ より安定した動作
|
||||
|
||||
ご不明な点がございましたら、お気軽にお問い合わせください。
|
||||
39
README.md
39
README.md
|
|
@ -12,6 +12,19 @@ Paper/Bukkit サーバー向けのコマンド定義を DSL で記述するた
|
|||
- Brigadier (Paper 1.21 Lifecycle API) 対応により、クライアント側で `<speed> <count>` のような構文ヒントや、数値範囲の検証エラー(赤文字)が表示されます
|
||||
- `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<Coordinates3>("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<io.papermc.paper.math.Position>("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<String>("name")` や `argument<Int>("value")` を呼び出してください。
|
||||
- `float("speed")` や `player("target")`/`players("targets")`/`selector("entities")` は Minecraft の標準セレクター (`@p`, `@a`, `@s` など) やプレイヤー名を型付きで扱えます。実行時は `argument<Double>("speed")`、`argument<Player>("target")`、`argument<List<Player>>("targets")` のように取得できます。
|
||||
- `suggests { prefix -> ... }` を指定すると、タブ補完時に任意の候補リストを返せます。
|
||||
- `coordinates("pos")` は `x y z` をまとめて 1 つの引数として受け取り、`argument<Coordinates3>("pos").resolve(player.location)` で現在位置を基準に解決できます (`~` を使用した相対座標に対応)。
|
||||
- `coordinates("pos")` は `x y z` をまとめて 1 つの引数として受け取り、`argument<io.papermc.paper.math.Position>("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<Player>` | `@a`/`@r` など複数指定、プレイヤー名入力も可 |
|
||||
| `selector("entities")` | `List<Entity>` | Bukkit の `Bukkit.selectEntities` をそのまま利用 |
|
||||
| `coordinates("pos")` | `Coordinates3` | `~` 相対座標を含む 3 軸をまとめて扱う |
|
||||
| `selector("entities")` | `List<Entity>` | エンティティセレクター (`@e` など) |
|
||||
| `coordinates("pos")` | `io.papermc.paper.math.Position` | `~` 相対座標を含む 3 軸をまとめて扱う |
|
||||
|
||||
`Coordinates3` は `coordinates("pos") { ... }` 直後のコンテキストで `argument<Coordinates3>("pos")` として取得でき、`resolve(baseLocation)` で基準座標に対して実座標を求められます。
|
||||
`Position` は `coordinates("pos") { ... }` 直後のコンテキストで `argument<io.papermc.paper.math.Position>("pos")` として取得でき、`position.toLocation(world)` で `Location` に変換できます。
|
||||
|
||||
## クライアント側構文ヒント (Brigadier)
|
||||
|
||||
|
|
@ -178,3 +191,11 @@ Paper 1.21 以降の環境では、`LifecycleEventManager` を通じてコマン
|
|||
```
|
||||
|
||||
ShadowJar タスクが実行され、`build/libs` に出力されます。Paper サーバーに配置して動作確認してください。
|
||||
|
||||
## ドキュメント
|
||||
|
||||
- **[マイグレーションガイド](./MIGRATION_GUIDE.md)** - 旧バージョンからの移行方法
|
||||
|
||||
## ライセンス
|
||||
|
||||
このプロジェクトは MIT ライセンスの下で公開されています。
|
||||
|
|
|
|||
|
|
@ -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<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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CommandDefinition>,
|
||||
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<PluginCommand>()
|
||||
|
||||
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<net.hareworks.kommand_lib.nodes.KommandNode>,
|
||||
val permissionOptions: PermissionOptions
|
||||
) {
|
||||
private val tree = CommandTree(nodes)
|
||||
|
||||
fun execute(plugin: JavaPlugin, sender: CommandSender, alias: String, args: Array<String>): 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<String>): List<String> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
88
src/main/kotlin/net/hareworks/kommand-lib/TreeCompiler.kt
Normal file
88
src/main/kotlin/net/hareworks/kommand-lib/TreeCompiler.kt
Normal file
|
|
@ -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<CommandSourceStack> {
|
||||
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<CommandSourceStack, *>? {
|
||||
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<CommandSourceStack, Any>).suggests { ctx: CommandContext<CommandSourceStack>, 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<out T> {
|
||||
data class Success<T>(val value: T) : ArgumentParseResult<T>()
|
||||
data class Failure(val reason: String) : ArgumentParseResult<Nothing>()
|
||||
/**
|
||||
* 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<T>(),
|
||||
* 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<T> {
|
||||
fun build(): ArgumentType<*>
|
||||
}
|
||||
|
||||
interface KommandArgumentType<T> {
|
||||
fun parse(input: String, context: KommandContext): ArgumentParseResult<T>
|
||||
fun suggestions(context: KommandContext, prefix: String): List<String> = emptyList()
|
||||
class WordArgument : KommandArgument<String> {
|
||||
override fun build(): ArgumentType<String> = StringArgumentType.word()
|
||||
}
|
||||
|
||||
object WordArgumentType : KommandArgumentType<String> {
|
||||
override fun parse(input: String, context: KommandContext): ArgumentParseResult<String> {
|
||||
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<Int> {
|
||||
override fun build(): ArgumentType<Int> = IntegerArgumentType.integer(min, max)
|
||||
}
|
||||
|
||||
object BooleanArgumentType : KommandArgumentType<Boolean> {
|
||||
override fun parse(input: String, context: KommandContext): ArgumentParseResult<Boolean> {
|
||||
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<String> =
|
||||
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<Double> {
|
||||
override fun build(): ArgumentType<Double> = DoubleArgumentType.doubleArg(min, max)
|
||||
}
|
||||
|
||||
class IntegerArgumentType(
|
||||
val min: Int? = null,
|
||||
val max: Int? = null
|
||||
) : KommandArgumentType<Int> {
|
||||
override fun parse(input: String, context: KommandContext): ArgumentParseResult<Int> {
|
||||
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<Boolean> {
|
||||
override fun build(): ArgumentType<Boolean> = BoolArgumentType.bool()
|
||||
}
|
||||
|
||||
class FloatArgumentType(
|
||||
val min: Double? = null,
|
||||
val max: Double? = null
|
||||
) : KommandArgumentType<Double> {
|
||||
override fun parse(input: String, context: KommandContext): ArgumentParseResult<Double> {
|
||||
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<Player> {
|
||||
override fun build(): ArgumentType<PlayerSelectorArgumentResolver> = ArgumentTypes.player()
|
||||
}
|
||||
|
||||
class PlayerArgumentType(
|
||||
private val allowSelectors: Boolean
|
||||
) : KommandArgumentType<Player> {
|
||||
override fun parse(input: String, context: KommandContext): ArgumentParseResult<Player> {
|
||||
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<String> {
|
||||
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<Player> after resolving the selector.
|
||||
* Supports player names and selectors like @a, @r.
|
||||
*/
|
||||
class PlayersArgument : KommandArgument<List<Player>> {
|
||||
override fun build(): ArgumentType<PlayerSelectorArgumentResolver> = ArgumentTypes.players()
|
||||
}
|
||||
|
||||
class PlayerSelectorArgumentType(
|
||||
private val allowDirectNames: Boolean
|
||||
) : KommandArgumentType<List<Player>> {
|
||||
override fun parse(input: String, context: KommandContext): ArgumentParseResult<List<Player>> {
|
||||
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<Player>()
|
||||
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<String> {
|
||||
val candidates = linkedSetOf<String>()
|
||||
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<Entity> after resolving the selector.
|
||||
* Supports all entity selectors like @e, @e[type=minecraft:zombie].
|
||||
*/
|
||||
class EntityArgument : KommandArgument<List<Entity>> {
|
||||
override fun build(): ArgumentType<EntitySelectorArgumentResolver> = ArgumentTypes.entities()
|
||||
}
|
||||
|
||||
class EntitySelectorArgumentType(
|
||||
private val requireMatch: Boolean
|
||||
) : KommandArgumentType<List<Entity>> {
|
||||
override fun parse(input: String, context: KommandContext): ArgumentParseResult<List<Entity>> {
|
||||
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<String> {
|
||||
return DEFAULT_SELECTOR_SUGGESTIONS.filter { it.startsWith(prefix) }
|
||||
}
|
||||
}
|
||||
|
||||
class CoordinateComponentArgumentType(
|
||||
private val allowRelative: Boolean
|
||||
) : KommandArgumentType<CoordinateComponent> {
|
||||
override fun parse(input: String, context: KommandContext): ArgumentParseResult<CoordinateComponent> {
|
||||
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<String> {
|
||||
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<io.papermc.paper.math.Position> {
|
||||
override fun build(): ArgumentType<*> = ArgumentTypes.finePosition()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <reified T> resolve(context: CommandContext<CommandSourceStack>, 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 <reified T> resolveOrNull(context: CommandContext<CommandSourceStack>, name: String): T? {
|
||||
return try {
|
||||
resolve<T>(context, name)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null
|
||||
} catch (e: IllegalStateException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String>,
|
||||
val mode: ParseMode
|
||||
val internal: CommandContext<CommandSourceStack>
|
||||
) {
|
||||
private val parsedArguments = linkedMapOf<String, Any?>()
|
||||
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 <reified T> argument(name: String): T {
|
||||
return ArgumentResolver.resolve(internal, name)
|
||||
}
|
||||
|
||||
internal fun drop(name: String) {
|
||||
parsedArguments.remove(name)
|
||||
inline fun <reified T> argumentOrNull(name: String): T? {
|
||||
return ArgumentResolver.resolveOrNull(internal, name)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> argument(name: String): T =
|
||||
parsedArguments[name] as? T
|
||||
?: error("Argument '$name' is not present in this context.")
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> argumentOrNull(name: String): T? = parsedArguments[name] as? T
|
||||
|
||||
fun arguments(): Map<String, Any?> = parsedArguments.toMap()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<CommandDefinition>()
|
||||
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 <T> argument(name: String, type: KommandArgumentType<T>, block: ValueBuilder<T>.() -> Unit = {}) {
|
||||
fun <T> argument(name: String, type: KommandArgument<T>, block: ValueBuilder<T>.() -> 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<String>.() -> Unit = {}) = argument(name, WordArgumentType, block)
|
||||
fun string(name: String, block: ValueBuilder<String>.() -> 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<Int>.() -> 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<Double>.() -> Unit = {}
|
||||
) = argument(name, FloatArgumentType(min, max), block)
|
||||
) = argument(name, FloatArgument(min, max), block)
|
||||
|
||||
fun bool(
|
||||
name: String,
|
||||
block: ValueBuilder<Boolean>.() -> 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<Player>.() -> Unit = {}
|
||||
) = argument(name, PlayerArgumentType(allowSelectors), block)
|
||||
) = argument(name, PlayerArgument(), block)
|
||||
|
||||
fun players(
|
||||
name: String,
|
||||
allowDirectNames: Boolean = true,
|
||||
block: ValueBuilder<List<Player>>.() -> Unit = {}
|
||||
) = argument(name, PlayerSelectorArgumentType(allowDirectNames), block)
|
||||
) = argument(name, PlayersArgument(), block)
|
||||
|
||||
fun selector(
|
||||
name: String,
|
||||
requireMatch: Boolean = true,
|
||||
block: ValueBuilder<List<Entity>>.() -> 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<io.papermc.paper.math.Position>.() -> 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<T> internal constructor(
|
||||
private val valueNode: ValueNode<T>
|
||||
) : 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<String>) {
|
||||
valueNode.suggestionProvider = { ctx, prefix -> block(ctx, prefix) }
|
||||
}
|
||||
}
|
||||
|
||||
@KommandDsl
|
||||
class CoordinateNodeScope internal constructor(node: KommandNode) : NodeScope(node)
|
||||
|
||||
@DslMarker
|
||||
annotation class KommandDsl
|
||||
|
|
|
|||
|
|
@ -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<KommandNode>) {
|
||||
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<String> {
|
||||
if (context.args.isEmpty()) return emptyList()
|
||||
return collect(roots, context, 0)
|
||||
}
|
||||
|
||||
private fun match(nodes: List<KommandNode>, 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<KommandNode>, context: KommandContext, index: Int): List<String> {
|
||||
val token = context.args[index]
|
||||
val last = index == context.args.lastIndex
|
||||
val suggestions = linkedSetOf<String>()
|
||||
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
|
||||
}
|
||||
|
|
@ -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<String>
|
||||
|
||||
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<String> {
|
||||
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<T> internal constructor(
|
||||
private val name: String,
|
||||
val argumentType: KommandArgumentType<T>
|
||||
class ValueNode<T> internal constructor(
|
||||
val name: String,
|
||||
val argument: KommandArgument<T>
|
||||
) : KommandNode() {
|
||||
var suggestionProvider: ((KommandContext, String) -> List<String>)? = 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<String> {
|
||||
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<CoordinateComponent>(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<CoordinateComponent>(coordinateAxisKey(aggregateName, Axis.X))
|
||||
val y = context.argumentOrNull<CoordinateComponent>(coordinateAxisKey(aggregateName, Axis.Y))
|
||||
val z = context.argumentOrNull<CoordinateComponent>(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 }
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user