UE4 CommandletでBPを変更してみる

# 概要
UnrealEngine の Commandlet についてのメモです。
BPの設定変更を試してみます。

▼関連
[UE4 Commandletでアセットを一括修正してみる](https://qiita.com/unknown_ds/items/e49aa05e142a04a1ee59)

## 環境
Windows10
Visual Studio 2017
UnrealEngine 4.25

## 参考
以下を参考にさせて頂きました、ありがとうございます。

[UnrealEngine : UCommandlet](https://docs.unrealengine.com/en-US/API/Runtime/Engine/Commandlets/UCommandlet/index.html)
[UnrealEngine : Commandlets](https://docs.unrealengine.com/en-US/API/Editor/UnrealEd/Commandlets/index.html)

## 関連ソース
"Engine\Source\Runtime\Engine\Classes\Commandlets\Commandlet.h"


# 作成例
ブループリントの設定をコマンドレットにて書き換えてみます。

## 元となるBP
サードパーソンのC++で新規作成した `AMyProjectCharacter`クラスに対し、テスト用のメンバ `AnimClassTest` を追加します。アニメーションブループリントを想定します。配列にしているのは特に意味はありません。

```cpp:MyProjectCharacter.h
UCLASS(config=Game)
class AMyProjectCharacter : public ACharacter
{
	GENERATED_BODY()
public:
	// ..省略..

	// テスト用
	UPROPERTY(Category = "Test", EditAnywhere, BlueprintReadWrite)
	TArray<TSubclassOf<class UAnimInstance>> AnimClassTest;

};
```

これを親とするBP `ThirdPersonCharacter` は以下の様になっています。
追加された `AnimClassTest` が確認できます。
ここを書き換えてみます。

![ThirdPersonCharacterBP.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/ef89a0f2-db59-de8c-b9de-463a4c128414.jpeg)


## BP書き換え用コマンドレットクラス
対象BPを探し出してデフォルトオブジェクトを取得し、書き換えます。
（ファイル検索時のアセットレジストリのフィルタの使い方が微妙です。）
また予めコンストラクタで書き換えるクラスを `FClassFinder` で探します。クラスなので、パスには `_C`を付ける必要があります。（もしくは拡張子を省略）


```cpp:MyCommandlet.h
#pragma once

#include "CoreMinimal.h"
#include "Commandlets/Commandlet.h"
#include "MyCommandlet.generated.h"

UCLASS()
class UMyCommandlet : public UCommandlet
{
	GENERATED_BODY()
	

public:
	UMyCommandlet(const FObjectInitializer& ObjectInitializer);

	virtual int32 Main(const FString& CmdLineParams) override;

	// 対象アセットリストを取得する
	void GetAssetList(TArray<FAssetData>& _AssetList) const;

	// 参照保持するクラス
	TArray<TSubclassOf<UAnimInstance>> AnimBPList;
};
```

コンストラクタで書き換え先のクラスを取得します。
ファイル名を配列にしているのはこのコード例では無意味です。直接Findしてしまってもいいです。
変更したBPに対し、`MarkPackageDirty`を付けていますが、コマンドレットの実行では不要かと思います。


```cpp:MyCommandlet.cpp
#include "MyCommandlet.h"
#include "AssetData.h"
#include "AssetRegistryModule.h"
#include "AssetRegistryHelpers.h"
#include "Misc/FileHelper.h"
#include "UObject/Object.h"
#include "UObject/Class.h"
#include "./MyProjectCharacter.h"

DEFINE_LOG_CATEGORY_STATIC(LogTextMyCommandlet, Log, All);

// コンストラクタ
UMyCommandlet::UMyCommandlet(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	LogToConsole = false;
	
	// 書き換えるファイルパス
	const TArray<const TCHAR*> _AnimBPList = {
		TEXT("AnimBlueprint'/Game/Mannequin/Animations/ThirdPerson_AnimBP.ThirdPerson_AnimBP_C'"),
	};
	AnimBPList.SetNum(1);

	for (int32 _Lp = 0; _Lp < AnimBPList.Num(); _Lp++) {
		ConstructorHelpers::FClassFinder<UAnimInstance> find_class(_AnimBPList[_Lp]);
		if (find_class.Succeeded()) {
			// アニムインスタンスクラス保持
			AnimBPList[_Lp] = find_class.Class;
		}
	}

}

// メイン処理
int32 UMyCommandlet::Main(const FString& CmdLineParams)
{
	TArray<FAssetData>	_AssetList;
	TArray<UPackage*>	_PackagesToSave;
	
	// アセットリストの取得
	GetAssetList(_AssetList);

	// アセット変更
	for (auto _AssetData : _AssetList) {
		// BPにキャスト
		UBlueprint* _Blueprint = Cast<UBlueprint>(_AssetData.GetAsset());

		if( _Blueprint ){
			// クラスデフォルトオブジェクトを取得
			auto _MyChara = _Blueprint->GeneratedClass->GetDefaultObject<AMyProjectCharacter>();

			if (_MyChara) {
				// 変数を書き換える				
				if( _MyChara->AnimClassTest.Num() > 0){
					_MyChara->AnimClassTest[0] = (UClass*)(AnimBPList[0]);
				}
				
				// 編集フラグを立てる
				_Blueprint->MarkPackageDirty();

				// 変更したパッケージ追加
				_PackagesToSave.Add(_AssetData.GetPackage());
			}
		}

	}
	
	// アセットセーブ
	for (const auto& _It : _PackagesToSave) {
		// 保存ファイル名（Chopで文字操作して無理矢理作成しています）
		const auto _FileName = FString::Printf(TEXT("%s%s%s"), *FPaths::GameContentDir(), *_It->GetFullName().RightChop(14), *FPackageName::GetAssetPackageExtension());

		// パッケージ保存
		bool _Ret = UPackage::SavePackage(_It, nullptr, EObjectFlags::RF_Public | EObjectFlags::RF_Standalone, *_FileName);
		
	}
	
	return(0);
}

// 対象アセットリストを取得する
void UMyCommandlet::GetAssetList(TArray<FAssetData>& _AssetList) const
{
	FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(FName("AssetRegistry"));
	IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
	
	IFileManager& _FileMgr = IFileManager::Get();
	
	
	FARFilter Filter;
	Filter.bRecursivePaths = true;
	// 対象パス（Contentフォルダ全体を対象にするなら "/Game/" を指定）
	Filter.PackagePaths.Add(TEXT("/Game/"));
	
	// 対象クラス指定
	Filter.ClassNames.Add(TEXT("Blueprint"));

	AssetRegistry.GetAssets(Filter, _AssetList);
}


```


## 実行テスト
エンジンフォルダにあるexeに uproject と`-run`でコマンドラインクラス名を指定します。`MyCommandlet` のうち、`Commandlet`の部分は省略できます。

```text:コマンドプロンプト
"Engine\Binaries\Win64\UE4Editor-Cmd.exe" "MyProject\MyProject.uproject" -run=My -stdout -UTF8Output
```

## 結果
テスト用の変数 `AnimClassTest` が書き変わります。

![Result.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/35bf476c-6881-ed9b-58f3-41f1c711f959.jpeg)

クラスだけではなく、もちろん変数なども同様の方法で書き換え可能です。


# 備考

## 起動するエンジンのバージョン違い
以下のようなエラーになります。
![VerError.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/ffa9c09a-b821-22a8-cd03-c61697203bef.jpeg)

`*.uproject` のエンジンバージョンを確認しましょう。

```cs:*.uproject
{
	"FileVersion": 3,
	"EngineAssociation": "4.24",
	"Category": "",
	"Description": "",
	"Modules": [
		{
			"Name": "MyProject",
			"Type": "Runtime",
			"LoadingPhase": "Default"
		}
	]
}
```

## C++側からのコマンドレット呼び出し
ログを出力するだけのコマンドレット `MyTest1Commandlet` と `MyTest2Commandlet` を呼び出す場合のコード例。


```cpp:MyCommandlet.cpp

// メイン処理
int32 UMyCommandlet::Main(const FString& CmdLineParams)
{
	// コマンドレット１呼び出し
	ExecuteCommandlet(FString("MyTest1Commandlet"), FString("-stdout -UTF8Output"));
	// コマンドレット２呼び出し
	ExecuteCommandlet(FString("MyTest2Commandlet"), FString("-stdout -UTF8Output"));

	return(0);
}

// コマンド実行
void UMyCommandlet::ExecuteCommandlet(const FString& _CommaneletName, const FString& _Params)
{
	// コマンドレットクラスを探す
	UClass* _CommandletClass = FindObject<UClass>(ANY_PACKAGE, *_CommaneletName, false);
	if( ::IsValid(_CommandletClass) ){
		// コマンドレットオブジェクトを作成
		UCommandlet* _Commandlet = NewObject<UCommandlet>(GetTransientPackage(), _CommandletClass);
		if( ::IsValid(_Commandlet) ){
			// 実行
			_Commandlet->Main(_Params);
		}
	}

}
```

この方法だと、エディタ1回の起動で複数コマンドレットを呼び出せます。


# まとめ
コマンドレットの有用な使い方を模索中です。:grinning:
多数のブループリントを条件によって書き換える場合などには使えると思います。