LoginSignup
2

More than 1 year has passed since last update.

posted at

updated at

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

最初に

  • この記事は応用で、メインクラスに何でもまとめてしまうのを避けるのがねらいです
  • コマンド処理の前提知識はあると仮定し、記事も長くなるので色々端折ってます。
    • 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の事をあまり知らないから応用に踏み込まなかったり、小技的な記事が少ないのかなとも思ったりしてます。

コードの扱いについて

抽象クラスBaseCommandを含め、全てのコードは自由にお使い下さい。
著作権や記事へのリンクを求めたりしません。その代わり、コメントなどでのサポートは一切受け付けません。

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

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

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

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

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

実装内容は

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

}

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

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
What you can do with signing up
2