1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Eclipse+Forgeでマイクラのmod開発~ 自作コマンドを実装してみる

Last updated at Posted at 2021-06-08

image.png
今回は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

まずは動かしてみる

やはりまずは簡単なコードを動かしてみるのがわかりやすいでしょう、ということで動かしてみます。

CommandTest.java
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> builderevent.getDispatcher().register(builder);で登録してあげればOKです。

コマンドの分岐

組込みのコマンドtimeなどを使うとき、timeの後に時間の指定方法がいくつか選択肢として出てくるのを見たことがあると思います。こんな感じ。

timeコマンドは時間の指定方法として朝昼晩を指定したり時刻を指定したり時間を進める、といったことができますが、それらを第二引数以降で指定させることができます。このようなコマンドの分岐や引数の指定は前述のコマンドの組み立ての際にあれやこれやすることでできます。

さっそくサンプルコードを見てみましょう。

CommandTest.java
	@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コマンドの後の分岐としてfugapiyoが出てくると。試しに動かしてみます。

うまく動きました。hogeを打った後スペースを一つ入れると分岐のfugapiyoが出てきますね。このどちらかを入力して実行するとサンプルコードにある通りのログが出力されます。fugapiyoの内側、一段ネストされた中にthen()を並べればさらに複雑な分岐条件も作ることができます。ただ、ネストしまくるとどんどん分岐の構造がわかりにくく。。パーレンの対応関係とインデントに気を付けて設計してくださいませ。。一応一つだけ例を載せておきますね。fugaの後を分岐させてみました。

CommandTest.java
	@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);
	}

こんな感じです。実行してみると。

うまく行ったようですね。ちなみにこの程度のコードを書くだけでもパーレンの対応を何度か確認しながら書きました。。(おっと、また愚痴りそうに)

ちなみにこのfugapiyo、コマンドの引数やん!って思われると思います。いや、そうなんですけどね。ただ、Forge(マイクラ自体?)のコマンドではこのthen()からCommands.literal()で指定されたものと、この後で開設する引数(Argument)は明確に扱いが違うのであえて「分岐」と書いています。

コマンドの引数

続いてコマンドの引数です。コマンドに対して整数値や浮動小数点、文字列、カスタム型の引数を渡すことができます。まずはサンプルコード。

CommandTest.java
	@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が来るようなコードを書いてみます。

CommandTest.java
	@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を使っていれば同梱されているので個別に落としてくる必要はないです。中身を少し眺めてみると。。。

上の方のサンプルでも使っていたStringArgumentTypeIntegerArgumentTypeがあることが確認できます。他にもBoolとかFloatとか。まだ使ったことないものもありますが、まぁ直感的に使えそうな感じですね。

権限

マイクラではコマンドを実行する権限を制御することができます。といっても、現時点で分かっているのはチートOn/Offで使える/使えないを切り替える、という使い方。どうやらもうちょっと色々できそうな感じですが、より詳細な使い方はわかってきたら加筆する、ということで。。

CommandTest.java
	@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を返せばいいわけです。

ちなみにこのコマンドのマスク、コマンドの分岐でも使えます。例えばこんなコード。

CommandTest.java
	@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"が選択肢から消えましたね。こんな使い方したいケースはあるかわからないですが、特定の条件下だけで分岐を許す、みたいなこともできそうですね。

でででで、お次は実用的(?)な使い方。権限によるマスクをやってみます。早速コード。。

CommandTest.java
	@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

チートがOnの時だけしか使えなくなってますね。で、解説。context.hasPermission(4);は現在の権限(Permission)が引数(ここでは4)以上の場合だけtrueを返します。つまり、チートがOnの時は権限が4以上になっているということですね。

「4以上」ということは権限が3や2や1になることもあるのかな。。と思ったんですが、試せた範囲だと以下のようになることがわかりました。

  • チートOff : 0
  • チートOn : 4

つまり0か4の2択。。。でも、CommandSourceクラスのソースを見ると

CommandSource.java
   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()でつないでいきますよね。これが一直線に並ぶならいいと思うんですが、分岐のための木構造をとるのでわけわからなくなるんですよね。例えばこのコード。

CommandTest.java
	@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);
	}

正しそうに見えません?実は正しくないっす。実行すると

ありゃりゃ。ランタイムエラーになったっちゃったよ。。。トホホ

まず正解から。これが正解。

CommandTest.java
	@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);
	}

どこが間違ってるかわかります?ここを修正したんです。

CommandTest.java
	@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の実装。まぁ見てよ。

TimeCommand.class
   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を並べて構造がそのままわかるような実装方法にすりゃいいと思うんですがね。

引数指定誤りのミスに気づけない

次はこんなコード。文字列の引数をとってそれをログに出力するという本編にも書いたようなコードです。

CommandTest.java
	@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か所で参照させざるを得なかったとか。。
コマンドの構造を事前登録させたかった理由まではわかりませんが、だったとしてももうちょっとマシにできたんじゃないのかなぁとか思ったり。

1
1
1

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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?