develop_plugin_ja - noonworks/Nursery GitHub Wiki
プラグイン制作
1. ビルド環境
- Visual Studio 2017
- .NET Framework 4.6.1
- C#
- ソースからのビルドを参照し、本体をビルドできるようにしておいてください
2. プラグインの基本
C#によるプログラミングの基本は理解している前提で説明します。
2-1. プラグイン・クラス
- プラグインは
IPlugin
インターフェースを実装したクラスです。 - ひとつのdllファイルの中に複数のプラグイン・クラスがあってもかまいません。
- 関連するプラグインはまとめてひとつのdllにした方がよいでしょう。
- 例:SoundEffectPlugin.dllには、音声の再生、停止、リスト表示、再読み込みをする4つのコマンド・プラグインがまとめられています。
2-2. プラグインの読み込み
Nursery本体のPluginManager
は、起動時に以下のようにプラグイン・クラスの読み込みと初期化を行います。
plugins
フォルダ内のdllファイルを読み込み- dllファイル内に定義されている型のうち、
IPlugin
にキャストできるものを読み込み plugins\plugins.config
のplugins
に記載されている順番で配列に格納- 各プラグインの
Initialize
メソッドを呼び出し
2-3. 基本形
// 何もしないプラグインのサンプル
using Nursery.Plugins;
namespace MyPlugin.SamplePlugins {
public class DoNothingCommand : IPlugin {
public string Name { get; } = "MyPlugins.SamplePlugins.DoNothingCommand";
public string HelpText { get; } = "Command to do nothing.";
Plugins.Type IPlugin.Type => Plugins.Type.Command;
public void Initialize(IPluginManager loader, IPlugin[] plugins) { }
public bool Execute(IBot bot, IMessage message) {
return false;
}
}
}
namespace MyPlugins.SamplePlugin
には、自分のプラグインに合った名前をつけてください。- クラス名に制限はありませんが、プラグイン名と同じにするといいでしょう。
Name
2-3-1. - プラグインの名前を定義します。設定ファイルなどで使われる値です。
- 他との重複を避けるために、
namespace.class
という形式の文字列にするといいでしょう。 - この値は国際化対応をしないようにしてください。
HelpText
2-3-2. - プラグインのヘルプテキストです。
help
コマンドで使われます。 - 国際化対応(後述)をするとよりわかりやすくなるでしょう。
Type
2-3-3. Command
は、「ユーザーがコマンドを送信し、それに応じた処理が行われる」プラグインです。@nursery-bot come
のように、Botへのメンション形式で送信されることが多いプラグインです。- ただし、必ずメンションでないといけないわけではありません。例えばSEプラグインの
SoundEffectCommand
は、メンション以外のキーワードに反応してSEを鳴らします。
Filter
は、「メッセージ加工処理を行う」プラグインです。- パターンなどに応じてメッセージの文字列を加工する処理を行います。
- また、条件に一致したメッセージを無視する(読み上げない)といった判断も行います。
Scheduler
は、「スケジュールを登録する」プラグインです。- 監視機能やタイマー機能などを登録します。
実際のところ、Command
、Filter
、Scheduler
に厳密な区別はありません。
目安としては、「ユーザーが実行を意図して送信するものはコマンド」「スケジュールを登録するものはスケジューラ」「それ以外はフィルタ」という区分でよいでしょう。
Initialize
2-3-4. - プラグインの初期化処理を行います。
- 典型的には、プラグイン用の設定ファイルを読み込みます。
- スケジューラの中には、初期化時にスケジュールを登録するだけで機能が完結するものもあり得ます。
// シンプルな設定ファイルを読み込むプラグインのサンプル
using Newtonsoft.Json;
using Nursery.Plugins;
using Nursery.Utility;
namespace MyPlugin.SamplePlugins {
// 設定ファイルのクラス。詳細はNewtonsoft.Jsonのドキュメントを参照。
// 設定ファイルが読めなかったときのために、デフォルト値を設定するようにしましょう
[JsonObject("MyPlugin.SamplePlugins.DoNothingCommandConfig")]
public class DoNothingCommandConfig {
[JsonProperty("name")]
public string Name { get; set; } = "SampleName";
[JsonProperty("age")]
public int Age { get; set; } = 20;
}
public class DoNothingCommand : IPlugin {
public string Name { get; } = "MyPlugins.SamplePlugins.DoNothingCommand";
public string HelpText { get; } = "Command to do nothing.";
Plugins.Type IPlugin.Type => Plugins.Type.Command;
// 設定を保持するメンバ
private DoNothingCommandConfig config = null;
public void Initialize(IPluginManager loader, IPlugin[] plugins) {
try {
// 設定の読み込み
this.config = loader.GetPluginSetting<DoNothingCommandConfig>(this.Name);
} catch (System.Exception e) {
// 設定ファイルがなくても気にしない場合は、エラーのログだけ出す
Logger.DebugLog(e.ToString());
this.config = null;
// エラーとしてプログラムの起動を失敗させたい場合はこの例外をキャッチしなければよい
}
if (this.config == null) {
// デフォルトの設定を使う
this.config = new DoNothingCommandConfig();
}
}
public bool Execute(IBot bot, IMessage message) {
return false;
}
}
}
IPluginManager.GetPluginSetting<T>(string Name)
2-3-4-1. - JSON形式の設定ファイルを読み込むヘルパーメソッドです。
- プラグインディレクトリにある、
Name
に指定した文字列に.json
をつけたファイルを読み込みます。通常はプラグイン名を使うといいでしょう。 T
は設定ファイルを読み込む型です。
IPlugin[] plugins
2-3-4-2. 引数 - 読み込まれたプラグインの配列です。
- 配列内の各プラグインは まだ
Initialize
が行われていない可能性がある ので注意してください。- 例えば「動作の前提として他のプラグインが必要なので、そのプラグインが読み込まれているか確認だけする」処理に使用します。
- ただし、「他のプラグインが
Initialize
で読み込んだ設定ファイルの値を利用する」ことは できません。 この時点では、まだそのプラグインのInitialize
が完了している保証がありません。 - 他のプラグインの設定ファイルを参照したい場合、プラグインの最初の
Execute
の際に読むようにしてください。SoundEffectPlugin
のSoundEffectReload
などのコードを参考にしてください。
Execute
2-3-5. - プラグインのメイン処理部分です。
- スケジューラの中には、初期化時にスケジュールを登録するだけで、メイン処理は何も行わないものもあり得ます。
// メッセージの最後に文字列を付け加えるプラグインのサンプル
using Nursery.Plugins;
namespace MyPlugin.SamplePlugins {
public class AddDokabenFilter : IPlugin {
public string Name { get; } = "MyPlugins.SamplePlugins.AddDokabenFilter";
public string HelpText { get; } = "Add dokaben to message.";
Plugins.Type IPlugin.Type => Plugins.Type.Filter;
public void Initialize(IPluginManager loader, IPlugin[] plugins) {}
public bool Execute(IBot bot, IMessage message) {
if (message.Content.Length == 0) {
// 本文が空だったらこのプラグインは適用しない
return false;
}
// 本文が空でなかったら、末尾に文字列を追加
message.Content = message.Content + " ドカベン";
// このプラグインを適用したことを明示
message.AppliedPlugins.Add(this.Name);
return true;
}
}
}
IBot bot
2-3-5-1. 引数 - Botのクラスです。Botの情報を取得したり、Botを入退室させたり、システムメッセージを発言させることができます。
- メンバは
IBot
インターフェースを参照してください。
IMessage message
2-3-5-2. 引数 - 送信されたメッセージのクラスです。
- メンバは
IMessage
インターフェースを参照してください。
IMessage.Content
の書き換え
2-3-5-3. - 読み上げる文字列を加工する場合は、
IMessage.Content
を書き換えてください。 IMessage.Content
は、自分より前に他のプラグインによって書き換えられている可能性があります。- また、自分より後に他のプラグインによって書き換えられる可能性もあります。
- これを防ぎたい場合は、後述の
Terminated
の使用を検討してください。
- これを防ぎたい場合は、後述の
IMessage.Original.Content
は「プラグインによる書き換えが行われていない元のメッセージ本文」です。
IMessage.AppliedPlugins
2-3-5-4. IMessage.AppliedPlugins
は、「今までにこのメッセージに適用されたプラグインの名前」のリストです。- これを利用して「〇〇プラグインが適用されている場合は処理をしない」などの条件分岐が可能です。
- 自分のプラグインで処理を行ったときは、
message.AppliedPlugins.Add(this.Name);
で自分のプラグインの名前を追加します。
IMessage.Terminated
2-3-5-5. IMessage.Terminated
をtrue
にした場合、次以降のプラグインの処理が行われなくなります。JoinCommand
などのコマンドでは、コマンドを実行したらそれ以降の処理(読み上げるべきかを判断する、文字列を変換するなど)は不要になります。そういった場合にTerminated
を使用します。
public bool Execute(IBot bot, IMessage message) {
//
// (処理は省略)
//
// このプラグインを適用したことを明示
message.AppliedPlugins.Add(this.Name);
// これ以降のプラグイン処理を実行させない
message.Terminated = true;
return true;
}
2-3-5-6. 戻り値
プラグインを適用した場合はtrue
、適用しなかった場合はfalse
を返します。
3. スケジュール
3-1. スケジュールの概要
スケジュールは、メッセージの受信とは異なるタイミングで実行可能な処理です。スケジュールを使うと以下のような機能を実現できます。
- Nurseryの状態やDiscordの状態を監視し、状態が変化したら何かをする。
- 特定の日時に何かをする。
3-2. スケジュールタスクとタイマーの概要
- スケジュールタスクは、条件と処理内容を持ったオブジェクトです。
- Nurseryのbotは、スケジュールタスクのリストを持っています。
- Nurseryのbotは、100ミリ秒ごとに起動するタイマーを持っています。
- タイマーが起動すると、リスト内のスケジュールタスクが順に起動されます。
- スケジュールタスクはその中で任意の処理を行います。
3-3. スケジュールタスクの登録
プラグインからは、IBot
もしくはIPluginManager
のAddSchedule
メソッドを使用して、botにスケジュールタスクを登録します。
public interface IBot {
void AddSchedule(IScheduledTask schedule);
}
public interface IPluginManager {
void AddSchedule(IScheduledTask schedule);
}
IScheduledTask
インターフェース
3-4. スケジュールタスクが最低限備えなくてはならないインターフェースです。
public interface IScheduledTask {
string Name { get; }
bool Finished { get; }
IScheduledTask[] Execute(IBot bot);
}
Name
プロパティ : スケジュールタスクに任意の名前をつけます。識別のために使用します。Finished
プロパティ : スケジュールタスクが完了したかどうかを示します。Finished
がtrue
のものは、スケジュールタスクのリストから消去されます。Finished
がfalse
のものは、たとえ処理が実行されたとしても、リストに残り続けます。
Execute
メソッド : タイマーから呼ばれるメソッドです。- 引数にはbotのインスタンスが渡されます。
- 戻り値は「新しく登録するスケジュールタスクの配列」です。
- スケジュールタスク内で新しいスケジュールタスクを生成し、それを登録したい場合に使います。
- スケジュールタスク内では
IBot
のAddSchedule
メソッドを使用しないでください。 - 空の配列もしくは
null
の場合、新しいスケジュールタスクを追加しません。
Nursery.Plugins.Schedules.ScheduledTaskBase
クラス
3-5. スケジュールタスクでよく使う機能を実装したヘルパー抽象クラスです。
Execute
メソッドをCheck
とDoExecute
に分割し、条件と処理を分けて記述できるようにしているCheck
で現在日時を使用できるようにしている- botのメッセージ送信に使えるヘルパーメソッド
Send
を実装している
一般的なスケジュールタスクは、IScheduledTask
インターフェースをそのまま実装するより、このヘルパークラスを継承した方が簡単に作成できます。
3-5-1. 継承後に実装する必要のあるメソッド
public abstract class ScheduledTaskBase : IScheduledTask {
public ScheduledTaskBase(string Name);
abstract protected bool DoCheck(IBot bot);
abstract protected IScheduledTask[] DoExecute(IBot bot);
}
実装例としてはNursery.BasicPlugins.WelcomeTask
を参考にしてください。
ScheduledTaskBase
(コンストラクタ)
3-5-1-1. サブクラスからこのクラスのコンストラクタを呼ぶようにしてください。
public class SampleTask : ScheduledTaskBase {
public SampleTask(): base("MyPlugin.SampleTask") {}
}
DoCheck
メソッド
3-5-1-2. - スケジュールタスクを実行するか判定するメソッドです。
- 引数にはbotのインスタンスが渡されます。
- 戻り値には
DoExecute
を実行するかどうかを返します。true
の場合は実行します。false
の場合実行しません。
DoExecute
メソッド
3-5-1-3. - スケジュールタスクの主な処理を行うメソッドです。
- 引数にはbotのインスタンスが渡されます。
- 戻り値は「新しく登録するスケジュールタスクの配列」です。
- スケジュールタスク内で新しいスケジュールタスクを生成し、それを登録したい場合に使います。
- スケジュールタスク内では
IBot
のAddSchedule
メソッドを使用しないでください。 - 空の配列もしくは
null
の場合、新しいスケジュールタスクを追加しません。
3-5-2. その他のヘルパーメソッドとメンバ変数
public abstract class ScheduledTaskBase : IScheduledTask {
protected DateTime CheckedAt { get; set; }
protected void Send(ScheduledMessage[] Messages, IBot bot);
}
CheckedAt
3-5-2-1. メンバ変数Check
メソッドが呼び出された日時≒Execute
メソッドが呼び出された日時が保存されています。
Send
メソッド
3-5-2-2. - 「botによるDiscordへのメッセージ送信」もしくは「botによる棒読みちゃんへの読み上げメッセージ送信」を行うメソッドです。
DoExecute
の処理中に呼び出すことができます。- 送信するメッセージは
ScheduledMessage
クラスのインスタンスで指定します。
public class ScheduledMessage {
public ScheduledMessageType Type = ScheduledMessageType.DoNothing;
public string Content = "";
public string[] TextChannelIds = new string[] { };
public bool CutIfTooLong = true;
}
public enum ScheduledMessageType {
DoNothing,
SendMessage,
Talk,
}
ScheduledMessageType
: メッセージの種類を指定します。ScheduledMessageType.DoNothing
: メッセージを送りません。ScheduledMessageType.SendMessage
: Discordへメッセージを送信します。ScheduledMessageType.Talk
: 棒読みちゃんへ音読メッセージを送信します。
Content
: メッセージのテキストを指定します。特殊書式が使用できます。- 特殊書式の変換処理は
Send
メソッド内で行われるので、事前に変換する必要はありません。
- 特殊書式の変換処理は
TextChannelIds
: Discordへメッセージを送信する場合、送信先のテキストチャンネルのIDをstring[]
で指定します。null
もしくは空の配列の場合、デフォルトのテキストチャンネルに送信されます。
CutIfTooLong
: Discordへメッセージを送信する場合で、テキストが長すぎたとき(2000文字を超えたとき)の処理を指定します。true
の場合、最大文字数から溢れたメッセージを削除します。false
の場合、メッセージを複数に分けてすべて送信します。
4. ヘルパークラス
AbstractMentionKeywordCommand
クラス
4-1. - コマンドプラグインによくある、
@nursery-bot keyword
形式のメンションとキーワードの判定を簡単に行うヘルパークラスです。 - 継承して使います。
// メンション・キーワード形式のコマンドサンプル
using Nursery.Plugins;
namespace MyPlugin.SamplePlugins {
public class DokabenCommand : AbstractMentionKeywordCommand {
public string Name { get; } = "MyPlugins.SamplePlugins.DokabenCommand";
public string HelpText { get; } = "Say dokaben command.";
// TypeはCommandに設定済み
// キーワード
public JoinCommand() : base(new string[] { "dokaben", "yamada", "tarou" }) { }
public void Initialize(IPluginManager loader, IPlugin[] plugins) {}
protected override bool DoExecute(int keywordIndex, IBot bot, IMessage message) {
// コマンド送信者に返信する
bot.SendMessageAsync(message.Original.Channel,
message.Original.Author.Mention + " ドカベン");
// メッセージ読み上げは無し(=空白)にする
message.Content = "";
// このプラグインを適用したことを明示
message.AppliedPlugins.Add(this.Name);
// これ以降のプラグイン処理を実行させない
message.Terminated = true;
return true;
}
}
}
4-1-1. コンストラクタ
ベースクラスのコンストラクタに、文字列の配列でキーワード一覧を渡します。
bool DoExecute(int keywordIndex, IBot bot, IMessage message)
4-1-2. - ベースクラスの
Execute
から呼ばれます。 - メンションになっていない場合や、キーワードが含まれていなかった場合、
DoExecute
は呼ばれません。 int keywordIndex
は、コンストラクタで渡したキーワードの配列のうち、マッチしたもののインデックスです。- それ以外は通常の
Execute
と同様に実装します。
Nursery.Utility.IJSWrapper
4-2. - 文字列をJavaScriptとして実行するためのヘルパークラスのインターフェースです。
- その実装として、現時点ではJintを使った
JintWrapper
が存在します。 - 実際の使用例としては、
SoundEffectPlugin
やUserDefinedPlugin
を参照してください。
Nursery.Utility.Logger
4-3. - ログを表示するためのユーティリティです。
- 通常のログ
Log
と、デバッグログDebugLog
が用意されています。 - 現時点では、ログはただコンソール画面に表示されるだけです。
4-4. その他
Nursery.PluginInterface
とNursery.Utility
のコードを参照してください。
5. 国際化
まず翻訳についてを一読し、Nurseryでの翻訳の扱いを理解しておいてください。
- 翻訳は必須ではありません。
- コマンドのヘルプやBotのメッセージは、国際化対応を行うのが好ましいでしょう。
- ログに表示するメッセージはほとんどの場合ユーザーの目に触れないので、国際化対応する必要性は高くありません。エラーメッセージを翻訳すればユーザーのトラブル解決の手助けにはなるでしょう。
5-1. 翻訳ファイルの単位について
Nurseryはプラグインシステムを導入しており、プラグインのdllを使う・使わないはユーザーに任せられています。そのため、複数のdll用の翻訳データをひとつのファイルにするのは都合がよくありません。
基本的に、dllファイル1つにつき.mo
ファイルが1つになるようにしましょう。
5-2. NGettextの導入
国際化対応を行う場合、自作プラグインのプロジェクトにNGettextを追加します。NuGetで、Nursery本体が使っているNGettextと同じバージョンをインストールしてください。
T.cs
のコピー
5-3. Nursery.Utility
プロジェクト内のT.cs
を、自作プラグインのプロジェクトにコピーします。- 名前空間を修正します。
DEFAULT_NAME
の値を変更します。これは.mo
ファイルの名前になるので、自作プラグインのdllと同じ名前にするといいでしょう。
5-4. 翻訳する文字列に関数を適用する
ソースコード中の文字列の中から、翻訳する必要のある文字列を探し、関数を適用していきます。
string text = "Some text.";
// ↓ このように変更
string text = T._("Some text.");
string jp_text = "何らかの文章。";
// ↓ 日本語の文章は、いったん英語に変更
string jp_text = T._("Some text.");
5-5. 翻訳ファイルを作成・編集・ビルドする
ソースコードからpoファイルを作成します。(poファイルの作成・編集手順はpoファイル編集ソフトによって異なります。各自その解説を読んでください。)
5-6. ビルドしたmoファイルを配置して実行
ビルドしたmoファイルは、実行ファイルのフォルダ内のlocale\ja_JP\LC_MESSAGES
に配置してください。
5-7. ファイルの配布
- プラグインを配布する場合は、moファイルも一緒に配布してください。
- ユーザーがシステムメッセージをカスタマイズできるよう、poファイル(もしくはpotファイル)も配布するといいでしょう。