Compare commits
9
Commits
e0613cd052
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99ad500915 | ||
|
|
48cb204cc2 | ||
|
|
9e1ee75736 | ||
|
|
1e2476a27b | ||
|
|
2670443135 | ||
|
|
66e1f74b5c | ||
|
|
3835c9b9e2 | ||
|
|
25b40427ed | ||
|
|
aab2b1169c |
@@ -0,0 +1,118 @@
|
|||||||
|
# kommand-lib マイグレーションガイド
|
||||||
|
|
||||||
|
旧バージョンから最新の Brigadier ネイティブ対応バージョンへの移行方法。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 変更の概要
|
||||||
|
|
||||||
|
### 主な変更点
|
||||||
|
|
||||||
|
1. **`coordinates()` の型変更** (破壊的変更)
|
||||||
|
- `Coordinates3` → `io.papermc.paper.math.Position`
|
||||||
|
- `coords.resolve(base)` → `position.toLocation(world)`
|
||||||
|
|
||||||
|
2. **内部処理の改善**
|
||||||
|
- Player/Entity セレクターの安定性向上
|
||||||
|
- Bukkit CommandMap → Brigadier Lifecycle API
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## マイグレーション手順
|
||||||
|
|
||||||
|
### 1. 依存関係の確認
|
||||||
|
|
||||||
|
Paper API 1.21 以降が必要です。
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
dependencies {
|
||||||
|
compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. coordinates の修正
|
||||||
|
|
||||||
|
#### Before
|
||||||
|
```kotlin
|
||||||
|
coordinates("point") {
|
||||||
|
executes {
|
||||||
|
val coords = argument<Coordinates3>("point")
|
||||||
|
val location = coords.resolve(player.location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### After
|
||||||
|
```kotlin
|
||||||
|
import io.papermc.paper.math.Position
|
||||||
|
|
||||||
|
coordinates("point") {
|
||||||
|
executes {
|
||||||
|
val position = argument<Position>("point")
|
||||||
|
val location = position.toLocation(player.world)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. ビルド確認
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Position API
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val position = argument<Position>("pos")
|
||||||
|
|
||||||
|
// 座標取得
|
||||||
|
val x = position.x()
|
||||||
|
val y = position.y()
|
||||||
|
val z = position.z()
|
||||||
|
|
||||||
|
// Location 変換
|
||||||
|
val location = position.toLocation(world)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## トラブルシューティング
|
||||||
|
|
||||||
|
### `Coordinates3` が見つからない
|
||||||
|
|
||||||
|
`Coordinates3` は存在しません。`Position` を使用してください。
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// ❌ 間違い
|
||||||
|
argument<Coordinates3>("pos")
|
||||||
|
|
||||||
|
// ✅ 正しい
|
||||||
|
argument<Position>("pos")
|
||||||
|
```
|
||||||
|
|
||||||
|
### `resolve()` メソッドが見つからない
|
||||||
|
|
||||||
|
`Position` には `resolve()` はありません。`toLocation(world)` を使用してください。
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// ❌ 間違い
|
||||||
|
position.resolve(baseLocation)
|
||||||
|
|
||||||
|
// ✅ 正しい
|
||||||
|
position.toLocation(world)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
**Q: 相対座標 (`~`) は使えますか?**
|
||||||
|
A: はい、`Position` は相対座標を完全にサポートしています。
|
||||||
|
|
||||||
|
**Q: 旧バージョンとの互換性は?**
|
||||||
|
A: `coordinates()` の型が変更されているため互換性はありません。他の引数(`player()`, `players()` など)は互換性があります。
|
||||||
|
|
||||||
|
**Q: 段階的な移行は可能?**
|
||||||
|
A: `coordinates()` を使用している場合は一度にすべて移行する必要があります。
|
||||||
@@ -12,6 +12,19 @@ Paper/Bukkit サーバー向けのコマンド定義を DSL で記述するた
|
|||||||
- Brigadier (Paper 1.21 Lifecycle API) 対応により、クライアント側で `<speed> <count>` のような構文ヒントや、数値範囲の検証エラー(赤文字)が表示されます
|
- Brigadier (Paper 1.21 Lifecycle API) 対応により、クライアント側で `<speed> <count>` のような構文ヒントや、数値範囲の検証エラー(赤文字)が表示されます
|
||||||
- `permits-lib` との連携により、コマンドツリーから Bukkit パーミッションを自動生成し、`compileOnly` 依存として参照可能
|
- `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 を利用しています。
|
`build.gradle.kts` では Paper API と Kotlin 標準ライブラリのみを `compileOnly` に追加しています。Paper 1.21.10 対応の API を利用しています。
|
||||||
@@ -77,11 +90,11 @@ class EconomyPlugin : JavaPlugin() {
|
|||||||
literal("setspawn") {
|
literal("setspawn") {
|
||||||
coordinates("point") { // "~ ~1 ~-2" のような入力を受け付ける
|
coordinates("point") { // "~ ~1 ~-2" のような入力を受け付ける
|
||||||
executes {
|
executes {
|
||||||
val base = (sender as? Player)?.location ?: return@executes
|
val player = sender as? Player ?: return@executes
|
||||||
val coords = argument<Coordinates3>("point")
|
val position = argument<io.papermc.paper.math.Position>("point")
|
||||||
val target = coords.resolve(base)
|
val location = position.toLocation(player.world)
|
||||||
plugin.server.worlds.first().setSpawnLocation(target)
|
player.world.setSpawnLocation(location)
|
||||||
sender.sendMessage("Spawn set to ${target.x}, ${target.y}, ${target.z}")
|
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")` を呼び出してください。
|
- `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")` のように取得できます。
|
- `float("speed")` や `player("target")`/`players("targets")`/`selector("entities")` は Minecraft の標準セレクター (`@p`, `@a`, `@s` など) やプレイヤー名を型付きで扱えます。実行時は `argument<Double>("speed")`、`argument<Player>("target")`、`argument<List<Player>>("targets")` のように取得できます。
|
||||||
- `suggests { prefix -> ... }` を指定すると、タブ補完時に任意の候補リストを返せます。
|
- `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 -> ... }` で実行条件 (例: コンソール禁止) を追加できます。
|
- `command` や各ノードの `condition { sender -> ... }` で実行条件 (例: コンソール禁止) を追加できます。
|
||||||
- ルートレベルで `executes { ... }` を指定すると、引数なしで `/eco` を実行した場合に呼び出されます。
|
- ルートレベルで `executes { ... }` を指定すると、引数なしで `/eco` を実行した場合に呼び出されます。
|
||||||
|
|
||||||
@@ -158,10 +171,10 @@ commands = kommand(this) {
|
|||||||
| `float("speed", min, max)` | `Double` | 小数/指数表記に対応 |
|
| `float("speed", min, max)` | `Double` | 小数/指数表記に対応 |
|
||||||
| `player("target", allowSelectors = true)` | `Player` | `@p` などのセレクターまたはプレイヤー名を 1 人に解決 |
|
| `player("target", allowSelectors = true)` | `Player` | `@p` などのセレクターまたはプレイヤー名を 1 人に解決 |
|
||||||
| `players("targets")` | `List<Player>` | `@a`/`@r` など複数指定、プレイヤー名入力も可 |
|
| `players("targets")` | `List<Player>` | `@a`/`@r` など複数指定、プレイヤー名入力も可 |
|
||||||
| `selector("entities")` | `List<Entity>` | Bukkit の `Bukkit.selectEntities` をそのまま利用 |
|
| `selector("entities")` | `List<Entity>` | エンティティセレクター (`@e` など) |
|
||||||
| `coordinates("pos")` | `Coordinates3` | `~` 相対座標を含む 3 軸をまとめて扱う |
|
| `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)
|
## クライアント側構文ヒント (Brigadier)
|
||||||
|
|
||||||
@@ -178,3 +191,11 @@ Paper 1.21 以降の環境では、`LifecycleEventManager` を通じてコマン
|
|||||||
```
|
```
|
||||||
|
|
||||||
ShadowJar タスクが実行され、`build/libs` に出力されます。Paper サーバーに配置して動作確認してください。
|
ShadowJar タスクが実行され、`build/libs` に出力されます。Paper サーバーに配置して動作確認してください。
|
||||||
|
|
||||||
|
## ドキュメント
|
||||||
|
|
||||||
|
- **[MIGRATION_GUIDE](./MIGRATION_GUIDE.md)** - 旧バージョンからの移行方法
|
||||||
|
|
||||||
|
## ライセンス
|
||||||
|
|
||||||
|
このプロジェクトは MIT ライセンスの下で公開されています。
|
||||||
|
|||||||
+14
-8
@@ -4,8 +4,8 @@ group = "net.hareworks"
|
|||||||
version = "1.1"
|
version = "1.1"
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "2.2.21"
|
kotlin("jvm") version "2.3.21"
|
||||||
id("de.eldoria.plugin-yml.paper") version "0.8.0"
|
id("de.eldoria.plugin-yml.paper") version "0.9.0"
|
||||||
id("com.gradleup.shadow") version "9.2.2"
|
id("com.gradleup.shadow") version "9.2.2"
|
||||||
}
|
}
|
||||||
repositories {
|
repositories {
|
||||||
@@ -14,10 +14,15 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT")
|
compileOnly("io.papermc.paper:paper-api:26.1.2.build.49-beta")
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib")
|
paperLibrary("org.jetbrains.kotlin:kotlin-stdlib")
|
||||||
implementation("net.hareworks:permits-lib:1.1")
|
implementation("net.hareworks:permits-lib:1.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(25)
|
||||||
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
withType<Jar> {
|
withType<Jar> {
|
||||||
archiveBaseName.set("Kommand-Lib")
|
archiveBaseName.set("Kommand-Lib")
|
||||||
@@ -33,10 +38,11 @@ paper {
|
|||||||
name = "kommand-lib"
|
name = "kommand-lib"
|
||||||
description = "Command library"
|
description = "Command library"
|
||||||
version = getVersion().toString()
|
version = getVersion().toString()
|
||||||
apiVersion = "1.21.10"
|
apiVersion = "26.1"
|
||||||
authors = listOf(
|
authors =
|
||||||
"Hare-K02"
|
listOf(
|
||||||
)
|
"Hare-K02",
|
||||||
|
)
|
||||||
serverDependencies {
|
serverDependencies {
|
||||||
register("permits-lib") {
|
register("permits-lib") {
|
||||||
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
|
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
|
||||||
|
|||||||
+1
-1
Submodule permits-lib updated: 90bad7f37c...2e35905599
@@ -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,20 +1,16 @@
|
|||||||
package net.hareworks.kommand_lib
|
package net.hareworks.kommand_lib
|
||||||
|
|
||||||
import net.hareworks.kommand_lib.context.KommandContext
|
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.dsl.KommandRegistry
|
||||||
import net.hareworks.kommand_lib.permissions.PermissionOptions
|
import net.hareworks.kommand_lib.permissions.PermissionOptions
|
||||||
import net.hareworks.kommand_lib.permissions.PermissionRuntime
|
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.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.java.JavaPlugin
|
||||||
|
|
||||||
fun kommand(plugin: JavaPlugin, block: KommandRegistry.() -> Unit): KommandLib {
|
fun kommand(
|
||||||
|
plugin: JavaPlugin,
|
||||||
|
block: KommandRegistry.() -> Unit,
|
||||||
|
): KommandLib {
|
||||||
val registry = KommandRegistry(plugin)
|
val registry = KommandRegistry(plugin)
|
||||||
registry.block()
|
registry.block()
|
||||||
return registry.build()
|
return registry.build()
|
||||||
@@ -26,64 +22,33 @@ fun kommand(plugin: JavaPlugin, block: KommandRegistry.() -> Unit): KommandLib {
|
|||||||
class KommandLib internal constructor(
|
class KommandLib internal constructor(
|
||||||
private val plugin: JavaPlugin,
|
private val plugin: JavaPlugin,
|
||||||
private val definitions: List<CommandDefinition>,
|
private val definitions: List<CommandDefinition>,
|
||||||
private val permissionRuntime: PermissionRuntime?
|
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 {
|
init {
|
||||||
registerAll()
|
registerAll()
|
||||||
permissionRuntime?.let {
|
|
||||||
if (it.config.autoApply) it.apply()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun registerAll() {
|
private fun registerAll() {
|
||||||
// Register via Paper Lifecycle API for 1.21+
|
|
||||||
val manager = plugin.lifecycleManager
|
val manager = plugin.lifecycleManager
|
||||||
@Suppress("UnstableApiUsage")
|
@Suppress("UnstableApiUsage")
|
||||||
manager.registerEventHandler(io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents.COMMANDS) { event ->
|
manager.registerEventHandler(io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents.COMMANDS) { event ->
|
||||||
val registrar = event.registrar()
|
val registrar = event.registrar()
|
||||||
for (definition in definitions) {
|
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)
|
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() {
|
fun unregister() {
|
||||||
registered.forEach { it.unregister(commandMap) }
|
// Lifecycle API handles unregistration automatically on disable usually?
|
||||||
registered.clear()
|
// 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()
|
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(
|
internal data class CommandDefinition(
|
||||||
@@ -95,31 +60,5 @@ internal data class CommandDefinition(
|
|||||||
val rootCondition: (CommandSender) -> Boolean,
|
val rootCondition: (CommandSender) -> Boolean,
|
||||||
val rootExecutor: (KommandContext.() -> Unit)?,
|
val rootExecutor: (KommandContext.() -> Unit)?,
|
||||||
val nodes: List<net.hareworks.kommand_lib.nodes.KommandNode>,
|
val nodes: List<net.hareworks.kommand_lib.nodes.KommandNode>,
|
||||||
val permissionOptions: PermissionOptions
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package net.hareworks.kommand_lib.plugin;
|
package net.hareworks.kommand_lib.plugin
|
||||||
|
|
||||||
import org.bukkit.plugin.java.JavaPlugin
|
import org.bukkit.plugin.java.JavaPlugin
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
public class Plugin : JavaPlugin() {}
|
public class Plugin : JavaPlugin()
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
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 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,91 @@
|
|||||||
package net.hareworks.kommand_lib.arguments
|
package net.hareworks.kommand_lib.arguments
|
||||||
|
|
||||||
import net.hareworks.kommand_lib.context.KommandContext
|
import com.mojang.brigadier.arguments.ArgumentType
|
||||||
import org.bukkit.Bukkit
|
import com.mojang.brigadier.arguments.BoolArgumentType
|
||||||
import org.bukkit.Location
|
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.EntitySelectorArgumentResolver
|
||||||
|
import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver
|
||||||
import org.bukkit.entity.Entity
|
import org.bukkit.entity.Entity
|
||||||
import org.bukkit.entity.Player
|
import org.bukkit.entity.Player
|
||||||
|
|
||||||
sealed class ArgumentParseResult<out T> {
|
/**
|
||||||
data class Success<T>(val value: T) : ArgumentParseResult<T>()
|
* A holder for the Brigadier ArgumentType and any metadata needed for the DSL.
|
||||||
data class Failure(val reason: String) : ArgumentParseResult<Nothing>()
|
*
|
||||||
|
* 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> {
|
class WordArgument : KommandArgument<String> {
|
||||||
fun parse(input: String, context: KommandContext): ArgumentParseResult<T>
|
override fun build(): ArgumentType<String> = StringArgumentType.word()
|
||||||
fun suggestions(context: KommandContext, prefix: String): List<String> = emptyList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object WordArgumentType : KommandArgumentType<String> {
|
class GreedyStringArgument : KommandArgument<String> {
|
||||||
override fun parse(input: String, context: KommandContext): ArgumentParseResult<String> {
|
override fun build(): ArgumentType<String> = StringArgumentType.greedyString()
|
||||||
if (input.isBlank()) return ArgumentParseResult.Failure("Value cannot be blank")
|
|
||||||
return ArgumentParseResult.Success(input)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object BooleanArgumentType : KommandArgumentType<Boolean> {
|
class IntegerArgument(
|
||||||
override fun parse(input: String, context: KommandContext): ArgumentParseResult<Boolean> {
|
private val min: Int = Int.MIN_VALUE,
|
||||||
val lower = input.lowercase()
|
private val max: Int = Int.MAX_VALUE,
|
||||||
return when (lower) {
|
) : KommandArgument<Int> {
|
||||||
"true" -> ArgumentParseResult.Success(true)
|
override fun build(): ArgumentType<Int> = IntegerArgumentType.integer(min, max)
|
||||||
"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 IntegerArgumentType(
|
class FloatArgument(
|
||||||
val min: Int? = null,
|
private val min: Double = -Double.MAX_VALUE,
|
||||||
val max: Int? = null
|
private val max: Double = Double.MAX_VALUE,
|
||||||
) : KommandArgumentType<Int> {
|
) : KommandArgument<Double> {
|
||||||
override fun parse(input: String, context: KommandContext): ArgumentParseResult<Int> {
|
override fun build(): ArgumentType<Double> = DoubleArgumentType.doubleArg(min, max)
|
||||||
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 FloatArgumentType(
|
class BooleanArgument : KommandArgument<Boolean> {
|
||||||
val min: Double? = null,
|
override fun build(): ArgumentType<Boolean> = BoolArgumentType.bool()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlayerArgumentType(
|
/**
|
||||||
private val allowSelectors: Boolean
|
* Single player argument. Returns a Player object after resolving the selector.
|
||||||
) : KommandArgumentType<Player> {
|
* Supports player names and selectors like @p, @s, @r[limit=1].
|
||||||
override fun parse(input: String, context: KommandContext): ArgumentParseResult<Player> {
|
*/
|
||||||
val trimmed = input.trim()
|
class PlayerArgument : KommandArgument<Player> {
|
||||||
if (allowSelectors && trimmed.startsWith("@")) {
|
override fun build(): ArgumentType<PlayerSelectorArgumentResolver> = ArgumentTypes.player()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlayerSelectorArgumentType(
|
/**
|
||||||
private val allowDirectNames: Boolean
|
* Multiple players argument. Returns a List<Player> after resolving the selector.
|
||||||
) : KommandArgumentType<List<Player>> {
|
* Supports player names and selectors like @a, @r.
|
||||||
override fun parse(input: String, context: KommandContext): ArgumentParseResult<List<Player>> {
|
*/
|
||||||
val trimmed = input.trim()
|
class PlayersArgument : KommandArgument<List<Player>> {
|
||||||
if (!trimmed.startsWith("@")) {
|
override fun build(): ArgumentType<PlayerSelectorArgumentResolver> = ArgumentTypes.players()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class EntitySelectorArgumentType(
|
/**
|
||||||
private val requireMatch: Boolean
|
* Entity selector argument. Returns a List<Entity> after resolving the selector.
|
||||||
) : KommandArgumentType<List<Entity>> {
|
* Supports all entity selectors like @e, @e[type=minecraft:zombie].
|
||||||
override fun parse(input: String, context: KommandContext): ArgumentParseResult<List<Entity>> {
|
*/
|
||||||
val trimmed = input.trim()
|
class EntityArgument : KommandArgument<List<Entity>> {
|
||||||
if (!trimmed.startsWith("@")) {
|
override fun build(): ArgumentType<EntitySelectorArgumentResolver> = ArgumentTypes.entities()
|
||||||
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
|
* Fine position argument for coordinates with decimal precision.
|
||||||
) : KommandArgumentType<CoordinateComponent> {
|
* Supports relative coordinates like ~ ~1 ~-2.
|
||||||
override fun parse(input: String, context: KommandContext): ArgumentParseResult<CoordinateComponent> {
|
* Returns a Position (io.papermc.paper.math.Position) after resolving.
|
||||||
val trimmed = input.trim()
|
*/
|
||||||
if (allowRelative && trimmed.startsWith("~")) {
|
class CoordinatesArgument : KommandArgument<io.papermc.paper.math.Position> {
|
||||||
val remainder = trimmed.removePrefix("~")
|
override fun build(): ArgumentType<*> = ArgumentTypes.finePosition()
|
||||||
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")
|
/**
|
||||||
|
* Block position argument for integer coordinates.
|
||||||
data class CoordinateComponent(val relative: Boolean, val value: Double) {
|
* Supports relative coordinates like ~ ~1 ~-2.
|
||||||
fun resolve(origin: Double): Double = if (relative) origin + value else value
|
* Returns a Position (io.papermc.paper.math.Position) after resolving (aligned to block).
|
||||||
}
|
*/
|
||||||
|
class BlockPositionArgument : KommandArgument<io.papermc.paper.math.Position> {
|
||||||
data class Coordinates3(
|
override fun build(): ArgumentType<*> = ArgumentTypes.blockPosition()
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
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.BlockPositionResolver
|
||||||
|
import io.papermc.paper.command.brigadier.argument.resolvers.FinePositionResolver
|
||||||
|
import io.papermc.paper.command.brigadier.argument.resolvers.selector.EntitySelectorArgumentResolver
|
||||||
|
import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver
|
||||||
|
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
|
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.command.CommandSender
|
||||||
import org.bukkit.plugin.java.JavaPlugin
|
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 plugin: JavaPlugin,
|
||||||
val sender: CommandSender,
|
val internal: CommandContext<CommandSourceStack>,
|
||||||
val alias: String,
|
|
||||||
val args: Array<String>,
|
|
||||||
val mode: ParseMode
|
|
||||||
) {
|
) {
|
||||||
private val parsedArguments = linkedMapOf<String, Any?>()
|
val sender: CommandSender
|
||||||
|
get() = internal.source.sender
|
||||||
|
|
||||||
internal fun remember(name: String, value: Any?) {
|
val commandSource: CommandSourceStack
|
||||||
parsedArguments[name] = value
|
get() = internal.source
|
||||||
|
|
||||||
|
inline fun <reified T> argument(name: String): T {
|
||||||
|
return ArgumentResolver.resolve(internal, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun drop(name: String) {
|
inline fun <reified T> argumentOrNull(name: String): T? {
|
||||||
parsedArguments.remove(name)
|
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()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,13 @@ package net.hareworks.kommand_lib.dsl
|
|||||||
|
|
||||||
import net.hareworks.kommand_lib.CommandDefinition
|
import net.hareworks.kommand_lib.CommandDefinition
|
||||||
import net.hareworks.kommand_lib.arguments.*
|
import net.hareworks.kommand_lib.arguments.*
|
||||||
|
import net.hareworks.kommand_lib.nodes.KommandNode
|
||||||
|
import net.hareworks.kommand_lib.nodes.LiteralNode
|
||||||
|
import net.hareworks.kommand_lib.nodes.ValueNode
|
||||||
import net.hareworks.kommand_lib.permissions.PermissionConfigBuilder
|
import net.hareworks.kommand_lib.permissions.PermissionConfigBuilder
|
||||||
import net.hareworks.kommand_lib.permissions.PermissionOptions
|
import net.hareworks.kommand_lib.permissions.PermissionOptions
|
||||||
import net.hareworks.kommand_lib.permissions.PermissionPlanner
|
import net.hareworks.kommand_lib.permissions.PermissionPlanner
|
||||||
import net.hareworks.kommand_lib.permissions.PermissionRuntime
|
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
|
|
||||||
import org.bukkit.command.CommandSender
|
import org.bukkit.command.CommandSender
|
||||||
import org.bukkit.entity.Entity
|
import org.bukkit.entity.Entity
|
||||||
import org.bukkit.entity.Player
|
import org.bukkit.entity.Player
|
||||||
@@ -21,16 +19,21 @@ class KommandRegistry internal constructor(private val plugin: JavaPlugin) {
|
|||||||
private val definitions = mutableListOf<CommandDefinition>()
|
private val definitions = mutableListOf<CommandDefinition>()
|
||||||
private var permissionConfigBuilder: PermissionConfigBuilder? = null
|
private var permissionConfigBuilder: PermissionConfigBuilder? = null
|
||||||
|
|
||||||
/**
|
fun command(
|
||||||
* Declares a new command root.
|
name: String,
|
||||||
*/
|
vararg aliases: String,
|
||||||
fun command(name: String, vararg aliases: String, block: CommandBuilder.() -> Unit) {
|
block: CommandBuilder.() -> Unit,
|
||||||
|
) {
|
||||||
val builder = CommandBuilder(name, aliases.toList())
|
val builder = CommandBuilder(name, aliases.toList())
|
||||||
builder.block()
|
builder.block()
|
||||||
definitions += builder.build()
|
definitions += builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun command(name: String, aliases: Iterable<String>, block: CommandBuilder.() -> Unit) {
|
fun command(
|
||||||
|
name: String,
|
||||||
|
aliases: Iterable<String>,
|
||||||
|
block: CommandBuilder.() -> Unit,
|
||||||
|
) {
|
||||||
val builder = CommandBuilder(name, aliases.toList())
|
val builder = CommandBuilder(name, aliases.toList())
|
||||||
builder.block()
|
builder.block()
|
||||||
definitions += builder.build()
|
definitions += builder.build()
|
||||||
@@ -44,10 +47,11 @@ class KommandRegistry internal constructor(private val plugin: JavaPlugin) {
|
|||||||
internal fun build(): net.hareworks.kommand_lib.KommandLib {
|
internal fun build(): net.hareworks.kommand_lib.KommandLib {
|
||||||
val snapshot = definitions.toList()
|
val snapshot = definitions.toList()
|
||||||
val config = permissionConfigBuilder?.build()
|
val config = permissionConfigBuilder?.build()
|
||||||
val runtime = config?.let {
|
val runtime =
|
||||||
val plan = PermissionPlanner(plugin, it, snapshot).plan()
|
config?.let {
|
||||||
if (plan.isEmpty()) null else PermissionRuntime(plugin, plan)
|
val plan = PermissionPlanner(plugin, it, snapshot).plan()
|
||||||
}
|
if (plan.isEmpty()) null else PermissionRuntime(plugin, plan)
|
||||||
|
}
|
||||||
return net.hareworks.kommand_lib.KommandLib(plugin, snapshot, runtime)
|
return net.hareworks.kommand_lib.KommandLib(plugin, snapshot, runtime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,7 +59,7 @@ class KommandRegistry internal constructor(private val plugin: JavaPlugin) {
|
|||||||
@KommandDsl
|
@KommandDsl
|
||||||
class CommandBuilder internal constructor(
|
class CommandBuilder internal constructor(
|
||||||
val name: String,
|
val name: String,
|
||||||
val aliases: List<String>
|
val aliases: List<String>,
|
||||||
) : BranchScope(mutableListOf()) {
|
) : BranchScope(mutableListOf()) {
|
||||||
var description: String? = null
|
var description: String? = null
|
||||||
var usage: String? = null
|
var usage: String? = null
|
||||||
@@ -103,18 +107,21 @@ class CommandBuilder internal constructor(
|
|||||||
rootCondition = condition,
|
rootCondition = condition,
|
||||||
rootExecutor = rootExecutor,
|
rootExecutor = rootExecutor,
|
||||||
nodes = children.toList(),
|
nodes = children.toList(),
|
||||||
permissionOptions = permissionOptions
|
permissionOptions = permissionOptions,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@KommandDsl
|
@KommandDsl
|
||||||
abstract class BranchScope internal constructor(
|
abstract class BranchScope internal constructor(
|
||||||
protected val children: MutableList<KommandNode>
|
protected val children: MutableList<KommandNode>,
|
||||||
) {
|
) {
|
||||||
protected abstract val inheritedPermission: String?
|
protected abstract val inheritedPermission: String?
|
||||||
protected abstract val inheritedCondition: (CommandSender) -> Boolean
|
protected abstract val inheritedCondition: (CommandSender) -> Boolean
|
||||||
|
|
||||||
fun literal(name: String, block: LiteralBuilder.() -> Unit = {}) {
|
fun literal(
|
||||||
|
name: String,
|
||||||
|
block: LiteralBuilder.() -> Unit = {},
|
||||||
|
) {
|
||||||
val node = LiteralNode(name)
|
val node = LiteralNode(name)
|
||||||
node.permission = inheritedPermission
|
node.permission = inheritedPermission
|
||||||
node.condition = inheritedCondition
|
node.condition = inheritedCondition
|
||||||
@@ -122,7 +129,11 @@ abstract class BranchScope internal constructor(
|
|||||||
LiteralBuilder(node).apply(block)
|
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)
|
val node = ValueNode(name, type)
|
||||||
node.permission = inheritedPermission
|
node.permission = inheritedPermission
|
||||||
node.condition = inheritedCondition
|
node.condition = inheritedCondition
|
||||||
@@ -131,71 +142,69 @@ abstract class BranchScope internal constructor(
|
|||||||
ValueBuilder(node).apply(block)
|
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 greedyString(
|
||||||
|
name: String,
|
||||||
|
block: ValueBuilder<String>.() -> Unit = {},
|
||||||
|
) = argument(name, GreedyStringArgument(), block)
|
||||||
|
|
||||||
fun integer(
|
fun integer(
|
||||||
name: String,
|
name: String,
|
||||||
min: Int? = null,
|
min: Int = Int.MIN_VALUE,
|
||||||
max: Int? = null,
|
max: Int = Int.MAX_VALUE,
|
||||||
block: ValueBuilder<Int>.() -> Unit = {}
|
block: ValueBuilder<Int>.() -> Unit = {},
|
||||||
) = argument(name, IntegerArgumentType(min, max), block)
|
) = argument(name, IntegerArgument(min, max), block)
|
||||||
|
|
||||||
fun float(
|
fun float(
|
||||||
name: String,
|
name: String,
|
||||||
min: Double? = null,
|
min: Double = -Double.MAX_VALUE,
|
||||||
max: Double? = null,
|
max: Double = Double.MAX_VALUE,
|
||||||
block: ValueBuilder<Double>.() -> Unit = {}
|
block: ValueBuilder<Double>.() -> Unit = {},
|
||||||
) = argument(name, FloatArgumentType(min, max), block)
|
) = argument(name, FloatArgument(min, max), block)
|
||||||
|
|
||||||
fun bool(
|
fun bool(
|
||||||
name: String,
|
name: String,
|
||||||
block: ValueBuilder<Boolean>.() -> Unit = {}
|
block: ValueBuilder<Boolean>.() -> Unit = {},
|
||||||
) = argument(name, BooleanArgumentType, block)
|
) = argument(name, BooleanArgument(), block)
|
||||||
|
|
||||||
fun player(
|
fun player(
|
||||||
name: String,
|
name: String,
|
||||||
allowSelectors: Boolean = true,
|
allowSelectors: Boolean = true, // Ignored logic-wise if using native, assuming it handles selectors
|
||||||
block: ValueBuilder<Player>.() -> Unit = {}
|
block: ValueBuilder<Player>.() -> Unit = {},
|
||||||
) = argument(name, PlayerArgumentType(allowSelectors), block)
|
) = argument(name, PlayerArgument(), block)
|
||||||
|
|
||||||
fun players(
|
fun players(
|
||||||
name: String,
|
name: String,
|
||||||
allowDirectNames: Boolean = true,
|
allowDirectNames: Boolean = true,
|
||||||
block: ValueBuilder<List<Player>>.() -> Unit = {}
|
block: ValueBuilder<List<Player>>.() -> Unit = {},
|
||||||
) = argument(name, PlayerSelectorArgumentType(allowDirectNames), block)
|
) = argument(name, PlayersArgument(), block)
|
||||||
|
|
||||||
fun selector(
|
fun selector(
|
||||||
name: String,
|
name: String,
|
||||||
requireMatch: Boolean = true,
|
requireMatch: Boolean = true,
|
||||||
block: ValueBuilder<List<Entity>>.() -> Unit = {}
|
block: ValueBuilder<List<Entity>>.() -> Unit = {},
|
||||||
) = argument(name, EntitySelectorArgumentType(requireMatch), block)
|
) = argument(name, EntityArgument(), block)
|
||||||
|
|
||||||
fun coordinates(
|
fun coordinates(
|
||||||
name: String,
|
name: String,
|
||||||
allowRelative: Boolean = true,
|
allowRelative: Boolean = true,
|
||||||
block: CoordinateNodeScope.() -> Unit = {}
|
block: ValueBuilder<io.papermc.paper.math.Position>.() -> Unit = {},
|
||||||
) {
|
) = argument(name, CoordinatesArgument(), block)
|
||||||
val xNode = CoordinateAxisNode(name, Axis.X, allowRelative)
|
|
||||||
val yNode = CoordinateAxisNode(name, Axis.Y, allowRelative)
|
fun blockCoordinates(
|
||||||
val zNode = CoordinateAxisNode(name, Axis.Z, allowRelative)
|
name: String,
|
||||||
val nodes = listOf(xNode, yNode, zNode)
|
allowRelative: Boolean = true,
|
||||||
nodes.forEach { node ->
|
block: ValueBuilder<io.papermc.paper.math.Position>.() -> Unit = {},
|
||||||
node.permission = inheritedPermission
|
) = argument(name, BlockPositionArgument(), block)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@KommandDsl
|
@KommandDsl
|
||||||
abstract class NodeScope internal constructor(
|
abstract class NodeScope internal constructor(
|
||||||
protected val node: KommandNode
|
protected val node: KommandNode,
|
||||||
) : BranchScope(node.children) {
|
) : BranchScope(node.children) {
|
||||||
override val inheritedPermission: String?
|
override val inheritedPermission: String?
|
||||||
get() = node.permission
|
get() = node.permission
|
||||||
@@ -227,23 +236,20 @@ abstract class NodeScope internal constructor(
|
|||||||
|
|
||||||
@KommandDsl
|
@KommandDsl
|
||||||
class LiteralBuilder internal constructor(
|
class LiteralBuilder internal constructor(
|
||||||
node: LiteralNode
|
private val literalNode: LiteralNode,
|
||||||
) : NodeScope(node)
|
) : NodeScope(literalNode)
|
||||||
|
|
||||||
@KommandDsl
|
@KommandDsl
|
||||||
class ValueBuilder<T> internal constructor(
|
class ValueBuilder<T> internal constructor(
|
||||||
private val valueNode: ValueNode<T>
|
private val valueNode: ValueNode<T>,
|
||||||
) : NodeScope(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<String>) {
|
fun suggests(block: net.hareworks.kommand_lib.context.KommandContext.(prefix: String) -> List<String>) {
|
||||||
valueNode.suggestionProvider = { ctx, prefix -> block(ctx, prefix) }
|
valueNode.suggestionProvider = { ctx, prefix -> block(ctx, prefix) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@KommandDsl
|
|
||||||
class CoordinateNodeScope internal constructor(node: KommandNode) : NodeScope(node)
|
|
||||||
|
|
||||||
@DslMarker
|
@DslMarker
|
||||||
annotation class KommandDsl
|
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
|
package net.hareworks.kommand_lib.nodes
|
||||||
|
|
||||||
import net.hareworks.kommand_lib.arguments.CoordinateComponent
|
import net.hareworks.kommand_lib.arguments.KommandArgument
|
||||||
import net.hareworks.kommand_lib.arguments.Coordinates3
|
|
||||||
import net.hareworks.kommand_lib.arguments.KommandArgumentType
|
|
||||||
import net.hareworks.kommand_lib.context.KommandContext
|
import net.hareworks.kommand_lib.context.KommandContext
|
||||||
import net.hareworks.kommand_lib.execution.ParseMode
|
|
||||||
import net.hareworks.kommand_lib.permissions.PermissionOptions
|
import net.hareworks.kommand_lib.permissions.PermissionOptions
|
||||||
import org.bukkit.command.CommandSender
|
import org.bukkit.command.CommandSender
|
||||||
|
|
||||||
@@ -21,86 +18,18 @@ abstract class KommandNode internal constructor() {
|
|||||||
return condition(sender)
|
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
|
open fun segment(): String? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
class LiteralNode internal constructor(private val literal: String) : KommandNode() {
|
class LiteralNode internal constructor(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()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun segment(): String = literal
|
override fun segment(): String = literal
|
||||||
}
|
}
|
||||||
|
|
||||||
open class ValueNode<T> internal constructor(
|
class ValueNode<T> internal constructor(
|
||||||
private val name: String,
|
val name: String,
|
||||||
val argumentType: KommandArgumentType<T>
|
val argument: KommandArgument<T>,
|
||||||
) : KommandNode() {
|
) : KommandNode() {
|
||||||
var suggestionProvider: ((KommandContext, String) -> List<String>)? = null
|
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
|
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 }
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class PermissionConfig internal constructor(
|
|||||||
val defaultDescription: (PermissionContext) -> String?,
|
val defaultDescription: (PermissionContext) -> String?,
|
||||||
val defaultValue: PermissionDefault,
|
val defaultValue: PermissionDefault,
|
||||||
val defaultWildcard: Boolean,
|
val defaultWildcard: Boolean,
|
||||||
private val sessionProvider: (JavaPlugin) -> MutationSession
|
private val sessionProvider: (JavaPlugin) -> MutationSession,
|
||||||
) {
|
) {
|
||||||
fun session(plugin: JavaPlugin): MutationSession = sessionProvider(plugin)
|
fun session(plugin: JavaPlugin): MutationSession = sessionProvider(plugin)
|
||||||
}
|
}
|
||||||
@@ -61,18 +61,18 @@ class PermissionConfigBuilder internal constructor(private val plugin: JavaPlugi
|
|||||||
defaultDescription = descriptionTemplate,
|
defaultDescription = descriptionTemplate,
|
||||||
defaultValue = defaultValue,
|
defaultValue = defaultValue,
|
||||||
defaultWildcard = wildcard,
|
defaultWildcard = wildcard,
|
||||||
sessionProvider = sessionFactory ?: { PermitsLib.session(it) }
|
sessionProvider = sessionFactory ?: { PermitsLib.session(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PermissionContext(
|
data class PermissionContext(
|
||||||
val commandName: String,
|
val commandName: String,
|
||||||
val path: List<String>,
|
val path: List<String>,
|
||||||
val kind: PermissionNodeKind
|
val kind: PermissionNodeKind,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class PermissionNodeKind {
|
enum class PermissionNodeKind {
|
||||||
COMMAND,
|
COMMAND,
|
||||||
LITERAL,
|
LITERAL,
|
||||||
ARGUMENT
|
ARGUMENT,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ class PermissionOptions {
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
fun rename(vararg segments: String) {
|
fun rename(vararg segments: String) {
|
||||||
customPath = segments
|
customPath =
|
||||||
.map { it.trim() }
|
segments
|
||||||
.filter { it.isNotEmpty() }
|
.map { it.trim() }
|
||||||
.toMutableList()
|
.filter { it.isNotEmpty() }
|
||||||
|
.toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun renameOverride(): List<String>? = customPath?.toList()
|
internal fun renameOverride(): List<String>? = customPath?.toList()
|
||||||
@@ -42,13 +43,14 @@ class PermissionOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class WildcardOptions internal constructor(
|
class WildcardOptions internal constructor(
|
||||||
private val sink: MutableList<List<String>>
|
private val sink: MutableList<List<String>>,
|
||||||
) {
|
) {
|
||||||
fun exclude(vararg segments: String) {
|
fun exclude(vararg segments: String) {
|
||||||
val normalized = segments
|
val normalized =
|
||||||
.flatMap { it.split('.') }
|
segments
|
||||||
.map { it.trim().lowercase() }
|
.flatMap { it.split('.') }
|
||||||
.filter { it.isNotEmpty() }
|
.map { it.trim().lowercase() }
|
||||||
|
.filter { it.isNotEmpty() }
|
||||||
if (normalized.isNotEmpty()) {
|
if (normalized.isNotEmpty()) {
|
||||||
sink += normalized
|
sink += normalized
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import org.bukkit.permissions.PermissionDefault
|
|||||||
|
|
||||||
data class PermissionPlan(
|
data class PermissionPlan(
|
||||||
val config: PermissionConfig,
|
val config: PermissionConfig,
|
||||||
val entries: List<PlannedPermission>
|
val entries: List<PlannedPermission>,
|
||||||
) {
|
) {
|
||||||
val namespace: String get() = config.namespace
|
val namespace: String get() = config.namespace
|
||||||
|
|
||||||
fun isEmpty(): Boolean = entries.isEmpty()
|
fun isEmpty(): Boolean = entries.isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,5 +21,5 @@ data class PlannedPermission(
|
|||||||
val wildcardExclusions: List<List<String>>,
|
val wildcardExclusions: List<List<String>>,
|
||||||
val inheritsParentDefault: Boolean,
|
val inheritsParentDefault: Boolean,
|
||||||
val wildcard: Boolean,
|
val wildcard: Boolean,
|
||||||
val registration: NodeRegistration
|
val registration: NodeRegistration,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,40 +11,44 @@ import org.bukkit.plugin.java.JavaPlugin
|
|||||||
internal class PermissionPlanner(
|
internal class PermissionPlanner(
|
||||||
private val plugin: JavaPlugin,
|
private val plugin: JavaPlugin,
|
||||||
private val config: PermissionConfig,
|
private val config: PermissionConfig,
|
||||||
private val definitions: List<CommandDefinition>
|
private val definitions: List<CommandDefinition>,
|
||||||
) {
|
) {
|
||||||
fun plan(): PermissionPlan {
|
fun plan(): PermissionPlan {
|
||||||
val entries = linkedMapOf<String, PlannedPermission>()
|
val entries = linkedMapOf<String, PlannedPermission>()
|
||||||
val (rootPath, rootDefault) = if (config.includeRootNode && config.rootSegment.isNotBlank()) {
|
val (rootPath, rootDefault) =
|
||||||
val path = listOf(config.rootSegment)
|
if (config.includeRootNode && config.rootSegment.isNotBlank()) {
|
||||||
val entry = createEntry(
|
val path = listOf(config.rootSegment)
|
||||||
options = PermissionOptions().apply { id = buildId(path) },
|
val entry =
|
||||||
pathSegments = path,
|
createEntry(
|
||||||
context = PermissionContext(commandName = "", path = path, kind = PermissionNodeKind.LITERAL),
|
options = PermissionOptions().apply { id = buildId(path) },
|
||||||
parentDefault = config.defaultValue,
|
pathSegments = path,
|
||||||
registration = NodeRegistration.STRUCTURAL
|
context = PermissionContext(commandName = "", path = path, kind = PermissionNodeKind.LITERAL),
|
||||||
)
|
parentDefault = config.defaultValue,
|
||||||
if (entry != null) entries[entry.id] = entry
|
registration = NodeRegistration.STRUCTURAL,
|
||||||
path to (entry?.defaultValue ?: config.defaultValue)
|
)
|
||||||
} else {
|
if (entry != null) entries[entry.id] = entry
|
||||||
emptyList<String>() to config.defaultValue
|
path to (entry?.defaultValue ?: config.defaultValue)
|
||||||
}
|
} else {
|
||||||
|
emptyList<String>() to config.defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
definitions.forEach { definition ->
|
definitions.forEach { definition ->
|
||||||
val overridePath = definition.permissionOptions.renameOverride()
|
val overridePath = definition.permissionOptions.renameOverride()
|
||||||
val commandPath = if (overridePath != null) {
|
val commandPath =
|
||||||
normalizeSegments(overridePath)
|
if (overridePath != null) {
|
||||||
} else {
|
normalizeSegments(overridePath)
|
||||||
val sanitized = sanitize(definition.name)
|
} else {
|
||||||
val base = if (rootPath.isNotEmpty()) rootPath else emptyList()
|
val sanitized = sanitize(definition.name)
|
||||||
base + sanitized
|
val base = if (rootPath.isNotEmpty()) rootPath else emptyList()
|
||||||
}
|
base + sanitized
|
||||||
val commandEntry = createEntry(
|
}
|
||||||
options = definition.permissionOptions,
|
val commandEntry =
|
||||||
pathSegments = commandPath,
|
createEntry(
|
||||||
context = PermissionContext(definition.name, commandPath, PermissionNodeKind.COMMAND),
|
options = definition.permissionOptions,
|
||||||
parentDefault = rootDefault
|
pathSegments = commandPath,
|
||||||
)
|
context = PermissionContext(definition.name, commandPath, PermissionNodeKind.COMMAND),
|
||||||
|
parentDefault = rootDefault,
|
||||||
|
)
|
||||||
if (commandEntry != null) {
|
if (commandEntry != null) {
|
||||||
entries[commandEntry.id] = commandEntry
|
entries[commandEntry.id] = commandEntry
|
||||||
if (definition.permission.isNullOrBlank()) {
|
if (definition.permission.isNullOrBlank()) {
|
||||||
@@ -64,14 +68,16 @@ internal class PermissionPlanner(
|
|||||||
basePath: List<String>,
|
basePath: List<String>,
|
||||||
entries: MutableMap<String, PlannedPermission>,
|
entries: MutableMap<String, PlannedPermission>,
|
||||||
commandName: String,
|
commandName: String,
|
||||||
parentDefault: PermissionDefault
|
parentDefault: PermissionDefault,
|
||||||
) {
|
) {
|
||||||
val rawOverride = node.permissionOptions.renameOverride()
|
val rawOverride = node.permissionOptions.renameOverride()
|
||||||
val shouldSkip =
|
val shouldSkip =
|
||||||
node.permissionOptions.skip ||
|
node.permissionOptions.skip ||
|
||||||
(node.permissionOptions.preferSkipByDefault &&
|
(
|
||||||
node.permissionOptions.id.isNullOrBlank() &&
|
node.permissionOptions.preferSkipByDefault &&
|
||||||
rawOverride == null)
|
node.permissionOptions.id.isNullOrBlank() &&
|
||||||
|
rawOverride == null
|
||||||
|
)
|
||||||
if (shouldSkip) {
|
if (shouldSkip) {
|
||||||
node.children.forEach { child ->
|
node.children.forEach { child ->
|
||||||
planNode(child, basePath, entries, commandName, parentDefault)
|
planNode(child, basePath, entries, commandName, parentDefault)
|
||||||
@@ -80,44 +86,48 @@ internal class PermissionPlanner(
|
|||||||
}
|
}
|
||||||
val segment = node.segment()?.let { sanitize(it) }
|
val segment = node.segment()?.let { sanitize(it) }
|
||||||
val pathAddition = rawOverride?.let { normalizeSegments(it) }
|
val pathAddition = rawOverride?.let { normalizeSegments(it) }
|
||||||
val path = when {
|
val path =
|
||||||
pathAddition != null -> basePath + pathAddition
|
when {
|
||||||
segment != null -> basePath + segment
|
pathAddition != null -> basePath + pathAddition
|
||||||
else -> basePath
|
segment != null -> basePath + segment
|
||||||
}
|
else -> basePath
|
||||||
val entry = createEntry(
|
}
|
||||||
options = node.permissionOptions,
|
val entry =
|
||||||
pathSegments = path,
|
createEntry(
|
||||||
context = PermissionContext(commandName, path, node.toKind()),
|
options = node.permissionOptions,
|
||||||
parentDefault = parentDefault
|
pathSegments = path,
|
||||||
)
|
context = PermissionContext(commandName, path, node.toKind()),
|
||||||
val currentBase = if (entry != null) {
|
parentDefault = parentDefault,
|
||||||
entries[entry.id] = entry
|
)
|
||||||
if (node.permission.isNullOrBlank()) {
|
val currentBase =
|
||||||
node.permission = entry.id
|
if (entry != null) {
|
||||||
|
entries[entry.id] = entry
|
||||||
|
if (node.permission.isNullOrBlank()) {
|
||||||
|
node.permission = entry.id
|
||||||
|
}
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
basePath
|
||||||
}
|
}
|
||||||
path
|
|
||||||
} else {
|
|
||||||
basePath
|
|
||||||
}
|
|
||||||
val nextDefault = entry?.defaultValue ?: parentDefault
|
val nextDefault = entry?.defaultValue ?: parentDefault
|
||||||
node.children.forEach { child ->
|
node.children.forEach { child ->
|
||||||
planNode(child, currentBase, entries, commandName, nextDefault)
|
planNode(child, currentBase, entries, commandName, nextDefault)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun KommandNode.toKind(): PermissionNodeKind = when (this) {
|
private fun KommandNode.toKind(): PermissionNodeKind =
|
||||||
is LiteralNode -> PermissionNodeKind.LITERAL
|
when (this) {
|
||||||
is ValueNode<*> -> PermissionNodeKind.ARGUMENT
|
is LiteralNode -> PermissionNodeKind.LITERAL
|
||||||
else -> PermissionNodeKind.LITERAL
|
is ValueNode<*> -> PermissionNodeKind.ARGUMENT
|
||||||
}
|
else -> PermissionNodeKind.LITERAL
|
||||||
|
}
|
||||||
|
|
||||||
private fun createEntry(
|
private fun createEntry(
|
||||||
options: PermissionOptions,
|
options: PermissionOptions,
|
||||||
pathSegments: List<String>,
|
pathSegments: List<String>,
|
||||||
context: PermissionContext,
|
context: PermissionContext,
|
||||||
parentDefault: PermissionDefault,
|
parentDefault: PermissionDefault,
|
||||||
registration: NodeRegistration = NodeRegistration.PERMISSION
|
registration: NodeRegistration = NodeRegistration.PERMISSION,
|
||||||
): PlannedPermission? {
|
): PlannedPermission? {
|
||||||
val finalId = (options.id?.takeIf { it.isNotBlank() } ?: buildId(pathSegments)).trim()
|
val finalId = (options.id?.takeIf { it.isNotBlank() } ?: buildId(pathSegments)).trim()
|
||||||
if (finalId.isEmpty()) return null
|
if (finalId.isEmpty()) return null
|
||||||
@@ -132,9 +142,10 @@ internal class PermissionPlanner(
|
|||||||
val explicitDefault = options.defaultValue
|
val explicitDefault = options.defaultValue
|
||||||
val defaultValue = explicitDefault ?: parentDefault
|
val defaultValue = explicitDefault ?: parentDefault
|
||||||
val wildcard = options.wildcard ?: config.defaultWildcard
|
val wildcard = options.wildcard ?: config.defaultWildcard
|
||||||
val wildcardExclusions = options.wildcardExclusions
|
val wildcardExclusions =
|
||||||
.map { normalizeSegments(it) }
|
options.wildcardExclusions
|
||||||
.filter { it.isNotEmpty() }
|
.map { normalizeSegments(it) }
|
||||||
|
.filter { it.isNotEmpty() }
|
||||||
options.resolve(finalId)
|
options.resolve(finalId)
|
||||||
val parentPath = if (relativePath.isNotEmpty()) relativePath.dropLast(1).takeIf { it.isNotEmpty() } else null
|
val parentPath = if (relativePath.isNotEmpty()) relativePath.dropLast(1).takeIf { it.isNotEmpty() } else null
|
||||||
return PlannedPermission(
|
return PlannedPermission(
|
||||||
@@ -146,16 +157,14 @@ internal class PermissionPlanner(
|
|||||||
wildcardExclusions = wildcardExclusions,
|
wildcardExclusions = wildcardExclusions,
|
||||||
inheritsParentDefault = explicitDefault == null,
|
inheritsParentDefault = explicitDefault == null,
|
||||||
wildcard = wildcard,
|
wildcard = wildcard,
|
||||||
registration = registration
|
registration = registration,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildId(pathSegments: List<String>): String =
|
private fun buildId(pathSegments: List<String>): String =
|
||||||
(listOf(config.namespace) + pathSegments).filter { it.isNotBlank() }.joinToString(".")
|
(listOf(config.namespace) + pathSegments).filter { it.isNotBlank() }.joinToString(".")
|
||||||
|
|
||||||
private fun sanitize(segment: String): String =
|
private fun sanitize(segment: String): String = segment.trim().lowercase().replace(Regex("[^a-z0-9._-]"), "-").trim('-')
|
||||||
segment.trim().lowercase().replace(Regex("[^a-z0-9._-]"), "-").trim('-')
|
|
||||||
|
|
||||||
private fun normalizeSegments(segments: List<String>): List<String> =
|
private fun normalizeSegments(segments: List<String>): List<String> = segments.map { sanitize(it) }.filter { it.isNotBlank() }
|
||||||
segments.map { sanitize(it) }.filter { it.isNotBlank() }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import org.bukkit.plugin.java.JavaPlugin
|
|||||||
|
|
||||||
internal class PermissionRuntime(
|
internal class PermissionRuntime(
|
||||||
private val plugin: JavaPlugin,
|
private val plugin: JavaPlugin,
|
||||||
private val plan: PermissionPlan
|
private val plan: PermissionPlan,
|
||||||
) {
|
) {
|
||||||
private val session: MutationSession by lazy { plan.config.session(plugin) }
|
private val session: MutationSession by lazy { plan.config.session(plugin) }
|
||||||
val config: PermissionConfig get() = plan.config
|
val config: PermissionConfig get() = plan.config
|
||||||
@@ -16,25 +16,28 @@ internal class PermissionRuntime(
|
|||||||
if (plan.isEmpty()) return
|
if (plan.isEmpty()) return
|
||||||
val mutable = MutablePermissionTree.create(plan.config.namespace)
|
val mutable = MutablePermissionTree.create(plan.config.namespace)
|
||||||
val sorted = plan.entries.sortedBy { it.relativePath.size }
|
val sorted = plan.entries.sortedBy { it.relativePath.size }
|
||||||
val registrations = sorted
|
val registrations =
|
||||||
.mapNotNull { entry ->
|
sorted
|
||||||
entry.relativePath.takeIf { it.isNotEmpty() }?.joinToString(".")?.let { it to entry.registration }
|
.mapNotNull { entry ->
|
||||||
}
|
entry.relativePath.takeIf { it.isNotEmpty() }?.joinToString(".")?.let { it to entry.registration }
|
||||||
.toMap()
|
}
|
||||||
val entriesByPath = sorted
|
.toMap()
|
||||||
.filter { it.relativePath.isNotEmpty() }
|
val entriesByPath =
|
||||||
.associateBy { it.relativePath.joinToString(".") }
|
sorted
|
||||||
|
.filter { it.relativePath.isNotEmpty() }
|
||||||
|
.associateBy { it.relativePath.joinToString(".") }
|
||||||
sorted.forEach { entry ->
|
sorted.forEach { entry ->
|
||||||
if (entry.relativePath.isEmpty()) {
|
if (entry.relativePath.isEmpty()) {
|
||||||
plugin.logger.warning("Skipping permission '${entry.id}' because it resolved to the namespace root.")
|
plugin.logger.warning("Skipping permission '${entry.id}' because it resolved to the namespace root.")
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
val nodeId = entry.relativePath.joinToString(".")
|
val nodeId = entry.relativePath.joinToString(".")
|
||||||
val currentNode = mutable.node(nodeId, entry.registration) {
|
val currentNode =
|
||||||
entry.description?.let { description = it }
|
mutable.node(nodeId, entry.registration) {
|
||||||
defaultValue = entry.defaultValue
|
entry.description?.let { description = it }
|
||||||
wildcard = entry.wildcard
|
defaultValue = entry.defaultValue
|
||||||
}
|
wildcard = entry.wildcard
|
||||||
|
}
|
||||||
if (entry.wildcard && entry.wildcardExclusions.isNotEmpty()) {
|
if (entry.wildcard && entry.wildcardExclusions.isNotEmpty()) {
|
||||||
entry.wildcardExclusions.forEach { exclusion ->
|
entry.wildcardExclusions.forEach { exclusion ->
|
||||||
val absolutePath = entry.relativePath + exclusion
|
val absolutePath = entry.relativePath + exclusion
|
||||||
|
|||||||
Reference in New Issue
Block a user