【VRC】Public SDK APIを使って自作拡張メニューからワールドをアップロードする

# 概要
VRChatのSDKにはPublicSDKAPIというものが公開されています。
これを使うことでワールドやアバターのビルド・アップロードに関連する拡張エディタを作ることができます。

https://creators.vrchat.com/sdk/public-sdk-api/

あまり情報がなかったので備忘録として簡単に使い方を書いておきます。

# 作るもの
VRChat SDKの「Build and Upload」に相当するボタンを作ってみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495102/eae6faa9-ec9c-d011-02c9-4747e488ae10.png)

# スクリプト全文
``` c#:WorldUploader.cs
using System;
using UnityEditor;
using UnityEngine;
using VRC.Core;
using VRC.SDK3.Editor;
using VRC.SDKBase.Editor;
using VRC.SDKBase.Editor.Api;

public class WorldUploader : MonoBehaviour
{

    [InitializeOnLoadMethod]
    public static void RegisterSDKCallback()
    {
        VRCSdkControlPanel.OnSdkPanelEnable += AddBuildHook;
    }

    private static void AddBuildHook(object sender, EventArgs e)
    {
        if (VRCSdkControlPanel.TryGetBuilder<IVRCSdkBuilderApi>(out var builder))
        {
            builder.OnSdkUploadProgress += ShowProgress;
            builder.OnSdkUploadSuccess += NoticeUploadSccess;
            builder.OnSdkUploadError += NoticeUploadSccess;
        }
    }

    private static void ShowProgress(object sender, (string status, float percentage) e)
    {
        Debug.Log("ワールドアップロード中.." + e.status + ":" + e.percentage * 100 + "%");
        // DisplayProgressBarを使うとなぜかアップロードに失敗する
        //EditorUtility.DisplayProgressBar("ワールドアップロード中..", e.status, e.percentage);//プログレスバー表示

    }

    private static void NoticeUploadSccess(object sender, string message)
    {
        EditorUtility.DisplayDialog("アップロード成功", message, "OK");
    }
    private static void NoticeUploadError(object sender, string message)
    {
        EditorUtility.DisplayDialog("アップロード失敗", message, "OK");
    }


    [MenuItem("Tools/UploadWorld")]
    private static async void UploadWorld()
    {
        bool IsNewWorld = false;
        PipelineManager[] _pipelineManagers = VRC.Tools.FindSceneObjectsOfTypeAll<PipelineManager>();

        var worldId = _pipelineManagers[0].blueprintId;
        VRCWorld _worldData = new VRCWorld();
        if (string.IsNullOrWhiteSpace(worldId))
        {
            IsNewWorld = true;
        }
        else
        {
            try
            {
                _worldData = await VRCApi.GetWorld(worldId, true);

                if (APIUser.CurrentUser != null && _worldData.AuthorId != APIUser.CurrentUser?.id)
                {
                    IsNewWorld = true;
                }
            }
            catch (Exception ex)
            {
                Debug.LogException(ex);
            }
        }

        if (IsNewWorld) throw new Exception("アップロード済みのワールドが見つかりません");

        // VRCのワールドビルド

        if (!VRCSdkControlPanel.TryGetBuilder<IVRCSdkWorldBuilderApi>(out var builder)) return;

        try
        {
            await builder.BuildAndUpload(_worldData);
        }
        catch (Exception e)
        {
            Debug.Log(e.Message);
        }
    }
}

```
使ってみる場合は**Editorフォルダを作ってその中に格納**してください。

# 解説
## イベントの追加
``` C#
[InitializeOnLoadMethod]
public static void RegisterSDKCallback()
{
    VRCSdkControlPanel.OnSdkPanelEnable += AddBuildHook;
}

private static void AddBuildHook(object sender, EventArgs e)
{
    if (VRCSdkControlPanel.TryGetBuilder<IVRCSdkBuilderApi>(out var builder))
    {
        builder.OnSdkUploadProgress += ShowProgress;
        builder.OnSdkUploadSuccess += NoticeUploadSccess;
        builder.OnSdkUploadError += NoticeUploadSccess;
    }
}

private static void ShowProgress(object sender, (string status, float percentage) e)
{
    Debug.Log("ワールドアップロード中.." + e.status + ":" + e.percentage * 100 + "%");
    // DisplayProgressBarを使うとなぜかアップロードに失敗する
    //EditorUtility.DisplayProgressBar("ワールドアップロード中..", e.status, e.percentage);//プログレスバー表示

}

private static void NoticeUploadSccess(object sender, string message)
{
    EditorUtility.DisplayDialog("アップロード成功", message, "OK");
}
private static void NoticeUploadError(object sender, string message)
{
    EditorUtility.DisplayDialog("アップロード失敗", message, "OK");
    }
```
ビルド・アップロードの処理の前後にイベントを追加することができます。
今回はアップロード時のイベントとしてアップロード処理の進捗をログに出力する`ShowProgress`、アップロード完了後にダイアログを表示する`NoticeUploadSccess`と`NoticeUploadError`の3つを作りました。
`RegisterSDKCallback`関数を`InitializeOnLoadMethod`のタイミングで呼び出し、VRCSDKパネルが開いた際に作成した3つのイベントを登録するための`AddBuildHook`を登録しています。

ややこしいですがタイミングとしては順に
1. `RegisterSDKCallback`：エディタを開いたときに`AddBuildHook`イベントをSDKパネルが開いたときのイベントとして登録
1. `AddBuildHook`：SDKパネルが開いたときに3つのイベントをアップロード時のイベントとして登録
1. アップロード処理のそれぞれのタイミングで3つのイベントが呼び出される
となります。

## アップロード処理
### ワールド情報の取得
``` C#
bool IsNewWorld = false;
PipelineManager[] _pipelineManagers = VRC.Tools.FindSceneObjectsOfTypeAll<PipelineManager>();

var worldId = _pipelineManagers[0].blueprintId;
VRCWorld _worldData = new VRCWorld();
if (string.IsNullOrWhiteSpace(worldId))
{
    IsNewWorld = true;
}
else
{
    try
    {
        _worldData = await VRCApi.GetWorld(worldId, true);

        if (APIUser.CurrentUser != null && _worldData.AuthorId != APIUser.CurrentUser?.id)
        {
            IsNewWorld = true;
        }
    }
    catch (Exception ex)
    {
        Debug.LogException(ex);
    }
}

if (IsNewWorld) throw new Exception("アップロード済みのワールドが見つかりません");
```
ワールドアップロードには`VRCWorld`というワールド情報を持ったクラスのオブジェクトが必要です。
Sceneに設置するPrefabとは別なので気を付けてください。

`VRC.Tools.FindSceneObjectsOfTypeAll<PipelineManager>()`でSceneに設置されたPipelineManagerを取得し、ブループリントIDを読み込んだのちに`await VRCApi.GetWorld(worldId, true)`でVRCのサービスへ問い合わせることで該当のブループリントIDをもったオブジェクトが返ってきます。

アップロード済みのワールドがない場合は新規でVRCWorldオブジェクトを作成し、ワールド名などの情報を入力もできますが今回はExceptionで処理を中断しています。
VRChatSDK内の`VRCSdkControlPanelWorldBuilder`に正式な処理があるので気になったら参照してみてください。

### ワールドをアップロードする
``` C#
        if (!VRCSdkControlPanel.TryGetBuilder<IVRCSdkWorldBuilderApi>(out var builder)) return;

        try
        {
            await builder.BuildAndUpload(_worldData);
        }
        catch (Exception e)
        {
            Debug.Log(e.Message);
        }
```
ワールド情報をもってアップロードを行ないます。

# まとめ
動かしてみるとわかるのですが、この拡張エディタでのアップロードは一度VRChatSDKのパネルを開きBuilderタブに移動してからでないとエラーが出ます。
つまりこれが動くときはもとからある「Build and Upload」ボタンが押せる状態なので基本的にこのエディタ拡張だけだと意味はないです。
ビルド時に何か作業が発生する場合に前後に処理を加えるなどすることで価値が出てくるかもしれません。ライトベイクを忘れないようにするとか。[^1]
[^1]:builder.OnSdkBuildStartにライトベイク処理を入れてみましたが、その時点ですでにビルドが始まっているため通常のアップロードにベイク処理の差し込みはできませんでした。
専用のビルドボタンを作るとうまくいくと思います。