Compare commits

..
9 Commits
Author SHA1 Message Date
Hare 99ad500915 bump 26.1 paper-api & jvmToolchain 2026-04-27 17:35:42 +09:00
Hare 48cb204cc2 bump 26.1 2026-04-27 17:33:14 +09:00
Hare 9e1ee75736 chore: permits-lib folia support 2026-03-11 17:33:06 +09:00
Hare 1e2476a27b chore: fmt/lint 2026-03-03 22:20:08 +09:00
Hare 2670443135 chore: paperLibrary対応 2025-12-09 18:19:46 +09:00
Hare 66e1f74b5c feat: block coordinates 2025-12-09 05:58:45 +09:00
Hare 3835c9b9e2 feat: greedyString 2025-12-07 06:54:05 +09:00
Hare 25b40427ed docs: マイグレーションガイド 2025-12-07 06:29:11 +09:00
Hare aab2b1169c feat: Brigadier対応 2025-12-07 06:03:18 +09:00
19 changed files with 610 additions and 728 deletions
+118
View File
@@ -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()` を使用している場合は一度にすべて移行する必要があります。
+30 -9
View File
@@ -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 ライセンスの下で公開されています。
+13 -7
View File
@@ -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,9 +38,10 @@ 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") {
@@ -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> = class FloatArgument(
listOf("true", "false").filter { it.startsWith(prefix, ignoreCase = true) } 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( class BooleanArgument : KommandArgument<Boolean> {
val min: Int? = null, override fun build(): ArgumentType<Boolean> = BoolArgumentType.bool()
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 FloatArgumentType( /**
val min: Double? = null, * Single player argument. Returns a Player object after resolving the selector.
val max: Double? = null * Supports player names and selectors like @p, @s, @r[limit=1].
) : KommandArgumentType<Double> { */
override fun parse(input: String, context: KommandContext): ArgumentParseResult<Double> { class PlayerArgument : KommandArgument<Player> {
val value = input.toDoubleOrNull() override fun build(): ArgumentType<PlayerSelectorArgumentResolver> = ArgumentTypes.player()
?: 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 * Multiple players argument. Returns a List<Player> after resolving the selector.
) : KommandArgumentType<Player> { * Supports player names and selectors like @a, @r.
override fun parse(input: String, context: KommandContext): ArgumentParseResult<Player> { */
val trimmed = input.trim() class PlayersArgument : KommandArgument<List<Player>> {
if (allowSelectors && trimmed.startsWith("@")) { override fun build(): ArgumentType<PlayerSelectorArgumentResolver> = ArgumentTypes.players()
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() * Entity selector argument. Returns a List<Entity> after resolving the selector.
.map { it.name } * Supports all entity selectors like @e, @e[type=minecraft:zombie].
.filter { it.startsWith(prefix, ignoreCase = true) } */
if (!allowSelectors) return names class EntityArgument : KommandArgument<List<Entity>> {
val selectors = DEFAULT_SELECTOR_SUGGESTIONS.filter { it.startsWith(prefix) } override fun build(): ArgumentType<EntitySelectorArgumentResolver> = ArgumentTypes.entities()
return (names + selectors).distinct()
}
} }
class PlayerSelectorArgumentType( /**
private val allowDirectNames: Boolean * Fine position argument for coordinates with decimal precision.
) : KommandArgumentType<List<Player>> { * Supports relative coordinates like ~ ~1 ~-2.
override fun parse(input: String, context: KommandContext): ArgumentParseResult<List<Player>> { * Returns a Position (io.papermc.paper.math.Position) after resolving.
val trimmed = input.trim() */
if (!trimmed.startsWith("@")) { class CoordinatesArgument : KommandArgument<io.papermc.paper.math.Position> {
if (!allowDirectNames) { override fun build(): ArgumentType<*> = ArgumentTypes.finePosition()
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) * Block position argument for integer coordinates.
} catch (ex: IllegalArgumentException) { * Supports relative coordinates like ~ ~1 ~-2.
return ArgumentParseResult.Failure(ex.message ?: "Invalid selector '$trimmed'") * Returns a Position (io.papermc.paper.math.Position) after resolving (aligned to block).
} */
val players = entities.filterIsInstance<Player>() class BlockPositionArgument : KommandArgument<io.papermc.paper.math.Position> {
if (players.isEmpty()) { override fun build(): ArgumentType<*> = ArgumentTypes.blockPosition()
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
) : 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
}
} }
@@ -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,7 +47,8 @@ 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 =
config?.let {
val plan = PermissionPlanner(plugin, it, snapshot).plan() val plan = PermissionPlanner(plugin, it, snapshot).plan()
if (plan.isEmpty()) null else PermissionRuntime(plugin, plan) if (plan.isEmpty()) null else PermissionRuntime(plugin, plan)
} }
@@ -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,7 +16,8 @@ class PermissionOptions {
private set private set
fun rename(vararg segments: String) { fun rename(vararg segments: String) {
customPath = segments customPath =
segments
.map { it.trim() } .map { it.trim() }
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
.toMutableList() .toMutableList()
@@ -42,10 +43,11 @@ 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 =
segments
.flatMap { it.split('.') } .flatMap { it.split('.') }
.map { it.trim().lowercase() } .map { it.trim().lowercase() }
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
@@ -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,18 +11,20 @@ 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) =
if (config.includeRootNode && config.rootSegment.isNotBlank()) {
val path = listOf(config.rootSegment) val path = listOf(config.rootSegment)
val entry = createEntry( val entry =
createEntry(
options = PermissionOptions().apply { id = buildId(path) }, options = PermissionOptions().apply { id = buildId(path) },
pathSegments = path, pathSegments = path,
context = PermissionContext(commandName = "", path = path, kind = PermissionNodeKind.LITERAL), context = PermissionContext(commandName = "", path = path, kind = PermissionNodeKind.LITERAL),
parentDefault = config.defaultValue, parentDefault = config.defaultValue,
registration = NodeRegistration.STRUCTURAL registration = NodeRegistration.STRUCTURAL,
) )
if (entry != null) entries[entry.id] = entry if (entry != null) entries[entry.id] = entry
path to (entry?.defaultValue ?: config.defaultValue) path to (entry?.defaultValue ?: config.defaultValue)
@@ -32,18 +34,20 @@ internal class PermissionPlanner(
definitions.forEach { definition -> definitions.forEach { definition ->
val overridePath = definition.permissionOptions.renameOverride() val overridePath = definition.permissionOptions.renameOverride()
val commandPath = if (overridePath != null) { val commandPath =
if (overridePath != null) {
normalizeSegments(overridePath) normalizeSegments(overridePath)
} else { } else {
val sanitized = sanitize(definition.name) val sanitized = sanitize(definition.name)
val base = if (rootPath.isNotEmpty()) rootPath else emptyList() val base = if (rootPath.isNotEmpty()) rootPath else emptyList()
base + sanitized base + sanitized
} }
val commandEntry = createEntry( val commandEntry =
createEntry(
options = definition.permissionOptions, options = definition.permissionOptions,
pathSegments = commandPath, pathSegments = commandPath,
context = PermissionContext(definition.name, commandPath, PermissionNodeKind.COMMAND), context = PermissionContext(definition.name, commandPath, PermissionNodeKind.COMMAND),
parentDefault = rootDefault parentDefault = rootDefault,
) )
if (commandEntry != null) { if (commandEntry != null) {
entries[commandEntry.id] = commandEntry entries[commandEntry.id] = commandEntry
@@ -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.preferSkipByDefault &&
node.permissionOptions.id.isNullOrBlank() && node.permissionOptions.id.isNullOrBlank() &&
rawOverride == null) 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,18 +86,21 @@ 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 =
when {
pathAddition != null -> basePath + pathAddition pathAddition != null -> basePath + pathAddition
segment != null -> basePath + segment segment != null -> basePath + segment
else -> basePath else -> basePath
} }
val entry = createEntry( val entry =
createEntry(
options = node.permissionOptions, options = node.permissionOptions,
pathSegments = path, pathSegments = path,
context = PermissionContext(commandName, path, node.toKind()), context = PermissionContext(commandName, path, node.toKind()),
parentDefault = parentDefault parentDefault = parentDefault,
) )
val currentBase = if (entry != null) { val currentBase =
if (entry != null) {
entries[entry.id] = entry entries[entry.id] = entry
if (node.permission.isNullOrBlank()) { if (node.permission.isNullOrBlank()) {
node.permission = entry.id node.permission = entry.id
@@ -106,7 +115,8 @@ internal class PermissionPlanner(
} }
} }
private fun KommandNode.toKind(): PermissionNodeKind = when (this) { private fun KommandNode.toKind(): PermissionNodeKind =
when (this) {
is LiteralNode -> PermissionNodeKind.LITERAL is LiteralNode -> PermissionNodeKind.LITERAL
is ValueNode<*> -> PermissionNodeKind.ARGUMENT is ValueNode<*> -> PermissionNodeKind.ARGUMENT
else -> PermissionNodeKind.LITERAL else -> PermissionNodeKind.LITERAL
@@ -117,7 +127,7 @@ internal class PermissionPlanner(
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,7 +142,8 @@ 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 =
options.wildcardExclusions
.map { normalizeSegments(it) } .map { normalizeSegments(it) }
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
options.resolve(finalId) options.resolve(finalId)
@@ -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,12 +16,14 @@ 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 =
sorted
.mapNotNull { entry -> .mapNotNull { entry ->
entry.relativePath.takeIf { it.isNotEmpty() }?.joinToString(".")?.let { it to entry.registration } entry.relativePath.takeIf { it.isNotEmpty() }?.joinToString(".")?.let { it to entry.registration }
} }
.toMap() .toMap()
val entriesByPath = sorted val entriesByPath =
sorted
.filter { it.relativePath.isNotEmpty() } .filter { it.relativePath.isNotEmpty() }
.associateBy { it.relativePath.joinToString(".") } .associateBy { it.relativePath.joinToString(".") }
sorted.forEach { entry -> sorted.forEach { entry ->
@@ -30,7 +32,8 @@ internal class PermissionRuntime(
return@forEach return@forEach
} }
val nodeId = entry.relativePath.joinToString(".") val nodeId = entry.relativePath.joinToString(".")
val currentNode = mutable.node(nodeId, entry.registration) { val currentNode =
mutable.node(nodeId, entry.registration) {
entry.description?.let { description = it } entry.description?.let { description = it }
defaultValue = entry.defaultValue defaultValue = entry.defaultValue
wildcard = entry.wildcard wildcard = entry.wildcard