MCPFetcher, and new code for FormatName

This commit is contained in:
2026-03-10 00:22:56 -04:00
parent 9329c8399b
commit 2a5833fe04
12 changed files with 592 additions and 230 deletions

View File

@@ -1,4 +1,5 @@
#include "BlueprintExporter.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
@@ -26,37 +27,6 @@ FlxBlueprintExporter::FlxBlueprintExporter(UEdGraph* InGraph)
//
////////////////////////////////////////////////////////
FString FlxBlueprintExporter::SanitizeName(const FString& Title)
{
FString Result = Title.TrimStartAndEnd().Replace(TEXT(" "), TEXT("_"));
return Result.IsEmpty() ? TEXT("_") : Result;
}
FString FlxBlueprintExporter::FormatPinType(const FEdGraphPinType& PinType)
{
if (UObject* SubObj = PinType.PinSubCategoryObject.Get())
{
return SubObj->GetName();
}
FString Type = PinType.PinCategory.ToString();
Type[0] = FChar::ToUpper(Type[0]);
return Type;
}
FString FlxBlueprintExporter::FormatPinType(UEdGraphPin* Pin)
{
return FormatPinType(Pin->PinType);
}
FString FlxBlueprintExporter::FormatNodeBaseName(UEdGraphNode* Node)
{
FString Title = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
int32 NewlineIdx;
if (Title.FindChar(TEXT('\n'), NewlineIdx))
Title.LeftInline(NewlineIdx);
return SanitizeName(Title);
}
UEdGraphPin* FlxBlueprintExporter::GetLinkedTo(UEdGraphPin* Pin)
{
while (true)
@@ -114,60 +84,6 @@ UEdGraphPin* FlxBlueprintExporter::FindFirstPin(UEdGraphNode* Node, EEdGraphPinD
return nullptr;
}
UEdGraphPin* FlxBlueprintExporter::BestMatchPin(UEdGraphNode* Node, EEdGraphPinDirection Direction, bool Exec, const FString& Name)
{
if (Name == TEXT("_")) return nullptr;
UEdGraphPin* DisplayMatch = nullptr;
int32 DisplayCount = 0;
UEdGraphPin* RawMatch = nullptr;
int32 RawCount = 0;
for (UEdGraphPin* Pin : Node->Pins)
{
if (Pin->Direction != Direction) continue;
bool PinIsExec = (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec);
if (PinIsExec != Exec) continue;
if (Name == SanitizeName(Node->GetPinDisplayName(Pin).ToString()))
{
DisplayMatch = Pin;
DisplayCount++;
}
if (Name == SanitizeName(Pin->PinName.ToString()))
{
RawMatch = Pin;
RawCount++;
}
}
if (DisplayCount == 1) return DisplayMatch;
if (RawCount == 1) return RawMatch;
return nullptr;
}
FString FlxBlueprintExporter::FormatPinName(UEdGraphPin *Pin)
{
UEdGraphNode* Node = Pin->GetOwningNode();
bool Exec = (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec);
// Try sanitized display name first.
FString SanitizedDisplay = SanitizeName(Node->GetPinDisplayName(Pin).ToString());
if (BestMatchPin(Node, Pin->Direction, Exec, SanitizedDisplay) == Pin)
return SanitizedDisplay;
// Try sanitized raw name.
FString SanitizedRaw = SanitizeName(Pin->PinName.ToString());
if (BestMatchPin(Node, Pin->Direction, Exec, SanitizedRaw) == Pin)
return SanitizedRaw;
// No unambiguous name found.
UE_LOG(LogTemp, Warning, TEXT("Blueprint export: ambiguous pin name '%s' on node '%s'"),
*Pin->PinName.ToString(), *Node->GetNodeTitle(ENodeTitleType::ListView).ToString());
return FString::Printf(TEXT("?%s"), *SanitizedRaw);
}
////////////////////////////////////////////////////////
//
@@ -175,12 +91,6 @@ FString FlxBlueprintExporter::FormatPinName(UEdGraphPin *Pin)
//
////////////////////////////////////////////////////////
FString FlxBlueprintExporter::FormatNodeName(UEdGraphNode* Node)
{
FString* Name = NodeNames.Find(Node);
return Name ? *Name : FormatNodeBaseName(Node);
}
FString FlxBlueprintExporter::FormatPinSource(UEdGraphPin* Pin)
{
// If connected, show source node.pin
@@ -191,10 +101,10 @@ FString FlxBlueprintExporter::FormatPinSource(UEdGraphPin* Pin)
// For variable get nodes, just show the variable name.
if (UK2Node_VariableGet* VarGet = Cast<UK2Node_VariableGet>(LinkedToNode))
return SanitizeName(VarGet->GetVarNameString());
return MCPUtils::FormatName(VarGet->VariableReference);
FString PinLabel = FormatPinName(LinkedTo);
return FString::Printf(TEXT("%s.%s"), *FormatNodeName(LinkedToNode), *PinLabel);
FString PinLabel = MCPUtils::FormatName(LinkedTo);
return FString::Printf(TEXT("%s.%s"), *MCPUtils::FormatName(LinkedToNode), *PinLabel);
}
// String pins: always show in quotes (even if empty).
@@ -286,16 +196,7 @@ void FlxBlueprintExporter::SortNodes()
void FlxBlueprintExporter::AssignNodeNames()
{
TMap<FString, int32> NextIndex;
for (UEdGraphNode* Node : SortedNodes)
{
FString Base = FormatNodeBaseName(Node);
int32& Idx = NextIndex.FindOrAdd(Base, 0);
FString Name = (Idx == 0) ? Base : FString::Printf(TEXT("%s_%d"), *Base, Idx + 1);
NodeNames.Add(Node, Name);
Idx++;
}
// Node names are now computed on the fly by FormatNodeName.
}
void FlxBlueprintExporter::EmitNode(UEdGraphNode* Node)
@@ -306,7 +207,7 @@ void FlxBlueprintExporter::EmitNode(UEdGraphNode* Node)
return;
}
Output.Appendf(TEXT("\nnode %s\n"), *FormatNodeName(Node));
Output.Appendf(TEXT("\nnode %s\n"), *MCPUtils::FormatName(Node));
// Emit input data pins.
for (UEdGraphPin* Pin : FilterPins(Node, EGPD_Input))
@@ -315,8 +216,8 @@ void FlxBlueprintExporter::EmitNode(UEdGraphNode* Node)
if (Pin->bHidden) continue;
Output.Appendf(TEXT(" input %s %s = %s\n"),
*FormatPinType(Pin),
*FormatPinName(Pin),
*MCPUtils::FormatPinType(Pin),
*MCPUtils::FormatName(Pin),
*FormatPinSource(Pin));
}
@@ -327,7 +228,7 @@ void FlxBlueprintExporter::EmitNode(UEdGraphNode* Node)
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue;
if (Pin->bHidden) continue;
if (!ReturnPins.IsEmpty()) ReturnPins += TEXT(", ");
ReturnPins += FormatPinName(Pin);
ReturnPins += MCPUtils::FormatName(Pin);
}
if (!ReturnPins.IsEmpty())
{
@@ -341,12 +242,12 @@ void FlxBlueprintExporter::EmitNode(UEdGraphNode* Node)
UEdGraphPin* LinkedTo = GetLinkedTo(Pin);
if (!LinkedTo) continue;
FString Target = FormatNodeName(LinkedTo->GetOwningNode());
FString Target = MCPUtils::FormatName(LinkedTo->GetOwningNode());
if (ExecOuts.Num() == 1)
Output.Appendf(TEXT(" goto %s\n"), *Target);
else
Output.Appendf(TEXT(" goto-if %s %s\n"), *FormatPinName(Pin), *Target);
Output.Appendf(TEXT(" goto-if %s %s\n"), *MCPUtils::FormatName(Pin), *Target);
}
}
@@ -361,8 +262,8 @@ void FlxBlueprintExporter::EmitLocalVariables()
{
FString Default = Var.DefaultValue.IsEmpty() ? TEXT("<default>") : Var.DefaultValue;
Output.Appendf(TEXT("local %s %s = %s\n"),
*FormatPinType(Var.VarType),
*SanitizeName(Var.VarName.ToString()),
*MCPUtils::FormatPinType(Var.VarType),
*MCPUtils::FormatName(Var),
*Default);
}
break;
@@ -387,6 +288,6 @@ void FlxBlueprintExporter::EmitNodeList()
if (Node->IsA<UEdGraphNode_Comment>()) continue;
if (Node->IsA<UK2Node_VariableGet>()) continue;
Details.Appendf(TEXT("%s = %s\n"),
*FormatNodeName(Node), *Node->NodeGuid.ToString());
*MCPUtils::FormatName(Node), *Node->NodeGuid.ToString());
}
}

View File

@@ -2,14 +2,10 @@
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "UObject/UObjectIterator.h"
#include "UMCPHandler_GetPinDetails.generated.h"
@@ -27,14 +23,8 @@ class UMCPHandler_GetPinDetails : public UObject, public IMCPHandler
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Node to look up (GUID)"))
FString Node;
UPROPERTY(meta=(Description="Pin name on the node"))
FString PinName;
UPROPERTY(meta=(Description="Path to the pin, e.g. /Game/Widgets/WB_Hotkeys,node:MyNode,pin:Result"))
FString Pin;
virtual FString GetDescription() const override
{
@@ -44,75 +34,47 @@ public:
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
MCPFetcher F(Result);
UEdGraphPin* P = F.Walk(Pin).Cast<UEdGraphPin>();
if (!P) return;
UEdGraph* Graph = nullptr;
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node, &Graph);
if (!FoundNode)
Result->SetStringField(TEXT("pinName"), P->PinName.ToString());
Result->SetStringField(TEXT("direction"), P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"));
Result->SetStringField(TEXT("type"), P->PinType.PinCategory.ToString());
if (!P->PinType.PinSubCategory.IsNone())
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node));
Result->SetStringField(TEXT("subCategory"), P->PinType.PinSubCategory.ToString());
}
if (P->PinType.PinSubCategoryObject.IsValid())
{
Result->SetStringField(TEXT("subtype"), P->PinType.PinSubCategoryObject->GetName());
}
UEdGraphPin* Pin = FoundNode->FindPin(FName(*PinName));
if (!Pin)
{
// List available pins
TArray<TSharedPtr<FJsonValue>> AvailPins;
for (UEdGraphPin* P : FoundNode->Pins)
{
if (P)
{
TSharedRef<FJsonObject> PinObj = MakeShared<FJsonObject>();
PinObj->SetStringField(TEXT("name"), P->PinName.ToString());
PinObj->SetStringField(TEXT("direction"), P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"));
PinObj->SetStringField(TEXT("type"), P->PinType.PinCategory.ToString());
AvailPins.Add(MakeShared<FJsonValueObject>(PinObj));
}
}
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *PinName, *Node));
Result->SetArrayField(TEXT("availablePins"), AvailPins);
return;
}
Result->SetBoolField(TEXT("isArray"), P->PinType.IsArray());
Result->SetBoolField(TEXT("isSet"), P->PinType.IsSet());
Result->SetBoolField(TEXT("isMap"), P->PinType.IsMap());
Result->SetBoolField(TEXT("isReference"), P->PinType.bIsReference);
Result->SetBoolField(TEXT("isConst"), P->PinType.bIsConst);
Result->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
Result->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"));
Result->SetStringField(TEXT("type"), Pin->PinType.PinCategory.ToString());
if (!Pin->PinType.PinSubCategory.IsNone())
if (!P->DefaultValue.IsEmpty())
{
Result->SetStringField(TEXT("subCategory"), Pin->PinType.PinSubCategory.ToString());
Result->SetStringField(TEXT("defaultValue"), P->DefaultValue);
}
if (Pin->PinType.PinSubCategoryObject.IsValid())
if (!P->DefaultTextValue.IsEmpty())
{
Result->SetStringField(TEXT("subtype"), Pin->PinType.PinSubCategoryObject->GetName());
Result->SetStringField(TEXT("defaultTextValue"), P->DefaultTextValue.ToString());
}
Result->SetBoolField(TEXT("isArray"), Pin->PinType.IsArray());
Result->SetBoolField(TEXT("isSet"), Pin->PinType.IsSet());
Result->SetBoolField(TEXT("isMap"), Pin->PinType.IsMap());
Result->SetBoolField(TEXT("isReference"), Pin->PinType.bIsReference);
Result->SetBoolField(TEXT("isConst"), Pin->PinType.bIsConst);
if (!Pin->DefaultValue.IsEmpty())
if (P->DefaultObject)
{
Result->SetStringField(TEXT("defaultValue"), Pin->DefaultValue);
}
if (!Pin->DefaultTextValue.IsEmpty())
{
Result->SetStringField(TEXT("defaultTextValue"), Pin->DefaultTextValue.ToString());
}
if (Pin->DefaultObject)
{
Result->SetStringField(TEXT("defaultObject"), Pin->DefaultObject->GetPathName());
Result->SetStringField(TEXT("defaultObject"), P->DefaultObject->GetPathName());
}
// Connected pins
if (Pin->LinkedTo.Num() > 0)
if (P->LinkedTo.Num() > 0)
{
TArray<TSharedPtr<FJsonValue>> Conns;
for (UEdGraphPin* Linked : Pin->LinkedTo)
for (UEdGraphPin* Linked : P->LinkedTo)
{
if (!Linked || !Linked->GetOwningNode()) continue;
TSharedRef<FJsonObject> CJ = MakeShared<FJsonObject>();

View File

@@ -0,0 +1,239 @@
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "UObject/PropertyAccessUtil.h"
#include "Engine/SimpleConstructionScript.h"
#include "Engine/SCS_Node.h"
#include "Engine/World.h"
#include "Engine/LevelScriptBlueprint.h"
MCPFetcher& MCPFetcher::SetError(const FString& Msg)
{
bError = true;
ErrorCB.SetError(Msg);
return *this;
}
MCPFetcher& MCPFetcher::TypeMismatch(const TCHAR* Walker, const TCHAR* Expected)
{
bError = true;
if (ResultPin)
ErrorCB.SetError(FString::Printf(TEXT("Input to '%s' is a pin, expected %s"), Walker, Expected));
else if (Obj)
ErrorCB.SetError(FString::Printf(TEXT("Input to '%s' is %s, expected %s"), Walker, *Obj->GetClass()->GetName(), Expected));
else
ErrorCB.SetError(FString::Printf(TEXT("Input to '%s' is null, expected %s"), Walker, Expected));
return *this;
}
MCPFetcher& MCPFetcher::Walk(const FString& Path)
{
if (bError) return *this;
// Split on commas
TArray<FString> Segments;
Path.ParseIntoArray(Segments, TEXT(","));
if (Segments.Num() == 0)
{
SetError(TEXT("Empty path"));
return *this;
}
// If no object yet, first segment is an asset path
int32 Start = 0;
if (!Obj && !ResultPin)
{
LoadUAsset(Segments[0]);
if (bError) return *this;
Start = 1;
}
// Walk each subsequent segment
for (int32 i = Start; i < Segments.Num(); i++)
{
FString Key, Value;
Segments[i].Split(TEXT(":"), &Key, &Value);
if (StrEq(Key, TEXT("graph"))) Graph(Value);
else if (StrEq(Key, TEXT("node"))) Node(Value);
else if (StrEq(Key, TEXT("pin"))) Pin(Value);
else if (StrEq(Key, TEXT("component"))) Component(Value);
else if (StrEq(Key, TEXT("property"))) Property(Value);
else if (StrEq(Key, TEXT("levelblueprint"))) LevelBlueprint();
else
{
SetError(FString::Printf(TEXT("Unknown walker '%s'"), *Key));
return *this;
}
if (bError) return *this;
}
return *this;
}
void MCPFetcher::LoadUAsset(const FString& PackagePath)
{
SetObj(LoadObject<UObject>(nullptr, *PackagePath));
if (!Obj)
SetError(FString::Printf(TEXT("Could not load asset '%s'"), *PackagePath));
}
MCPFetcher& MCPFetcher::Graph(const FString& Value)
{
if (bError) return *this;
UBlueprint* BP = ::Cast<UBlueprint>(Obj);
if (!BP)
return TypeMismatch(TEXT("graph"), TEXT("Blueprint"));
TArray<UEdGraph*> Matches = MCPUtils::AllGraphsNamed(BP, Value);
if (Matches.Num() == 0)
return SetError(FString::Printf(TEXT("Graph '%s' not found in %s"), *Value, *BP->GetName()));
if (Matches.Num() > 1)
return SetError(FString::Printf(TEXT("Ambiguous graph '%s' in %s — %d matches"), *Value, *BP->GetName(), Matches.Num()));
SetObj(Matches[0]);
return *this;
}
MCPFetcher& MCPFetcher::Node(const FString& Value)
{
if (bError) return *this;
// If current object is a graph, search that graph
if (UEdGraph* G = ::Cast<UEdGraph>(Obj))
{
UEdGraphNode* Found = nullptr;
for (UEdGraphNode* N : G->Nodes)
{
if (!N || !MCPUtils::Identifies(Value, N))
continue;
if (Found)
return SetError(FString::Printf(TEXT("Ambiguous node '%s' in graph %s"), *Value, *G->GetName()));
Found = N;
}
if (!Found)
return SetError(FString::Printf(TEXT("Node '%s' not found in graph %s"), *Value, *G->GetName()));
SetObj(Found);
return *this;
}
// If current object is a blueprint, search all graphs
if (UBlueprint* BP = ::Cast<UBlueprint>(Obj))
{
UEdGraphNode* Found = nullptr;
for (UEdGraph* G : MCPUtils::AllGraphs(BP))
{
for (UEdGraphNode* N : G->Nodes)
{
if (!N || !MCPUtils::Identifies(Value, N))
continue;
if (Found)
return SetError(FString::Printf(TEXT("Ambiguous node '%s' in %s"), *Value, *BP->GetName()));
Found = N;
}
}
if (!Found)
return SetError(FString::Printf(TEXT("Node '%s' not found in %s"), *Value, *BP->GetName()));
SetObj(Found);
return *this;
}
return TypeMismatch(TEXT("node"), TEXT("graph or Blueprint"));
}
MCPFetcher& MCPFetcher::Pin(const FString& Value)
{
if (bError) return *this;
UEdGraphNode* N = ::Cast<UEdGraphNode>(Obj);
if (!N)
return TypeMismatch(TEXT("pin"), TEXT("node"));
UEdGraphPin* Found = nullptr;
for (UEdGraphPin *P : N->Pins)
{
if (!MCPUtils::Identifies(Value, P))
continue;
if (Found)
return SetError(FString::Printf(TEXT("Ambiguous pin '%s' on node %s"),
*Value, *MCPUtils::FormatName(N)));
Found = P;
}
if (!Found)
return SetError(FString::Printf(TEXT("Pin '%s' not found on node %s"),
*Value, *MCPUtils::FormatName(N)));
SetPin(Found);
return *this;
}
MCPFetcher& MCPFetcher::Component(const FString& Value)
{
if (bError) return *this;
UBlueprint* BP = ::Cast<UBlueprint>(Obj);
if (!BP)
return TypeMismatch(TEXT("component"), TEXT("Blueprint"));
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
if (!SCS)
return SetError(FString::Printf(TEXT("Blueprint %s has no SimpleConstructionScript (not an Actor Blueprint)"), *BP->GetName()));
FName SearchName(*Value);
for (USCS_Node* SCSNode : SCS->GetAllNodes())
{
if (SCSNode && SCSNode->GetVariableName() == SearchName)
{
SetObj(SCSNode->ComponentTemplate);
return *this;
}
}
return SetError(FString::Printf(TEXT("Component '%s' not found in %s"), *Value, *BP->GetName()));
}
MCPFetcher& MCPFetcher::Property(const FString& Value)
{
if (bError) return *this;
if (!Obj)
return TypeMismatch(TEXT("property"), TEXT("UObject"));
FProperty* Prop = Obj->GetClass()->FindPropertyByName(FName(*Value));
if (!Prop)
return SetError(FString::Printf(TEXT("Property '%s' not found on %s"), *Value, *Obj->GetClass()->GetName()));
FObjectProperty* ObjProp = CastField<FObjectProperty>(Prop);
if (!ObjProp)
return SetError(FString::Printf(TEXT("Property '%s' is not an object property (type: %s)"), *Value, *Prop->GetCPPType()));
UObject* PropValue = ObjProp->GetObjectPropertyValue(Prop->ContainerPtrToValuePtr<void>(Obj));
if (!PropValue)
return SetError(FString::Printf(TEXT("Property '%s' is null on %s"), *Value, *Obj->GetName()));
SetObj(PropValue);
return *this;
}
MCPFetcher& MCPFetcher::LevelBlueprint()
{
if (bError) return *this;
UWorld* World = ::Cast<UWorld>(Obj);
if (!World)
return TypeMismatch(TEXT("levelblueprint"), TEXT("World"));
if (!World->PersistentLevel)
return SetError(TEXT("World has no PersistentLevel"));
ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(true);
if (!LevelBP)
return SetError(TEXT("World has no level blueprint"));
SetObj(LevelBP);
return *this;
}

View File

@@ -7,6 +7,9 @@
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"
#include "Engine/Blueprint.h"
#include "Engine/MemberReference.h"
#include "Engine/World.h"
#include "Components/ActorComponent.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
@@ -98,6 +101,157 @@ MCPErrorCallback::MCPErrorCallback(FStringBuilderBase& OutResult)
: Func([&OutResult](const FString& Msg) { OutResult.Reset(); OutResult.Appendf(TEXT("ERROR: %s\n"), *Msg); })
{}
// ============================================================
// Name Formatting
// ============================================================
void MCPUtils::SanitizeNameInPlace(FString &Name)
{
int32 Dst = 0;
for (int32 Src = 0; Src < Name.Len(); Src++)
{
TCHAR c = Name[Src];
if (c <= 0x20 || c == '_' || c == 0x7F) continue;
if (c >= 0x21 && c <= 0x7E && !FChar::IsAlnum(c))
Name[Dst++] = '_';
else
Name[Dst++] = c;
}
Name.LeftInline(Dst);
if (Name.IsEmpty()) Name = TEXT("_");
}
void MCPUtils::AppendNumericSuffix(FString &Name, int32 N)
{
if (N > 0)
Name += FString::Printf(TEXT("_%d"), N - 1);
}
FString MCPUtils::FormatName(UWorld *World)
{
return World->GetPathName();
}
FString MCPUtils::FormatName(UBlueprint *BP)
{
return BP->GetPathName();
}
FString MCPUtils::FormatName(UActorComponent *C)
{
return C->GetName();
}
FString MCPUtils::FormatName(UEdGraph *Graph)
{
FString Name = Graph->GetName();
SanitizeNameInPlace(Name);
return Name;
}
FString MCPUtils::FormatName(UEdGraphNode* Node)
{
// Sanitized first line of the node title.
FString Title = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
int32 NewlineIdx;
if (Title.FindChar(TEXT('\n'), NewlineIdx))
Title.LeftInline(NewlineIdx);
SanitizeNameInPlace(Title);
AppendNumericSuffix(Title, Node->GetFName().GetNumber());
return Title;
}
FString MCPUtils::FormatName(UEdGraphPin *Pin)
{
FString Name = Pin->PinName.ToString();
SanitizeNameInPlace(Name);
return Name;
}
FString MCPUtils::FormatName(const FMemberReference &Ref)
{
FString Name = Ref.GetMemberName().ToString();
SanitizeNameInPlace(Name);
return Name;
}
FString MCPUtils::FormatName(const FBPVariableDescription &Var)
{
FString Name = Var.VarName.ToString();
SanitizeNameInPlace(Name);
return Name;
}
FString MCPUtils::FormatName(const UClass *Class)
{
FString Name = Class->GetName();
SanitizeNameInPlace(Name);
return Name;
}
bool MCPUtils::Identifies(const FString &Name, const UClass *Class)
{
return FormatName(Class).Equals(Name, ESearchCase::IgnoreCase);
}
// ============================================================
// Identifies
// ============================================================
bool MCPUtils::Identifies(const FString &Name, UWorld *World)
{
return FormatName(World).Equals(Name, ESearchCase::IgnoreCase);
}
bool MCPUtils::Identifies(const FString &Name, UBlueprint *BP)
{
return FormatName(BP).Equals(Name, ESearchCase::IgnoreCase);
}
bool MCPUtils::Identifies(const FString &Name, UActorComponent *C)
{
return FormatName(C).Equals(Name, ESearchCase::IgnoreCase);
}
bool MCPUtils::Identifies(const FString &Name, UEdGraph *Graph)
{
return FormatName(Graph).Equals(Name, ESearchCase::IgnoreCase);
}
bool MCPUtils::Identifies(const FString &Name, UEdGraphNode* Node)
{
if (Node->NodeGuid.ToString().Equals(Name, ESearchCase::IgnoreCase))
return true;
return FormatName(Node).Equals(Name, ESearchCase::IgnoreCase);
}
bool MCPUtils::Identifies(const FString &Name, UEdGraphPin *Pin)
{
return FormatName(Pin).Equals(Name, ESearchCase::IgnoreCase);
}
bool MCPUtils::Identifies(const FString &Name, const FMemberReference &Ref)
{
return FormatName(Ref).Equals(Name, ESearchCase::IgnoreCase);
}
FString MCPUtils::FormatPinType(const FEdGraphPinType& PinType)
{
if (UObject* SubObj = PinType.PinSubCategoryObject.Get())
{
return SubObj->GetName();
}
FString Type = PinType.PinCategory.ToString();
Type[0] = FChar::ToUpper(Type[0]);
return Type;
}
FString MCPUtils::FormatPinType(UEdGraphPin* Pin)
{
return FormatPinType(Pin->PinType);
}
// ============================================================
// JSON helpers
// ============================================================
@@ -204,7 +358,7 @@ TArray<UEdGraph*> MCPUtils::AllGraphsNamed(UBlueprint* BP, const FString& Name)
{
TArray<UEdGraph*> Result;
for (UEdGraph* Graph : AllGraphs(BP))
if (Graph->GetName().Equals(Name, ESearchCase::IgnoreCase))
if (Identifies(Name, Graph))
Result.Add(Graph);
return Result;
}
@@ -221,7 +375,7 @@ TArray<TSharedPtr<FJsonValue>> MCPUtils::AllGraphNamesJson(UBlueprint* BP)
{
TArray<TSharedPtr<FJsonValue>> Result;
for (UEdGraph* Graph : AllGraphs(BP))
Result.Add(MakeShared<FJsonValueString>(Graph->GetName()));
Result.Add(MakeShared<FJsonValueString>(FormatName(Graph)));
return Result;
}

View File

@@ -0,0 +1,11 @@
Fetches a graph pin:
/Game/Widgets/WB_Hotkeys,graph:ReadLuaConfiguration,node:Self_Reference_03,pin:Result
Fetches an skeletal mesh:
/Game/Tangibles/TAN_Character,component:CharacterMesh0,property:SkeletalMeshAsset

View File

@@ -21,25 +21,6 @@ private:
//
////////////////////////////////////////////////////////
// Sanitize a name: trim whitespace, and replace space
// with underscores.
//
static FString SanitizeName(const FString& Title);
// Get the pin type as a string. This is lossy,
// we don't differentiate between object reference
// or struct, for example. But that's OK, we don't
// need precise type info, we just need readability.
//
static FString FormatPinType(const FEdGraphPinType& PinType);
static FString FormatPinType(UEdGraphPin* Pin);
// Get the node base name as a sanitized string.
// Later, we may append a number to this base name
// in order to turn it into a unique string.
//
static FString FormatNodeBaseName(UEdGraphNode* Node);
// Get the pin that this pin is linked to. If the
// pin is linked to multiple pins, returns the first.
// If the pin is linked to a knot node, follow the
@@ -69,32 +50,12 @@ private:
//
static UEdGraphPin* FindFirstPin(UEdGraphNode* Node, EEdGraphPinDirection Direction);
// Given a sanitized pin display name or a sanitized pin
// name, find the one pin that matches. If the string
// provided doesn't match any pin, or if it matches
// multiple pins ambiguously, returns nullptr. If the
// string is the sanitized empty string, returns nullptr
// even if that matches a pin.
//
static UEdGraphPin* BestMatchPin(UEdGraphNode* Node, EEdGraphPinDirection Direction, bool Exec, const FString& Name);
// Returns either the sanitized display name or
// sanitized pin name. Chooses the one that
// unambiguously identifies the pin. If neither is
// ambiguous, prefers the display name. If both are
// ambiguous, returns the display name with a question
// mark prepended to indicate that it doesn't uniquely
// identify the pin.
//
static FString FormatPinName(UEdGraphPin *Pin);
////////////////////////////////////////////////////////
//
// Traverse and Emit the Nodes.
//
////////////////////////////////////////////////////////
FString FormatNodeName(UEdGraphNode* Node);
FString FormatPinSource(UEdGraphPin* Pin);
void Traverse(UEdGraphNode* Node);
void SortNodes();
@@ -114,7 +75,6 @@ private:
UEdGraph* Graph;
// Data populated by passes.
TMap<UEdGraphNode*, FString> NodeNames;
TArray<UEdGraphNode*> SortedNodes;
TSet<UEdGraphNode*> Visited;

View File

@@ -0,0 +1,80 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPUtils.h"
class UEdGraphPin;
// Resolves a path string into a UObject or UEdGraphPin.
//
// A path starts with a package path (e.g. "/Game/Widgets/WB_Hotkeys"),
// followed by zero or more comma-separated walker segments of the form
// "key:value". Each segment navigates from the current object to a child.
//
// Supported walkers:
// graph:Name - find a named UEdGraph within a UBlueprint
// node:Name - find a named UEdGraphNode within a UEdGraph
// pin:Name - find a named UEdGraphPin on a UEdGraphNode
// component:Name - find a named component in a Blueprint's SCS
// property:Name - follow an object-valued UProperty to its value
// levelblueprint - get the level blueprint from a UWorld
//
// Example paths:
// /Game/Widgets/WB_Hotkeys,graph:ReadLuaConfiguration,node:Self_Reference_03,pin:Result
// /Game/Tangibles/TAN_Character,component:CharacterMesh0,property:SkeletalMeshAsset
//
// Builder-style usage:
// MCPFetcher F(cb);
// if (!F.Walk(Path).Ok()) return;
//
// MCPFetcher F(cb, ExistingObj);
// if (!F.Graph("EventGraph").Node("MyNode").Ok()) return;
//
class MCPFetcher
{
public:
bool bError = false;
UObject* Obj = nullptr;
UEdGraphPin* ResultPin = nullptr;
MCPErrorCallback ErrorCB = nullptr;
MCPFetcher(MCPErrorCallback CB) : ErrorCB(CB) {}
MCPFetcher(MCPErrorCallback CB, UObject* O) : Obj(O), ErrorCB(CB) {}
// Walk one step.
MCPFetcher& Graph(const FString& Value);
MCPFetcher& Node(const FString& Value);
MCPFetcher& Pin(const FString& Value);
MCPFetcher& Component(const FString& Value);
MCPFetcher& Property(const FString& Value);
MCPFetcher& LevelBlueprint();
// Parse string and walk multiple steps.
MCPFetcher& Walk(const FString& Path);
bool Ok() const { return !bError; }
template<class T> T *Cast()
{
if (bError) return nullptr;
T* Result = ::Cast<T>(Obj);
if (Result == nullptr)
TypeMismatch(TEXT("Cast"), *T::StaticClass()->GetName());
return Result;
}
private:
static bool StrEq(const FString& A, const TCHAR* B) { return A.Equals(B, ESearchCase::IgnoreCase); }
void SetObj(UObject* InObj) { Obj = InObj; ResultPin = nullptr; }
void SetPin(UEdGraphPin* InPin) { ResultPin = InPin; Obj = nullptr; }
MCPFetcher& SetError(const FString& Msg);
MCPFetcher& TypeMismatch(const TCHAR* Walker, const TCHAR* Expected);
void LoadUAsset(const FString& PackagePath);
};
template<> inline UEdGraphPin* MCPFetcher::Cast<UEdGraphPin>()
{
if (bError) return nullptr;
if (!ResultPin)
TypeMismatch(TEXT("Cast"), TEXT("pin"));
return ResultPin;
}

View File

@@ -14,6 +14,10 @@ class UBlueprintNodeSpawner;
class UAnimationStateMachineGraph;
class UAnimStateNode;
class UAnimStateTransitionNode;
class UActorComponent;
class UWorld;
struct FMemberReference;
struct FBPVariableDescription;
// ----- Log capture -----
@@ -87,6 +91,55 @@ struct MCPErrorCallback
class MCPUtils
{
public:
////////////////////////////////////////////////////////
//
// Name Formatting
//
// The goal here is to centralize the code that outputs
// names, and have everybody use it, so that names are
// used consistently. The secondary goal is to choose
// names that are as uniquely-identifying as is practical.
// It's not always 100% possible to get perfectly unique
// names, though, so our code needs to check for ambiguity.
//
////////////////////////////////////////////////////////
static FString FormatName(UWorld *World);
static FString FormatName(UBlueprint *BP);
static FString FormatName(UActorComponent *C);
static FString FormatName(UEdGraph *Graph);
static FString FormatName(UEdGraphNode* Node);
static FString FormatName(UEdGraphPin *Pin);
static FString FormatName(const FMemberReference &Ref);
static FString FormatName(const FBPVariableDescription &Var);
static FString FormatName(const UClass *Class);
////////////////////////////////////////////////////////
//
// Identifies
//
// Return true if the name identifies the object. The
// FormatName functions, above, always return names that
// identify the object. However, there may be other
// names that also identify the object. Identifying names
// aren't 100% guaranteed to be unique, but very likely.
//
////////////////////////////////////////////////////////
static bool Identifies(const FString &Name, UWorld *World);
static bool Identifies(const FString &Name, UBlueprint *BP);
static bool Identifies(const FString &Name, UActorComponent *C);
static bool Identifies(const FString &Name, UEdGraph *Graph);
static bool Identifies(const FString &Name, UEdGraphNode* Node);
static bool Identifies(const FString &Name, UEdGraphPin *Pin);
static bool Identifies(const FString &Name, const FMemberReference &Ref);
static bool Identifies(const FString &Name, const UClass *Class);
////////////////////////////////////////////////////////
static FString FormatPinType(const FEdGraphPinType& PinType);
static FString FormatPinType(UEdGraphPin* Pin);
// ----- Asset path helpers -----
// Splits "/Game/Foo/Bar" into PackagePath="/Game/Foo" and AssetName="Bar".
// Returns false if the path has no slash or the asset name is empty.
@@ -210,5 +263,7 @@ public:
static void FormatCommandHelp(UClass* HandlerClass, FStringBuilderBase& Result);
private:
static void SanitizeNameInPlace(FString& Name);
static void AppendNumericSuffix(FString &Name, int32 N);
static FString SetPropertyFromJson(void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json);
};