kommand-lib/README.md

8.6 KiB

Kommand Lib

Paper/Bukkit サーバー向けのコマンド定義を DSL で記述するためのライブラリです。ルート定義から引数の型、補完、パーミッション伝播までを宣言的に表現でき、手続き的な CommandExecutor 実装を大きく簡略化します。

特徴

  • Kotlin DSL で command { literal { argument { ... } } } のようにネストを表現
  • 型付き引数 (string, integer, float, player, selector, coordinates など) と検証ロジックを組み込み
  • 1 つの定義から実行とタブ補完の両方を生成
  • パーミッションや条件をノード単位で宣言し、子ノードへ自動伝播
  • suggests {} で引数ごとの補完候補を柔軟に制御
  • permits-lib との連携により、コマンドツリーから Bukkit パーミッションを自動生成し、compileOnly 依存として参照可能

依存関係

build.gradle.kts では Paper API と Kotlin 標準ライブラリのみを compileOnly に追加しています。Paper 1.21.10 対応の API を利用しています。

plugins {
    kotlin("jvm") version "2.2.21"
    id("de.eldoria.plugin-yml.paper") version "0.8.0"
    id("com.gradleup.shadow") version "9.2.2"
}

dependencies {
    compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT")
    compileOnly("org.jetbrains.kotlin:kotlin-stdlib")
    // ../permits-lib を includeBuild しているので module 参照でOK
    compileOnly("net.hareworks.hcu:permits-lib:1.0")
}

使い方

  1. プラグインの onEnable などで kommand(plugin) { ... } DSL を呼び出します。
  2. command("root", "alias") { ... } でコマンドを宣言し、literalstring/integer 引数を追加します。
  3. executes { ... } 内で string("player")int("amount") を使ってパース済みの値を取得します。

サンプル: 経済コマンド

class EconomyPlugin : JavaPlugin() {
    private lateinit var commands: KommandLib

    override fun onEnable() {
        commands = kommand(this) {
            command("eco", "economy") {
                description = "Economy management"
                permission = "example.eco"

                literal("give") {
                    player("target") // プレイヤー名 or セレクター (@p 等)
                    integer("amount", min = 1)

                    executes {
                        val target = player("target")
                        val amount = int("amount")
                        sender.sendMessage("Giving $amount to ${target.name}")
                    }
                }

                literal("speed") {
                    players("targets") // @a, プレイヤー名, などをまとめて取得
                    float("value", min = 0.1, max = 5.0)

                    executes {
                        val targets = players("targets")
                        val speed = float("value")
                        targets.forEach { it.walkSpeed = speed.toFloat() / 5.0f }
                        sender.sendMessage("Updated ${targets.size} players")
                    }
                }

                literal("setspawn") {
                    coordinates("point") // "~ ~1 ~-2" のような入力を受け付ける

                    executes {
                        val base = (sender as? Player)?.location ?: return@executes
                        val target = location("point", base)
                        plugin.server.worlds.first().setSpawnLocation(target)
                        sender.sendMessage("Spawn set to ${target.x}, ${target.y}, ${target.z}")
                    }
                }

                literal("inspect") {
                    selector("entities")

                    executes {
                        val entities = selector("entities")
                        sender.sendMessage("Selector resolved ${entities.size} entities")
                    }
                }
            }
        }
    }

    override fun onDisable() {
        commands.unregister()
    }
}

DSL 構文のポイント

  • literal("sub") { ... } は固定語句を表すノードです。requires("permission.node") でその枝のみにパーミッションを設定できます。
  • string("name")integer("value", min = 0) は値をパースし、成功すると KommandContext に記憶されます。
  • float("speed") は倍精度を、player("target")/players("targets")/selector("entities") は Minecraft の標準セレクター (@p, @a, @s など) やプレイヤー名を型付きで扱えます。
  • suggests { prefix -> ... } を指定すると、タブ補完時に任意の候補リストを返せます。
  • coordinates("pos")x y z をまとめて 1 つの引数として受け取り、location("pos", player.location) で現在位置を基準に解決できます (~ を使用した相対座標に対応)。
  • command や各ノードの condition { sender -> ... } で実行条件 (例: コンソール禁止) を追加できます。
  • ルートレベルで executes { ... } を指定すると、引数なしで /eco を実行した場合に呼び出されます。

自動パーミッション生成 (permits-lib)

permits-libcompileOnly で参照している場合、DSL から Bukkit パーミッションツリーを自動生成できます。

commands = kommand(this) {
    permissions {
        namespace = "hareworks"
        rootSegment = "command"
        defaultDescription { ctx ->
            "Allows /${ctx.commandName} (${ctx.path.joinToString(" ")})"
        }
    }

    command("eco") {
        permission {
            description = "Allows /eco"
            tags("economy")
        }

        literal("give") {
            permission {
                description = "Allows /eco give"
            }
        }
    }
}
  • permissions { ... } で名前空間やルートセグメント (rootSegment = "command") を定義すると、hareworks.command.eco, hareworks.command.eco.give のような ID が自動生成され、permits-libMutationSession を使って Bukkit に適用されます。
  • literalcommand ノードはデフォルトで Bukkit パーミッションとして登録されます。stringinteger などの引数ノードは、requires("permission.id") もしくは permission { id = ... } を記述した場合のみ登録され、それ以外は構造上のノードに留まります。これにより /money give <player> <amount> では ...money.give だけ付与すれば実行できます。
  • 各ブロック内の permission { ... } で説明文・デフォルト値 (PermissionDefault.TRUE/FALSE/OP/NOT_OP)・タグ・パスを細かく制御できます。requires(...) を使うと DSL での検証と permits 側の登録が同じ ID になります。
  • 引数ノードを明示的に登録したい場合は permission { id = "example.money.give.amount" }requires("example.money.give.amount") を設定してください。逆にリテラルでも不要なら skipPermission() で除外できます。
  • requires("custom.id") を指定した場合も、同じ ID が DSL の実行と permits-lib への登録の両方で利用されます (名前空間外の ID は登録対象外になります)。
  • KommandLib のライフサイクルに合わせて MutationSession が適用/解除されるため、プラグインの有効化・無効化に伴い Bukkit のパーミッションリストも最新状態に保たれます。

組み込み引数の一覧

DSL 返り値 補足
string("name") String 任意のトークン。suggests {} で補完可
integer("value", min, max) Int 範囲チェック付き
float("speed", min, max) Double 小数/指数表記に対応
player("target", allowSelectors = true) Player @p などのセレクターまたはプレイヤー名を 1 人に解決
players("targets") List<Player> @a/@r など複数指定、プレイヤー名入力も可
selector("entities") List<Entity> Bukkit の Bukkit.selectEntities をそのまま利用
coordinates("pos") Coordinates3 ~ 相対座標を含む 3 軸をまとめて扱う

Coordinates3coordinates("pos") { ... } 直後のコンテキストで coordinates("pos") もしくは location("pos", baseLocation) として取得できます。

ビルドとテスト

./gradlew build

ShadowJar タスクが実行され、build/libs に出力されます。Paper サーバーに配置して動作確認してください。