Files
integration/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp
2026-04-01 17:45:33 -04:00

627 lines
17 KiB
C++

#include "WingUtils.h"
#include "WingActorComponent.h"
#include "WingProperty.h"
#include "WingTypes.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingTokenizer.h"
#include "Engine/Blueprint.h"
#include "Engine/MemberReference.h"
#include "Engine/World.h"
#include "Components/ActorComponent.h"
#include "Engine/SCS_Node.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "K2Node_EditablePinBase.h"
#include "EdGraph/EdGraphSchema.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/Kismet2NameValidators.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "UObject/UObjectIterator.h"
#include "UObject/UnrealType.h"
#include "Misc/Paths.h"
#include "Misc/PackageName.h"
// Reparent validation
#include "Engine/LevelScriptActor.h"
#include "Animation/AnimInstance.h"
#include "Blueprint/UserWidget.h"
// Animation Blueprint support
#include "AnimStateNode.h"
#include "AnimStateTransitionNode.h"
#include "AnimationStateMachineGraph.h"
// Material support
#include "Materials/Material.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialFunction.h"
#include "Materials/MaterialInstanceConstant.h"
#include "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphSchema.h"
#include "IMaterialEditor.h"
#include "Subsystems/AssetEditorSubsystem.h"
// Mesh, animation, texture support
#include "Engine/StaticMesh.h"
#include "Engine/SkeletalMesh.h"
#include "Animation/AnimSequence.h"
#include "Animation/BlendSpace.h"
#include "Engine/Texture.h"
// ============================================================
// Name sanitization
// ============================================================
FName WingUtils::GetFName(const FWingProperty &Prop) { return Prop.Prop->GetFName(); }
FString WingUtils::ExternalizeID(FName Name)
{
return WingTokenizer::ExternalizeID(Name);
}
FName WingUtils::CheckInternalizeID(const FString &ExternalID)
{
FString Error;
FName InternalID = WingTokenizer::TryInternalizeID(ExternalID, Error);
if (!Error.IsEmpty())
{
UWingServer::Printf(TEXT("%s\n"), *Error);
UWingServer::SuggestManual(WingManual::Section::EscapeSequences);
}
return InternalID;
}
FName WingUtils::CheckProposedName(const FString &ExternalID)
{
FName InternalID = CheckInternalizeID(ExternalID);
if (!InternalID.IsNone() && !WingTokenizer::WouldExternalizeReadably(InternalID))
{
UWingServer::Printf(TEXT("ERROR: id %s would not be a readable id, may not create item with this name"),
*ExternalID);
UWingServer::SuggestManual(WingManual::Section::EscapeSequences);
return FName();
}
return InternalID;
}
FString WingUtils::StandardizeMenuItem(const FString &Item)
{
FString Sanitized = Item;
int32 Dst = 0;
bool Upper = true;
for (int32 Src = 0; Src < Sanitized.Len(); Src++)
{
TCHAR c = Sanitized[Src];
if (FChar::IsAlnum(c))
{
if (Upper) c = FChar::ToUpper(c);
Sanitized[Dst++] = c;
Upper = false;
}
else
{
Upper = true;
if ((c <= 0x20)||(c == 0x7F)) continue;
if (c == ':') c = L'';
Sanitized[Dst++] = c;
}
}
Sanitized.LeftInline(Dst);
return Sanitized;
}
// ============================================================
// Name Lookup
// ============================================================
FString WingUtils::FormatName(const UWorld *World)
{
return World->GetPathName();
}
FString WingUtils::FormatName(const UBlueprint *BP)
{
return UWingTypes::TypeToTextOrDie(BP);
}
FString WingUtils::FormatName(const UActorComponent *C)
{
return ExternalizeID(C->GetFName());
}
FString WingUtils::FormatName(const USCS_Node *Node)
{
return ExternalizeID(Node->GetVariableName());
}
FString WingUtils::FormatName(const UEdGraph *Graph)
{
return ExternalizeID(Graph->GetFName());
}
FString WingUtils::FormatName(const TObjectPtr<UEdGraph> &Graph)
{
return ExternalizeID(Graph->GetFName());
}
FString WingUtils::FormatName(const UEdGraphNode* Node)
{
return ExternalizeID(Node->GetFName());
}
FString WingUtils::FormatName(const UEdGraphPin *Pin)
{
return ExternalizeID(Pin->GetFName());
}
FString WingUtils::FormatName(const FMemberReference &Ref)
{
return ExternalizeID(Ref.GetMemberName());
}
FString WingUtils::FormatName(const FBPVariableDescription &Var)
{
return ExternalizeID(Var.VarName);
}
FString WingUtils::FormatName(const UStruct *Struct)
{
if (Cast<UScriptStruct>(Struct) || Cast<UClass>(Struct))
return UWingTypes::TypeToTextOrDie(Struct);
return ExternalizeID(Struct->GetFName());
}
FString WingUtils::FormatName(const UClass *Class)
{
return UWingTypes::TypeToTextOrDie(Class);
}
FString WingUtils::FormatName(const UMaterial *Material)
{
return Material->GetPathName();
}
FString WingUtils::FormatName(const UMaterialInstance *MaterialInstance)
{
return MaterialInstance->GetPathName();
}
FString WingUtils::FormatName(const UMaterialFunction *MaterialFunction)
{
return MaterialFunction->GetPathName();
}
FString WingUtils::FormatName(const UMaterialExpression *Expression)
{
return ExternalizeID(Expression->GetFName());
}
FString WingUtils::FormatName(const UStaticMesh *Mesh)
{
return Mesh->GetPathName();
}
FString WingUtils::FormatName(const USkeletalMesh *Mesh)
{
return Mesh->GetPathName();
}
FString WingUtils::FormatName(const UAnimSequence *Anim)
{
return Anim->GetPathName();
}
FString WingUtils::FormatName(const UBlendSpace *BlendSpace)
{
return BlendSpace->GetPathName();
}
FString WingUtils::FormatName(const UTexture *Texture)
{
return Texture->GetPathName();
}
FString WingUtils::FormatName(const UScriptStruct *Struct)
{
return UWingTypes::TypeToTextOrDie(Struct);
}
FString WingUtils::FormatName(const UEnum *Enum)
{
return UWingTypes::TypeToTextOrDie(Enum);
}
FString WingUtils::FormatName(const FProperty *Prop)
{
return ExternalizeID(Prop->GetFName());
}
FString WingUtils::FormatName(const FUserPinInfo &Pin)
{
return ExternalizeID(Pin.PinName);
}
FString WingUtils::FormatName(const FBPInterfaceDescription &IFace)
{
return FormatName(IFace.Interface);
}
FString WingUtils::FormatName(const UWingComponentReference *Ref)
{
return ExternalizeID(Ref->VariableName);
}
FString WingUtils::FormatName(const UWidget *Widget)
{
return ExternalizeID(Widget->GetFName());
}
// ============================================================
// Formatting other things
// ============================================================
FString WingUtils::FormatNodeTitle(const UEdGraphNode *Node)
{
FString Title = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
int32 NewlineIdx;
if (Title.FindChar(TEXT('\n'), NewlineIdx))
Title.LeftInline(NewlineIdx);
return Title;
}
// ============================================================
// JSON helpers
// ============================================================
// ============================================================
// Text formatting
// ============================================================
FString WingUtils::WrapText(const FString& Text, int32 ColLimit, const FString& Prefix)
{
TArray<FString> Words;
Text.ParseIntoArrayWS(Words);
TStringBuilder<1024> Result;
int32 Col = 0;
for (const FString& Word : Words)
{
if ((Col > 0) && (Col + 1 + Word.Len() > ColLimit))
{
Result.Append(TEXT("\n"));
Col = 0;
}
if (Col == 0)
{
Result.Append(Prefix);
Result.Append(Word);
Col = Prefix.Len() + Word.Len();
}
else
{
Result.Append(TEXT(" "));
Result.Append(Word);
Col = 1 + Word.Len();
}
}
if (Col > 0) Result.Append(TEXT("\n"));
return Result.ToString();
}
// ============================================================
// Enum helpers
// ============================================================
FString WingUtils::EnumToString(UEnum* Enum, int64 Value)
{
return Enum->GetNameStringByValue(Value);
}
bool WingUtils::StringToEnum(UEnum* Enum, const FString& Str, int64& OutValue)
{
int32 Index = Enum->GetIndexByNameString(Str);
if (Index == INDEX_NONE)
{
FString Prefix = Enum->GenerateEnumPrefix();
if (!Prefix.IsEmpty())
Index = Enum->GetIndexByNameString(Prefix + TEXT("_") + Str);
}
if (Index == INDEX_NONE)
{
UWingServer::Printf(TEXT("ERROR: '%s' is not a valid value for %s\n"), *Str, *Enum->GetName());
OutValue = 0;
return false;
}
OutValue = Enum->GetValueByIndex(Index);
return true;
}
// ============================================================
// Common Error Reporting
// ============================================================
bool WingUtils::CheckExactlyOneNamed(int Count, const TCHAR *Kind, const FString &Name)
{
if (Count == 0)
{
UWingServer::Printf(TEXT("Could not find a %s named %s.\n"), Kind, *Name);
return false;
}
if (Count > 1)
{
UWingServer::Printf(TEXT("More than one %s named %s\n"), Kind, *Name);
return false;
}
return true;
}
bool WingUtils::CheckExactlyOneNamed(int Count, const TCHAR *Kind, FName Name)
{
return CheckExactlyOneNamed(Count, Kind, ExternalizeID(Name));
}
bool WingUtils::CheckExactlyNoneNamed(int Count, const TCHAR *Kind, const FString &Name)
{
if (Count > 0)
{
UWingServer::Printf(TEXT("A %s named %s already exists."), Kind, *Name);
return false;
}
return true;
}
bool WingUtils::CheckExactlyNoneNamed(int Count, const TCHAR *Kind, FName Name)
{
return CheckExactlyNoneNamed(Count, Kind, ExternalizeID(Name));
}
bool WingUtils::CheckCanRename(UEdGraphNode* Node, const FString &Name)
{
if (!Node->bCanRenameNode)
{
UWingServer::Printf(TEXT("ERROR: Node %s does not support renaming.\n"), *FormatName(Node));
return false;
}
TSharedPtr<INameValidatorInterface> Validator = FNameValidatorFactory::MakeValidator(Node);
EValidatorResult Result = Validator->IsValid(Name, false);
if (Result != EValidatorResult::Ok && Result != EValidatorResult::ExistingName)
{
UWingServer::Printf(TEXT("ERROR: %s\n"), *INameValidatorInterface::GetErrorString(Name, Result));
return false;
}
return true;
}
// ============================================================
// Reparent validation
// ============================================================
bool WingUtils::CanReparentBlueprint(UClass* CurrentGenerated, UClass* Proposed)
{
if (!CurrentGenerated || !Proposed) return false;
UClass* CurrentParent = CurrentGenerated->GetSuperClass();
if (!CurrentParent) return false;
// Don't allow parenting to itself or one of its children
if (Proposed->IsChildOf(CurrentGenerated)) return false;
// Don't allow parenting to an interface
if (Proposed->IsChildOf(UInterface::StaticClass())) return false;
// LevelScriptActor blueprints stay in the LevelScriptActor hierarchy
if (CurrentParent->IsChildOf(ALevelScriptActor::StaticClass()))
return Proposed->IsChildOf(ALevelScriptActor::StaticClass());
// Actor blueprints stay in the Actor hierarchy, but not LevelScriptActor
if (CurrentParent->IsChildOf(AActor::StaticClass()))
return Proposed->IsChildOf(AActor::StaticClass()) &&
!Proposed->IsChildOf(ALevelScriptActor::StaticClass());
// Component blueprints stay in the ActorComponent hierarchy
if (CurrentParent->IsChildOf(UActorComponent::StaticClass()))
return Proposed->IsChildOf(UActorComponent::StaticClass());
// Anim blueprints stay in the AnimInstance hierarchy
if (CurrentParent->IsChildOf(UAnimInstance::StaticClass()))
return Proposed->IsChildOf(UAnimInstance::StaticClass());
// Widget blueprints stay in the UserWidget hierarchy
if (CurrentParent->IsChildOf(UUserWidget::StaticClass()))
return Proposed->IsChildOf(UUserWidget::StaticClass());
return false;
}
// ============================================================
// Blueprint helpers
// ============================================================
TArray<UEdGraph*> WingUtils::AllGraphs(UBlueprint* BP)
{
TArray<UEdGraph*> Graphs;
BP->GetAllGraphs(Graphs);
return Graphs;
}
TArray<UEdGraphNode*> WingUtils::AllNodes(UBlueprint* BP)
{
TArray<UEdGraphNode*> Nodes;
for (UEdGraph* Graph : AllGraphs(BP))
Nodes.Append(Graph->Nodes);
return Nodes;
}
TArray<UEdGraphNode*> WingUtils::AllNodes(UEdGraph *Graph)
{
TArray<UEdGraphNode*> Result;
Result.Append(Graph->Nodes);
return Result;
}
TArray<UBlueprint*> WingUtils::GetAncestorBlueprints(UBlueprint *BP, bool OldestFirst)
{
TArray<UBlueprint*> Blueprints;
for (UBlueprint* WalkBP = BP; WalkBP; )
{
Blueprints.Add(WalkBP);
WalkBP = UBlueprint::GetBlueprintFromClass(WalkBP->ParentClass);
}
if (OldestFirst) Algo::Reverse(Blueprints);
return Blueprints;
}
UObject *WingUtils::GetGeneratedCDO(UBlueprint *BP)
{
if (BP->GeneratedClass == nullptr) return nullptr;
return BP->GeneratedClass->GetDefaultObject();
}
// ============================================================
// Material helpers
// ============================================================
void WingUtils::EnsureMaterialGraph(UMaterial* Material)
{
if (!Material) return;
if (!Material->MaterialGraph)
{
// In commandlet/headless mode the MaterialGraph is not auto-created.
// Replicate what the Material Editor does on open (MaterialEditor.cpp:619).
Material->MaterialGraph = CastChecked<UMaterialGraph>(
FBlueprintEditorUtils::CreateNewGraph(
Material, NAME_None,
UMaterialGraph::StaticClass(),
UMaterialGraphSchema::StaticClass()));
Material->MaterialGraph->Material = Material;
Material->MaterialGraph->RebuildGraph();
}
}
UMaterial* WingUtils::ReplaceMaterialWithTransientCopy(UMaterial* Material)
{
if (!Material) return nullptr;
// Already a preview material — nothing to do.
if (Material->GetOutermost() == GetTransientPackage())
return Material;
// If the material editor has a transient preview copy open, get it
// via the editor API. This follows the same pattern as Epic's
// MaterialEditingLibrary (FindMaterialEditorForAsset).
UAssetEditorSubsystem* Sub = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
IAssetEditorInstance* EditorInstance = Sub ? Sub->FindEditorForAsset(Material, false) : nullptr;
if (EditorInstance)
{
// This is a weird hack. We know that the IAssetEditorInstance for a material
// is always going to be an FMaterialEditor, which conforms to IMaterialEditor.
// If that weren't the case, this unsafe code would crash hard. However,
// lots of places in unreal use this same unsafe pattern.
IMaterialEditor* MatEditor = static_cast<IMaterialEditor*>(EditorInstance);
UMaterialInterface* Edited = MatEditor->GetMaterialInterface();
if (UMaterial* EditedMat = Cast<UMaterial>(Edited))
return EditedMat;
}
return Material;
}
// ============================================================
// Anim blueprint helpers
// ============================================================
UAnimationStateMachineGraph* WingUtils::FindStateMachineGraph(UBlueprint* BP, const FString& GraphName)
{
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
if (UAnimationStateMachineGraph* SMGraph = Cast<UAnimationStateMachineGraph>(Graph))
{
if (SMGraph->GetName() == GraphName)
{
return SMGraph;
}
}
}
return nullptr;
}
UAnimStateNode* WingUtils::FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName)
{
for (UEdGraphNode* Node : SMGraph->Nodes)
{
if (UAnimStateNode* StateNode = Cast<UAnimStateNode>(Node))
{
if (StateNode->GetStateName() == StateName)
{
return StateNode;
}
}
}
UWingServer::Printf(TEXT("ERROR: State '%s' not found in graph '%s'\n"), *StateName, *SMGraph->GetName());
return nullptr;
}
UAnimStateTransitionNode* WingUtils::FindTransition(UAnimationStateMachineGraph* SMGraph,
const FString& FromStateName, const FString& ToStateName)
{
for (UEdGraphNode* Node : SMGraph->Nodes)
{
if (UAnimStateTransitionNode* TransNode = Cast<UAnimStateTransitionNode>(Node))
{
UAnimStateNode* FromState = Cast<UAnimStateNode>(TransNode->GetPreviousState());
UAnimStateNode* ToState = Cast<UAnimStateNode>(TransNode->GetNextState());
if (FromState && ToState &&
(FromState->GetStateName() == FromStateName) &&
(ToState->GetStateName() == ToStateName))
{
return TransNode;
}
}
}
return nullptr;
}
// ============================================================
// Support for locating UE Wingman Handlers
// ============================================================
TArray<UClass*> WingUtils::CollectHandlerClasses()
{
TArray<UClass*> Result;
for (TObjectIterator<UClass> It; It; ++It)
{
UClass* Class = *It;
if (Class->HasAnyClassFlags(CLASS_Abstract)) continue;
if (!Class->IsChildOf(UWingHandler::StaticClass())) continue;
Result.Add(Class);
}
Result.Sort([](UClass& A, UClass& B) { return GetHandlerName(&A) < GetHandlerName(&B); });
return Result;
}
FString WingUtils::GetHandlerName(UClass* HandlerClass)
{
FString Name = HandlerClass->GetName();
Name.RemoveFromStart(TEXT("Wing_"));
return Name;
}
FString WingUtils::GetHandlerGroup(UClass* HandlerClass)
{
FString Name = HandlerClass->GetName();
Name.RemoveFromStart(TEXT("Wing_"));
// Everything before the underscore is the group
int32 UnderscoreIdx;
if (Name.FindChar(TEXT('_'), UnderscoreIdx))
return Name.Left(UnderscoreIdx);
return Name;
}