4
2

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.

高品質なPMMPプラグインを作るコツ

Last updated at Posted at 2020-07-04

はじめに

PMMP(Poggit)が制定している、PQRS (Plugin Quality Requirement Standards)と呼ばれるプラグイン品質の要件標準があります。
これは、それを翻訳して一般的なプラグイン向けにまとめて解説しているものです。

※移動や変更を繰り返したのちPoggitのSubmission Rulesとなっていますが、今でも実質的にPMMPプラグインのPQRSと言えます。

注意: この記事では、RFC 2119 や RFC 6919 に則って要件レベルを表しているわけではありません。

冗長なAPIバージョン

プラグインは、plugin.ymlに冗長なAPIバージョンを指定してはいけません。
各メジャーバージョンでサポートされている最も古いAPIバージョンのみを指定する必要があります。
例えば、「3.1.0」が既に指定されている場合、「3.2.0」は不要です。

  • 悪い例
plugin.yml
api:
  - 3.1.0
  - 3.2.0
  • 良い例
plugin.yml
api:
  - 3.1.0

まず前提として、plugin.ymlのapiディレクティブは以下のような仕様になっています。

サーバー側\指定 3.0.0 3.1.0 3.4.5 3.5.0 3.8.0 3.12.0 3.13.0 4.0.0
3.0.0 ×
3.4.4 × × ×
3.4.6 × × × ×
3.5.0 × × × ×
3.12.0 × × × × × ×

これが何故かというと、現在のPMMPではセマンティック バージョニングを採用しているからです。
この方式は、メジャー.マイナー.パッチとしてバージョニングする決まりです。
要約すると以下の通りです。

  • 後方互換性の無い変更の場合: メジャーバージョンが上がります(例: 3.1.0 => 4.0.0)
  • 後方互換性のある変更(主に機能追加)の場合: マイナーバージョンが上がります(例: 3.1.0 => 3.2.0)
  • 後方互換性のある変更(主にバグ修正)の場合: パッチバージョンが上がります(例: 3.1.0 => 3.1.1)

このように、メジャーバージョンの変更以外では後方互換性があるため、複数指定する必要はないわけです。
前方互換性はもちろんないので、とりあえず「3.0.0」と指定したり、現在の最新バージョンなどを指定するのはあまり適切ではありません。「3.1.0」で追加された機能を使用したい場合は「3.1.0」と指定し、「3.12.0」で追加された機能を使用したい場合は「3.12.0」と指定する必要があります。

APIバージョンには、サポートされている最も古いAPIバージョンを指定する必要があります。
最低限要求されるAPIバージョンにすることで、幅広いバージョンで動作させることが出来ます。

PMMPのソースコードを注意深く読んでいると、関数が@ deprecatedになっていたりしますが、その関数が削除されないで残っている(その関数の内部で新規追加された関数などをラップして呼び出していることが多い)のはこの後方互換性を維持するためです。
(例えば、addTitle関数がsendTitle関数に変更されても、addTitle関数は非推奨なものの、引き続き同じように使用できる)

鋭い方は、「APIバージョンを複数指定できる意味はあるのか?」と感じるかもしれません。
これは、APIバージョンがALPHAである場合などに使用されます。
(これはよくある例であって、メジャーバージョンにも同じことが言えます)

3.0.0-ALPHA1や2などのAPIバージョンを見かけたことはあるでしょうか。
このバージョンでは、毎回APIの後方互換性が失われます。(毎回メジャーバージョンが上がるようなイメージです)
そのため、「3.0.0-ALPHA5」を指定したら「3.0.0-ALPHA6」でも動くといったシステムにはできません。
つまり、ALPHAバージョンが変わるたびに使っている機能に変更がないか確認する必要があります。

plugin.yml
api:
  - 3.0.0-ALPHA1
  - 3.0.0-ALPHA2
  - 3.0.0-ALPHA3
  - 3.0.0-ALPHA4

このときに複数指定する必要が出てくるわけです。
すこし話がずれますが、まれに「3.0.0」と「4.0.0」を両方指定することがあります。
この行為自体は悪いことではないのですが、自分が使っている機能が「3.0.0」でも「4.0.0」でも使用できることをしっかり確認するようにしてみてください。

起動/停止での無駄なメッセージ

プラグインは、起動/停止に本当に時間がかかる場合(概ね1秒程度)を除いて、「有効になりました」や「製作者: xxx」などの不要なメッセージを出力してはいけません。

これは、コンソールを無駄に汚さないための配慮です。
例えば、一般的なメッセージで「赤や黄や青」などを使用したとします。
するとユーザーは本当のエラーや警告に気付くことができなくなってしまいます。

コンソールを注意深く見ていると、プラグインが有効になったとき、PMMPによって「[00:00:00] [Server Thread/INFO]: <プラグイン名> を有効にしています」などが自動的に出力されていることが分かります。
それを再度プラグインから出力する必要があるか考えてみてください。

では、プラグインを有効にしていることではなく、プラグインが有効になった(有効化が完了した)ことをユーザーに伝えたいかもしれません。
例えば虹色と複数行のASCIIアートなどで派手なメッセージを作成する必要がありますか?

派手なメッセージを作成することによって、プラグインの素晴らしさを伝えたいかもしれません。
しかし、サーバー上のほかのすべてのプラグインも同じことをしました。

ロガーには標準で色が付いています。(例えば、NOTICEは青、WARNINGは黄色、ERRORは赤など)

さらに詳細を知りたい方はこちらのスレッドも確認してみてください。

依存関係を宣言

プラグインは、plugin.ymlで依存関係を宣言します。

これによって、必須プラグインが無い状態でプラグインが読み込まれることを防げます。

  • 一般的な処理
public function onEnable() {
  $this->money = $this->getServer()->getPluginManager()->getPlugin("EconomyAPI");
  if ($this->money === null) {
    $this->getLogger()->error("EconomyAPIが読み込まれていません!");
    $this->getServer()->shutdown();
  }
}
  • 推奨される処理
plugin.yml
depend:
  - EconomyAPI
  - FormAPI

こうすることで、ここにあるプラグインが存在しなかった場合に「不明な依存関係です:<プラグイン名>」などの形で表示され、自動的にプラグインは無効化されます。プラグイン側でわざわざそのような処理を書く必要はありません。

メインスレッドをブロック

プラグインは、起動/停止時を除いて、スレッドが応答を待機するような処理でメインスレッドをブロックしないようにする必要があります。
小規模なローカルI/O処理(PocketMineで使用される範囲と比較)を除いて、メインスレッドをブロックする処理(cURL呼び出し、重いMySQLクエリ、重いSQLiteクエリ、playersスキャンなど)はメインスレッドで実行できません。

これは、時間が掛かる処理では非同期処理を使おうということです。
例えば、cURLなどでWEB APIを呼ぶ場合などです。
メインスレッドでこのような処理を行うと、その間何も処理されなくなってしまうので、当たり前ですがサーバーが重くなります。
そのため、非同期処理(AsyncTaskやThread)などを効率的に使用する必要があります。

プロトコルを宣言

pocketmine\network\mcpe 名前空間のいずれかの機能を使う場合は mcpe-protocol でプロトコルバージョンを宣言する必要があります。

plugin.yml
mcpe-protocol:
  - 408

名前空間の形式

プラグインは、他のプラグインと衝突しない一意な名前空間を使用する必要があります。
作者名で始まり、その後に対応するプラグインを続けた名前空間にする必要があります。
推奨される名前空間の形式は、製作者名\プラグイン名です。
衝突を防ぐために、製作者名にはGitHubのユーザー名や組織名、名前などに対応するものを使用する必要があります。
公式のプラグインでない限り名前空間「pocketmine」は許可されていません。

  • 悪い例
Main.php
<?php

namespace src\FooPlugin;

use pocketmine\plugin\PluginBase;

class Main extends PluginBase {}
Main.php
<?php

namespace pocketmine\FooPlugin;

use pocketmine\plugin\PluginBase;

class Main extends PluginBase {}
  • 良い例
Main.php
<?php

namespace Steve\FooPlugin;

use pocketmine\plugin\PluginBase;

class Main extends PluginBase {}

名前空間の中に留まる

プラグインによって宣言されたすべてのクラス、インターフェース、トレイトは、名前空間の形式に基づいて決めたこの一意の名前空間の下にある必要があります。これは、プラグインにライブラリを内蔵している場合も含まれます。
ただし、プラグインにライブラリを内蔵させるのではなく、Virionフレームワークを使用することも検討してください。

  • 悪い例

  • Steve\FooPlugin\Main

  • Steve\FooPlugin\EventListener

  • Steve\FooLibrary\API

  • 良い例

  • Steve\FooPlugin\Main

  • Steve\FooPlugin\EventListener

  • Steve\FooPlugin\Library\API

名前空間の変更

名前空間の変更を必要とするほどの巨大な変更がない限り、一度リリースしたらプラグインの名前空間を変更しないでください。
それがしたい場合、開発者は古いバージョンを廃止し、代わりに新規プラグインを送信することをお勧めします。

  • 悪い変更例(同じプラグインのままで)

  • Steve\FooPlugin\Main

  • Steve\FooPlugin\EventListener

  • Steve\FooPl\Main
  • Steve\FooPl\EventListener

コマンドのfallbackPrefix

プラグインが、CommandMap->registerを直接呼び出してコマンドを登録する場合、この関数に渡されるfallbackPrefixパラメータはプラグイン名でなければいけません。プラグインは、fallbackPrefixにイニシャルなどを使用出来ません。

※plugin.ymlでコマンドを登録している場合は、これについて考慮する必要はありません。

  • 悪い例
public function onEnable() {
  $this->getServer()->getCommandMap()->register("say", new SayTheMessageCommand("say", $this));
}
public function onEnable() {
  $this->getServer()->getCommandMap()->register("FooPlugin-Command", new SayTheMessageCommand("say", $this));
}
  • 良い例
public function onEnable() {
  $this->getServer()->getCommandMap()->register($this->getName(), new SayTheMessageCommand("say", $this));
}

もしくは

public function onEnable() {
  $this->getServer()->getCommandMap()->register("FooPlugin", new SayTheMessageCommand("say", $this));
}

Plugin-identifiable

全てのコマンドは、PluginIdentifiableCommandインターフェースを実装し、プラグインのインスタンスを返す必要があります。

※プラグインのメインクラスでコマンドを実装している場合は、これについて考慮する必要はありません。

これは、多くの場合はPluginCommandクラスを継承して作成しようということです。
PluginCommandには、既にPluginIdentifiableCommandが実装されているためです。
例えば、Commandクラスを継承してコマンドを実装する場合などには、別途PluginIdentifiableCommandインターフェースを実装する必要があります。

  • 悪い例
<?php

use pocketmine\command\Command;
use pocketmine\command\CommandSender;
use pocketmine\plugin\Plugin;

class SayTheMessageCommand extends Command
{
    /** @var Plugin */
    private $plugin;

    public function __construct(string $name, Plugin $plugin)
    {
        parent::__construct(
            $name,
            "description",
            "usageMessage"
        );
        $this->plugin = $plugin;
    }

    public function execute(CommandSender $sender, string $commandLabel, array $args)
    {
        $this->plugin->getServer()->broadcastMessage("Hello!");
    }
}
  • 良い例
<?php

use pocketmine\command\Command;
use pocketmine\command\CommandSender;
use pocketmine\command\PluginIdentifiableCommand;
use pocketmine\plugin\Plugin;

class SayTheMessageCommand extends Command implements PluginIdentifiableCommand
{
    /** @var Plugin */
    private $plugin;

    public function __construct(string $name, Plugin $plugin)
    {
        parent::__construct(
            $name,
            "description",
            "usageMessage"
        );
        $this->plugin = $plugin;
    }

    public function execute(CommandSender $sender, string $commandLabel, array $args)
    {
        $this->plugin->getServer()->broadcastMessage("Hello!");
    }

    public function getPlugin(): Plugin
    {
        return $this->plugin;
    }
}

もしくは

<?php

use pocketmine\command\Command;
use pocketmine\command\CommandExecutor;
use pocketmine\command\CommandSender;
use pocketmine\command\PluginCommand;
use pocketmine\plugin\Plugin;

class SayTheMessageCommand extends PluginCommand implements CommandExecutor
{
    public function __construct(string $name, Plugin $owner)
    {
        parent::__construct($name, $owner);
        $this->setExecutor($this);
    }

    public function onCommand(CommandSender $sender, Command $command, string $label, array $args): bool
    {
        $this->getPlugin()->getServer()->broadcastMessage("Hello!");
        return true;
    }
}

パーミッション名

プラグインがパーミッションを登録する場合、すべてのパーミッション名はプラグイン名で始まる必要があります。
名前空間のように製作者名を含める必要はありません。
パーミッション名は、アルファベット、数字、ハイフン、ドットのみで構成する必要があります。

  • 悪い例

  • Steve.FooPlugin.Command.say

  • Say.The.Message.Command

  • SayCommand

  • FooPlugin@SayTheMessageCommand

  • FooPlugin.SayTheMessage!Command!12345

  • 良い例

  • FooPlugin.Command.say

  • FooPlugin.Say.The.Message.Command

  • FooPlugin.SayCommand

  • FooPlugin-SayTheMessageCommand

  • FooPlugin.SayTheMessage-Command12345

永続的データの名前空間

プラグインの一般的なデータは、プラグインのデータフォルダ内に保存する必要があります。
エンティティ/アイテム固有のデータ(NBT)は、プラグイン名が指定された固有のCompoundTag内に保存する必要があります。
外部に保存されたデータ(MySQLなど)は、他プラグインとの衝突を防ぐために、できるだけプラグイン名を前につけた構成にする必要があります。

これは、プラグイン間での競合を防ぐためです。

データフォルダ

  • 悪い例
<?php

use pocketmine\plugin\Plugin;

/** @var Plugin $plugin */

file_put_contents("C:\\Foo\\Bar\\example.txt", "HogeFuga");

//pluginsやplugin_nata、worldsフォルダなどがあるフォルダに、example.txtを作成しています。
file_put_contents($plugin->getServer()->getDataPath() . "example.txt", "HogeFuga");
  • 良い例
<?php

use pocketmine\plugin\Plugin;

/** @var Plugin $plugin */

file_put_contents($plugin->getDataFolder() . "example.txt", "HogeFuga");

NBT

  • 悪い例
<?php

use pocketmine\entity\Entity;

/** @var Entity $target */
$target->namedtag->setString("message", "こんにちは!");

var_dump($target->namedtag->getString("message"));//string("こんにちは!")
  • 良い例
<?php

use pocketmine\entity\Entity;
use pocketmine\nbt\tag\CompoundTag;

/** @var Entity $target */
$pluginTag = new CompoundTag("FooPlugin");
$pluginTag->setString("message", "こんにちは!");
$target->namedtag->setTag($pluginTag);

var_dump($target->namedtag->getCompoundTag("FooPlugin")->getString("message"));//string("こんにちは!")

参考リンク

4
2
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
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?