C#でTRPG用のDiscordのBotを開発したよくある話

まあ表題で完結してるんですが、一応事細かに書いて行きたいと思います。
先に言っておくと運用コストとかルールブックの著作権だとかでいろいろ問題があるのでBotの公開はありません。

# 開発動機

ダイスロールをしてくれるDiscordのBotといえば [Sidekick](https://github.com/ArtemGr/Sidekick) などが有名で、多くのTRPGサーバーで使用されています。
例えば ```/r 1d100``` と打てば100面ダイスのダイスロールをしてくれます。便利ですね。

## でもやっぱりそれだけでは(機能が)足りないし、可愛げがない

TRPGといえば有名所で言えばSW2.0や、DX3rdですね。僕の参加しているDiscordグループでもやはりこの2つがメジャーです(本当はネクロニカとかアリアンロッドもしたいんですけど)。
そしてSW2.0(ソード・ワールド2.0)でよくやる処理と言えば

* 成長
* 経歴表

などがあります。
DX3rd(ダブルクロス The 3rd Edition)でよくやる処理といえば

* 経験表
* 覚醒表
* 衝動表

などがあります。
これを通常のダイスロールで行うのは面倒です。
まあ、サイコロを1つ1つ転がして一喜一憂する楽しみも否定されるべきではないと思いますが、それなら(Botを)使わなければいいだけの話なので僕は作ります。
## 可愛げ？
必要です。

# ほしい機能

* 経歴表とかを振る
* [SSMS](https://docs.microsoft.com/ja-jp/sql/ssms/sql-server-management-studio-ssms?view=sql-server-2017)と連携して卓データの管理をする(卓の時間になったら通知したりする機能)
* [葵ちゃん](http://www.ah-soft.com/voiceroid/kotonoha/)の声で喋って欲しい(個人の趣味)

# 開発言語
C#です。今回Botを制作するにあたり

* [Discord.NET](https://github.com/RogueException/Discord.Net)
* [NCalc](https://archive.codeplex.com/?p=ncalc)
* [Discord-Audio-Stream-Bot](https://github.com/BinkanSalaryman/Discord-Audio-Stream-Bot)
* [C#用のMersenne Twister](http://www.rei.to/random.html)

を使っています。

# 実施詳細
まずDiscordのBotをサクッと作ります。
これについてはググれば [■ここ](https://qiita.com/HAGITAKO/items/fff2e029064ea38ff13a) とか [■ここ](https://skyblock.jp/2017/11/13/csharp-discordbot/) とか [■ここ](https://kagasu.hatenablog.com/entry/2017/07/18/113335) とかが出てくるので紹介しません。

## 埋め込みオブジェクトで自己紹介させる
いわゆる```--help```コマンドですね。これはなんてことはなくて、一部抜粋すれば

```csharp:Introduction.cs
public override async Task<bool> Run(SocketMessage msg)
{
    var asm = Assembly.GetExecutingAssembly();

    //AssemblyCopyrightの取得
    var copyright = ((AssemblyCopyrightAttribute)Attribute.GetCustomAttribute(asm, typeof(AssemblyCopyrightAttribute))).Copyright;
    //バージョンの取得
    var version = asm.GetName().Version.ToString();

    var abuilder = new EmbedAuthorBuilder();
    abuilder.WithName("[Botの名前]");
    abuilder.WithIconUrl("[アイコンのURL]");
    abuilder.WithUrl("[マニュアルのURL]");

    var builder = new EmbedBuilder();

    builder.WithTitle("[タイトル]");
    builder.WithThumbnailUrl("[アイコンのURL]");
    builder.WithAuthor(abuilder);
    builder.WithColor(Color.DarkRed);
    builder.WithDescription("[Botの説明]");

    builder.AddInlineField("マニュアル", "[マニュアルのURL]");
    builder.AddInlineField("名前", "[Botの名前]");
    builder.AddInlineField("特別協賛", "[特別協賛のリスト(実際は関数でJoinしています)]");
    builder.AddInlineField("バージョン", version);
    builder.AddInlineField("コピーライト", copyright);

    await msg.Channel.SendMessageAsync(string.Empty, false, builder); //ここらへんは実際はWrapしたFactoryクラスに任せてる

    return true;
}
```

みたいに書けばいいだけです。楽でいいわ。ありがとうDiscord.NETの人。

## SSMSと連携するコマンド

これも楽で、```App.config```に```<connectionStrings>```を書いていろいろ書いてたらできます。

```csharp:SqlConnectionFactory.cs
public static string ConnectionString => ConfigurationManager.ConnectionStrings["Database"].ConnectionString;
public static SqlConnection CreateConnection()
{
    return new SqlConnection(ConnectionString);
}

public static void RunSql(Action<SqlConnection> action)
{
    using (var con = CreateConnection())
    {
        action(con);
    }
}
```
とか書いてコマンドの方から```RunSql```を呼び出してあげれば楽でいいです。

```csharp:CommandSample.cs
public override async Task<bool> Run(SocketMessage msg)
{
    var 好感度 = default(int);

    SqlConnectionFactory.RunSql(con =>
    {
        var cmd = con.CreateCommand();
        cmd.CommandText = "SELECT 好感度 FROM UserData WHERE ID=@id";
        cmd.Parameters.AddWithValue("@id", msg.Author.Id);
        con.Open();

        var sdr = cmd.ExecuteReader();
        if(sdr.HasRows)
        {
            if (sdr.Read())//コマンドによってwhileだったりifだったりする
            {
                好感度 = (int)sdr["好感度"];
            }
        }
    });

    //ここに処理を書く

    return true;
}
```

## Botに喋らせる
### 仕組みを考える
```
Discord -> 拙作プログラム1 -> 葵ちゃん -> 拙作プログラム2 -> Discord
```
でいけると思います。

* 拙作プログラム1
 * Discordからメッセージを受け取り葵ちゃんに読ませる( UIAutomation )
 * 葵ちゃんのウィンドウをSpyでSpySpyするとWPFでできているのがわかる(らしい)のでこの方法を採用(したい)
* 拙作プログラム2
 * 葵ちゃんの生音声をDiscordにリダイレクト
 * 難関

最初からUIAutomationに挑むのは難関なので、試しにRemoteTalkという便利なものが用意されている棒読みちゃんに喋らせます。
つまり
```
Discord -> 拙作プログラム1 -> RemoteTalk.exe -> 棒読みちゃん -> 拙作プログラム2 -> Discord
```
でやります。
### 拙作プログラム1

Processで呼んであげるだけなので楽です。

```csharp:VoiceProxy.cs
private static Process VoiceProxy { set; get; }

public static void Start()
{
    VoiceProxy = Process.Start(@"[パス]\Discord Audio Stream Bot.exe");
}
        
public static void Exit()
{
    VoiceProxy.WaitForExit();
}

public static void TalkBouyomi(string message)
{
    Process.Start(@"[棒読みちゃんのパス]\RemoteTalk\RemoteTalk.exe", $"/Talk {message}");
}
```
Nullチェックとか各種検査(例えばmessageに棒読みちゃんに有効なパラメータとかが入ってた場合とか、| rm -rfとか)をしてないのが気になりますが、いいんです。コストがかかるだけです。[YAGNI](https://ja.wikipedia.org/wiki/YAGNI)だよ兄貴。

### 拙作プログラム2
これが問題で、まず1つ思いついたのは[NET DUETTO](http://netduetto.net/)の出力を入力にしてくれるデバイスを拝借して、葵ちゃんの出力をマイクとして使用する方法です。
```
葵ちゃん -> 《NET DUETTO(出力) -> NET DUETTO(入力)》 -> 拙作プログラム2 -> Discord
```
で、多分これで問題ないと思うんですが、どうやってコード上でBotのマイクを設定すればいいんでしょうか……？

そんなニッチな需要のものググっても流石に出てこないだろうしな～～～[出ました](https://github.com/BinkanSalaryman/Discord-Audio-Stream-Bot)。神か。

…………
(ソースコード)読めない……

という訳でバイナリをありがたく拝借し、この項目についてはひとまず完成となりました。詳しく知りたいならDiscord Audio Stream Botのリポジトリを見るといいぞ。僕は諦めました。

## ボイチャのユーザーを移動させたい

卓をやっていると、その時間に普段のGeneralなボイチャから、特定のシステムのボイチャに集まりたいことがあります。
ググってもあまり出てこなくて地味にハマったところなので一応。ソースコード中の```Client```は接続済みの```DiscordSocketClient```です。

```csharp:CommandMoveVoiceUser.cs
var guild = Bot.Client.GetGuild([サーバーのID]);
var vcID = [ボイチャのID];
var channel = guild.GetVoiceChannel(vcID);
var user = guild.GetUser((ulong)[移動させたいユーザーのID]);
await user.ModifyAsync(e => e.Channel = channel);
```

でできます。

# 参考

* VOICEROID2にプログラムから喋らせる https://hgotoh.jp/wiki/doku.php/documents/voiceroid/voiceroid-008
* DiscordにVOICEROID2の音声を出力するためのデバイス(NET DUETTO)を用意 http://ch.nicovideo.jp/ironia/blomaga/ar1468999
* ボイチャユーザーを移動させる https://github.com/RogueException/Discord.Net/issues/899
* 埋め込みオブジェクト https://discordapp.com/developers/docs/resources/channel#embed-object
* デバイスからDiscordに音声を出力してくれるBot https://github.com/BinkanSalaryman/Discord-Audio-Stream-Bot