UE4 Editor コンパイル通知をDiscordに流したい話

この記事は[Unreal Engine 4(UE4) Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ue4)の6日目の投稿記事です。

---

### 目的

UE4 EditorでC++のソースをコンパイルした際の通知をDiscordで受け取りたい。

### やっていくこと

1. 空のプラグインを作成する 
2. `HTTP`, `Json` を `PrivateDependencyModuleNames` に追加
3. イベントリスナを作成し、DiscordにWebhookリクエストをかけられるようにする
4. `IHotReloadInterface` の `OnModuleCompilerFinished` にイベントリスナをバインドする

### やっていく

#### Discord Webhook URLの取得

Discordで任意のチャンネルにWebhookを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55719/231cab36-b6a6-6894-13d8-1bf235a317e6.png)

任意の名前やアイコンを設定し、「ウェブフックURL」をコピーしておきます。

このURLがプラグインからのリクエストに必要になります。

#### 空のプラグイン作成

Editor -> Plugins -> New Plugins より適当な名前を設定し、プラグインを作成します。

**この記事では `HotReloadNotifications` という名前で設定して進めていきます。**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55719/f0913a73-364e-4ebb-9670-2e23151d6c6b.png)

#### 必要なモジュールの依存を追加

Visual Studio を起動して、`HotReloadNotificationsModule.Build.cs`を編集します。

`PrivateDependencyModuleNames` に `HTTP` と `Json` を追記します。

```csharp
PrivateDependencyModuleNames.AddRange(
    new string[]
    {
        "CoreUObject",
        "Engine",
        "Slate",
        "SlateCore",
        "HTTP",
        "Json"
        // ... add private dependencies that you statically link with here ...	
    }
);
```

`HTTP` は Webhookの送信に必須で、 `Json` はDiscord Webhookで送信するJson文字列の生成にあると便利です。

#### イベントリスナの定義を作成する

`HotReloadNotificationsModule.h` を開き、引数が `(const FString&, ECompilationResult::Type, bool)` なメンバ関数を用意します。

```c++
void OnCompileFinished(const FString& LogDump, ECompilationResult::Type CompilationResult, bool bShowLog) const;
```

なお、 `ECompilationResult::Type` を使用するために `Misc/CompilationResult.h` をインクルードする必要があります。

```c++
#include "Misc/CompilationResult.h"
```

#### 実装
続いて、 `HotReloadNotificationsModule.cpp` を編集していきます。

##### ヘッダのインクルード

`HttpModule.h`, `Interfaces/IHttpRequest.h`, `Serialization/JsonSerializerMacros.h`, `Misc/HotReloadInterface.h` の4ファイルをインクルードします。

```c++
#include "HttpModule.h" // -> HTTP Request
#include "Interfaces/IHttpRequest.h" // -> HTTP Request
#include "Serialization/JsonSerializerMacros.h" // Serialize Struct
#include "Misc/HotReloadInterface.h" // -> Listen HotReload Event
```

##### イベントリスナのバインド

[IHotReloadInterface | Unreal Engine Documentation](https://docs.unrealengine.com/en-US/API/Runtime/CoreUObject/Misc/IHotReloadInterface/index.html)
今回はこの `IHotReloadInterface` より `FModuleCompilerFinishedEvent` にイベントをバインドして機能を作ります。

`IHotReloadInterface` のインスタンスを静的な関数 `GetPtr()` から取得します。

```c++
const auto& HotReloadModule = IHotReloadInterface::GetPtr();
```

`FModuleCompilerFinishedEvent` はメンバ関数 `OnModuleCompilerFinished` より取得できます。
Eventに `AddRaw` してバインドは完了です。

```c++
HotReloadModule->OnModuleCompilerFinished().AddRaw(this, &FHotReloadNotificationsModule::OnCompileFinished);
```

#### Webhook送信する構造体の作成

```c++
struct FDiscordWebhookRequestBody : public FJsonSerializable
{
	FString Content;

	BEGIN_JSON_SERIALIZER
		JSON_SERIALIZE("content", Content);
	END_JSON_SERIALIZER

	FDiscordWebhookRequestBody(const FString& Content) : Content(Content) {}
	FDiscordWebhookRequestBody() : FDiscordWebhookRequestBody(FString()) {}
};
```

このように `FJsonSerializable` を継承し `JSON_SERIALIZE` などのマクロを使って構造体を定義します。
こうすることで、`ToJson` 関数を呼ぶだけで `FString` 型のJson文字列に変換することができます。

#### Discord Webhook URLを書いておく

別にこれはどうだっていいんですが、先程コピーしたDicordのWebhook URLをここでコードにベタ書きします。

```c++
static constexpr auto DISCORD_WEBHOOK_URL = "<WEBHOOK_URL>";
```

~~この辺はエディタの設定拡張とかでどうにかしたかったけど面倒なので諦めました~~

##### イベントリスナの実装

`OnCompileFinished` の実装を書いていきます。

```c++
void FHotReloadNotificationsModule::OnCompileFinished(const FString& LogDump, ECompilationResult::Type CompilationResult, bool bShowLog) const {
    const auto Body = FDiscordWebhookRequestBody(ECompilationResult::Succeeded == CompilationResult ? FString(L"[HotReloadNotifications] COMPILE SUCCEEDED") : FString(L"[HotReloadNotifications] COMPILE FAILED"));
    auto Request = FHttpModule::Get().CreateRequest();
    Request->SetVerb("POST");
    Request->SetHeader("Content-Type", "application/json");
    Request->SetURL(FString(DISCORD_WEBHOOK_URL));
    Request->SetContentAsString(Body.ToJson());
    Request->ProcessRequest();
}
```

よくあるHTTPリクエストを組み立てて送信しているというコードです。

`EComppilationResult::Type` 列挙型には `Succeeded` 以外にもいくつかの列挙値が存在しますが、ここでは `Succeeded` のみの判定としています。

先程 `FJsonSerializable` を継承して作成した `FDiscordWebhookRequestBody` のインスタンスから、 `ToJson` 関数を使って文字列に変換しています。（めちゃくちゃ便利）

#### 確認

Visual Studioからのビルドが完了した上で、UE4のコンパイルを実行します。

DiscordにちゃんとWebhookが飛んできていたらOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55719/760a53ca-489d-0261-593e-17623e924485.png)

### まとめ

今回の成果物 [ekuinox/UE4-HotReloadNotifications](https://github.com/ekuinox/UE4-HotReloadNotifications)はこちらです。

株式会社ヒストリアさんの[[UE4] Slack連携してライトビルドの完了を通知する](http://historia.co.jp/archives/12054) を読んだところ、コンパイル通知もやりたいなと思って実現したものです。（JsonやHTTP周りも大変助かりました）
ただ `IHotReloadInterface` にたどり着くまでには地味に時間がかかっていて、GitHub上のUE4のソースを検索かけて探しました。
~~UE4で用意されたクラスの使用例が見たくて検索かけてんのにエンジンのクローンリポジトリばかりヒットするのクソだろ　謎クローンをやめろ~~

できることなら、より時間のかかるパッケージングの通知も取得したかったのですが、今回は心が折れました。知見のある方はコメントとかでご教示いただけるとめちゃくちゃ嬉しくなります...。