0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Minecraft】コマンド解析に brigadier という解法

Posted at

はじめに

いまさら Minecraft Java 版向けコマンドパースライブラリである brigadier に触れてみたので、それについての記事を書いてみます。

本記事に記載されているコードの検証環境は以下のとおりです。

  • brigadier 1.0.18
  • Kotlin 2.2.0 (on JDK 21)
  • Paper API ver. 1.21
  • PaperMC 1.21.3 build 83

また本記事では特筆のない限り brigadier の振る舞いや実装については Paper API 上のものを記載しています。

brigadier とは

brigadier とは Mojang が開発しているコマンドパースと実行をサポートするライブラリです。Paper では公式にサポートされており、引数についてのツールチップを出せるようになるなど様々な機能を利用することができます。

構造に関しての解説は intsuc さんの記事 に譲るとして、本記事では brigadier を用いてコマンドを実装し具体的な利用方法についてフォーカスを当てることとします。

コマンドの定義

まずはコマンドのルートノードを定義します。

val command = Commands.literal("time")

brigadier では主に (Commands) literalargument キーワードを使い分けてノードを定義していきます。

それらのノードを繋ぐために then, forward, fork, redirect キーワードが用いられ、各ノードでの入力補完や条件を設けるのに suggestsrequires キーワードを利用することが出来ます。

そして最終的には executes キーワードを用いてコマンドの処理を定義するといった流れになります。

literal

literal は主に「コマンド名」や「選択肢」に利用され、処理が分岐するような位置で使われ、literal の引数に入れた文字列とユーザーからの入力が一致することを求めます。
定義に用いた文字列以外が入力されると構文エラーとみなされます。

(例)
image.png
上記の例は time コマンドの第 1 引数に定義されていない destroy という文字列を入力して構文エラーが表示されている様子です。
自作コマンドでもこのように定義されていない文字列をエラー扱いすることが出来るようになります。

argument

argument は主にユーザーが入力する任意の値を定義するのに利用し、その型や範囲などを指定するのに使われます。
具体的にはプレイヤー名、座標、スケール、アイテム名やカスタム引数など実に様々なところで利用することが出来ます。

then

then は各ノードを接続するのに利用します。 brigadier で単純なコマンドを定義する際には、取りうるノード同士の接続をほとんどこのキーワードで表すことになります。

forward, fork, redirect

このキーワードについて解説するとすごく長くなってしまうので、紹介した解説記事の当該項目を参照してください。

requires

入力された文字列 (コマンド全体) にこのキーワードが対象とするノードを接続したとき、それを正常なコマンド構文とするか、不正なコマンド構文としてエラーにするかを決定します。
簡単にいうとコマンドを実行できるユーザーを制限するためのキーワードです。

suggets

引数の補完を提供するために利用するキーワードで、 Command.argument で定義された引数ノード

実装例

バニラの time コマンドと tp コマンドを brigadier で擬似的に実装し、コマンドの定義方法についての解説を行います。
(あくまで擬似的な実装なので、本家の実装とは異なる場合があります。)

time コマンドでの例

const val SINGLE_SUCCESS: Int = 1

fun CommandContext<CommandSourceStack>.getWorld(): World {
    return (this.source.sender as? Player)?.world
        ?: Bukkit.getWorlds().first()
}

fun CommandContext<CommandSourceStack>.sendMessage(msg: String) {
    this.source.sender.sendMessage(msg)
}

fun setTimeTo(ctx: CommandContext<CommandSourceStack>, key: String): Int {
    val world: World = ctx.getWorld()
    val value: Int = IntegerArgumentType.getInteger(ctx, key)
    val setTo: Long = (world.getGameTime() + value) % 24000
    world.setGameTime(setTo)
    ctx.sendMessage("Set the time to ${setTo}")
    return SINGLE_SUCCESS
}

val timeCommand = Commands.literal("time")
    .then(Commands.literal("add")
        .then(Commands.argument("value", ArgumentTypes.time())
            .executes { ctx ->
                setTimeTo(ctx, "value")
            }
        )
    ).then(Commands.literal("query")
        .then(Commands.literal("daytime")
            .executes { ctx ->
                ctx.sendMessage("The time is ${ctx.getWorld().getTime()}")
                return SINGLE_SUCCESS
            }
        ).then(Commands.literal("gametime")
            .executes { ctx ->
                ctx.sendMessage("The time is ${ctx.getWorld().getGameTime()}")
                return SINGLE_SUCCESS
            }
        ).then(Commands.literal("day")
            .executes { ctx ->
                ctx.sendMessage("The time is ${ctx.getWorld().getGameTime() / 24000}")
                return SINGLE_SUCCESS
            }
        )
    ).then(Commands.literal("set")
        .then(Commands.literal("day")
            .executes { ctx ->
                ctx.getWorld().setTime(1000)
                ctx.sendMessage("Set the time to 1000")
                return SINGLE_SUCCESS
            }
        ).then(Commands.literal("noon")
            .executes { ctx ->
                ctx.getWorld().setTime(6000)
                ctx.sendMessage("Set the time to 6000")
                return SINGLE_SUCCESS
            }
        ).then(Commands.literal("night")
            .executes { ctx ->
                ctx.getWorld().setTime(13000)
                ctx.sendMessage("Set the time to 13000")
                return SINGLE_SUCCESS
            }
        ).then(Commands.literal("midnight")
            .executes { ctx ->
                ctx.getWorld().setTime(18000)
                ctx.sendMessage("Set the time to 18000")
                return SINGLE_SUCCESS
            }
        ).then(Commands.argument("value", ArgumentTypes.time())
            .executes { ctx ->
                setTimeTo(ctx, "value")
            }
        )
    )

time コマンドの場合、コマンド全体は以下のように表記することが出来ます。

  • time add [MINECRAFT_TIME_FORMAT]
  • time query [day|daytime|gametime]
  • time set [day|noon|night|midnight|MINECRAFT_TIME_FORMAT]

このとき、ルートノード time 配下には add, query, set が存在します。
また、リダイレクト、フォワード、フォークといった複雑な引数入力は求められないため、それぞれが then キーワードで直接接続されています。
そしてそれぞれのサブコマンド配下に literal, argument で定義される引数が存在し、 executes 内で処理が実行されます。

コマンドの登録

このようにして組み立てられたコマンドは Lifecycle API によってコマンドシステムへと登録され、プレイヤーが利用できるようになります。
Kotlin でのコード例を以下に示します。

// このコードにおける this は JavaPlugin クラスを継承したプラグインのインスタンスです
// `timeCommand` は #コマンドの定義 におけるコマンド本体を示す timeCommand です
this.lifecycleManager.registerEventHandler(
    LifecycleEvents.COMMANDS,
    LifecycleEventHandler { commands ->
        commands.registrar().register(timeCommand.build()))
    }
)

最後に

自分であれこれパースして条件を設定してとやることなく、コマンドの処理本体の作成に集中できるのでとても良いものを見つけたなと思っています。
それでは。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?