Files
integration/Plugins/UEWingman/Source/UEWingman/Private/WingGraphExport.cpp
2026-04-08 03:14:08 -04:00

343 lines
9.2 KiB
C++

#include "WingGraphExport.h"
#include "WingProperty.h"
#include "WingTypes.h"
#include "WingUtils.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "EdGraphNode_Comment.h"
#include "K2Node_CallFunction.h"
#include "WingVariables.h"
#include "MaterialGraph/MaterialGraphNode.h"
WingGraphExport::WingGraphExport(UEdGraph* InGraph, bool Locals, bool Details)
: Graph(InGraph), ShowLocals(Locals), ShowDetails(Details)
{
SortNodes();
EmitLocalVariables();
EmitGraph();
EmitComments();
}
WingGraphExport::WingGraphExport(UEdGraphNode* InNode, bool Locals, bool Details)
: Graph(InNode->GetGraph()), ShowLocals(Locals), ShowDetails(Details)
{
SortedNodes.Add(InNode);
Visited.Add(InNode);
EmitLocalVariables();
EmitGraph();
EmitComments();
}
////////////////////////////////////////////////////////
//
// General utilities for manipulating UEdGraph nodes.
//
////////////////////////////////////////////////////////
UEdGraphPin* WingGraphExport::GetLinkedTo(UEdGraphPin* Pin)
{
if (Pin == nullptr) return nullptr;
if (Pin->LinkedTo.IsEmpty()) return nullptr;
return Pin->LinkedTo[0];
}
bool WingGraphExport::IsDefaultToSelf(UEdGraphPin* Pin)
{
// Only valid call-function nodes can have default-to-self.
UK2Node_CallFunction* CallNode = Cast<UK2Node_CallFunction>(Pin->GetOwningNode());
if (!CallNode) return false;
UFunction* Function = CallNode->GetTargetFunction();
if (!Function) return false;
// In a call function node, the pin name 'self' is reserved
// for the C++ 'this' pointer. This always has 'DefaultToSelf'
// behavior. Note that 'self' in other nodes is not special.
if (Pin->PinName == UEdGraphSchema_K2::PN_Self) return true;
// For any other pin name, we have to check for
// the presence of meta = (DefaultToSelf = "Pin")
const FString& DefaultToSelfPinName = Function->GetMetaData(FBlueprintMetadata::MD_DefaultToSelf);
return Pin->PinName.ToString() == DefaultToSelfPinName;
}
TArray<UEdGraphPin*> WingGraphExport::FilterPins(UEdGraphNode* Node, EEdGraphPinDirection Direction, FName Category)
{
TArray<UEdGraphPin*> Result;
for (UEdGraphPin* Pin : Node->Pins)
{
if (Direction != EGPD_MAX && Pin->Direction != Direction) continue;
if (!Category.IsNone() && Pin->PinType.PinCategory != Category) continue;
Result.Add(Pin);
}
return Result;
}
bool WingGraphExport::HasExecPin(UEdGraphNode* Node, EEdGraphPinDirection Direction)
{
return FilterPins(Node, Direction, UEdGraphSchema_K2::PC_Exec).Num() > 0;
}
UEdGraphPin* WingGraphExport::FindFirstPin(UEdGraphNode* Node, EEdGraphPinDirection Direction)
{
for (UEdGraphPin* Pin : Node->Pins)
{
if (Pin->Direction == Direction) return Pin;
}
return nullptr;
}
////////////////////////////////////////////////////////
//
// Traverse and Emit the Nodes.
//
////////////////////////////////////////////////////////
FString WingGraphExport::FormatPinSource(UEdGraphPin* Pin)
{
// If connected, show source node.pin
UEdGraphPin* LinkedTo = GetLinkedTo(Pin);
if (LinkedTo != nullptr)
{
UEdGraphNode* LinkedToNode = LinkedTo->GetOwningNode();
FString PinLabel = WingUtils::FormatName(LinkedTo);
return FString::Printf(TEXT("%s.%s"), *WingUtils::FormatName(LinkedToNode), *PinLabel);
}
// String pins: always show in quotes (even if empty).
FName Category = Pin->PinType.PinCategory;
if (Category == UEdGraphSchema_K2::PC_String ||
Category == UEdGraphSchema_K2::PC_Name ||
Category == UEdGraphSchema_K2::PC_Text)
{
return FString::Printf(TEXT("\"%s\""), *Pin->DefaultValue);
}
// If has a non-empty default, show it.
if (!Pin->DefaultValue.IsEmpty())
{
return Pin->DefaultValue;
}
if (Pin->DefaultObject)
{
return Pin->DefaultObject->GetName();
}
if (!Pin->DefaultTextValue.IsEmpty())
{
return FString::Printf(TEXT("\"%s\""), *Pin->DefaultTextValue.ToString());
}
if (IsDefaultToSelf(Pin))
{
return TEXT("<self>");
}
else
{
return TEXT("<default>");
}
}
void WingGraphExport::Traverse(UEdGraphNode* Node)
{
if (Visited.Contains(Node)) return;
Visited.Add(Node);
// First, traverse input nodes
for (UEdGraphPin* Pin : FilterPins(Node, EGPD_Input))
for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
Traverse(LinkedPin->GetOwningNode());
// Add this node to the sorted list.
SortedNodes.Add(Node);
// Then, traverse exec output nodes only.
// Data outputs are not followed — data nodes get pulled in
// through their consumers' input traversal.
for (UEdGraphPin* Pin : FilterPins(Node, EGPD_Output, UEdGraphSchema_K2::PC_Exec))
for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
Traverse(LinkedPin->GetOwningNode());
}
void WingGraphExport::SortNodes()
{
// Find starter nodes: have exec output but no exec input.
TArray<UEdGraphNode*> Starters;
for (UEdGraphNode* Node : Graph->Nodes)
{
if (HasExecPin(Node, EGPD_Output) && !HasExecPin(Node, EGPD_Input))
{
Starters.Add(Node);
}
}
// Sort starters by Y position.
Starters.Sort([](const UEdGraphNode& A, const UEdGraphNode& B)
{
return A.NodePosY < B.NodePosY;
});
// Traverse from each starter.
for (UEdGraphNode* Starter : Starters)
{
Traverse(Starter);
}
// Traverse all nodes.
for (UEdGraphNode* Node : Graph->Nodes)
{
Traverse(Node);
}
}
void WingGraphExport::EmitNode(UEdGraphNode* Node)
{
if (Node->IsA<UEdGraphNode_Comment>()) return;
Output.Appendf(TEXT("node %s: %s\n"), *WingUtils::FormatName(Node), *WingUtils::FormatNodeTitle(Node));
Output.Appendf(TEXT(" pos %d, %d\n"), Node->NodePosX, Node->NodePosY);
// Emit node properties.
TArray<FWingProperty> Primary, Secondary;
GetProperties(Node, Primary, Secondary);
for (const FWingProperty &Prop : Primary) Prop.Print(Output);
// Emit secondary node properties.
if (ShowDetails || Secondary.Num() < 2)
{
for (const FWingProperty &Prop : Secondary) Prop.Print(Output);
}
else if (Secondary.Num() > 0)
{
Output.Appendf(TEXT(" minor properties: %d\n"), Secondary.Num());
SuppressedDetails = true;
}
// Emit input data pins.
for (UEdGraphPin* Pin : FilterPins(Node, EGPD_Input))
{
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue;
if (Pin->bHidden) continue;
Output.Appendf(TEXT(" input %s %s = %s\n"),
*UWingTypes::TypeToText(Pin->PinType),
*WingUtils::FormatName(Pin),
*FormatPinSource(Pin));
}
// Emit output data pins as a return line.
FString ReturnPins;
for (UEdGraphPin* Pin : FilterPins(Node, EGPD_Output))
{
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue;
if (Pin->bHidden) continue;
if (!ReturnPins.IsEmpty()) ReturnPins += TEXT(", ");
ReturnPins += WingUtils::FormatName(Pin);
}
if (!ReturnPins.IsEmpty())
{
Output.Appendf(TEXT(" output-pins %s\n"), *ReturnPins);
}
// Emit output exec pins as goto statements.
TArray<UEdGraphPin*> ExecOuts = FilterPins(Node, EGPD_Output, UEdGraphSchema_K2::PC_Exec);
for (UEdGraphPin* Pin : ExecOuts)
{
UEdGraphPin* LinkedTo = GetLinkedTo(Pin);
if (!LinkedTo) continue;
FString Target = WingUtils::FormatName(LinkedTo->GetOwningNode());
if (ExecOuts.Num() == 1)
Output.Appendf(TEXT(" goto %s\n"), *Target);
else
Output.Appendf(TEXT(" goto-if %s %s\n"), *WingUtils::FormatName(Pin), *Target);
}
Output.Append(TEXT("\n"));
}
void WingGraphExport::GetProperties(UEdGraphNode* Node,
TArray<FWingProperty> &Primary, TArray<FWingProperty> &Secondary)
{
Primary = FWingProperty::GetDetails(Node, false);
Secondary.Empty();
if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Node))
{
FString PrimaryCategory = MatNode->MaterialExpression->GetClass()->GetName();
TArray<FWingProperty> NewPrimary;
for (const FWingProperty& P : Primary)
{
if (P.GetCategory() == PrimaryCategory) NewPrimary.Add(P);
else Secondary.Add(P);
}
Swap(NewPrimary, Primary);
}
}
void WingGraphExport::EmitLocalVariables()
{
if (!ShowLocals) return;
WingVariables Vars;
Vars.SetBackingStore(Graph, WingOut::Stdout);
Vars.Load(WingOut::Stdout);
Vars.Print(Output);
if (!Vars.IsEmpty()) Output.Append(TEXT("\n"));
}
void WingGraphExport::EmitGraph()
{
for (UEdGraphNode* Node : SortedNodes)
{
EmitNode(Node);
}
}
void WingGraphExport::EmitComments()
{
for (UEdGraphNode* CommentNode : SortedNodes)
{
if (!CommentNode->IsA<UEdGraphNode_Comment>()) continue;
int32 CX = CommentNode->NodePosX;
int32 CY = CommentNode->NodePosY;
int32 CW = CommentNode->NodeWidth;
int32 CH = CommentNode->NodeHeight;
// Emit header.
Output.Appendf(TEXT("comment %s:\n"), *WingUtils::FormatName(CommentNode));
// Emit wrapped, indented body.
Output.Append(WingUtils::WrapText(CommentNode->NodeComment, 70, TEXT(" - ")));
// Find contained nodes.
TArray<FString> ContainedNames;
for (UEdGraphNode* Node : SortedNodes)
{
if (Node->IsA<UEdGraphNode_Comment>()) continue;
int32 NX = Node->NodePosX;
int32 NY = Node->NodePosY;
if (NX >= CX && NY >= CY && NX <= CX + CW && NY <= CY + CH)
ContainedNames.Add(WingUtils::FormatName(Node));
}
if (ContainedNames.Num() > 0)
{
Output.Appendf(TEXT(" applies to: %s\n"), *FString::Join(ContainedNames, TEXT(", ")));
}
Output.Append(TEXT("\n"));
}
}
void WingGraphExport::EmitDetailsSuggestion()
{
if (SuppressedDetails)
{
Output.Appendf(TEXT("Minor node properties were suppressed, use details=true to show.\n\n"));
}
}