More MCP work
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
#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"
|
||||
#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(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);
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
MCPAssetsBase::MCPAssetsBase(UClass* InTargetClass)
|
||||
: TargetClass(InTargetClass)
|
||||
{
|
||||
Scans.Add(InTargetClass);
|
||||
}
|
||||
|
||||
MCPAssetsBase& MCPAssetsBase::Exact(const FString& InName)
|
||||
@@ -62,10 +63,8 @@ bool MCPAssetsBase::Info()
|
||||
IAssetRegistry& AR = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry").Get();
|
||||
while (AR.IsLoadingAssets()) FPlatformProcess::Sleep(0.1f);
|
||||
|
||||
FARFilter Filter;
|
||||
TArray<FAssetData> Candidates;
|
||||
ConfigureFilterClassPaths(Filter);
|
||||
AR.GetAssets(Filter, Candidates);
|
||||
AR.GetAssets(ConfigureFilter(), Candidates);
|
||||
for (const FAssetData &Data : Candidates)
|
||||
{
|
||||
if (AssetMatches(Data)) AssetResults.Add(Data);
|
||||
@@ -117,22 +116,17 @@ bool MCPAssetsBase::Load()
|
||||
return true;
|
||||
}
|
||||
|
||||
void MCPAssetsBase::ConfigureFilterClassPaths(FARFilter &Filter)
|
||||
FARFilter MCPAssetsBase::ConfigureFilter()
|
||||
{
|
||||
if (Classes.IsEmpty())
|
||||
{
|
||||
Filter.ClassPaths.Add(TargetClass->GetClassPathName());
|
||||
}
|
||||
else
|
||||
{
|
||||
for (UClass* C : Classes) Filter.ClassPaths.Add(C->GetClassPathName());
|
||||
}
|
||||
FARFilter Filter;
|
||||
for (UClass* C : Scans) Filter.ClassPaths.Add(C->GetClassPathName());
|
||||
Filter.bRecursiveClasses = !bNoDerived;
|
||||
if (!bAllContent)
|
||||
{
|
||||
Filter.PackagePaths.Add(FName(TEXT("/Game")));
|
||||
Filter.bRecursivePaths = true;
|
||||
}
|
||||
return Filter;
|
||||
}
|
||||
|
||||
bool MCPAssetsBase::AssetMatches(const FAssetData &Asset)
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
#include "MCPEditorSubsystem.h"
|
||||
#include "MCPServer.h"
|
||||
#include "BlueprintExporter.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "Exporters/Exporter.h"
|
||||
#include "UnrealExporter.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
|
||||
void UBlueprintMCPEditorSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
@@ -11,6 +17,9 @@ void UBlueprintMCPEditorSubsystem::Initialize(FSubsystemCollectionBase& Collecti
|
||||
return;
|
||||
}
|
||||
|
||||
OnAssetSavedHandle = UPackage::PackageSavedWithContextEvent.AddUObject(
|
||||
this, &UBlueprintMCPEditorSubsystem::OnAssetSaved);
|
||||
|
||||
Server = MakeUnique<FBlueprintMCPServer>();
|
||||
if (Server->Start(9847, /*bEditorMode=*/true))
|
||||
{
|
||||
@@ -25,6 +34,8 @@ void UBlueprintMCPEditorSubsystem::Initialize(FSubsystemCollectionBase& Collecti
|
||||
|
||||
void UBlueprintMCPEditorSubsystem::Deinitialize()
|
||||
{
|
||||
UPackage::PackageSavedWithContextEvent.Remove(OnAssetSavedHandle);
|
||||
|
||||
if (Server)
|
||||
{
|
||||
Server->Stop();
|
||||
@@ -52,3 +63,47 @@ TStatId UBlueprintMCPEditorSubsystem::GetStatId() const
|
||||
{
|
||||
RETURN_QUICK_DECLARE_CYCLE_STAT(UBlueprintMCPEditorSubsystem, STATGROUP_Tickables);
|
||||
}
|
||||
|
||||
void UBlueprintMCPEditorSubsystem::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(LogTemp, Warning, TEXT("Blueprint export: %s"), *FilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,9 +597,9 @@ public:
|
||||
return TEXT("List all available commands with their descriptions.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
// Collect all handler classes sorted by tool name.
|
||||
TArray<TPair<FString, UClass*>> CollectHandlers() const
|
||||
{
|
||||
// Collect all handler classes sorted by tool name
|
||||
TArray<TPair<FString, UClass*>> Handlers;
|
||||
for (TObjectIterator<UClass> It; It; ++It)
|
||||
{
|
||||
@@ -612,84 +612,35 @@ public:
|
||||
Handlers.Add({ToolName, Class});
|
||||
}
|
||||
Handlers.Sort();
|
||||
return Handlers;
|
||||
}
|
||||
|
||||
if (!Verbose)
|
||||
{
|
||||
// Compact format: "command_name(param1,param2,?optional)"
|
||||
TArray<TSharedPtr<FJsonValue>> Lines;
|
||||
for (const auto& Pair : Handlers)
|
||||
{
|
||||
FString Line = Pair.Key + TEXT("(");
|
||||
bool bFirst = true;
|
||||
for (TFieldIterator<FProperty> PropIt(Pair.Value, EFieldIterationFlags::None); PropIt; ++PropIt)
|
||||
{
|
||||
if (!bFirst) Line += TEXT(",");
|
||||
bFirst = false;
|
||||
if (PropIt->HasMetaData(TEXT("Optional"))) Line += TEXT("?");
|
||||
Line += MCPUtils::PropertyNameToJsonKey(PropIt->GetName());
|
||||
}
|
||||
Line += TEXT(")");
|
||||
Lines.Add(MakeShared<FJsonValueString>(Line));
|
||||
}
|
||||
Result->SetNumberField(TEXT("count"), Lines.Num());
|
||||
Result->SetArrayField(TEXT("commands"), Lines);
|
||||
return;
|
||||
}
|
||||
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
||||
{
|
||||
auto Handlers = CollectHandlers();
|
||||
Result.Appendf(TEXT("%d commands:\n"), Handlers.Num());
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> CommandsArray;
|
||||
for (const auto& Pair : Handlers)
|
||||
{
|
||||
if (Verbose)
|
||||
{
|
||||
MCPUtils::FormatCommandHelp(Pair.Value, Result);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Non-verbose: just the signature line
|
||||
UClass* Class = Pair.Value;
|
||||
const IMCPHandler* Handler = Cast<IMCPHandler>(Class->GetDefaultObject());
|
||||
|
||||
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
||||
Entry->SetStringField(TEXT("command"), Pair.Key);
|
||||
Entry->SetStringField(TEXT("description"), Handler->GetDescription());
|
||||
|
||||
// Document parameters from UPROPERTY fields
|
||||
TArray<TSharedPtr<FJsonValue>> ParamsArray;
|
||||
Result.Append(Pair.Key);
|
||||
Result.Append(TEXT("("));
|
||||
bool bFirst = true;
|
||||
for (TFieldIterator<FProperty> PropIt(Class, EFieldIterationFlags::None); PropIt; ++PropIt)
|
||||
{
|
||||
FProperty* Prop = *PropIt;
|
||||
TSharedRef<FJsonObject> ParamObj = MakeShared<FJsonObject>();
|
||||
ParamObj->SetStringField(TEXT("name"), MCPUtils::PropertyNameToJsonKey(Prop->GetName()));
|
||||
ParamObj->SetBoolField(TEXT("required"), !Prop->HasMetaData(TEXT("Optional")));
|
||||
|
||||
// Type
|
||||
if (CastField<FStrProperty>(Prop))
|
||||
ParamObj->SetStringField(TEXT("type"), TEXT("string"));
|
||||
else if (CastField<FIntProperty>(Prop))
|
||||
ParamObj->SetStringField(TEXT("type"), TEXT("integer"));
|
||||
else if (CastField<FFloatProperty>(Prop) || CastField<FDoubleProperty>(Prop))
|
||||
ParamObj->SetStringField(TEXT("type"), TEXT("number"));
|
||||
else if (CastField<FBoolProperty>(Prop))
|
||||
ParamObj->SetStringField(TEXT("type"), TEXT("boolean"));
|
||||
else if (FStructProperty* SP = CastField<FStructProperty>(Prop))
|
||||
{
|
||||
FString StructName = SP->Struct->GetName();
|
||||
StructName.ReplaceInline(TEXT("MCP"), TEXT(""));
|
||||
ParamObj->SetStringField(TEXT("type"), StructName);
|
||||
}
|
||||
else
|
||||
ParamObj->SetStringField(TEXT("type"), TEXT("string"));
|
||||
|
||||
// Description from metadata
|
||||
const FString& Desc = Prop->GetMetaData(TEXT("Description"));
|
||||
if (!Desc.IsEmpty())
|
||||
{
|
||||
ParamObj->SetStringField(TEXT("description"), Desc);
|
||||
}
|
||||
|
||||
ParamsArray.Add(MakeShared<FJsonValueObject>(ParamObj));
|
||||
if (!bFirst) Result.Append(TEXT(","));
|
||||
bFirst = false;
|
||||
if (PropIt->HasMetaData(TEXT("Optional"))) Result.Append(TEXT("?"));
|
||||
Result.Append(MCPUtils::PropertyNameToJsonKey(PropIt->GetName()));
|
||||
}
|
||||
if (ParamsArray.Num() > 0)
|
||||
{
|
||||
Entry->SetArrayField(TEXT("parameters"), ParamsArray);
|
||||
}
|
||||
|
||||
CommandsArray.Add(MakeShared<FJsonValueObject>(Entry));
|
||||
Result.Append(TEXT(")\n"));
|
||||
}
|
||||
Result->SetNumberField(TEXT("count"), CommandsArray.Num());
|
||||
Result->SetArrayField(TEXT("commands"), CommandsArray);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -38,101 +38,54 @@ public:
|
||||
UPROPERTY(meta=(Optional, Description="Filter by parent class name (substring match)"))
|
||||
FString ParentClass;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Type filter: 'all' (default), 'regular', or 'level'"))
|
||||
FString Type;
|
||||
UPROPERTY(meta=(Optional, Description="Include regular blueprints (default true)"))
|
||||
bool IncludeRegular = true;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Include regular blueprints (default true)"))
|
||||
bool IncludeLevel = true;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("List all Blueprint assets in the project, with optional filtering by name, parent class, or type.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
||||
{
|
||||
// type: "all" (default), "regular", "level"
|
||||
bool bIncludeRegular = Type.IsEmpty() || Type == TEXT("all") || Type == TEXT("regular");
|
||||
bool bIncludeLevel = Type.IsEmpty() || Type == TEXT("all") || Type == TEXT("level");
|
||||
|
||||
MCPAssets<UBlueprint> AllBlueprints;
|
||||
AllBlueprints.Info();
|
||||
MCPAssets<UWorld> AllWorlds;
|
||||
AllWorlds.Info();
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Entries;
|
||||
if (bIncludeRegular)
|
||||
for (const FAssetData& Asset : AllBlueprints.AllData())
|
||||
MCPAssets<UObject> Assets;
|
||||
Assets.NoScans().Substring(Filter).Limit(500);
|
||||
if (IncludeRegular) Assets.Scan<UBlueprint>();
|
||||
if (IncludeLevel) Assets.Scan<UWorld>();
|
||||
Assets.Info();
|
||||
for (const FAssetData& Asset : Assets.AllData())
|
||||
{
|
||||
FString Name = Asset.AssetName.ToString();
|
||||
FString Path = Asset.PackageName.ToString();
|
||||
|
||||
if (!Filter.IsEmpty())
|
||||
{
|
||||
if (!Name.Contains(Filter, ESearchCase::IgnoreCase) &&
|
||||
!Path.Contains(Filter, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract parent class name
|
||||
FString ParentClassName;
|
||||
Asset.GetTagValue(FName(TEXT("ParentClass")), ParentClassName);
|
||||
// Tag stores full path — extract short name
|
||||
int32 DotIndex;
|
||||
if (ParentClassName.FindLastChar('.', DotIndex))
|
||||
if (Asset.AssetClassPath == UWorld::StaticClass()->GetClassPathName())
|
||||
{
|
||||
ParentClassName = ParentClassName.Mid(DotIndex + 1);
|
||||
ParentClassName = TEXT("LevelScriptActor");
|
||||
}
|
||||
else
|
||||
{
|
||||
Asset.GetTagValue(FName(TEXT("ParentClass")), ParentClassName);
|
||||
int32 DotIndex;
|
||||
if (ParentClassName.FindLastChar('.', DotIndex))
|
||||
{
|
||||
ParentClassName = ParentClassName.Mid(DotIndex + 1);
|
||||
}
|
||||
ParentClassName.RemoveFromEnd(TEXT("'"));
|
||||
}
|
||||
|
||||
// Apply parent class filter
|
||||
if (!ParentClass.IsEmpty())
|
||||
{
|
||||
if (!ParentClassName.Contains(ParentClass, ESearchCase::IgnoreCase))
|
||||
if (!ParentClassName.Equals(ParentClass, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
||||
Entry->SetStringField(TEXT("name"), Name);
|
||||
Entry->SetStringField(TEXT("path"), Path);
|
||||
Entry->SetStringField(TEXT("parentClass"), ParentClassName);
|
||||
Entries.Add(MakeShared<FJsonValueObject>(Entry));
|
||||
Result.Appendf(TEXT("%30s %s\n"), *ParentClassName, *Asset.PackageName.ToString());
|
||||
}
|
||||
|
||||
// Also include level blueprints from maps
|
||||
if (bIncludeLevel)
|
||||
for (const FAssetData& Asset : AllWorlds.AllData())
|
||||
{
|
||||
FString Name = Asset.AssetName.ToString();
|
||||
FString Path = Asset.PackageName.ToString();
|
||||
|
||||
if (!Filter.IsEmpty())
|
||||
{
|
||||
if (!Name.Contains(Filter, ESearchCase::IgnoreCase) &&
|
||||
!Path.Contains(Filter, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// No parent class filter for level blueprints
|
||||
if (!ParentClass.IsEmpty())
|
||||
{
|
||||
if (!FString(TEXT("LevelScriptActor")).Contains(ParentClass, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
||||
Entry->SetStringField(TEXT("name"), Name);
|
||||
Entry->SetStringField(TEXT("path"), Path);
|
||||
Entry->SetStringField(TEXT("parentClass"), TEXT("LevelScriptActor"));
|
||||
Entry->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||
Entries.Add(MakeShared<FJsonValueObject>(Entry));
|
||||
}
|
||||
|
||||
Result->SetNumberField(TEXT("count"), Entries.Num());
|
||||
Result->SetNumberField(TEXT("total"), AllBlueprints.AllData().Num() + AllWorlds.AllData().Num());
|
||||
Result->SetArrayField(TEXT("blueprints"), Entries);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -112,158 +112,42 @@ FMCPServer* FMCPServer::Get()
|
||||
return Sub ? Sub->GetServer() : nullptr;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// SEH wrappers for crash-safe compilation and saving.
|
||||
// MSVC constraint: __try/__except functions must NOT contain C++
|
||||
// objects with destructors. We factor the actual work into
|
||||
// separate "inner" functions and only do try/except in thin wrappers.
|
||||
// ============================================================
|
||||
#if PLATFORM_WINDOWS
|
||||
|
||||
// Inner functions that do the actual C++ work (may have destructors)
|
||||
static void CompileBlueprintInner(UBlueprint* BP, EBlueprintCompileOptions Opts)
|
||||
FString FMCPServer::DispatchToolCall(const FString& ToolName, const FJsonObject* Params)
|
||||
{
|
||||
FKismetEditorUtilities::CompileBlueprint(BP, Opts, nullptr);
|
||||
}
|
||||
|
||||
static ESavePackageResult SavePackageInner(
|
||||
UPackage* Package, UObject* Asset, const TCHAR* Filename,
|
||||
FSavePackageArgs* SaveArgs)
|
||||
{
|
||||
FSavePackageResultStruct Result = UPackage::Save(Package, Asset, Filename, *SaveArgs);
|
||||
return Result.Result;
|
||||
}
|
||||
|
||||
// SEH wrappers — absolutely NO C++ objects with destructors here.
|
||||
// EXCEPTION_EXECUTE_HANDLER = 1 (avoiding Windows.h include)
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable: 4611) // interaction between '_setjmp' and C++ object destruction
|
||||
int32 TryCompileBlueprintSEH(UBlueprint* BP, EBlueprintCompileOptions Opts)
|
||||
{
|
||||
__try
|
||||
UClass** HandlerClass = MCPHandlerRegistry.Find(ToolName);
|
||||
if (!HandlerClass)
|
||||
{
|
||||
CompileBlueprintInner(BP, Opts);
|
||||
return 0;
|
||||
return FString::Printf(TEXT("Unknown tool: %s"), *ToolName);
|
||||
}
|
||||
__except (1)
|
||||
|
||||
TStrongObjectPtr<UObject> HandlerObj(NewObject<UObject>(GetTransientPackage(), *HandlerClass));
|
||||
IMCPHandler* Handler = Cast<IMCPHandler>(HandlerObj.Get());
|
||||
|
||||
TStringBuilder<4096> PopulateError;
|
||||
if (!MCPUtils::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), Params, PopulateError))
|
||||
{
|
||||
return -1;
|
||||
PopulateError.Append(TEXT("\nUsage:\n"));
|
||||
MCPUtils::FormatCommandHelp(*HandlerClass, PopulateError);
|
||||
return PopulateError.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
static int32 TrySavePackageSEH(
|
||||
UPackage* Package, UObject* Asset, const TCHAR* Filename,
|
||||
FSavePackageArgs* SaveArgs, ESavePackageResult* OutResult)
|
||||
{
|
||||
__try
|
||||
// Try text handler first; fall back to JSON if nothing was written.
|
||||
TStringBuilder<32768> TextResult;
|
||||
Handler->Handle(Params, TextResult);
|
||||
if (TextResult.Len() > 0)
|
||||
{
|
||||
*OutResult = SavePackageInner(Package, Asset, Filename, SaveArgs);
|
||||
return 0;
|
||||
}
|
||||
__except (1)
|
||||
{
|
||||
*OutResult = ESavePackageResult::Error;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Inner: create expression, register in material, and trigger PostEditChange.
|
||||
// All of this may crash for classes that are effectively abstract.
|
||||
static void AddMaterialExpressionInner(
|
||||
UObject* Owner, UClass* ExprClass, UMaterial* Material, UMaterialFunction* MatFunc,
|
||||
int32 PosX, int32 PosY, UMaterialExpression** OutExpr)
|
||||
{
|
||||
*OutExpr = NewObject<UMaterialExpression>(Owner, ExprClass);
|
||||
if (!*OutExpr) return;
|
||||
|
||||
(*OutExpr)->MaterialExpressionEditorX = PosX;
|
||||
(*OutExpr)->MaterialExpressionEditorY = PosY;
|
||||
|
||||
if (Material)
|
||||
{
|
||||
Material->GetExpressionCollection().AddExpression(*OutExpr);
|
||||
if (Material->MaterialGraph)
|
||||
FString Result = TextResult.ToString();
|
||||
for (int32 i = 0; i < Result.Len(); ++i)
|
||||
{
|
||||
Material->MaterialGraph->RebuildGraph();
|
||||
if (Result[i] == TEXT('\0')) Result[i] = TEXT(' ');
|
||||
}
|
||||
Material->PreEditChange(nullptr);
|
||||
Material->PostEditChange();
|
||||
Material->MarkPackageDirty();
|
||||
return Result;
|
||||
}
|
||||
else if (MatFunc)
|
||||
{
|
||||
MatFunc->GetExpressionCollection().AddExpression(*OutExpr);
|
||||
MatFunc->PreEditChange(nullptr);
|
||||
MatFunc->PostEditChange();
|
||||
MatFunc->MarkPackageDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Inner: remove a bad expression from a material after a crash
|
||||
static void CleanupBadExpressionInner(UMaterial* Material, UMaterialFunction* MatFunc, UMaterialExpression* BadExpr)
|
||||
{
|
||||
if (!BadExpr) return;
|
||||
if (Material)
|
||||
{
|
||||
Material->GetExpressionCollection().RemoveExpression(BadExpr);
|
||||
if (Material->MaterialGraph)
|
||||
{
|
||||
Material->MaterialGraph->RebuildGraph();
|
||||
}
|
||||
}
|
||||
else if (MatFunc)
|
||||
{
|
||||
MatFunc->GetExpressionCollection().RemoveExpression(BadExpr);
|
||||
}
|
||||
BadExpr->MarkAsGarbage();
|
||||
}
|
||||
|
||||
int32 TryAddMaterialExpressionSEH(
|
||||
UObject* Owner, UClass* ExprClass, UMaterial* Material, UMaterialFunction* MatFunc,
|
||||
int32 PosX, int32 PosY, UMaterialExpression** OutExpr)
|
||||
{
|
||||
__try
|
||||
{
|
||||
AddMaterialExpressionInner(Owner, ExprClass, Material, MatFunc, PosX, PosY, OutExpr);
|
||||
return 0;
|
||||
}
|
||||
__except (1)
|
||||
{
|
||||
// Try to clean up the partially-added expression
|
||||
__try
|
||||
{
|
||||
CleanupBadExpressionInner(Material, MatFunc, *OutExpr);
|
||||
}
|
||||
__except (1)
|
||||
{
|
||||
// Cleanup also crashed — nothing more we can do
|
||||
}
|
||||
*OutExpr = nullptr;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
#pragma warning(pop)
|
||||
|
||||
#endif // PLATFORM_WINDOWS
|
||||
|
||||
|
||||
void FMCPServer::DispatchToolCall(const FString& ToolName, const FJsonObject* Params, FJsonObject* Result)
|
||||
{
|
||||
if (UClass** HandlerClass = MCPHandlerRegistry.Find(ToolName))
|
||||
{
|
||||
TStrongObjectPtr<UObject> HandlerObj(NewObject<UObject>(GetTransientPackage(), *HandlerClass));
|
||||
IMCPHandler* Handler = Cast<IMCPHandler>(HandlerObj.Get());
|
||||
if (MCPUtils::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), Params, Result))
|
||||
{
|
||||
Handler->Handle(Params, Result);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Unknown tool: %s"), *ToolName));
|
||||
}
|
||||
// Invoke the Json handler.
|
||||
TSharedRef<FJsonObject> JsonResult = MakeShared<FJsonObject>();
|
||||
Handler->Handle(Params, &*JsonResult);
|
||||
return MCPUtils::JsonToString(JsonResult);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -289,9 +173,7 @@ FString FMCPServer::HandleRequest(const FString& Line)
|
||||
}
|
||||
Request->RemoveField(TEXT("command"));
|
||||
|
||||
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
||||
DispatchToolCall(Command, Request.Get(), &*Result);
|
||||
return MCPUtils::JsonToString(Result);
|
||||
return DispatchToolCall(Command, Request.Get());
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -482,11 +364,10 @@ void FMCPServer::ClientThreadFunc(FMCPServer* Server, TSharedPtr<FClientConnecti
|
||||
// Empty response means either notification or shutdown
|
||||
if (Response.IsEmpty()) continue;
|
||||
|
||||
// Write the response line back (blocking)
|
||||
FString ResponseLine = Response + TEXT("\n");
|
||||
FTCHARToUTF8 Utf8(*ResponseLine);
|
||||
// Write the response back, null-terminated (blocking)
|
||||
FTCHARToUTF8 Utf8(*Response);
|
||||
int32 BytesSent = 0;
|
||||
Socket->Send((const uint8*)Utf8.Get(), Utf8.Length(), BytesSent);
|
||||
Socket->Send((const uint8*)Utf8.Get(), Utf8.Length() + 1, BytesSent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,10 @@ MCPErrorCallback::MCPErrorCallback(FJsonObject* Result)
|
||||
: Func([Result](const FString& Msg) { MCPUtils::MakeErrorJson(Result, Msg); })
|
||||
{}
|
||||
|
||||
MCPErrorCallback::MCPErrorCallback(FStringBuilderBase& OutResult)
|
||||
: Func([&OutResult](const FString& Msg) { OutResult.Reset(); OutResult.Appendf(TEXT("ERROR: %s\n"), *Msg); })
|
||||
{}
|
||||
|
||||
// ============================================================
|
||||
// JSON helpers
|
||||
// ============================================================
|
||||
@@ -1494,3 +1498,64 @@ bool MCPUtils::PopulateFromJson(
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FormatPropertyType — human-readable type name for a UPROPERTY
|
||||
// ============================================================
|
||||
|
||||
FString MCPUtils::FormatPropertyType(FProperty* Prop)
|
||||
{
|
||||
if (CastField<FStrProperty>(Prop)) return TEXT("string");
|
||||
if (CastField<FIntProperty>(Prop)) return TEXT("integer");
|
||||
if (CastField<FFloatProperty>(Prop) || CastField<FDoubleProperty>(Prop)) return TEXT("number");
|
||||
if (CastField<FBoolProperty>(Prop)) return TEXT("boolean");
|
||||
if (FStructProperty* SP = CastField<FStructProperty>(Prop))
|
||||
{
|
||||
FString StructName = SP->Struct->GetName();
|
||||
StructName.ReplaceInline(TEXT("MCP"), TEXT(""));
|
||||
return StructName;
|
||||
}
|
||||
return TEXT("string");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FormatCommandHelp — verbose description of one handler command
|
||||
// ============================================================
|
||||
|
||||
void MCPUtils::FormatCommandHelp(UClass* HandlerClass, FStringBuilderBase& Result)
|
||||
{
|
||||
const IMCPHandler* Handler = Cast<IMCPHandler>(HandlerClass->GetDefaultObject());
|
||||
if (!Handler) return;
|
||||
|
||||
const FString& ToolName = HandlerClass->GetMetaData(TEXT("ToolName"));
|
||||
|
||||
// Command signature line
|
||||
Result.Append(ToolName);
|
||||
Result.Append(TEXT("("));
|
||||
bool bFirst = true;
|
||||
for (TFieldIterator<FProperty> PropIt(HandlerClass, EFieldIterationFlags::None); PropIt; ++PropIt)
|
||||
{
|
||||
if (!bFirst) Result.Append(TEXT(","));
|
||||
bFirst = false;
|
||||
if (PropIt->HasMetaData(TEXT("Optional"))) Result.Append(TEXT("?"));
|
||||
Result.Append(PropertyNameToJsonKey(PropIt->GetName()));
|
||||
}
|
||||
Result.Append(TEXT(")\n"));
|
||||
|
||||
// Description and parameter details
|
||||
Result.Appendf(TEXT(" %s\n"), *Handler->GetDescription());
|
||||
for (TFieldIterator<FProperty> PropIt(HandlerClass, EFieldIterationFlags::None); PropIt; ++PropIt)
|
||||
{
|
||||
FProperty* Prop = *PropIt;
|
||||
FString Name = PropertyNameToJsonKey(Prop->GetName());
|
||||
FString Type = FormatPropertyType(Prop);
|
||||
bool bOptional = Prop->HasMetaData(TEXT("Optional"));
|
||||
const FString& Desc = Prop->GetMetaData(TEXT("Description"));
|
||||
|
||||
Result.Appendf(TEXT(" %s %s%s"),
|
||||
*Type, *Name, bOptional ? TEXT(" (optional)") : TEXT(""));
|
||||
if (!Desc.IsEmpty())
|
||||
Result.Appendf(TEXT(" — %s"), *Desc);
|
||||
Result.Append(TEXT("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
#pragma once
|
||||
|
||||
#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;
|
||||
};
|
||||
@@ -9,15 +9,93 @@
|
||||
|
||||
struct FARFilter;
|
||||
|
||||
// ============================================================
|
||||
// MCPAssetsBase — non-template base for MCPAssets<T>
|
||||
// ============================================================
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// MCPAssets - search for assets.
|
||||
//
|
||||
//
|
||||
// Construct an object of class MCPAssets like this:
|
||||
//
|
||||
// MCPAssets<UBlueprint> Assets;
|
||||
//
|
||||
// The UBlueprint template parameter means that this example
|
||||
// Assets loader is capable of storing pointers to
|
||||
// UBlueprint.
|
||||
//
|
||||
// It also means that by default, it will scan
|
||||
// all UBlueprint assets. You can narrow that:
|
||||
//
|
||||
// Assets.NoScans();
|
||||
// Assets.Scan<UAnimBlueprint>();
|
||||
// Assets.Scan<ULuprexBlueprint>();
|
||||
//
|
||||
// To get string matching, call either 'Exact' or
|
||||
// 'Substring'. If you don't call either of these, there's
|
||||
// no string filter. If the string you pass in contains a
|
||||
// slash, then the search is by asset-path, otherwise, by
|
||||
// asset-name:
|
||||
//
|
||||
// Assets.Substring(TEXT("MyAsset"));
|
||||
//
|
||||
// By default, the asset finder limits itself to assets in
|
||||
// the /Game folder. You can expand that:
|
||||
//
|
||||
// Assets.AllContent();
|
||||
//
|
||||
// You can specify that you don't want to see derived
|
||||
// classes:
|
||||
//
|
||||
// Assets.NoDerived()
|
||||
//
|
||||
// You can specify a limit on the number of results
|
||||
// returned:
|
||||
//
|
||||
// Assets.Limit(100)
|
||||
//
|
||||
// You can specify what constitutes an error condition. If
|
||||
// the asset finder detects an error, then it will report
|
||||
// it:
|
||||
//
|
||||
// Assets.ENone() - it's an error if nothing is found
|
||||
// Assets.EAny() - it's an error if anything is found
|
||||
// Assets.ETwo() - it's an error if two or more are found
|
||||
//
|
||||
// Errors can be stored in variables, or in string builders,
|
||||
// or in json trees. This example tells it to put error
|
||||
// messages into a string variable:
|
||||
//
|
||||
// FString ErrorMessage;
|
||||
// Assets.Errors(ErrorMessage)
|
||||
//
|
||||
// Once the Assets object is configured, it's time to scan
|
||||
// the assets. Use 'Info' if you just want to see
|
||||
// FAssetData. Use 'Load' if you want to load the assets
|
||||
// into memory. Both of these functions return true if
|
||||
// there were no errors, or false if there was one.
|
||||
//
|
||||
// bool ok = Assets.Load();
|
||||
//
|
||||
// Once you've scanned the assets, you can examine the
|
||||
// results using the following methods. The objects array
|
||||
// will be empty if you called 'Info' instead of 'Load':
|
||||
//
|
||||
// const TArray<FAssetData>& AllData();
|
||||
// const FAssetData& OneData();
|
||||
// const TArray<UBlueprint*> Objects();
|
||||
// UBlueprint* Object();
|
||||
//
|
||||
//
|
||||
// MCPAssets configuration methods can be chained:
|
||||
//
|
||||
// Assets.Limit(100).Errors(ErrMsg).ENone().ETwo();
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
class MCPAssetsBase
|
||||
{
|
||||
public:
|
||||
MCPAssetsBase(UClass* InTargetClass);
|
||||
MCPAssetsBase& Scan(UClass* Class) { Classes.Add(Class); return *this; }
|
||||
MCPAssetsBase& NoScans() { Scans.Empty(); return *this; }
|
||||
MCPAssetsBase& Scan(UClass* Class) { Scans.Add(Class); return *this; }
|
||||
template<class T> MCPAssetsBase& Scan() { return Scan(T::StaticClass()); }
|
||||
MCPAssetsBase& Exact(const FString& InName);
|
||||
MCPAssetsBase& Substring(const FString& InFilter);
|
||||
@@ -36,15 +114,16 @@ public:
|
||||
const FAssetData& OneData() const { return AssetResults[0]; }
|
||||
|
||||
private:
|
||||
bool Execute(bool bLoad);
|
||||
void ConfigureFilterClassPaths(FARFilter &Filter);
|
||||
FARFilter ConfigureFilter();
|
||||
bool AssetMatches(const FAssetData &Data);
|
||||
UObject *TryLoadAsset(const FAssetData &Asset);
|
||||
void SetError(const FString &Msg);
|
||||
|
||||
protected:
|
||||
MCPAssetsBase(UClass* InTargetClass);
|
||||
|
||||
UClass* TargetClass;
|
||||
TArray<UClass*, TInlineAllocator<4>> Classes;
|
||||
TSet<UClass*> Scans;
|
||||
TArray<FAssetData> AssetResults;
|
||||
TArray<UObject*> UObjectResults;
|
||||
FString MatchName;
|
||||
@@ -59,18 +138,6 @@ protected:
|
||||
MCPErrorCallback ErrorCB = MCPErrorCallback(nullptr);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// MCPAssets<T> — builder-pattern asset finder
|
||||
//
|
||||
// Usage:
|
||||
// MCPAssets<UBlueprint> Assets;
|
||||
// if (!Assets.Exact(Name).Errors(Result).ENone().ETwo().Load()) return;
|
||||
// UBlueprint* BP = Assets.Object();
|
||||
//
|
||||
// MCPAssets<UMaterial> Materials;
|
||||
// if (!Materials.Substring(Filter).Limit(100).Load()) return;
|
||||
// for (UMaterial* Mat : Materials.Objects()) { ... }
|
||||
// ============================================================
|
||||
|
||||
template<class T>
|
||||
class MCPAssets : public MCPAssetsBase
|
||||
@@ -82,5 +149,5 @@ public:
|
||||
{
|
||||
return TArrayView<T* const>(reinterpret_cast<T* const*>(UObjectResults.GetData()), UObjectResults.Num());
|
||||
}
|
||||
T* Object() const { return static_cast<T*>(UObjectResults[0]); }
|
||||
T* Object() const { return UObjectResults.IsEmpty() ? nullptr : static_cast<T*>(UObjectResults[0]); }
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "CoreMinimal.h"
|
||||
#include "EditorSubsystem.h"
|
||||
#include "Tickable.h"
|
||||
#include "UObject/ObjectSaveContext.h"
|
||||
#include "MCPServer.h"
|
||||
#include "MCPEditorSubsystem.generated.h"
|
||||
|
||||
@@ -32,5 +33,7 @@ public:
|
||||
FBlueprintMCPServer* GetServer() const { return Server.Get(); }
|
||||
|
||||
private:
|
||||
void OnAssetSaved(const FString& PackageFilename, UPackage* Package, FObjectPostSaveContext Context);
|
||||
FDelegateHandle OnAssetSavedHandle;
|
||||
TUniquePtr<FBlueprintMCPServer> Server;
|
||||
};
|
||||
|
||||
@@ -50,5 +50,9 @@ public:
|
||||
virtual FString GetDescription() const = 0;
|
||||
|
||||
// Called after parameter fields have been populated from JSON.
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) = 0;
|
||||
// Override the FStringBuilderBase version for plain-text responses, or the
|
||||
// FJsonObject version for JSON responses. The dispatcher calls the text
|
||||
// version first; if the builder remains empty, it falls back to JSON.
|
||||
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) {}
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) {}
|
||||
};
|
||||
|
||||
@@ -53,8 +53,8 @@ private:
|
||||
TMap<FString, UClass*> MCPHandlerRegistry; // tool name -> UMCPHandler subclass
|
||||
void BuildMCPHandlerRegistry();
|
||||
|
||||
// Dispatch a tool call to the appropriate handler
|
||||
void DispatchToolCall(const FString& ToolName, const FJsonObject* Params, FJsonObject* Result);
|
||||
// Dispatch a tool call to the appropriate handler, returning the response string.
|
||||
FString DispatchToolCall(const FString& ToolName, const FJsonObject* Params);
|
||||
|
||||
// Handle a complete JSON line and return the response JSON
|
||||
FString HandleRequest(const FString& Line);
|
||||
|
||||
@@ -77,6 +77,7 @@ struct MCPErrorCallback
|
||||
MCPErrorCallback(std::nullptr_t);
|
||||
MCPErrorCallback(FString& OutError);
|
||||
MCPErrorCallback(FJsonObject* Result);
|
||||
MCPErrorCallback(FStringBuilderBase& OutResult);
|
||||
|
||||
void SetError(const FString& Msg) const { Func(Msg); }
|
||||
};
|
||||
@@ -111,6 +112,12 @@ public:
|
||||
|
||||
// ----- Enum helpers -----
|
||||
// Convert enum value to string. If Prefix is specified, strip "Prefix_" from the front.
|
||||
template<typename T>
|
||||
static FString EnumToString(TEnumAsByte<T> Value, const FString& Prefix = FString())
|
||||
{
|
||||
return EnumToString<T>((T)Value, Prefix);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static FString EnumToString(T Value, const FString& Prefix = FString())
|
||||
{
|
||||
@@ -192,6 +199,10 @@ public:
|
||||
static bool PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr<FJsonValue>& JsonValue, MCPErrorCallback Error);
|
||||
static bool PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json, MCPErrorCallback Error);
|
||||
|
||||
// ----- Command help -----
|
||||
static FString FormatPropertyType(FProperty* Prop);
|
||||
static void FormatCommandHelp(UClass* HandlerClass, FStringBuilderBase& Result);
|
||||
|
||||
private:
|
||||
static FString SetPropertyFromJson(void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user