Blueprint Function Library - Function Redirectors

I am trying to refactor a complex Blueprint function library into smaller libraries; largely to reduce the asset refences graph explosion that can occur from big libraries that cover multiple areas. These BPFL are created in blueprint and do not have a C++ representation.

Part of this work I’ve been moving functions from one BPFL to another and trying to use Function Core Directs to redirect the references from the old library to the new one.

I am hitting all the issues in this [old (closed) [Content removed] in that whatever I attempt, the function redirect never applies. I’ve tried all sorts of permutations of the asset name (from `MyFunctionLib.Function` to `/Game/MyFunctionLib.MyFunctionLib_C.Function` and none of them are working.

Is it possible to use core redirectors with blueprint-based function libraries?

[Attachment Removed]

Steps to Reproduce
Repro steps:

  • Create a Blueprint-based BPFL with a function
  • Create a blueprint that calls this function
  • Create a new Blueprint-based BPFL with a clone of the function
  • Add a core redirector of type “Function” from the old BPFL function to the new library
  • Delete the original function
  • Note that the blueprint creates a compile error as it doesn’t resolve the function
    [Attachment Removed]

Good day. Although I’m not sure if CoreRedirects will be useful to redirect specific functions in a BPFL, it’s possible to achieve what you want by writing an editor utility function that automatically finds and updates blueprint nodes.

Put the following function in an editor module (to avoid packaging problems later) with “UnrealEd” and “BlueprintGraph” added to Build.cs:

#pragma once
 
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "BlueprintRefactorUtils.generated.h"
 
UCLASS()
class ABILITIESLAB_API UBlueprintRefactorUtils : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()
 
public:
	// Searches all loaded blueprints to find function call nodes of some name, 
	// then replaces them to call the new function. Signatures must match.
	// Blueprints must have been loaded, and must be compiled afterwards.
	// bPrintOnly controls whether to replace nodes or just print results.
	UFUNCTION(BlueprintCallable, meta=(CallInEditor))
	static void ReplaceFunction(TSubclassOf<UBlueprintFunctionLibrary> OldLib, const FName OldFunctionName,
		TSubclassOf<UBlueprintFunctionLibrary> NewLib, const FName NewFunctionName, const bool bPrintOnly);
};
#include "BlueprintRefactorUtils.h"
#include "K2Node_CallFunction.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "GenericPlatform/GenericPlatformMisc.h"
 
void UBlueprintRefactorUtils::ReplaceFunction(TSubclassOf<UBlueprintFunctionLibrary> OldLib, const FName OldFunctionName, TSubclassOf<UBlueprintFunctionLibrary> NewLib, const FName NewFunctionName, const bool bPrintOnly)
{
	UFunction* OldFunc = OldLib->FindFunctionByName(OldFunctionName);
	if (!OldFunc)
	{
		FString Msg = FString::Printf(TEXT("Did not find old function: %s"), *OldFunctionName.ToString());
		FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(Msg));
		return;
	}
 
	UFunction* NewFunc = NewLib->FindFunctionByName(NewFunctionName);
	if (!NewFunc)
	{
		FString Msg = FString::Printf(TEXT("Did not find new function: %s"), * NewFunctionName.ToString());
		FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(Msg));
		return;
	}
 
	if (!OldFunc->IsSignatureCompatibleWith(NewFunc))
	{
		FMessageDialog::Open(EAppMsgType::Ok, FText::FromString("Found function signatures were incompatible"));
		return;
	}
 
	UFunction* SkelFunction = nullptr;
	UBlueprint* OldLibBP = nullptr;
	if (UBlueprintGeneratedClass* BpClassOwner = Cast<UBlueprintGeneratedClass>(OldLib))
	{
		OldLibBP = CastChecked<UBlueprint>(BpClassOwner->ClassGeneratedBy.Get(), ECastCheckedType::NullAllowed);
		if (OldLibBP && OldLibBP->SkeletonGeneratedClass)
		{
			SkelFunction = OldLibBP->SkeletonGeneratedClass->FindFunctionByName(OldFunctionName);
		}
	}
	if (!SkelFunction)
	{
		FString Msg = FString::Printf(TEXT("Did not find old function on skeleton class: %s"), *OldFunctionName.ToString());
		return;
	}
 
	TArray<UObject*> FoundObjects;
	GetObjectsOfClass(UBlueprint::StaticClass(), FoundObjects);
 
	int32 NumTotalChanges = 0;
	for (UObject* Obj : FoundObjects)
	{
		UBlueprint* BP = CastChecked<UBlueprint>(Obj);
 
		// Don't do find and replace in the old BP library itself
		if (BP == OldLibBP)
		{
			continue;
		}
 
		// Gather all graphs to process. This does NOT cover all possible
		// blueprint graphs. There may be special graphs on other asset
		// types like animation blueprints that aren't processed with this.
		TArray<TObjectPtr<UEdGraph>> GraphsToProcess;
		GraphsToProcess.Append(BP->EventGraphs);
		GraphsToProcess.Append(BP->FunctionGraphs);
		GraphsToProcess.Append(BP->UbergraphPages);
		GraphsToProcess.Append(BP->MacroGraphs);
 
		// Process the graphs of the current BP
		int32 NumFileChanges = 0;
		for (TObjectPtr<UEdGraph>& Graph : GraphsToProcess)
		{
			// Find and process all function call nodes
			TArray<UK2Node_CallFunction*> CallFunctionNodes;
			Graph->GetNodesOfClass<UK2Node_CallFunction>(CallFunctionNodes);
 
			for (UK2Node_CallFunction* CallFuncNode : CallFunctionNodes)
			{
				// Find the function currently being called by the node
				UFunction* NodeFunc = CallFuncNode->GetTargetFunction();
				if (!NodeFunc)
				{
					continue;
				}
 
				// Check if the function is the one we want to replace
				if (NodeFunc == OldFunc || NodeFunc == SkelFunction)
				{
					// Replace the function
					if (!bPrintOnly)
					{
						CallFuncNode->SetFromFunction(NewFunc);
						CallFuncNode->ReconstructNode();
					}
					++NumFileChanges;
				}
			}
		}
 
		if (NumFileChanges > 0)
		{
			UE_LOG(LogTemp, Warning, TEXT("%d nodes %s in blueprint '%s'"), NumFileChanges, bPrintOnly ? TEXT("found") : TEXT("changed"), *BP->GetPackage()->GetPathName());
			if (!bPrintOnly)
			{
				FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
			}
			NumTotalChanges += NumFileChanges;
		}
	}
 
	// Print total
	if (!bPrintOnly)
	{
		FString Msg = FString::Printf(TEXT("Replaced %s -> %s in %d places"), *OldFunctionName.ToString(), *NewFunctionName.ToString(), NumTotalChanges);
		FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(Msg));
	}
	else
	{
		FString Msg = FString::Printf(TEXT("Found %s -> %s to replace in %d places"), *OldFunctionName.ToString(), *NewFunctionName.ToString(), NumTotalChanges);
		FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(Msg));
	}
}

And add this to the editor module’s Build.cs:

if (Target.bBuildEditor)
{
     PrivateDependencyModuleNames.AddRange(new string[] { "BlueprintGraph", "UnrealEd" });
}

Then use the function in the following way:

  1. Launch the editor
  2. Open the old blueprint
  3. Find References > By Class Member (All) on the function to replace
  4. Open all the blueprints that reference the old function. Note: this step is because my utility function will only process loaded blueprints, and it could be very heavy to load all blueprints. So here we’re leveraging that blueprint search can be used to load only the relevant BPs.
  5. Create an editor utility widget with a button that will run the above functions. Pass in the old BP class + function, the new BPFL class + function. Set PrintOnly to true if you want to just see first how many hits you’d find, or false to do actual replacement.

Be sure to back up assets before doing this, and test thoroughly afterwards. Be aware that I just wrote this today, for this specific use case. Besides checking that the new function is being called at runtime, of course, also check the following:

  • Whether Find References on modified assets after resaving still result in a reference to the old library. It shouldn’t if all function calls have been updated.
  • Whether Find References > By Class Member (All) on the old BPFL function still finds the modified assets

And check if there are other types of blueprint graphs/nodes to update. The above function only replaces the basic ‘Call Function’ node types in common BP graphs, but other graph types might not be covered.

[Attachment Removed]

Here is an example editor utility widget with a run button that finds and replace function calls of a BP function library ‘BPFL_OldLib’ with a function from ‘BPFL_NewLib’.

[Image Removed]

Hopefully this covers your refactoring needs.

[Attachment Removed]

Hey Oli,

(Sorry for the delay)

So I just tried this on my end:

[CoreRedirects]

+FunctionRedirects=(OldName=“/Game/NewFunctionLibrary.NewFunctionLibrary_C.TestFunction”,NewName=“/Game/NewFunctionLibrary.NewFunctionLibrary_C.OtherFunction”)

And that definitely worked for me. This was also in my game’s DefaultEngine.ini file. Is this for a plugin? What other entries did you try here?

But to answer your initial question: BPFLs should work with CoreRedirects, and it’s a bug if you found a case where it doesn’t. Usually, the trick is finding the right incantation in the .ini file to use. _Sometimes_, running the editor with “-DebugCoreRedirects” can help diagnose the problem, but there’s usually a lot of noise to go through.

Zhi Kang’s script will definitely help, but it shouldn’t be necessary in theory.

Thanks,

Dave

[Attachment Removed]

Thank you for this - I appreciate the work you put in to help us!

I have run a test locally and it does seem to work well in the test case I put together; I’ll take a look at testing it a little more in the production codebase next week.

>Be sure to back up assets before doing this, and test thoroughly afterwards. Be aware that I just wrote this today, for this specific use case. Besides checking that the new function is being called at runtime, of course, also check the following… <snip>

Noted; thank you.

[Attachment Removed]

I’m happy to hear the snippet helped!

[Attachment Removed]