[UE4]コマンドレットを使用してBPの関数ノードを一括置換

# 1.はじめに

開発を進めていて、あとからBPの特定のノードの差し替えを迫られたことは誰しもあるとは思うのですが、そんな時、Blueprintの検索(https://docs.unrealengine.com/4.26/ja/ProgrammingAndScripting/Blueprints/Search/) から検索して、ひとつづつ置換をやっていたのですが、あまりに数が多く困った時があったのでコマンドレットを使用して、一括置換をやってみました。

# 2.コマンドレットとは

https://qiita.com/unknown_ds/items/e49aa05e142a04a1ee59

https://qiita.com/Takezoh/items/d419c6b7771157233cca

上記記事やドキュメントを参考にするとよさそうです。

https://docs.unrealengine.com/4.26/en-US/API/Runtime/Engine/Commandlets/UCommandlet/

公式ドキュメント。

# 3.コマンドレットのクラス作成

コマンドレット名を`ReplaceBPFunctionNode`として`UCommandlet`クラスを継承した`UReplaceBPFunctionNodeCommandlet`クラスを作成。

```c++:ReplaceBPFunctionNodeCommandlet.h
#pragma once
#include "CoreMinimal.h"
#include "Commandlets/Commandlet.h"
#include "BPNodeChangeCommandlet.generated.h"

UCLASS()
class UReplaceBPFunctionNodeCommandlet: public UCommandlet
{
    GENERATED_UCLASS_BODY()

public:
    virtual int32 Main(const FString& CmdLineParams) override;
};
```

```c++:ReplaceBPFunctionNodeCommandlet.cpp
#include "ReplaceBPFunctionNodeCommandlet.h"

DEFINE_LOG_CATEGORY_STATIC(LogReplaceBPFunctionNodeCommandlet, Log, All);

UReplaceBPFunctionNodeCommandlet::UReplaceBPFunctionNodeCommandlet(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
    LogToConsole = false;
}

int32 UReplaceBPFunctionNodeCommandlet::Main(const FString& CmdLineParams)
{
    return 0;
}
```

# 4.パラメータを設定する

```
-FunctionOnwerClassName={ターゲットする親のクラス}
-NewFunctionOwnerClassName={置換後の関数の親クラス}
-FunctionName={ターゲットする関数名}
-NewFunctionName={置換後の関数}
-AssetPath={ターゲットとするアセットのパス}
```

上記パラメータコマンド実行時に渡して`Main`関数で取得。

```c++:ReplaceBPFunctionNodeCommandlet.cpp
int32 UReplaceBPFunctionNodeCommandlet::Main(const FString& CmdLineParams)
{
 FString FunctionOnwerClassName;
    if (FParse::Value(*CmdLineParams, TEXT("FunctionOnwerClassName="), FunctionOnwerClassName))
    {
        UE_LOG(LogDebugBPNodeChangeCommandlet, Log, TEXT("FunctionOnwerClassName : %s"), *FunctionOnwerClassName);
        if (FunctionOnwerClassName.IsEmpty())
        {
            UE_LOG(LogDebugBPNodeChangeCommandlet, Error, TEXT("FunctionOnwerClassName is Unknown"));
        }
    }

    FName AssetPath;
    if (FParse::Value(*CmdLineParams, TEXT("AssetPath="), AssetPath))
    {
        UE_LOG(LogDebugBPNodeChangeCommandlet, Log, TEXT("AssetPath : %s"), *AssetPath.ToString());
    }

    FString NewFunctionOwnerClassName;
    if (FParse::Value(*CmdLineParams, TEXT("NewFunctionOwnerClassName="), NewFunctionOwnerClassName))
    {
        UE_LOG(LogDebugBPNodeChangeCommandlet, Log, TEXT("NewFunctionOwnerClassName : %s"), *NewFunctionOwnerClassName);
    }
    
    FString FunctionName;
    if (FParse::Value(*CmdLineParams, TEXT("FunctionName="), FunctionName))
    {
        UE_LOG(LogDebugBPNodeChangeCommandlet, Log, TEXT("FunctionName : %s"), *FunctionName);
    }

    FString NewFunctionName;
    if (FParse::Value(*CmdLineParams, TEXT("NewFunctionName="), NewFunctionName))
    {
        UE_LOG(LogDebugBPNodeChangeCommandlet, Log, TEXT("NewFunctionName : %s"), *NewFunctionName);
    }
}
```

# 5.BP内の特定のノードを検索してノードを置換

https://kubotti0131.hatenablog.jp/entry/2019/12/14/000000

こちらの記事が解説付きで分かりやすかったので、ほぼこちらを参考に実装しました。
適宜`Build.cs`にモジュールを追加してください。
関数ノードのピンからつながっている関数ノードに関しては差し替えるよう対応したのですが、関数ノード->関数以外のノードに関しては対応できなかったので、別途調整が必要です。
コードは以下。

```c++:ReplaceBPFunctionNodeCommandlet.h
#pragma once

#include "CoreMinimal.h"
#include "AssetData.h"
#include "K2Node_CallFunction.h"
#include "Commandlets/Commandlet.h"
#include "ReplaceBPFunctionNodeCommandlet.generated.h"

struct FCommandletParameter
{
	FString OwnerClassName;
	FString ReplaceOwnerClassName;
	FString FunctionName;
	FString ReplaceFunctionName;
	FName AssetPath;
	FCommandletParameter(
		FString InOwnerClassName,
		FString InReplaceOwnerClassName,
		FString InFunctionName,
		FString InReplaceFunctionName,
		FName InAssetPath
	)
	{
		OwnerClassName = InOwnerClassName;
		ReplaceOwnerClassName = InReplaceOwnerClassName;
		FunctionName = InFunctionName;
		ReplaceFunctionName = InReplaceFunctionName;
		AssetPath = InAssetPath;
	}
};

UCLASS()
class UReplaceBPFunctionNodeCommandlet: public UCommandlet
{
	GENERATED_BODY()
	
public:
	UReplaceBPFunctionNodeCommandlet(const FObjectInitializer& ObjectInitializer);

	virtual int32 Main(const FString& CmdLineParams) override;

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

	// 参照保持するBPクラス
	TArray<TSubclassOf<UBlueprint>> BPList;

private:

	const FCommandletParameter GetParameter(const FString& CmdLineParams);
	
	void ReplaceNodePinLinks(UEdGraphNode* OldNode, UEdGraphNode* NewNode);
	void ReConnectPinLinks(UK2Node_CallFunction* Node, UEdGraphNode* NextNode);

	UK2Node_CallFunction* CreateFunctionNode(UEdGraph* TargetGraph, UFunction* SetFunction, const int32 NodePosX, const int32 NodePosY);
};
```

```c++:ReplaceBPFunctionNodeCommandlet.cpp
#include "ReplaceBPFunctionNodeCommandlet.h"

#include "AssetRegistryModule.h"
#include "CompilerResultsLog.h"
#include "EdGraphSchema_K2.h"
#include "FileHelpers.h"
#include "FileManager.h"
#include "IAssetRegistry.h"
#include "K2Node_CallFunction.h"
#include "KismetEditorUtilities.h"
#include "EdGraph/EdGraph.h"
#include "Editor/UMGEditor/Public/WidgetBlueprint.h"
#include "Engine/World.h"

DEFINE_LOG_CATEGORY_STATIC(LogDebugBPNodeChangeCommandlet, Log, All);

// コンストラクタ
ReplaceBPFunctionNodeCommandlet::ReplaceBPFunctionNodeCommandlet(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{
    LogToConsole = false;
}

// メイン処理
int32 ReplaceBPFunctionNodeCommandlet::Main(const FString& CmdLineParams)
{
    const FCommandletParameter CommandletParameter = GetParameter(CmdLineParams);

    if (CommandletParameter.AssetPath.ToString().IsEmpty() ||
        CommandletParameter.FunctionName.IsEmpty() ||
        CommandletParameter.ReplaceFunctionName.IsEmpty() ||
        CommandletParameter.OwnerClassName.IsEmpty() ||
        CommandletParameter.ReplaceOwnerClassName.IsEmpty()
    )
    {
        return 1;
    }
    
    // 関数クラスを先に保持
    UClass* FunctionOwnerClass =  FindObject<UClass>(ANY_PACKAGE, *CommandletParameter.ReplaceOwnerClassName);
    if (!FunctionOwnerClass)
    {
        UE_LOG(LogDebugBPNodeChangeCommandlet, Error, TEXT("OwnerClass(%s) is null"), *CommandletParameter.ReplaceOwnerClassName);
        return 1;
    }

    TArray<FAssetData> AssetDataList;
    TArray<UPackage*> PackagesToSave;

    // アセットリストの取得
    GetAssetList(AssetDataList, CommandletParameter.AssetPath);

    UE_LOG(LogDebugBPNodeChangeCommandlet, Log, TEXT("AssetDataList Num(%i)"), AssetDataList.Num());
    
    for (auto AssetData : AssetDataList)
    {
        bool bNeedSaveAsset = false;
        
        if (auto Blueprint = Cast<UBlueprint>(AssetData.GetAsset()))
        {
            FCompilerResultsLog LogResult;
            FKismetEditorUtilities::CompileBlueprint(Blueprint, EBlueprintCompileOptions::None, &LogResult);

            if (LogResult.NumErrors > 0)
            {
                UE_LOG(LogDebugBPNodeChangeCommandlet, Error, TEXT("Blueprint is NumErrors"));
                continue;
            }

            
            TArray<UEdGraph*> AllGraphs;
            Blueprint->GetAllGraphs(AllGraphs);

            bool ReplaceResult = false;
            
            for (UEdGraph* Graph : AllGraphs)
            {
                TArray<UK2Node_CallFunction*> FunctionNodes;
                TMap<UK2Node_CallFunction*, UK2Node_CallFunction*> ReplacedNodeMap;
            
                // Node取得
                for(UEdGraphNode* GraphNode : Graph->Nodes)
                {
                    // 関数ノードのチェック
                    if (auto CallFuncNode = Cast<UK2Node_CallFunction>(GraphNode))
                    {
                        if (!CallFuncNode->GetTargetFunction())
                        {
                            UE_LOG(LogDebugBPNodeChangeCommandlet, Display, TEXT("CallFuncNode Function is null"));
                            continue;
                        }
                        
                        const FString CallFunctionName = CallFuncNode->GetTargetFunction()->GetName();
                        const FString OwnerClassName = CallFuncNode->GetTargetFunction()->GetOwnerClass()->GetName();

                        if(CallFunctionName == CommandletParameter.FunctionName && OwnerClassName == CommandletParameter.OwnerClassName)
                        {
                            UE_LOG(LogDebugBPNodeChangeCommandlet, Display, TEXT("Find Target Function Node. Function Name(%s), Owner Class Name(%s)"), *CommandletParameter.FunctionName, *OwnerClassName);

                            // 該当の関数であれば保持
                            FunctionNodes.Add(CallFuncNode);
                        }
                    }
                }
                if (FunctionNodes.Num() == 0)
                {
                    UE_LOG(LogDebugBPNodeChangeCommandlet, Display, TEXT("Target Function Nodes is zero"));
                    continue;
                }

                for (const auto OldNode : FunctionNodes)
                {
                    // 新しい関数取得
                    UFunction* ReplaceFunction = FunctionOwnerClass->FindFunctionByName(*CommandletParameter.ReplaceFunctionName);
                    if (!ReplaceFunction)
                    {
                        UE_LOG(LogDebugBPNodeChangeCommandlet, Error, TEXT("CommandletParameter.ReplaceFunctionName(%s)  Node is null"), *CommandletParameter.ReplaceFunctionName);
                        continue;
                    }

                    auto NewNode = CreateFunctionNode(
                        Graph,
                        ReplaceFunction,
                        OldNode->NodePosX,
                        OldNode->NodePosY
                    );

                    if (!NewNode)
                    {
                        continue;
                    }
                    ReplacedNodeMap.Add(OldNode, NewNode);

                    ReplaceResult = true;
                }

                // 次のピンを確認して、必要あれば差し替え
                for(auto Node : ReplacedNodeMap)
                {
                    auto NewNode = Node.Value;
                    auto OldNode = Node.Key;

                    if (!OldNode->IsValidLowLevel() || !NewNode->IsValidLowLevel())
                    {
                        continue;
                    }

                    TArray<UEdGraphPin*> OldNodePins = OldNode->Pins;
                    for(int32 i = 0 ; i < OldNodePins.Num(); i++)
                    {
                        auto LinkedToList = OldNodePins[i]->LinkedTo;
                        for(int32 j = 0 ; j < LinkedToList.Num(); j++)
                        {
                            // 関数ノード以外もあるので、そちらは別途対応必要
                            if (auto NextNode = Cast<UK2Node_CallFunction>(LinkedToList[j]->GetOwningNode()))
                            {
                                if (!NextNode->GetTargetFunction())
                                {
                                    UE_LOG(LogDebugBPNodeChangeCommandlet, Display, TEXT("CallFuncNode Function is null"));
                                    continue;
                                }
                                
                                auto NextClassName = NextNode->GetTargetFunction()->GetOwnerClass()->GetName();
                                // 次のピンで該当のクラスが参照されていればそちらも更新
                                if (NextClassName == CommandletParameter.OwnerClassName)
                                {
                                    UE_LOG(LogDebugBPNodeChangeCommandlet, Log, TEXT("NextClassName(%s) is Update!!!"), *NextClassName);
                                    // 新しい関数取得
                                    UFunction* NextFunction = FunctionOwnerClass->FindFunctionByName(*NextNode->GetTargetFunction()->GetName());
                                    if (!NextFunction)
                                    {
                                        UE_LOG(LogDebugBPNodeChangeCommandlet, Error, TEXT("FunctionName(%s)  Node is null"), *NextNode->GetTargetFunction()->GetName());
                                        continue;
                                    }
                                    
                                    auto NewNextNode = CreateFunctionNode(
                                        Graph,
                                        NextFunction,
                                        NextNode->NodePosX,
                                        NextNode->NodePosY
                                    );
                                    
                                    ReplaceNodePinLinks(NextNode, NewNextNode);
                                    
                                    ReConnectPinLinks(NewNode, NewNextNode);

                                    // 削除対象として追加
                                    FunctionNodes.Add(NextNode);
                                } 
                            }
                        }
                    }
                    // 差し替え
                    ReplaceNodePinLinks(OldNode, NewNode);
                }
                // 古いノードを削除
                for (auto DeleteFunctionNode : FunctionNodes)
                {
                    if (Graph->RemoveNode(DeleteFunctionNode) == false)
                    {
                        UE_LOG(LogDebugBPNodeChangeCommandlet, Error, TEXT("%s is Remove Failed"), *DeleteFunctionNode->GetFName().ToString());
                        ReplaceResult = false;
                    }
                }

                // 置換配列を空にする 
                FunctionNodes.Empty();
                ReplacedNodeMap.Empty();
            }
            if (ReplaceResult == false)
            {
                continue;
            }

            // 置換でエラーがないか確認するためにコンパイル 
            FKismetEditorUtilities::CompileBlueprint(Blueprint);

            bool bDirty = Blueprint->MarkPackageDirty();

            bNeedSaveAsset = true;
        }

        if (bNeedSaveAsset)
        {
            PackagesToSave.Add(AssetData.GetPackage());
        }
    }

    // アセットセーブ
    FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSave, false, false, nullptr, true);
    
    return 0;
}

const FCommandletParameter ReplaceBPFunctionNodeCommandlet::GetParameter(const FString& CmdLineParams)
{
    FString FunctionOnwerClassName;
    if (FParse::Value(*CmdLineParams, TEXT("FunctionOnwerClassName="), FunctionOnwerClassName))
    {
        UE_LOG(LogDebugBPNodeChangeCommandlet, Log, TEXT("FunctionOnwerClassName : %s"), *FunctionOnwerClassName);
        if (FunctionOnwerClassName.IsEmpty())
        {
            UE_LOG(LogDebugBPNodeChangeCommandlet, Error, TEXT("FunctionOnwerClassName is Unknown"));
        }
    }

    FName AssetPath;
    if (FParse::Value(*CmdLineParams, TEXT("AssetPath="), AssetPath))
    {
        UE_LOG(LogDebugBPNodeChangeCommandlet, Log, TEXT("AssetPath : %s"), *AssetPath.ToString());
        if (AssetPath.ToString().IsEmpty())
        {
            UE_LOG(LogDebugBPNodeChangeCommandlet, Error, TEXT("AssetPath is Unknown"));
        }
    }

    FString NewFunctionOwnerClassName;
    if (FParse::Value(*CmdLineParams, TEXT("NewFunctionOwnerClassName="), NewFunctionOwnerClassName))
    {
        UE_LOG(LogDebugBPNodeChangeCommandlet, Log, TEXT("NewFunctionOwnerClassName : %s"), *NewFunctionOwnerClassName);
        if (NewFunctionOwnerClassName.IsEmpty())
        {
            UE_LOG(LogDebugBPNodeChangeCommandlet, Error, TEXT("NewFunctionOwnerClassName is Unknown"));
        }
    }
    
    FString FunctionName;
    if (FParse::Value(*CmdLineParams, TEXT("FunctionName="), FunctionName))
    {
        UE_LOG(LogDebugBPNodeChangeCommandlet, Log, TEXT("FunctionName : %s"), *FunctionName);
        if (FunctionName.IsEmpty())
        {
            UE_LOG(LogDebugBPNodeChangeCommandlet, Error, TEXT("FunctionName is Unknown"));
        }
    }

    FString NewFunctionName;
    if (FParse::Value(*CmdLineParams, TEXT("NewFunctionName="), NewFunctionName))
    {
        UE_LOG(LogDebugBPNodeChangeCommandlet, Log, TEXT("NewFunctionName : %s"), *NewFunctionName);
        if (NewFunctionName.IsEmpty())
        {
            UE_LOG(LogDebugBPNodeChangeCommandlet, Error, TEXT("NewFunctionName is Unknown"));
        }
    }
    return FCommandletParameter(
        FunctionOnwerClassName,
        NewFunctionOwnerClassName,
        FunctionName,
        NewFunctionName,
        AssetPath
    );
 
}

// 対象アセットリストを取得する
void ReplaceBPFunctionNodeCommandlet::GetAssetList(TArray<FAssetData>& OutAssetList, const FName TargetAssetPath) const
{
    const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(FName("AssetRegistry"));
    const IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();

    IFileManager& FileMgr = IFileManager::Get();

    FARFilter Filter;
    Filter.bRecursivePaths = true;
    Filter.PackagePaths.Add(TargetAssetPath);

    // 対象クラス指定
    Filter.ClassNames.Add(UBlueprint::StaticClass()->GetFName());
    Filter.ClassNames.Add(UWorld::StaticClass()->GetFName());
    Filter.ClassNames.Add(UWidgetBlueprint::StaticClass()->GetFName());

    for(auto Name : Filter.ClassNames )
    {
        UE_LOG(LogDebugBPNodeChangeCommandlet, Display, TEXT("Filter.ClassNames : %s"), *Name.ToString());
    }

    AssetRegistry.GetAssets(Filter, OutAssetList);
}

void ReplaceBPFunctionNodeCommandlet::ReplaceNodePinLinks(UEdGraphNode* OldNode, UEdGraphNode* NewNode)
{
    if (!OldNode || !NewNode)
    {
        UE_LOG(LogDebugBPNodeChangeCommandlet, Display, TEXT("OldNodePin : OldNode->Pins null"));
        return;
    }
    for (const auto NewNodePin : NewNode->Pins)
    {
        // 置き換え対象のピンがあったら 
        auto OldNodePin = OldNode->FindPin(NewNodePin->PinName);
        if (OldNodePin != nullptr && OldNodePin->GetSchema())
        {
            // ピンの置き換え 
            OldNodePin->GetSchema()->MovePinLinks(*OldNodePin, *NewNodePin);
        }
    }
    // 古いノードのピンのリンクを全て切る 
    OldNode->BreakAllNodeLinks(); 
}

void ReplaceBPFunctionNodeCommandlet::ReConnectPinLinks(UK2Node_CallFunction* Node, UEdGraphNode* NextNode)
{
    if (Node == nullptr || NextNode == nullptr)
    {
        return;
    }
    
    for (const auto Pin : NextNode->Pins)
    {
        if (Pin->Direction == EGPD_Input && Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Object)
        {
            UE_LOG(LogDebugBPNodeChangeCommandlet, Log, TEXT("New Node Connected. Pin->GetDisplayName(%s)"), *Pin->GetDisplayName().ToString()); 
            Node->GetReturnValuePin()->MakeLinkTo(Pin);
            break;
        }
    }
}

UK2Node_CallFunction* ReplaceBPFunctionNodeCommandlet::CreateFunctionNode(
    UEdGraph* TargetGraph,
    UFunction* NewFunction,
    const int32 NodePosX,
    const int32 NodePosY)
{
    UK2Node_CallFunction* NewNode = nullptr;
    if (TargetGraph == nullptr || NewFunction == nullptr)
    {
        return NewNode;
    }
    
    // ノード生成
    FGraphNodeCreator<UK2Node_CallFunction> FunctionNodeCreator(*TargetGraph);
    
    NewNode = FunctionNodeCreator.CreateNode(false);
    if (NewNode != nullptr)
    {
        // 関数を設定
        NewNode->SetFromFunction(NewFunction);
    
        // 設定した関数に対応しピンを生成
        NewNode->CreatePinsForFunctionCall(NewFunction);
    
        // 座標設定 
        NewNode->NodePosX = NodePosX;
        NewNode->NodePosY = NodePosY;
    
        // ノード生成完了 
        FunctionNodeCreator.Finalize();
    }
    return NewNode;
}

```

# 6.コマンドレット実行

```
/{Root}/Engine/Binaries/Win64/UE4Editor-Cmd.exe /{Root}/{ProjectName}/{ProjectName}.uproject -run=DebugReplaceBPFunctionNode FunctionOnwerClassName={FunctionOnwerClassName} NewFunctionOwnerClassName={NewFunctionOwnerClassName} FunctionName={FunctionName} NewFunctionName={NewFunctionName} AssetPath={AssetPath} -stdout -UTF8Output
```

※コードの修正後はコンパイルしてから実行しないと反映されません。

# 参考

https://historia.co.jp/archives/3841/

https://qiita.com/unknown_ds/items/fc53af4a0c46cba63201