More MCP work

This commit is contained in:
2026-03-08 21:28:47 -04:00
parent 93d4ed2038
commit 695de53b30
19 changed files with 388 additions and 546 deletions

View File

@@ -1,397 +0,0 @@
#if WITH_EDITOR
#include "BlueprintExporter.h"
#include "Common.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "K2Node_Knot.h"
#include "EdGraphNode_Comment.h"
#include "K2Node_VariableGet.h"
#include "K2Node_CallFunction.h"
#include "K2Node_FunctionEntry.h"
FlxBlueprintExporter::FlxBlueprintExporter(UEdGraph* InGraph)
: Graph(InGraph)
{
SortNodes();
AssignNodeNames();
EmitLocalVariables();
EmitNodeList();
EmitGraph();
}
////////////////////////////////////////////////////////
//
// General utilities for manipulating UEdGraph nodes.
//
////////////////////////////////////////////////////////
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)
{
if (Pin == nullptr) return nullptr;
if (Pin->LinkedTo.IsEmpty()) return nullptr;
UEdGraphPin *LinkedTo = Pin->LinkedTo[0];
if (!LinkedTo->GetOwningNode()->IsA<UK2Node_Knot>()) return LinkedTo;
Pin = FindFirstPin(LinkedTo->GetOwningNode(), Pin->Direction);
}
}
bool FlxBlueprintExporter::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*> FlxBlueprintExporter::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 FlxBlueprintExporter::HasExecPin(UEdGraphNode* Node, EEdGraphPinDirection Direction)
{
return FilterPins(Node, Direction, UEdGraphSchema_K2::PC_Exec).Num() > 0;
}
UEdGraphPin* FlxBlueprintExporter::FindFirstPin(UEdGraphNode* Node, EEdGraphPinDirection Direction)
{
for (UEdGraphPin* Pin : Node->Pins)
{
if (Pin->Direction == Direction) return Pin;
}
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(LogLuprexIntegration, 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);
}
////////////////////////////////////////////////////////
//
// Traverse and Emit the Nodes.
//
////////////////////////////////////////////////////////
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
UEdGraphPin* LinkedTo = GetLinkedTo(Pin);
if (LinkedTo != nullptr)
{
UEdGraphNode* LinkedToNode = LinkedTo->GetOwningNode();
// For variable get nodes, just show the variable name.
if (UK2Node_VariableGet* VarGet = Cast<UK2Node_VariableGet>(LinkedToNode))
return SanitizeName(VarGet->GetVarNameString());
FString PinLabel = FormatPinName(LinkedTo);
return FString::Printf(TEXT("%s.%s"), *FormatNodeName(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 FlxBlueprintExporter::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 FlxBlueprintExporter::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 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++;
}
}
void FlxBlueprintExporter::EmitNode(UEdGraphNode* Node)
{
if (Node->IsA<UEdGraphNode_Comment>())
{
Output.Appendf(TEXT("\n// %s\n"), *Node->NodeComment);
return;
}
Output.Appendf(TEXT("\nnode %s\n"), *FormatNodeName(Node));
// 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"),
*FormatPinType(Pin),
*FormatPinName(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 += FormatPinName(Pin);
}
if (!ReturnPins.IsEmpty())
{
Output.Appendf(TEXT(" return %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 = FormatNodeName(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);
}
}
void FlxBlueprintExporter::EmitLocalVariables()
{
for (UEdGraphNode* Node : Graph->Nodes)
{
UK2Node_FunctionEntry* EntryNode = Cast<UK2Node_FunctionEntry>(Node);
if (!EntryNode) continue;
for (const FBPVariableDescription& Var : EntryNode->LocalVariables)
{
FString Default = Var.DefaultValue.IsEmpty() ? TEXT("<default>") : Var.DefaultValue;
Output.Appendf(TEXT("local %s %s = %s\n"),
*FormatPinType(Var.VarType),
*SanitizeName(Var.VarName.ToString()),
*Default);
}
break;
}
}
void FlxBlueprintExporter::EmitGraph()
{
for (UEdGraphNode* Node : SortedNodes)
{
if (Node->IsA<UK2Node_Knot>()) continue;
if (Node->IsA<UK2Node_VariableGet>()) continue;
EmitNode(Node);
}
}
void FlxBlueprintExporter::EmitNodeList()
{
for (UEdGraphNode* Node : SortedNodes)
{
if (Node->IsA<UK2Node_Knot>()) continue;
if (Node->IsA<UEdGraphNode_Comment>()) continue;
if (Node->IsA<UK2Node_VariableGet>()) continue;
Details.Appendf(TEXT("%s = %s\n"),
*FormatNodeName(Node), *Node->NodeGuid.ToString());
}
}
#endif

View File

@@ -1,128 +0,0 @@
#pragma once
#if WITH_EDITOR
#include "CoreMinimal.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
class FlxBlueprintExporter
{
public:
FlxBlueprintExporter(UEdGraph* InGraph);
const FString GetOutput() { return Output.ToString(); }
const FString GetDetails() { return Details.ToString(); }
private:
////////////////////////////////////////////////////////
//
// General utilities for manipulating UEdGraph nodes.
//
////////////////////////////////////////////////////////
// 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
// chain of knot nodes and find the pin at the other
// end of the chain. Returns nullptr if this pin
// is not linked to anything.
//
static UEdGraphPin* GetLinkedTo(UEdGraphPin *Pin);
// Return true if the pin in question defaults
// to self.
//
static bool IsDefaultToSelf(UEdGraphPin* Pin);
// Get a subset of the pins in the node, filtered
// by direction, category, or both.
//
static TArray<UEdGraphPin*> FilterPins(UEdGraphNode* Node,
EEdGraphPinDirection Direction = EGPD_MAX, FName Category = FName());
// Return true if the node has an exec pin that points
// in the specified direction.
//
static bool HasExecPin(UEdGraphNode* Node, EEdGraphPinDirection Direction);
// Find the first pin that points in the specified direction.
//
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();
void AssignNodeNames();
void EmitNode(UEdGraphNode* Node);
void EmitLocalVariables();
void EmitGraph();
void EmitNodeList();
////////////////////////////////////////////////////////
//
// Values recorded during traversal.
//
////////////////////////////////////////////////////////
UEdGraph* Graph;
// Data populated by passes.
TMap<UEdGraphNode*, FString> NodeNames;
TArray<UEdGraphNode*> SortedNodes;
TSet<UEdGraphNode*> Visited;
// Output buffers.
TStringBuilder<4096> Output;
TStringBuilder<4096> Details;
};
#endif

View File

@@ -7,79 +7,14 @@
#include "Misc/Paths.h"
#include "ShaderCore.h"
#if WITH_EDITOR
#include "Engine/Blueprint.h"
#include "UObject/SavePackage.h"
#include "UObject/ObjectSaveContext.h"
#include "EdGraph/EdGraph.h"
#include "BlueprintExporter.h"
#include "Misc/FileHelper.h"
#include "Exporters/Exporter.h"
#include "UnrealExporter.h"
#endif
IMPLEMENT_PRIMARY_GAME_MODULE(FlxIntegrationModuleImpl, Integration, "Integration");
void FlxIntegrationModuleImpl::StartupModule()
{
FString ShaderDir = FPaths::Combine(FPaths::ProjectDir(), TEXT("Shaders"));
AddShaderSourceDirectoryMapping(TEXT("/Project/Integration"), ShaderDir);
#if WITH_EDITOR
OnAssetSavedHandle = UPackage::PackageSavedWithContextEvent.AddRaw(
this, &FlxIntegrationModuleImpl::OnAssetSaved);
#endif
}
void FlxIntegrationModuleImpl::ShutdownModule()
{
#if WITH_EDITOR
UPackage::PackageSavedWithContextEvent.Remove(OnAssetSavedHandle);
#endif
}
#if WITH_EDITOR
void FlxIntegrationModuleImpl::OnAssetSaved(const FString& PackageFilename, UPackage* Package, FObjectPostSaveContext Context)
{
if (!Package) return;
FString PkDir = FPaths::ProjectDir() / TEXT("Saved") / TEXT("BlueprintExports") / FPaths::GetBaseFilename(PackageFilename);
IFileManager::Get().DeleteDirectory(*PkDir, false, true);
// Export the whole package in both formats for comparison.
{
FStringOutputDevice Archive;
const FExportObjectInnerContext InnerContext;
UExporter::ExportToOutputDevice(&InnerContext, Package, nullptr, Archive, TEXT("copy"), 0);
FFileHelper::SaveStringToFile(Archive, *(PkDir / TEXT("COPY_DUMP.txt")));
}
{
FStringOutputDevice Archive;
const FExportObjectInnerContext InnerContext;
UExporter::ExportToOutputDevice(&InnerContext, Package, nullptr, Archive, TEXT("t3d"), 0);
FFileHelper::SaveStringToFile(Archive, *(PkDir / TEXT("T3D_DUMP.txt")));
}
TArray<UObject*> AllObjects;
GetObjectsWithPackage(Package, AllObjects);
for (UObject *Obj : AllObjects)
{
if (UBlueprint *BP = Cast<UBlueprint>(Obj))
{
FString BPDir = PkDir / BP->GetName();
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
FlxBlueprintExporter Exporter(Graph);
FString FilePath = BPDir / Graph->GetName() + TEXT(".txt");
FString DetailsPath = BPDir / TEXT("DETAILS") / Graph->GetName() + TEXT(".txt");
FFileHelper::SaveStringToFile(Exporter.GetOutput(), *FilePath);
FFileHelper::SaveStringToFile(Exporter.GetDetails(), *DetailsPath);
UE_LOG(LogLuprexIntegration, Warning, TEXT("Blueprint export: %s"), *FilePath);
}
}
}
}
#endif

View File

@@ -4,17 +4,10 @@
#include "CoreMinimal.h"
#include "Modules/ModuleInterface.h"
#include "UObject/ObjectSaveContext.h"
class FlxIntegrationModuleImpl : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
private:
#if WITH_EDITOR
void OnAssetSaved(const FString& PackageFilename, UPackage* Package, FObjectPostSaveContext Context);
FDelegateHandle OnAssetSavedHandle;
#endif
};