はじめに
いまさら 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) literal
と argument
キーワードを使い分けてノードを定義していきます。
それらのノードを繋ぐために then
, forward
, fork
, redirect
キーワードが用いられ、各ノードでの入力補完や条件を設けるのに suggests
や requires
キーワードを利用することが出来ます。
そして最終的には executes
キーワードを用いてコマンドの処理を定義するといった流れになります。
literal
literal は主に「コマンド名」や「選択肢」に利用され、処理が分岐するような位置で使われ、literal の引数に入れた文字列とユーザーからの入力が一致することを求めます。
定義に用いた文字列以外が入力されると構文エラーとみなされます。
(例)
上記の例は 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()))
}
)
最後に
自分であれこれパースして条件を設定してとやることなく、コマンドの処理本体の作成に集中できるのでとても良いものを見つけたなと思っています。
それでは。