Add a blueprint exporter for claude code
This commit is contained in:
340
Source/Integration/BlueprintExporter.cpp
Normal file
340
Source/Integration/BlueprintExporter.cpp
Normal file
@@ -0,0 +1,340 @@
|
||||
#if WITH_EDITOR
|
||||
|
||||
#include "BlueprintExporter.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"
|
||||
|
||||
FlxBlueprintExporter::FlxBlueprintExporter(UEdGraph* InGraph)
|
||||
: Graph(InGraph)
|
||||
{
|
||||
SortNodes();
|
||||
AssignNodeNames();
|
||||
EmitNodeList();
|
||||
EmitGraph();
|
||||
}
|
||||
|
||||
void FlxBlueprintExporter::AssignNodeNames()
|
||||
{
|
||||
TMap<FString, int32> NextIndex;
|
||||
|
||||
for (UEdGraphNode* Node : SortedNodes)
|
||||
{
|
||||
FString Base = SanitizeName(Node->GetNodeTitle(ENodeTitleType::ListView).ToString());
|
||||
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::Traverse(UEdGraphNode* Node)
|
||||
{
|
||||
if (Visited.Contains(Node)) return;
|
||||
Visited.Add(Node);
|
||||
|
||||
// First, traverse input nodes
|
||||
for (UEdGraphPin* Pin : Node->Pins)
|
||||
{
|
||||
if (Pin->Direction != EGPD_Input) continue;
|
||||
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 : Node->Pins)
|
||||
{
|
||||
if (Pin->Direction != EGPD_Output) continue;
|
||||
if (Pin->PinType.PinCategory != UEdGraphSchema_K2::PC_Exec) continue;
|
||||
for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
|
||||
{
|
||||
Traverse(LinkedPin->GetOwningNode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FlxBlueprintExporter::HasExecPin(UEdGraphNode* Node, EEdGraphPinDirection Direction)
|
||||
{
|
||||
for (UEdGraphPin* Pin : Node->Pins)
|
||||
{
|
||||
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec)
|
||||
{
|
||||
if (Pin->Direction == Direction) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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::EmitNodeList()
|
||||
{
|
||||
Output.Appendf(TEXT("NodeList:\n"));
|
||||
for (UEdGraphNode* Node : SortedNodes)
|
||||
{
|
||||
if (Node->IsA<UK2Node_Knot>()) continue;
|
||||
if (Node->IsA<UEdGraphNode_Comment>()) continue;
|
||||
if (Node->IsA<UK2Node_VariableGet>()) continue;
|
||||
Output.Appendf(TEXT(" %s = %s\n"),
|
||||
*NodeNames[Node], *Node->NodeGuid.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
FString FlxBlueprintExporter::GetPinDisplayName(UEdGraphPin *Pin)
|
||||
{
|
||||
FString Result = Pin->GetOwningNode()->GetPinDisplayName(Pin).ToString();
|
||||
if (!Result.IsEmpty()) return Result;
|
||||
return Pin->PinName.ToString();
|
||||
}
|
||||
|
||||
UEdGraphPin* FlxBlueprintExporter::FindFirstPin(UEdGraphNode* Node, EEdGraphPinDirection Direction)
|
||||
{
|
||||
for (UEdGraphPin* Pin : Node->Pins)
|
||||
{
|
||||
if (Pin->Direction == Direction) return Pin;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
FString FlxBlueprintExporter::FormatPinType(UEdGraphPin* Pin)
|
||||
{
|
||||
if (UObject* SubObj = Pin->PinType.PinSubCategoryObject.Get())
|
||||
{
|
||||
return SubObj->GetName();
|
||||
}
|
||||
FString Type = Pin->PinType.PinCategory.ToString();
|
||||
Type[0] = FChar::ToUpper(Type[0]);
|
||||
return Type;
|
||||
}
|
||||
|
||||
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 = SanitizeName(GetPinDisplayName(LinkedTo));
|
||||
FString* Name = NodeNames.Find(LinkedToNode);
|
||||
return FString::Printf(TEXT("%s.%s"), **Name, *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::EmitNode(UEdGraphNode* Node)
|
||||
{
|
||||
if (Node->IsA<UEdGraphNode_Comment>())
|
||||
{
|
||||
Output.Appendf(TEXT("\n // %s\n"), *Node->NodeComment);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Node->IsA<UK2Node_VariableGet>())
|
||||
return;
|
||||
|
||||
Output.Appendf(TEXT("\n %s\n"), *NodeNames[Node]);
|
||||
|
||||
// Emit input data pins.
|
||||
for (UEdGraphPin* Pin : Node->Pins)
|
||||
{
|
||||
if (Pin->Direction != EGPD_Input) continue;
|
||||
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue;
|
||||
if (Pin->bHidden) continue;
|
||||
|
||||
Output.Appendf(TEXT(" %s %s = %s\n"),
|
||||
*FormatPinType(Pin),
|
||||
*SanitizeName(GetPinDisplayName(Pin)),
|
||||
*FormatPinSource(Pin));
|
||||
}
|
||||
|
||||
// Emit output data pins as a return line.
|
||||
FString ReturnPins;
|
||||
for (UEdGraphPin* Pin : Node->Pins)
|
||||
{
|
||||
if (Pin->Direction != EGPD_Output) continue;
|
||||
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue;
|
||||
if (Pin->bHidden) continue;
|
||||
if (!ReturnPins.IsEmpty()) ReturnPins += TEXT(",");
|
||||
ReturnPins += SanitizeName(GetPinDisplayName(Pin));
|
||||
}
|
||||
if (!ReturnPins.IsEmpty())
|
||||
Output.Appendf(TEXT(" return %s\n"), *ReturnPins);
|
||||
|
||||
// Detect output exec pin patterns.
|
||||
int32 ExecOutCount = 0;
|
||||
bool HasElsePin = false;
|
||||
for (UEdGraphPin* Pin : Node->Pins)
|
||||
{
|
||||
if (Pin->Direction != EGPD_Output) continue;
|
||||
if (Pin->PinType.PinCategory != UEdGraphSchema_K2::PC_Exec) continue;
|
||||
ExecOutCount++;
|
||||
if (Pin->PinName == TEXT("Else"))
|
||||
HasElsePin = true;
|
||||
}
|
||||
|
||||
// Emit output exec pins as goto statements.
|
||||
for (UEdGraphPin* Pin : Node->Pins)
|
||||
{
|
||||
if (Pin->Direction != EGPD_Output) continue;
|
||||
if (Pin->PinType.PinCategory != UEdGraphSchema_K2::PC_Exec) continue;
|
||||
UEdGraphPin* LinkedTo = GetLinkedTo(Pin);
|
||||
if (!LinkedTo) continue;
|
||||
|
||||
FString* TargetName = NodeNames.Find(LinkedTo->GetOwningNode());
|
||||
FString Target = TargetName ? *TargetName : TEXT("?");
|
||||
|
||||
if (ExecOutCount == 1)
|
||||
Output.Appendf(TEXT(" goto %s\n"), *Target);
|
||||
else if (HasElsePin)
|
||||
Output.Appendf(TEXT(" %s goto %s\n"), *Pin->PinName.ToString().ToLower(), *Target);
|
||||
else
|
||||
Output.Appendf(TEXT(" goto %s if %s\n"), *Target, *Pin->PinName.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
void FlxBlueprintExporter::EmitGraph()
|
||||
{
|
||||
Output.Appendf(TEXT("\nGraph:\n"));
|
||||
for (UEdGraphNode* Node : SortedNodes)
|
||||
{
|
||||
if (Node->IsA<UK2Node_Knot>()) continue;
|
||||
EmitNode(Node);
|
||||
}
|
||||
}
|
||||
|
||||
bool FlxBlueprintExporter::IsDefaultToSelf(UEdGraphPin* Pin)
|
||||
{
|
||||
// The engine's own IsSelfPin check.
|
||||
if (Pin->PinName == UEdGraphSchema_K2::PN_Self)
|
||||
return true;
|
||||
|
||||
// Check if the owning node is a function call with DefaultToSelf metadata naming this pin.
|
||||
UK2Node_CallFunction* CallNode = Cast<UK2Node_CallFunction>(Pin->GetOwningNode());
|
||||
if (!CallNode) return false;
|
||||
|
||||
UFunction* Function = CallNode->GetTargetFunction();
|
||||
if (!Function) return false;
|
||||
|
||||
const FString& DefaultToSelfPinName = Function->GetMetaData(FBlueprintMetadata::MD_DefaultToSelf);
|
||||
return Pin->PinName.ToString() == DefaultToSelfPinName;
|
||||
}
|
||||
|
||||
FString FlxBlueprintExporter::SanitizeName(const FString& Title)
|
||||
{
|
||||
FString Result;
|
||||
bool PrevWasUnderscore = true; // suppress leading underscore
|
||||
for (TCHAR Ch : Title)
|
||||
{
|
||||
if (FChar::IsAlnum(Ch))
|
||||
{
|
||||
Result += Ch;
|
||||
PrevWasUnderscore = false;
|
||||
}
|
||||
else if (!PrevWasUnderscore)
|
||||
{
|
||||
Result += TEXT('_');
|
||||
PrevWasUnderscore = true;
|
||||
}
|
||||
}
|
||||
// Trim trailing underscore
|
||||
if (Result.Len() > 0 && Result[Result.Len() - 1] == TEXT('_'))
|
||||
{
|
||||
Result.LeftChopInline(1);
|
||||
}
|
||||
return Result.IsEmpty() ? TEXT("_") : Result;
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
45
Source/Integration/BlueprintExporter.h
Normal file
45
Source/Integration/BlueprintExporter.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
class UBlueprint;
|
||||
class UEdGraph;
|
||||
class UEdGraphNode;
|
||||
|
||||
class FlxBlueprintExporter
|
||||
{
|
||||
public:
|
||||
FlxBlueprintExporter(UEdGraph* InGraph);
|
||||
|
||||
const FString GetOutput() { return Output.ToString(); }
|
||||
|
||||
private:
|
||||
void SortNodes();
|
||||
void AssignNodeNames();
|
||||
void EmitNodeList();
|
||||
void EmitGraph();
|
||||
void EmitNode(UEdGraphNode* Node);
|
||||
void Traverse(UEdGraphNode* Node);
|
||||
bool HasExecPin(UEdGraphNode* Node, EEdGraphPinDirection Direction);
|
||||
UEdGraphPin* FindFirstPin(UEdGraphNode* Node, EEdGraphPinDirection Direction);
|
||||
bool IsDefaultToSelf(UEdGraphPin* Pin);
|
||||
FString SanitizeName(const FString& Title);
|
||||
FString FormatPinType(UEdGraphPin* Pin);
|
||||
FString FormatPinSource(UEdGraphPin* Pin);
|
||||
UEdGraphPin* GetLinkedTo(UEdGraphPin *Pin);
|
||||
FString GetPinDisplayName(UEdGraphPin *Pin);
|
||||
|
||||
UEdGraph* Graph;
|
||||
|
||||
// Data populated by passes.
|
||||
TMap<UEdGraphNode*, FString> NodeNames;
|
||||
TArray<UEdGraphNode*> SortedNodes;
|
||||
TSet<UEdGraphNode*> Visited;
|
||||
|
||||
// Output buffer.
|
||||
TStringBuilder<4096> Output;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -3,4 +3,56 @@
|
||||
#include "Integration.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, Integration, "Integration" );
|
||||
#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"
|
||||
#endif
|
||||
|
||||
IMPLEMENT_PRIMARY_GAME_MODULE(FlxIntegrationModuleImpl, Integration, "Integration");
|
||||
|
||||
void FlxIntegrationModuleImpl::StartupModule()
|
||||
{
|
||||
#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;
|
||||
|
||||
ForEachObjectWithPackage(Package, [&](UObject* Object)
|
||||
{
|
||||
if (UBlueprint* BP = Cast<UBlueprint>(Object))
|
||||
{
|
||||
FString BPDir = FPaths::ProjectDir() / TEXT("Saved") / TEXT("BlueprintExports") / BP->GetName();
|
||||
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
FlxBlueprintExporter Exporter(Graph);
|
||||
|
||||
FString FilePath = BPDir / Graph->GetName() + TEXT(".txt");
|
||||
FFileHelper::SaveStringToFile(Exporter.GetOutput(), *FilePath);
|
||||
UE_LOG(LogTemp, Warning, TEXT("Blueprint export: %s"), *FilePath);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -4,3 +4,15 @@
|
||||
|
||||
#include "CoreMinimal.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
|
||||
};
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "SampleActorComponent.h"
|
||||
|
||||
// Sets default values for this component's properties
|
||||
USampleActorComponent::USampleActorComponent()
|
||||
{
|
||||
// Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features
|
||||
// off to improve performance if you don't need them.
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
|
||||
// Called when the game starts
|
||||
void USampleActorComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// ...
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Called every frame
|
||||
void USampleActorComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "SampleActorComponent.generated.h"
|
||||
|
||||
|
||||
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
|
||||
class INTEGRATION_API USampleActorComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// Sets default values for this component's properties
|
||||
USampleActorComponent();
|
||||
|
||||
protected:
|
||||
// Called when the game starts
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
public:
|
||||
// Called every frame
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||
|
||||
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "SampleEmptyClass.h"
|
||||
|
||||
SampleEmptyClass::SampleEmptyClass()
|
||||
{
|
||||
}
|
||||
|
||||
SampleEmptyClass::~SampleEmptyClass()
|
||||
{
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class INTEGRATION_API SampleEmptyClass
|
||||
{
|
||||
public:
|
||||
SampleEmptyClass();
|
||||
~SampleEmptyClass();
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "SampleUObject.h"
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/NoExportTypes.h"
|
||||
#include "SampleUObject.generated.h"
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class INTEGRATION_API USampleUObject : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
};
|
||||
Reference in New Issue
Block a user