今回はmodでコマンドを作成してみます。前回の記事からだいぶ間が空いてしまったのは、今更ながら(Yet Another)JavaでVoIPするためのライブラリの記事に書いたVoIPで遊んでいたから。。またmod開発に戻ってきました。
ちなみに今回は書くことが多かったり、いっぱいサンプルコードが必要だったので長編です。
環境
- OS: Windows10
- Jdk: openjdk version "15.0.2" 2021-01-19
※16.xでもいいかもだけどgradleやらなにやら色々変わってしまって心配だったので今回は15で。 - IDE: Eclipse IDE for Java Developers (Version 4.19.0)
- Buildship(Eclipseのgradleプラグイン): 3.0
- Minecraft: Java版 1.16.5
- Forge: 1.16.5
まずは動かしてみる
やはりまずは簡単なコードを動かしてみるのがわかりやすいでしょう、ということで動かしてみます。
package jp.munecraft.mod.commandtestmod;
// importは省略
@Mod("commandtestmod")
public class CommandTest {
private static final Logger LOGGER = LogManager.getLogger();
public CommandTest() {
LOGGER.info("Hello command test mod");
MinecraftForge.EVENT_BUS.register(this);
}
@SubscribeEvent
public void onRegisterCommand(RegisterCommandsEvent event) {
LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge")
.executes(context -> {
LOGGER.info("hoge command executed");
return Command.SINGLE_SUCCESS;
});
event.getDispatcher().register(builder);
}
}
では、さっそく動かしてみる。

お、ちゃんとh
を打った時点でhoge
コマンドが候補に出てきて認識しているようですね。hoge
まで打ってエンターをするとログにhoge command executed
が出てきたので動いている模様。簡単ですね!
とはいえ一つずつ解説。。
まずはコマンド登録。登録はForgeのイベントバスでRegisterCommandEvent
を拾って行います。上のコードの中ではonRegisterCommand
メソッドがイベントハンドラですね(イベントハンドリングについて、詳しくはこちらの過去記事参照)。この中でコマンドを組み立てます。この、コマンドの組み立て方が(私には。。)かなりトリッキーなのですが、とりあえず最初のCommand.literal("hoge")
がコマンド名だと思ってくださいませ。
続いてのexecutes
がコマンド実行時の処理本体です。ここにはCommandContent<CommandSource>
を引数に取るラムダを渡します。今回はhoge command executed
がログ出力されるだけですが、ここに好きな処理を書いてください。
最後に組み立てたコマンドの実態であるLiteralArgumentBuilder<CommandSource> builder
をevent.getDispatcher().register(builder);
で登録してあげればOKです。
コマンドの分岐
組込みのコマンドtime
などを使うとき、time
の後に時間の指定方法がいくつか選択肢として出てくるのを見たことがあると思います。こんな感じ。

time
コマンドは時間の指定方法として朝昼晩を指定したり時刻を指定したり時間を進める、といったことができますが、それらを第二引数以降で指定させることができます。このようなコマンドの分岐や引数の指定は前述のコマンドの組み立ての際にあれやこれやすることでできます。
さっそくサンプルコードを見てみましょう。
@SubscribeEvent
public void onRegisterCommand(RegisterCommandsEvent event) {
LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge")
.then(Commands.literal("fuga")
.executes(context -> {
LOGGER.info("hoge fuga command executed");
return Command.SINGLE_SUCCESS;
}))
.then(Commands.literal("piyo")
.executes(contect -> {
LOGGER.info("hoge piyo command executed");
return Command.SINGLE_SUCCESS;
}));
event.getDispatcher().register(builder);
}
onRegisterCommand
メソッドだけ書き換えてみました。ちょっと複雑になってきました。正直わかりにくいです。多分何とかパターンとかいうデザインパターンで作られてるんでしょうが普通にif-else
とかswitch
で書かせてくれた方が可読性も高いしいいと思うのだけど。。。と愚痴ってもしょうがないので解説。(最後にちょっとまとめて愚痴りますw)
then()
メソッドやexecute()
メソッドはLiteralArgumentBuilder<CommandSource>
のインスタンスを返してくれます。これに対してthen()
やらexecutes()
を数珠つなぎにしてコマンドの分岐の構造を定義していきます。上の例ではインデントを気を付けているのでまだ理解しやすいと思いますが、同列に並んでるthen
がコマンドの分岐になります。なので、hoge
コマンドの後の分岐としてfuga
とpiyo
が出てくると。試しに動かしてみます。

うまく動きました。hoge
を打った後スペースを一つ入れると分岐のfuga
とpiyo
が出てきますね。このどちらかを入力して実行するとサンプルコードにある通りのログが出力されます。fuga
やpiyo
の内側、一段ネストされた中にthen()
を並べればさらに複雑な分岐条件も作ることができます。ただ、ネストしまくるとどんどん分岐の構造がわかりにくく。。パーレンの対応関係とインデントに気を付けて設計してくださいませ。。一応一つだけ例を載せておきますね。fuga
の後を分岐させてみました。
@SubscribeEvent
public void onRegisterCommand(RegisterCommandsEvent event) {
LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge")
.then(Commands.literal("fuga")
.then(Commands.literal("hogehoge")
.executes(context -> {
LOGGER.info("hoge fuga hogehoge executed");
return Command.SINGLE_SUCCESS;
}))
.then(Commands.literal("piyopiyo")
.executes(context -> {
LOGGER.info("hoge fuga piyopiyo executed");
return Command.SINGLE_SUCCESS;
})))
.then(Commands.literal("piyo")
.executes(contect -> {
LOGGER.info("hoge piyo executed");
return Command.SINGLE_SUCCESS;
}));
event.getDispatcher().register(builder);
}
こんな感じです。実行してみると。

うまく行ったようですね。ちなみにこの程度のコードを書くだけでもパーレンの対応を何度か確認しながら書きました。。(おっと、また愚痴りそうに)
ちなみにこのfuga
とpiyo
、コマンドの引数やん!って思われると思います。いや、そうなんですけどね。ただ、Forge(マイクラ自体?)のコマンドではこのthen()
からCommands.literal()
で指定されたものと、この後で開設する引数(Argument)は明確に扱いが違うのであえて「分岐」と書いています。
コマンドの引数
続いてコマンドの引数です。コマンドに対して整数値や浮動小数点、文字列、カスタム型の引数を渡すことができます。まずはサンプルコード。
@SubscribeEvent
public void onRegisterCommand(RegisterCommandsEvent event) {
LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge")
.then(Commands.argument("argumentName", IntegerArgumentType.integer())
.executes(context -> {
return executeHoge(context);
}));
event.getDispatcher().register(builder);
}
public int executeHoge(CommandContext<CommandSource> context) {
int argumentInteger = IntegerArgumentType.getInteger(context, "argumentName");
LOGGER.info("hoge command executed: " + argumentInteger);
return Command.SINGLE_SUCCESS;
}
コードをだいぶすっきりさせました。まず分岐の説明のために複雑に構造化されていた部分はバッサリ消して、hoge
の後に整数の引数を一つとるようなコードに変えています。また、ラムダの中にごちゃごちゃ処理を書くとわけがわからなくなるのでexecuteHoge()
メソッドに切り出しました。実行してみます。

hoge
の後に数字を入力したところ何やら<argumentName>
なる表示が上に出るようになりました。そのままコマンドを実行してみます。
[01:38:13] [Server thread/INFO] [minecraft/MinecraftServer]: Saving chunks for level 'ServerLevel[New World]'/minecraft:overworld
[01:38:14] [Render thread/INFO] [minecraft/AdvancementList]: Loaded 0 advancements
[01:38:15] [Server thread/INFO] [minecraft/MinecraftServer]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_nether
[01:38:15] [Server thread/INFO] [minecraft/MinecraftServer]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_end
[01:38:15] [Server thread/DEBUG] [ne.mi.fm.FMLWorldPersistenceHook/WP]: Gathering id map for writing to world save New World
[01:38:15] [Server thread/DEBUG] [ne.mi.fm.FMLWorldPersistenceHook/WP]: ID Map collection complete New World
[01:38:22] [Server thread/INFO] [jp.mu.mo.co.CommandTest/]: hoge command executed: 1234
最後の行にログが出ていて、引数の整数がちゃんと受け取れているのがわかりますね。パチパチパチ。では解説。
引数を受け取る場合、then()
の中でCommands.argument()
メソッドを呼びます。このCommands.argument()
メソッドの第一引数(サンプルコードではargumentName
)は引数の名前を指定します。あとで引数の値を取り出すときのキーになります。
第二引数にはArgumentType
型のインスタンスを受け取りますが、このサンプルではIntegerArgumentType
を渡しています。IntegerArgumentType.integer()
はオーバーロードされていて、最小値や範囲を指定したIntegerArgumentType
を作って渡すことができます。
続いて引数の受け取り側、executeHoge()
の中を見てみます。ポイントはint argumentInteger = IntegerArgumentType.getInteger(context, "argumentName");
ですね。みりゃわかるけど。第一引数はCommandContext
、第二引数は先ほど指定した引数の名前です。これでコマンド実行時に指定した引数を受け取れる、というわけですな。
引数の後に前述の分岐を置くこともできます。分岐にはなってませんが、整数の引数の後に"fuga"
のliteralが来るようなコードを書いてみます。
@SubscribeEvent
public void onRegisterCommand(RegisterCommandsEvent event) {
LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge")
.then(Commands.argument("integerArgument", IntegerArgumentType.integer())
.then(Commands.literal("fuga")
.then(Commands.argument("stringArgument", StringArgumentType.string())
.executes(context -> {
return executeHoge(context);
}))));
event.getDispatcher().register(builder);
}
public int executeHoge(CommandContext<CommandSource> context) {
int integerArgument = IntegerArgumentType.getInteger(context, "integerArgument");
String stringArgument = StringArgumentType.getString(context, "stringArgument");
LOGGER.info("hoge command executed");
LOGGER.info("integerArgument : "+ integerArgument);
LOGGER.info("stringArgument : "+ stringArgument);
return Command.SINGLE_SUCCESS;
}
実行してみると。。。

途中の分岐である"fuga
"が表示されてますね。このまま入力を続けて最後の文字列引数に"abcd"
を入れてコマンドを実行してみると。。。
[13:48:32] [Server thread/DEBUG] [ne.mi.fm.FMLWorldPersistenceHook/WP]: Gathering id map for writing to world save New World
[13:48:32] [Server thread/DEBUG] [ne.mi.fm.FMLWorldPersistenceHook/WP]: ID Map collection complete New World
[13:51:10] [Server thread/INFO] [jp.mu.mo.co.CommandTest/]: hoge command executed
[13:51:10] [Server thread/INFO] [jp.mu.mo.co.CommandTest/]: integerArgument : 4321
[13:51:10] [Server thread/INFO] [jp.mu.mo.co.CommandTest/]: stringArgument : abcd
ちゃんと引数も受け取れてますね。
引数の型
Forge(マイクラ)ではいくつかの組み込みの型が用意されていて、大体のことはそれらを使えば事足りると思います。筆者も全部は全然使いこなせていないのですが、どこにそれらのクラスがあるのかだけご紹介
minecraftの組み込み引数
net.minecraft.command.arguments
パッケージ内に存在しています。ちょっと中身を眺めてみると。。。

XxxxxxArgument.class
と命名されているクラスが引数クラスのようですね。名前だけではぱっと見何に使うのか想像がつかないものもたくさんあります。。まぁ、いずれ。。
brigadierライブラリ
本家のMojangがMITライセンスで公開しているコマンドライブラリです。gitリポジトリはこちら。ただ、Mdkを使っていれば同梱されているので個別に落としてくる必要はないです。中身を少し眺めてみると。。。

上の方のサンプルでも使っていたStringArgumentType
やIntegerArgumentType
があることが確認できます。他にもBoolとかFloatとか。まだ使ったことないものもありますが、まぁ直感的に使えそうな感じですね。
権限
マイクラではコマンドを実行する権限を制御することができます。といっても、現時点で分かっているのはチートOn/Offで使える/使えないを切り替える、という使い方。どうやらもうちょっと色々できそうな感じですが、より詳細な使い方はわかってきたら加筆する、ということで。。
@SubscribeEvent
public void onRegisterCommand(RegisterCommandsEvent event) {
LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge")
.requires(context -> {
return true;
})
.executes(context -> {
return executeHoge(context);
});
event.getDispatcher().register(builder);
}
public int executeHoge(CommandContext<CommandSource> context) {
LOGGER.info("hoge command executed");
return Command.SINGLE_SUCCESS;
}
またコードをシンプルに戻しました。ポイントはCommand.literal("hoge")
の後のrequires()
ですね。このrequires()
はPredicate
を引数に取ります。つまり、ラムダでbool
を返せばよいと。で、このサンプルコードのようにfalse
を返すとそのコマンドは使えなくなる(マスクされる)というわけです。動かしてみましょう。

ほら、hoge
コマンドが出てこなくなった。もちろん、サンプルコードみたいに常にfalse
を返すような実装をすると一生使えないコマンドになるので、使わせたくないときだけfalse
を返せばいいわけです。
ちなみにこのコマンドのマスク、コマンドの分岐でも使えます。例えばこんなコード。
@SubscribeEvent
public void onRegisterCommand(RegisterCommandsEvent event) {
LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge")
.then(Commands.literal("fuga")
.requires(context -> {
return false;
})
.executes(context -> {
return executeHoge(context);
}))
.then(Commands.literal("piyo")
.executes(context -> {
return executeHoge(context);
}));
event.getDispatcher().register(builder);
}
public int executeHoge(CommandContext<CommandSource> context) {
LOGGER.info("hoge command executed");
return Command.SINGLE_SUCCESS;
}
"hoge"
の後の"fuga"
だけマスクしてみました。すると、

"fuga"
が選択肢から消えましたね。こんな使い方したいケースはあるかわからないですが、特定の条件下だけで分岐を許す、みたいなこともできそうですね。
でででで、お次は実用的(?)な使い方。権限によるマスクをやってみます。早速コード。。
@SubscribeEvent
public void onRegisterCommand(RegisterCommandsEvent event) {
LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge")
.requires(context -> {
return context.hasPermission(4);
})
.executes(context -> {
return executeHoge(context);
});
event.getDispatcher().register(builder);
}
public int executeHoge(CommandContext<CommandSource> context) {
LOGGER.info("hoge command executed");
return Command.SINGLE_SUCCESS;
}
今回のポイントはrequires()
の中のreturn context.hasPermission(4);
ですね。あとで解説しますが、これはチートか許可されているかどうかを判定するコードになっています。チートOn/Offで試してみると。。。
チートOff


チートがOnの時だけしか使えなくなってますね。で、解説。context.hasPermission(4);
は現在の権限(Permission)が引数(ここでは4)以上の場合だけtrue
を返します。つまり、チートがOnの時は権限が4以上になっているということですね。
「4以上」ということは権限が3や2や1になることもあるのかな。。と思ったんですが、試せた範囲だと以下のようになることがわかりました。
- チートOff : 0
- チートOn : 4
つまり0か4の2択。。。でも、CommandSourceクラスのソースを見ると
public boolean hasPermission(int p_197034_1_) {
return this.permissionLevel >= p_197034_1_;
}
となっているので、数字が大きくなるほど権限が多い、ということを示唆してますよね。。。現時点では0か4以外の権限になるケースがわかっていないので、この謎はまたわかった時に追記します。。
おしまい
コマンド編は一応こんなところです。完璧に使いこなそうと思うとMdkやbrigadierのソースを追わないとダメなケースも出てくると思いますが、この記事の基本が理解できていればきっとサクサクと作れるようになるでしょう!(ほんとかな)
最後に参考情報です。自作コマンドを作るうえではnet.minecraft.command.impl
の中に組み込みのコマンドのコードがいっぱい入っているのでこれらを参考にするとよいでしょう。後述の愚痴に書くように、なかなか読むのもおっくうになるような実装もありますが、まぁ頑張って読み解けば参考になる情報はいっぱい埋まってると思います。
愚痴
こっからはmodの開発に直接関係ないです。。が、今回調べててこの作りはひどくね?と思ったところがいくつかあったので愚痴っていこうと思います。。。Mdkもminecraftも私より優秀な人が書いているので的外れな指摘かもですが。。。
コマンドの引数、分岐をthen()
、requires()
でつないでいきますよね。これが一直線に並ぶならいいと思うんですが、分岐のための木構造をとるのでわけわからなくなるんですよね。例えばこのコード。
@SubscribeEvent
public void onRegisterCommand(RegisterCommandsEvent event) {
LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge")
.then(Commands.literal("piyo"))
.executes(context -> {
LOGGER.info("hoge piyo");
return Command.SINGLE_SUCCESS;
})
.then((Commands.literal("fuga"))
.executes(context -> {
LOGGER.info("hoge fuga");
return Command.SINGLE_SUCCESS;
}));
event.getDispatcher().register(builder);
}
正しそうに見えません?実は正しくないっす。実行すると

ありゃりゃ。ランタイムエラーになったっちゃったよ。。。トホホ
まず正解から。これが正解。
@SubscribeEvent
public void onRegisterCommand(RegisterCommandsEvent event) {
LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge")
.then(Commands.literal("piyo")
.executes(context -> {
LOGGER.info("hoge piyo");
return Command.SINGLE_SUCCESS;
}))
.then((Commands.literal("fuga"))
.executes(context -> {
LOGGER.info("hoge fuga");
return Command.SINGLE_SUCCESS;
}));
event.getDispatcher().register(builder);
}
どこが間違ってるかわかります?ここを修正したんです。
@SubscribeEvent
public void onRegisterCommand(RegisterCommandsEvent event) {
LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge")
.then(Commands.literal("piyo") // <- ここ!
.executes(context -> {
LOGGER.info("hoge piyo");
return Command.SINGLE_SUCCESS;
})) // <- とここ!
.then((Commands.literal("fuga"))
.executes(context -> {
LOGGER.info("hoge fuga");
return Command.SINGLE_SUCCESS;
}));
event.getDispatcher().register(builder);
}
最初の誤ってるコードと比較してみてください。パーレンの対応が間違ってたんですね。つまり、then()
でコマンドを分岐させる場合、分岐させるもう一つのthen()
の戻り値に対してthen()
してやる必要があるんですが、誤っていたコードでは最初のexecutes()
の戻り値に対してthen()
しちゃってるんですよね。
パーレンの対応一個間違っただけで動かないなんて。。。といっても「プログラムなんてそんなもんだろ!」という声も聞こえてきそうですが、この実装がイケてない理由は
コマンドを実行するまで分岐構造のミスに気付かない
先ほどの誤ったコード、コンパイルはおろかコマンドの登録までできちゃいます。で、誤りに気付くのはコマンド実行時。。しかも出力されるエラー、ログはクソ非常にわかりづらく、何が間違ってるのかわからんという。。
可読性低杉
ここに載せたサンプルコードはかなり意識してインデントしたのでまだ構造が読めると思います。ただ、IDEのインデントに任せてももっとごちゃごちゃしちゃって読めまへん。IntlliJだともっときれいにインデントしてくれんのかいな。。ちなみにさらに絶望したのは組み込みコマンドtime
の実装。まぁ見てよ。
public static void register(CommandDispatcher<CommandSource> p_198823_0_) {
p_198823_0_.register(Commands.literal("time").requires((p_198828_0_) -> {
return p_198828_0_.hasPermission(2);
}).then(Commands.literal("set").then(Commands.literal("day").executes((p_198832_0_) -> {
return setTime(p_198832_0_.getSource(), 1000);
})).then(Commands.literal("noon").executes((p_198825_0_) -> {
return setTime(p_198825_0_.getSource(), 6000);
})).then(Commands.literal("night").executes((p_198822_0_) -> {
return setTime(p_198822_0_.getSource(), 13000);
})).then(Commands.literal("midnight").executes((p_200563_0_) -> {
return setTime(p_200563_0_.getSource(), 18000);
})).then(Commands.argument("time", TimeArgument.time()).executes((p_200564_0_) -> {
return setTime(p_200564_0_.getSource(), IntegerArgumentType.getInteger(p_200564_0_, "time"));
}))).then(Commands.literal("add").then(Commands.argument("time", TimeArgument.time()).executes((p_198830_0_) -> {
return addTime(p_198830_0_.getSource(), IntegerArgumentType.getInteger(p_198830_0_, "time"));
}))).then(Commands.literal("query").then(Commands.literal("daytime").executes((p_198827_0_) -> {
return queryTime(p_198827_0_.getSource(), getDayTime(p_198827_0_.getSource().getLevel()));
})).then(Commands.literal("gametime").executes((p_198821_0_) -> {
return queryTime(p_198821_0_.getSource(), (int)(p_198821_0_.getSource().getLevel().getGameTime() % 2147483647L));
})).then(Commands.literal("day").executes((p_198831_0_) -> {
return queryTime(p_198831_0_.getSource(), (int)(p_198831_0_.getSource().getLevel().getDayTime() / 24000L % 2147483647L));
}))));
}
これで分岐の木構造が頭の中にコンパイルされた人は病気ですすごいです。なんかのデザインパターンを使ってるのかもしれないですが、ここまでくると乱用じゃね?可読性やらデバッグの容易性を考えたら愚直にif/else
を並べて構造がそのままわかるような実装方法にすりゃいいと思うんですがね。
引数指定誤りのミスに気づけない
次はこんなコード。文字列の引数をとってそれをログに出力するという本編にも書いたようなコードです。
@SubscribeEvent
public void onRegisterCommand(RegisterCommandsEvent event) {
LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge")
.then(Commands.argument("stringArgument", StringArgumentType.string())
.executes(context -> {
return executeHoge(context);
}));
event.getDispatcher().register(builder);
}
public int executeHoge(CommandContext<CommandSource> context) {
String stringArgument = StringArgumentType.getString(context, "stringArgment");
LOGGER.info("hoge command executed: " + stringArgument);
return Command.SINGLE_SUCCESS;
}
コマンドを実行してみると。

でました、ランタイムエラー。例によって出力されるエラー、ログはクソ非常にわかりづらくって何がお起こってるのかわからねぇ。このコード、何が間違っていたかというと、executeHoge()
内で引数を取得する時の引数のキーstringArgument
の綴りが誤ってるところ。
キーをString STRING_ARGUMENT = "stringArgument"
こんな感じで定数にして、引数定義と参照のところで使えばいいじゃん、という声が聞こえてきそうですが。。。これの何がイケてないかというと
そもそも2か所で同じキーを使わせるところ
上に書いた定数を使う方法は、誤りを回避する方法であって、そもそも2か所で同じキーを使わせないような作りにしてくれればいいのに、と思うわけですよ。例えば、String stringArgument = context.requireArgument("stringArgument")
みたいに引数定義と同時にその値がとれてしまえばいいわけじゃないかなぁと思うわけですわ。
原因が分からなさ杉
今回の誤りの裏で何が起こっていたのか。デバッガを使って調べたところ実はIllegalArgumentException
がthrowされてます。で、minecraftの実装が見事にその例外をもみ消してクソみたいな分かりにくいエラーだけ出してくれてます。余計なことをせずにログにStackTraceを出してくれりゃいいのに。。もしくは、getString()
が存在しないキーを指定された場合はnullを返すような作りになってた方が未だなんぼかわかりやすいと思うんだけどねぇ。。
で、最後に思ったこと
全ての原因は、マイクラが起動時のイベントでコマンドの構造をすべて組み立てて事前登録させる、という設計になっている点なのかなぁと思いました。なのでthen()
の数珠つなぎで木構造を作らせたり、引数のキーを引数定義のところと実行時のラムダの2か所で参照させざるを得なかったとか。。
コマンドの構造を事前登録させたかった理由まではわかりませんが、だったとしてももうちょっとマシにできたんじゃないのかなぁとか思ったり。