マインクラフトver1.21.10のPaperプラグインを作る方法を解説します。

第4回はPaperプラグインにおけるコマンド処理について説明します。
コマンドとは
プラグインサーバーでは、あなたが考えたオリジナルのコマンドを登録し、独自の処理を追加することができます。
まずは、プラグインサーバーにおけるコマンドがどのような流れで処理されるかどうかを確認しましょう。
基本的に、プラグインはAPIを介してサーバーにコマンドを登録し、なおかつコマンドが実行された時の処理を登録し委託する形となります。
①プラグインがコマンド名やコマンド処理をAPIを通じて登録します。
②APIがその委託に応じて、サーバーにコマンド登録処理を行います。
③サーバーがプレイヤーの操作に応じてコマンドリストを送信し、プレイヤーに使用可能なコマンドを通知します。
④プレイヤーは与えられたコマンドのうち使用可能なものを送信します。
⑤サーバーは登録された処理を実行します。
以下、簡単なプラグインさえできればよいという人はパターンA、汎用的な手法が知りたいという人はパターンBを見て学習してください。
パターンA: CommandExecutorを使う
こちらはコマンドを実装するのに以前から利用されてきた、基本的な方法です。いわゆるBukkit Commandと呼ばれるものです。
具体例として、/hatで手に持っているアイテムを頭に装備するようにしてみます。
★ToDo com.example.papertutorialパッケージ下に、commandパッケージを作成

★ToDo com.example.papertutorial.commandパッケージ下に、HatCommandクラスを作成

★ToDo 以下のようにHatCommandクラスを編集
package com.example.papertutorial.command;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
public class HatCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String @NotNull [] strings) {
if(!(commandSender instanceof Player player)) return false;
var inv = player.getInventory();
var head = inv.getHelmet();
var hand = inv.getItemInMainHand();
inv.setHelmet(hand);
inv.setItemInMainHand(head);
player.sendMessage("頭の装備と手に持っているアイテムを入れ替えました。");
return true;
}
}
- onCommand関数の詳細
- 引数commandSender: コマンドの送信者(プレイヤー、エンティティ、サーバーのコンソールなどになり得る)
- 引数command: 送信されたコマンドの情報
- 引数s: 実際に送信されたコマンド名(エイリアス)
- 引数strings: 送信されたコマンドの引数配列(引数なしならば長さ0)
- 変数inv: プレイヤーのインベントリ
- 変数head: プレイヤーの頭に装備されているアイテム
- 変数hand: プレイヤーが手に持っているアイテム
★ToDo 以下のようにPaperTutorialクラスを編集
package com.example.papertutorial;
import com.example.papertutorial.command.HatCommand;
import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.java.JavaPlugin;
public final class PaperTutorial extends JavaPlugin {
@Override
public void onEnable() {
// Plugin startup logic
// ここから
PluginCommand hatCommand = getCommand("hat");
hatCommand.setExecutor(new HatCommand());
// ここまで
}
@Override
public void onDisable() {
// Plugin shutdown logic
}
}
★ToDo 以下のように src/main/resources/plugin.yml を編集
name: PaperTutorial
version: '1.0-SNAPSHOT'
main: com.example.papertutorial.PaperTutorial
api-version: '1.21.10'
commands:
hat:
description: "手に持っているアイテムを頭の装備と交換するコマンドです。"
usage: "/hat"
aliases: [h]
permission: papertutorial.command.hat
permission-message: "このコマンドを実行する権限を持っていません。"
permissions:
papertutorial.command.hat:
description: "hatコマンドを実行する権限です。"
default: true
★ToDo Gradleプロジェクトのjarタスクを実行

★ToDo jarタスクで生成された build/libs/PaperTutorial-1.0-SNAPSHOT.jar を、サーバーの plugins フォルダに配置する
★ToDo サーバーを起動
★確認 以下の動作を確認してください。
- サーバーの起動時、ワールドのロード終了後に
[XX:XX:XX INFO]: [PaperTutorial] Enabling PaperTutorial v1.0‑SNAPSHOT
というログが表示される - プレイヤーはop権限の有無にかかわらず、/hatコマンドが利用できる
- /hatコマンドを使用すると、使用者の頭の装備と手に持っているアイテムが交換される
- そのとき、「頭の装備と手に持っているアイテムを入れ替えました。」と表示される
- plugin.ymlで設定した/hコマンドがそのエイリアスであり、同じ動作をするコマンドとして利用できる
★ToDo サーバーを停止
CommandExecutorを使ったコマンド処理の内容は、
- CommandExecutorを実装するクラスAを作成する
- onCommand関数にコマンド処理を実装する
- プラグインの有効化(onEnable関数)の際に、クラスAのインスタンスを登録する
- plugin.ymlにコマンドの情報と権限を設定する
パターンB: BrigadierコマンドAPIを使う
こちらはPaper1.20.6からPaper APIで利用可能になった、汎用的で強力なコマンドAPIです。CommandExecutorを使うものより見た目は少々複雑ですが、こちらを使うことを推奨します。
公式による解説はこちら↓

具体例として、/landmarkというコマンドを実装して、保存した地点にいつでもテレポートできるようにしてみます。
★ToDo com.example.papertutorial.commandパッケージ下に、LandmarkCommandクラスを作成
★ToDo 以下のようにLandmarkCommandクラスを編集
package com.example.papertutorial.command;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.command.brigadier.Commands;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import java.util.Map;
import java.util.TreeMap;
public class LandmarkCommand {
private static final Map<String, Location> landmarks = new TreeMap<>();
public static final LiteralArgumentBuilder<CommandSourceStack> builder = Commands.literal("landmark")
.then(Commands.literal("set")
.then(Commands.argument("name", StringArgumentType.string())
.requires(source -> source.getExecutor() instanceof Player)
.executes(LandmarkCommand::set)
)
).then(Commands.literal("list")
.executes(LandmarkCommand::list)
).then(Commands.literal("tp")
.then(Commands.argument("landmark", StringArgumentType.string())
.suggests((ctx, builder) -> {
landmarks.keySet().forEach(builder::suggest);
return builder.buildFuture();
})
.requires(source -> source.getExecutor() instanceof Player)
.executes(LandmarkCommand::tp)
)
).then(Commands.literal("remove")
.then(Commands.argument("landmark", StringArgumentType.string())
.suggests((ctx, builder) -> {
landmarks.keySet().forEach(builder::suggest);
return builder.buildFuture();
})
.executes(LandmarkCommand::remove)
)
);
private static int set(CommandContext<CommandSourceStack> ctx){
String name = StringArgumentType.getString(ctx, "name");
Player player = (Player) ctx.getSource().getExecutor();
landmarks.put(name, player.getLocation());
player.sendMessage(String.format("ランドマーク%sを現在地に設定しました。", name));
return Command.SINGLE_SUCCESS;
}
private static int list(CommandContext<CommandSourceStack> ctx){
var sender = ctx.getSource().getSender();
for(var entry : landmarks.entrySet()){
String name = entry.getKey();
var loc = entry.getValue();
sender.sendMessage(String.format("%s (%.2f, %.2f, %.2f)", name, loc.x(), loc.y(), loc.z()));
}
return Command.SINGLE_SUCCESS;
}
private static int tp(CommandContext<CommandSourceStack> ctx) {
String landmark = StringArgumentType.getString(ctx, "landmark");
Player player = (Player) ctx.getSource().getExecutor();
if(landmarks.containsKey(landmark)){
var loc = landmarks.get(landmark);
player.teleport(loc);
player.sendMessage(String.format("ランドマーク%sにテレポートしました。", landmark));
return Command.SINGLE_SUCCESS;
} else {
player.sendMessage(String.format("%sは登録されていないランドマーク名です。", landmark));
return 0;
}
}
private static int remove(CommandContext<CommandSourceStack> ctx) {
String landmark = StringArgumentType.getString(ctx, "landmark");
var sender = ctx.getSource().getSender();
if(landmarks.containsKey(landmark)){
landmarks.remove(landmark);
sender.sendMessage(String.format("ランドマーク%sを削除しました。", landmark));
return Command.SINGLE_SUCCESS;
} else {
sender.sendMessage(String.format("%sは登録されていないランドマーク名です。", landmark));
return 0;
}
}
}
★ToDo PaperTutorialクラスのonEnable関数に以下のコードを追加
this.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, commands -> {
commands.registrar().register(LandmarkCommand.builder.build());
});
★ToDo Gradleプロジェクトのjarタスクを実行
★ToDo jarタスクで生成された build/libs/PaperTutorial-1.0-SNAPSHOT.jar を、サーバーの plugins フォルダに配置する
★ToDo サーバーを起動
★確認 試しに、hogeという名前のランドマークを作成します。以下の動作を確認してください。
- プレイヤーはop権限の有無にかかわらず、/landmarkコマンドが利用できる
- /landmark set hogeと入力すると、
ランドマークhogeを現在地に設定しました。
と表示される - /landmark listと入力すると、
hoge (x座標, y座標, z座標)
と表示される - 別の場所に移動し、/landmark tp hogeと入力すると、ランドマークhogeを作成した地点にテレポートし、
ランドマークhogeにテレポートしました。
と表示される - /landmark remove hogeと入力すると、
ランドマークhogeを削除しました。
と表示される - 再び/landmark listを実行すると、何も表示されない
★確認 また、コマンドの条件について確認します。以下の動作を確認してください。
- サーバーコンソールから、/landmark set fugaを実行することはできない
- サーバーコンソールから、/landmark listを実行することができる
- サーバーコンソールから、/landmark tp fugaを実行することはできない
- サーバーコンソールから、/landmark remove fugaを実行することができる
★ToDo サーバーを停止
BrigadierコマンドAPIによるコマンド処理の内容は、
- LiteralArgumentBuilder<CommandSourceStack>(以下、ビルダー)のインスタンスを作成する
- Commands::literal関数でコマンドの文字列要素を表す
- Commands:argument関数で、コマンドの引数を表す
- 文字列、数値、座標、エンティティ、ユーザー定義の引数など
- ビルダーのthen関数で、サブコマンドをチェーンする
- ビルダーのsuggests関数で、サブコマンドのTab補完を与える
- ビルダーのrequires関数で、サブコマンドを実行する条件を与える
- ビルダーのexecutes関数で、そのサブコマンドが実行された時の処理を与える
- プラグインの有効化(onEnable関数)の際に、ビルダーのビルド結果を登録する
まとめ
今回は、Paperプラグインでオリジナルのコマンドを実装する方法を学習しました。それには代表的な2つの手法があり、
- CommandExecutorを使った、昔から使われている手法
- Brigadierコマンドシステムを使った、新しく汎用的な手法
でした。それぞれ、作りたいプラグインの目的に応じて選択してください。

コメント