Bukkitプラグイン開発 - コマンド処理、最適化編

# 最初に

- **この記事は応用で、メインクラスに何でもまとめてしまうのを避けるのがねらいです**。
- コマンド処理の前提知識はあると仮定し、記事も長くなるので色々端折ってます。
 - `plugin.yml` の記載も省いてます（既に書いてある体）。

この記事を書いた当時の環境は

- Spigot-API
- Java 11（コンパイルは8）
- APIバージョン 1.16（`api-version`では未指定）
- Apache Maven

です。当然ながらBukkit、Paperでも応用できますし、固有の機能などは使用しません。
Java 8やAPIバージョンが多少古くても新しくても大半は動くと思いますので、まずは試して下さい。

## ソースコードの扱いについて

この記事に書かれたソースコードを、パブリックドメインとするので自由にお使い下さい。
サポートは一切受け付けない。
<!--
著作権や記事へのリンクを求めたりしません。その代わり、コメントなどでのサポートは一切受け付けません。

今後「Bukkitプラグイン開発」をシリーズ化する予定ですが、`BaseCommand`を含め独自クラスが採用されている予定です。
-->

## 資料

- [Creating a Simple Command | SpigotMC - High Performance Minecraft](https://www.spigotmc.org/wiki/create-a-simple-command/)
- [コマンド - Plugin Tutorial - Minecraft Modding Wiki](https://mcmodding.jp/modding/index.php/Plugin_Tutorial#.E3.82.B3.E3.83.9E.E3.83.B3.E3.83.89)
- [\[MineCraftプラグイン\]コマンドを受け付けてイベントを発生させる - Qiita](/kubo_programmer/items/52613ffbc5eb3c5587ef)

# 肥大化は必ず来る

コマンドの受け取りイベントならメインクラスの`onCommand()`で実装しても良いですが、それはケースによるでしょう。

コマンドが複数になるとコードも膨大になります。
`if`文の嵐にしたくないなら1コマンド1クラスで分けちゃった方が良いこともあります。

# 親となる抽象クラスを書く

1コマンド1クラスと言っても似たような処理をクラス毎に書くのは出来るなら避けたいところです。

そこで便利な抽象クラスを先に書いておく。
これをベースにして、子クラス側では`onCommand()`を処理する感じです。
パッケージも`***.***.command`みたいに分けておきます。

```java:command/BaseCommand.java
package com.stsynthe.bukkit.how2.command;

import org.bukkit.command.CommandExecutor;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabCompleter;

import com.stsynthe.bukkit.how2.HowTo;

public abstract class BaseCommand implements CommandExecutor {

	private static HowTo PLUGIN = null;// 直接扱わず、メソッド ***Plugin() で介す

	public BaseCommand(HowTo plugin) {
		if (this.getPlugin() == null)
			this.setPlugin(plugin);
		if (this.getInstance() == null)
			throw new NullPointerException("Instance is null");
		if (this.getCommandName() == null)
			throw new NullPointerException("CommandName is null");

		this.register();
	}

	/**
	 * プラグインのインスタンスをゲット
	 * 
	 */
	final HowTo getPlugin() {
		return BaseCommand.PLUGIN;
	}

	/**
	 * プラグインのインスタンスをセットする
	 * 
	 * @param plugin
	 */
	final void setPlugin(HowTo plugin) {
		if (plugin == null)
			throw new IllegalArgumentException("Argument \"plugin\" is null");
		BaseCommand.PLUGIN = plugin;
	}

	/**
	 * CommandExecutor と TabCompleter の登録
	 * 
	 */
	public void register() {
		PluginCommand c = this.getPlugin().getCommand(this.getCommandName());
		if (c != null) {
			c.setExecutor(this.getInstance());
			if (this.getInstance() instanceof TabCompleter)
				c.setTabCompleter((TabCompleter) this.getInstance());
		}
	}

	/**
	 * 自身のインスタンスを返す
	 * 
	 */
	abstract BaseCommand getInstance();

	/**
	 * コマンド名をゲットする
	 * 
	 * @return コマンド名を返す
	 */
	public abstract String getCommandName();
}
```

とりあえずベースは最低限と言うことでここまで。内訳は割愛します。

試しにコマンド「test」を実装する。
同パッケージ内にクラス`Test`を作成。親（スーパー）クラスに`BaseCommand`を指定する。

実装内容は

- コマンド「test」を実装
- プレイヤーチャットで`/test`を送信すると「コマンドを実行しました！」というメッセージを送信、表示させる
- 結果にかかわらず、コンソールに「コマンドを実行」のメッセージを表示する
 - プレイヤーチャット以外（コンソール）からはコマンド実行結果を失敗（偽）で返す

```java:command/Test.java
package com.stsynthe.bukkit.how2.command;

import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;

import com.stsynthe.bukkit.how2.HowTo;

final public class Test extends BaseCommand {

	public Test(HowTo plugin) {
		super(plugin);
	}

	@Override
	public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
		boolean result = false;

		if (sender instanceof Player) {
			Player player = (Player) sender;
			player.sendMessage("コマンドを実行しました！");
			result = true;
		}

		this.getPlugin().getLogger().info("コマンドを実行");

		return result;
	}

	@Override
	BaseCommand getInstance() {
		return this;
	}

	@Override
	public String getCommandName() {
		return "test";
	}

}

```

親を抽象クラスにしてあるので以上4つのメソッドは必ず実装しておく。

- コンストラクタは親のコンストラクタを先に実行しておきます。
 - 受け取った`plugin`は子クラスで保持しない。既に`this.getPlugin()`で使えます。
- `getInstance()`メソッドも自身のインスタンスを返すだけなので`return this;`で固定です。
- `getCommandName()`メソッドは今回コマンド「test」を実装するので`test`を返してやります。

後は`onCommand()`の内容をゴリゴリ書いていくだけです。

# メインクラスでインスタンスを生成し、登録実装してもらう

```java:HowTo.java
public class HowTo extends JavaPlugin {

	@Override
	public void onEnable() {
		new Test(this);
	}

}
```

もう少し色々やった方が良いですが、最低限だとこれだけで実装できます。

----

`BaseCommand`でメインクラス（上記例では`HowTo`）をスコープさせるように成ってるので、わざわざ子クラス毎でインスタンスを保持しなくて良いようになってます。

また、`BaseCommand`はコモンにもなれるので共通のコードがあればそこに書いておけば使い回しが出来ます。あとは煮るなり焼くなりどうぞ。`plugin.yml`を省いてあるので**そちらの追記を忘れずに**。

# 最後に

この時期の[Qiita Advent Calendar](/advent-calendar)は「[本番環境でやらかしちゃった人](/advent-calendar/2020/yarakashi-production)」をよく見てたりしますが参加は初めてです。後で見返すとニッチな記事なので少し場違いな気もしたりしなかったり。
以前だと[Minecraft Advent Calendar 2017](/advent-calendar/2017/minecraft)が有ったのですが近年では見かけなくなりました。

Bukkitにかからず、新型ForgeやFabricの一応のドキュメントは有るし、基礎的な記事はいくらでも出てきますが、「はい、基礎はここまで。応用は各自でねー。」なのが多すぎてお腹いっぱいです。自分もそうですけどJavaの事をあまり知らないから応用に踏み込まなかったり、小技的な記事が少ないのかなとも思ったりしてます。

# おまけ：タブ補完`TabCompleter`の実装

コマンドのタブ補完になるとQiita内で触れている記事は「[CraftBukkit・Spigotプラグインを作るときの小技のような何か - Qiita](/rushuu_r/items/677bf24db821838a7569)」ぐらいでした。
今までの流れで`TabCompleter`も実装してみましょう。

## 追加するのは子クラスだけ！

お気づきかと思いますが、抽象クラス`BaseCommand`の子クラスに`TabCompleter`が実装されていると`setTabCompleter()`を実行してタブ補完を登録してくれます。タブ補完はオプショナルなので`BaseCommand`には実装してません。

実装内容は

- コマンド「check」の実装
- タブ補完では1つ目の引数に「one」「two」「three」のリストを返す
- コマンドを発信したのがプレイヤーならチャットに「コマンドは（プレイヤー名）が実行」を送信
 - 引数「three」が指定されるとアイテム「石」を3個インベントリに入れる
- 返り値は実行結果に関わらず`true`とする

```java:command/Check.java
package com.stsynthe.bukkit.how2.command;

import java.util.Arrays;
import java.util.List;

import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;

import com.stsynthe.bukkit.how2.HowTo;

final public class Check extends BaseCommand implements TabCompleter {
	
	private String[] completeList = new String[] { "one", "two", "three" };

	public Check(HowTo plugin) {
		super(plugin);
	}

	@Override
	public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
		if (sender instanceof Player) {
			Player player = (Player) sender;

			if (args.length == 1 && args[0].equals(this.completeList[2]))
				player.getInventory().addItem(new ItemStack[] {
					new ItemStack(Material.STONE, 3)// 引数が three なら 石3個を得る
				});

			player.sendMessage("コマンドは" + player.getDisplayName() + "が実行");
		}

		return true;// 成功で返す
	}

	@Override
	public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
		if (args.length == 1)
			return Arrays.asList(this.completeList);

		return null;// 1つ目以外の引数には null で返す
	}

	@Override
	BaseCommand getInstance() {
		return this;
	}

	@Override
	public String getCommandName() {
		return "check";
	}

}
```

後はメインクラスに

```java:HowTo.java
public class HowTo extends JavaPlugin {

	@Override
	public void onEnable() {
		new Test(this);
		new Check(this);// 追加部分
	}

}
```

を、足して動作確認しましょう。