5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaAdvent Calendar 2020

Day 10

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

Last updated at Posted at 2020-12-09

最初に

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

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

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

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

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

この記事に書かれたソースコードを、パブリックドメインとするので自由にお使い下さい。
サポートは一切受け付けない。

資料

肥大化は必ず来る

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

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

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

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

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

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を送信すると「コマンドを実行しました!」というメッセージを送信、表示させる
  • 結果にかかわらず、コンソールに「コマンドを実行」のメッセージを表示する
  • プレイヤーチャット以外(コンソール)からはコマンド実行結果を失敗(偽)で返す
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()の内容をゴリゴリ書いていくだけです。

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

HowTo.java
public class HowTo extends JavaPlugin {

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

}

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


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

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

最後に

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

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

おまけ:タブ補完TabCompleterの実装

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

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

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

実装内容は

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

}

後はメインクラスに

HowTo.java
public class HowTo extends JavaPlugin {

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

}

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

5
3
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
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?