1476 lines
46 KiB
C++
1476 lines
46 KiB
C++
#pragma once
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "MCPHandler.h"
|
|
#include "MCPAssetFinder.h"
|
|
#include "MCPServer.h"
|
|
#include "MCPUtils.h"
|
|
#include "Engine/Blueprint.h"
|
|
#include "Materials/Material.h"
|
|
#include "Materials/MaterialInstanceConstant.h"
|
|
#include "Materials/MaterialFunction.h"
|
|
#include "Engine/World.h"
|
|
#include "Engine/LevelScriptBlueprint.h"
|
|
#include "EdGraph/EdGraph.h"
|
|
#include "EdGraph/EdGraphNode.h"
|
|
#include "EdGraph/EdGraphPin.h"
|
|
#include "EdGraphSchema_K2.h"
|
|
#include "K2Node.h"
|
|
#include "K2Node_CallFunction.h"
|
|
#include "K2Node_Event.h"
|
|
#include "K2Node_CustomEvent.h"
|
|
#include "K2Node_FunctionEntry.h"
|
|
#include "K2Node_EditablePinBase.h"
|
|
#include "K2Node_VariableGet.h"
|
|
#include "K2Node_VariableSet.h"
|
|
#include "K2Node_BreakStruct.h"
|
|
#include "K2Node_MakeStruct.h"
|
|
#include "K2Node_DynamicCast.h"
|
|
#include "K2Node_CallParentFunction.h"
|
|
#include "K2Node_IfThenElse.h"
|
|
#include "K2Node_ExecutionSequence.h"
|
|
#include "K2Node_MacroInstance.h"
|
|
#include "K2Node_SpawnActorFromClass.h"
|
|
#include "K2Node_Select.h"
|
|
#include "K2Node_Knot.h"
|
|
#include "EdGraphNode_Comment.h"
|
|
#include "GameFramework/Actor.h"
|
|
#include "Kismet2/BlueprintEditorUtils.h"
|
|
#include "Kismet2/KismetEditorUtilities.h"
|
|
#include "Serialization/JsonReader.h"
|
|
#include "Serialization/JsonWriter.h"
|
|
#include "Serialization/JsonSerializer.h"
|
|
#include "UObject/SavePackage.h"
|
|
#include "UObject/UObjectIterator.h"
|
|
#include "Misc/PackageName.h"
|
|
#include "AssetRegistry/AssetRegistryModule.h"
|
|
#include "AssetRegistry/IAssetRegistry.h"
|
|
#include "AssetToolsModule.h"
|
|
#include "IAssetTools.h"
|
|
#include "BlueprintNodeSpawner.h"
|
|
#include "MCPHandlers_Mutation.generated.h"
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
USTRUCT()
|
|
struct FMoveNodeEntry
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
UPROPERTY()
|
|
FString Node;
|
|
|
|
UPROPERTY()
|
|
int32 X = 0;
|
|
|
|
UPROPERTY()
|
|
int32 Y = 0;
|
|
};
|
|
|
|
|
|
UCLASS(meta=(ToolName="set_node_positions"))
|
|
class UMCPHandler_SetNodePositions : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
FString Blueprint;
|
|
|
|
UPROPERTY(meta=(Description="Array of {nodeId, x, y} objects"))
|
|
FMCPJsonArray Nodes;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Reposition one or more nodes in a Blueprint graph.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
|
|
MCPAssets<UBlueprint> Assets;
|
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
UBlueprint* BP = Assets.Object();
|
|
|
|
TArray<TSharedPtr<FJsonValue>> Results;
|
|
int32 SuccessCount = 0;
|
|
|
|
for (const TSharedPtr<FJsonValue>& NodeVal : Nodes.Array)
|
|
{
|
|
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
|
|
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
|
|
|
|
FMoveNodeEntry Entry;
|
|
FString PopulateError = MCPUtils::PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal);
|
|
if (!PopulateError.IsEmpty())
|
|
{
|
|
EntryResult->SetStringField(TEXT("error"), PopulateError);
|
|
continue;
|
|
}
|
|
|
|
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node);
|
|
if (!Node)
|
|
{
|
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.Node));
|
|
continue;
|
|
}
|
|
|
|
int32 OldX = Node->NodePosX;
|
|
int32 OldY = Node->NodePosY;
|
|
Node->NodePosX = Entry.X;
|
|
Node->NodePosY = Entry.Y;
|
|
EntryResult->SetNumberField(TEXT("oldX"), OldX);
|
|
EntryResult->SetNumberField(TEXT("oldY"), OldY);
|
|
EntryResult->SetNumberField(TEXT("newX"), Node->NodePosX);
|
|
EntryResult->SetNumberField(TEXT("newY"), Node->NodePosY);
|
|
SuccessCount++;
|
|
}
|
|
|
|
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: MoveNode — %d/%d succeeded"),
|
|
SuccessCount, Nodes.Array.Num());
|
|
|
|
Result->SetNumberField(TEXT("movedCount"), SuccessCount);
|
|
Result->SetNumberField(TEXT("totalRequested"), Nodes.Array.Num());
|
|
Result->SetArrayField(TEXT("results"), Results);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="duplicate_nodes_in_graph"))
|
|
class UMCPHandler_DuplicateNodesInGraph : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
FString Blueprint;
|
|
|
|
UPROPERTY(meta=(Description="Graph name"))
|
|
FString Graph;
|
|
|
|
UPROPERTY(meta=(Description="Array of node GUIDs to duplicate"))
|
|
FMCPJsonArray Nodes;
|
|
|
|
UPROPERTY(meta=(Optional, Description="X offset for duplicated nodes"))
|
|
int32 OffsetX = 50;
|
|
|
|
UPROPERTY(meta=(Optional, Description="Y offset for duplicated nodes"))
|
|
int32 OffsetY = 50;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Duplicate one or more nodes in a Blueprint graph. "
|
|
"Creates copies offset from the originals with new GUIDs. "
|
|
"Connections are not preserved on the duplicates.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
|
|
MCPAssets<UBlueprint> Assets;
|
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
UBlueprint* BP = Assets.Object();
|
|
|
|
// Find the target graph
|
|
FString DecodedGraphName = MCPUtils::UrlDecode(Graph);
|
|
UEdGraph* TargetGraph = nullptr;
|
|
TArray<UEdGraph*> AllGraphs;
|
|
BP->GetAllGraphs(AllGraphs);
|
|
|
|
for (UEdGraph* G : AllGraphs)
|
|
{
|
|
if (G && G->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
|
|
{
|
|
TargetGraph = G;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!TargetGraph)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
|
|
}
|
|
|
|
if (Nodes.Array.Num() == 0)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("nodeIds array is empty"));
|
|
}
|
|
|
|
// Find all source nodes
|
|
TArray<UEdGraphNode*> SourceNodes;
|
|
TArray<FString> NotFound;
|
|
|
|
for (const TSharedPtr<FJsonValue>& IdVal : Nodes.Array)
|
|
{
|
|
FString Node = IdVal->AsString();
|
|
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node);
|
|
if (FoundNode)
|
|
{
|
|
if (FoundNode->GetGraph() == TargetGraph)
|
|
{
|
|
SourceNodes.Add(FoundNode);
|
|
}
|
|
else
|
|
{
|
|
NotFound.Add(FString::Printf(TEXT("%s (in different graph)"), *Node));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
NotFound.Add(Node);
|
|
}
|
|
}
|
|
|
|
if (SourceNodes.Num() == 0)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("No valid nodes found to duplicate"));
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Duplicating %d node(s) in graph '%s' of '%s'"),
|
|
SourceNodes.Num(), *DecodedGraphName, *Blueprint);
|
|
|
|
// Duplicate each node
|
|
TArray<TSharedPtr<FJsonValue>> DuplicatedNodes;
|
|
TMap<FGuid, FGuid> OldToNewGuidMap;
|
|
|
|
for (UEdGraphNode* SourceNode : SourceNodes)
|
|
{
|
|
UEdGraphNode* NewNode = DuplicateObject<UEdGraphNode>(SourceNode, TargetGraph);
|
|
if (!NewNode)
|
|
{
|
|
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
Entry->SetStringField(TEXT("sourceNodeId"), SourceNode->NodeGuid.ToString());
|
|
Entry->SetStringField(TEXT("error"), TEXT("DuplicateObject failed"));
|
|
DuplicatedNodes.Add(MakeShared<FJsonValueObject>(Entry));
|
|
continue;
|
|
}
|
|
|
|
NewNode->CreateNewGuid();
|
|
OldToNewGuidMap.Add(SourceNode->NodeGuid, NewNode->NodeGuid);
|
|
|
|
NewNode->NodePosX += OffsetX;
|
|
NewNode->NodePosY += OffsetY;
|
|
|
|
for (UEdGraphPin* Pin : NewNode->Pins)
|
|
{
|
|
if (Pin)
|
|
{
|
|
Pin->LinkedTo.Empty();
|
|
}
|
|
}
|
|
|
|
TargetGraph->AddNode(NewNode, false, false);
|
|
|
|
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
Entry->SetStringField(TEXT("sourceNodeId"), SourceNode->NodeGuid.ToString());
|
|
Entry->SetStringField(TEXT("newNodeId"), NewNode->NodeGuid.ToString());
|
|
Entry->SetNumberField(TEXT("posX"), NewNode->NodePosX);
|
|
Entry->SetNumberField(TEXT("posY"), NewNode->NodePosY);
|
|
Entry->SetStringField(TEXT("nodeClass"), NewNode->GetClass()->GetName());
|
|
Entry->SetStringField(TEXT("nodeTitle"), NewNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
|
|
DuplicatedNodes.Add(MakeShared<FJsonValueObject>(Entry));
|
|
}
|
|
|
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Duplicated %d node(s)"),
|
|
DuplicatedNodes.Num());
|
|
|
|
Result->SetNumberField(TEXT("duplicatedCount"), DuplicatedNodes.Num());
|
|
Result->SetArrayField(TEXT("nodes"), DuplicatedNodes);
|
|
|
|
if (NotFound.Num() > 0)
|
|
{
|
|
TArray<TSharedPtr<FJsonValue>> NotFoundArr;
|
|
for (const FString& NF : NotFound)
|
|
{
|
|
NotFoundArr.Add(MakeShared<FJsonValueString>(NF));
|
|
}
|
|
Result->SetArrayField(TEXT("notFound"), NotFoundArr);
|
|
}
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
USTRUCT()
|
|
struct FSpawnNodeEntry
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
UPROPERTY()
|
|
FString ActionName;
|
|
|
|
UPROPERTY()
|
|
int32 PosX = 0;
|
|
|
|
UPROPERTY()
|
|
int32 PosY = 0;
|
|
};
|
|
|
|
|
|
UCLASS(meta=(ToolName="spawn_nodes_in_graph"))
|
|
class UMCPHandler_SpawnNodesInGraph : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
FString Blueprint;
|
|
|
|
UPROPERTY(meta=(Description="Graph name (e.g. 'EventGraph')"))
|
|
FString Graph;
|
|
|
|
UPROPERTY(meta=(Description="Array of {actionName, posX, posY} objects. Use search_node_types to find action names."))
|
|
FMCPJsonArray Nodes;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Create nodes in a Blueprint graph using the editor's action database. "
|
|
"Can create ANY node type that appears in the editor's right-click menu, including custom K2 nodes. "
|
|
"Use search_node_types first to find the exact action name.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
|
|
// Load Blueprint
|
|
MCPAssets<UBlueprint> Assets;
|
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
UBlueprint* BP = Assets.Object();
|
|
|
|
// Find the target graph
|
|
FString DecodedGraphName = MCPUtils::UrlDecode(Graph);
|
|
UEdGraph* TargetGraph = nullptr;
|
|
TArray<UEdGraph*> AllGraphs;
|
|
BP->GetAllGraphs(AllGraphs);
|
|
|
|
for (UEdGraph* G : AllGraphs)
|
|
{
|
|
if (G && G->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
|
|
{
|
|
TargetGraph = G;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!TargetGraph)
|
|
{
|
|
TArray<TSharedPtr<FJsonValue>> GraphNames;
|
|
for (UEdGraph* G : AllGraphs)
|
|
{
|
|
if (G) GraphNames.Add(MakeShared<FJsonValueString>(G->GetName()));
|
|
}
|
|
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
|
|
Result->SetArrayField(TEXT("availableGraphs"), GraphNames);
|
|
return;
|
|
}
|
|
|
|
TArray<TSharedPtr<FJsonValue>> Results;
|
|
int32 SuccessCount = 0;
|
|
|
|
for (const TSharedPtr<FJsonValue>& NodeVal : Nodes.Array)
|
|
{
|
|
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
|
|
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
|
|
|
|
FSpawnNodeEntry Entry;
|
|
FString PopulateError = MCPUtils::PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal);
|
|
if (!PopulateError.IsEmpty())
|
|
{
|
|
EntryResult->SetStringField(TEXT("error"), PopulateError);
|
|
continue;
|
|
}
|
|
|
|
// Find the spawner by exact full name
|
|
TArray<UBlueprintNodeSpawner*> Matches = MCPUtils::SearchNodeSpawners(Entry.ActionName, 0, /*ExactMatch=*/true, TargetGraph);
|
|
if (Matches.Num() == 0)
|
|
{
|
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(
|
|
TEXT("No action found matching '%s'. Use search_node_types to find available actions."),
|
|
*Entry.ActionName));
|
|
continue;
|
|
}
|
|
if (Matches.Num() > 1)
|
|
{
|
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(
|
|
TEXT("Ambiguous: %d spawners match '%s'. Cannot determine which one to use."),
|
|
Matches.Num(), *Entry.ActionName));
|
|
continue;
|
|
}
|
|
UBlueprintNodeSpawner* Spawner = Matches[0];
|
|
|
|
// Invoke the spawner
|
|
FVector2D Location(Entry.PosX, Entry.PosY);
|
|
IBlueprintNodeBinder::FBindingSet Bindings;
|
|
UEdGraphNode* NewNode = Spawner->Invoke(TargetGraph, Bindings, Location);
|
|
|
|
if (!NewNode)
|
|
{
|
|
EntryResult->SetStringField(TEXT("error"), TEXT("Spawner Invoke() returned null — node creation failed."));
|
|
continue;
|
|
}
|
|
|
|
// Ensure valid GUID
|
|
if (!NewNode->NodeGuid.IsValid())
|
|
{
|
|
NewNode->CreateNewGuid();
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Spawned node '%s' (class %s) via action '%s' in graph '%s' of '%s'"),
|
|
*NewNode->NodeGuid.ToString(),
|
|
*NewNode->GetClass()->GetName(),
|
|
*Entry.ActionName,
|
|
*DecodedGraphName,
|
|
*Blueprint);
|
|
|
|
// Serialize result
|
|
TSharedPtr<FJsonObject> NodeState = MCPUtils::SerializeNode(NewNode);
|
|
|
|
EntryResult->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString());
|
|
EntryResult->SetStringField(TEXT("nodeClass"), NewNode->GetClass()->GetName());
|
|
EntryResult->SetStringField(TEXT("nodeTitle"), NewNode->GetNodeTitle(ENodeTitleType::ListView).ToString());
|
|
if (NodeState.IsValid())
|
|
{
|
|
EntryResult->SetObjectField(TEXT("node"), NodeState);
|
|
}
|
|
SuccessCount++;
|
|
}
|
|
|
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SpawnNode — %d/%d succeeded in graph '%s' of '%s'"),
|
|
SuccessCount, Nodes.Array.Num(), *DecodedGraphName, *Blueprint);
|
|
|
|
Result->SetNumberField(TEXT("successCount"), SuccessCount);
|
|
Result->SetNumberField(TEXT("totalCount"), Nodes.Array.Num());
|
|
Result->SetArrayField(TEXT("results"), Results);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="get_node_comment"))
|
|
class UMCPHandler_GetNodeComment : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
FString Blueprint;
|
|
|
|
UPROPERTY(meta=(Description="Node GUID"))
|
|
FString Node;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Get the comment text and bubble visibility of a node.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
|
|
MCPAssets<UBlueprint> Assets;
|
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
UBlueprint* BP = Assets.Object();
|
|
|
|
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node);
|
|
if (!FoundNode)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node));
|
|
}
|
|
|
|
Result->SetStringField(TEXT("comment"), FoundNode->NodeComment);
|
|
Result->SetBoolField(TEXT("commentBubbleVisible"), FoundNode->bCommentBubbleVisible);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="set_node_comment"))
|
|
class UMCPHandler_SetNodeComment : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
FString Blueprint;
|
|
|
|
UPROPERTY(meta=(Description="Node GUID"))
|
|
FString Node;
|
|
|
|
UPROPERTY(meta=(Description="Comment text to set"))
|
|
FString Comment;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Set a node's comment text. Makes the comment bubble visible if non-empty.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
|
|
MCPAssets<UBlueprint> Assets;
|
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
UBlueprint* BP = Assets.Object();
|
|
|
|
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node);
|
|
if (!FoundNode)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node));
|
|
}
|
|
|
|
FString OldComment = FoundNode->NodeComment;
|
|
FoundNode->NodeComment = Comment;
|
|
|
|
// Make the comment bubble visible if setting a non-empty comment
|
|
if (!Comment.IsEmpty())
|
|
{
|
|
FoundNode->bCommentBubbleVisible = true;
|
|
FoundNode->bCommentBubblePinned = true;
|
|
}
|
|
|
|
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set comment on node '%s' in '%s'"),
|
|
*Node, *Blueprint);
|
|
|
|
Result->SetStringField(TEXT("oldComment"), OldComment);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="delete_node_from_graph"))
|
|
class UMCPHandler_DeleteNodeFromGraph : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
FString Blueprint;
|
|
|
|
UPROPERTY(meta=(Description="Node GUID"))
|
|
FString Node;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Delete a node from a Blueprint graph. "
|
|
"Cannot delete entry nodes (FunctionEntry, Event, CustomEvent).");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
|
|
MCPAssets<UBlueprint> Assets;
|
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
UBlueprint* BP = Assets.Object();
|
|
|
|
UEdGraph* Graph = nullptr;
|
|
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node, &Graph);
|
|
if (!FoundNode)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node));
|
|
}
|
|
if (!Graph)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph not found for node '%s'"), *Node));
|
|
}
|
|
|
|
FString NodeClass = FoundNode->GetClass()->GetName();
|
|
FString NodeTitle = FoundNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
|
FString GraphName = Graph->GetName();
|
|
|
|
// Protect root/entry nodes — deleting these leaves the graph in an invalid
|
|
// state with no root node, causing compiler errors that can't be fixed
|
|
// without recreating the entire function/event.
|
|
if (Cast<UK2Node_FunctionEntry>(FoundNode))
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
TEXT("Cannot delete FunctionEntry node '%s' in graph '%s'. ")
|
|
TEXT("This is the root node of the function — removing it would leave an empty, uncompilable graph. ")
|
|
TEXT("To remove the entire function, delete it from the Blueprint editor."),
|
|
*NodeTitle, *GraphName));
|
|
}
|
|
if (Cast<UK2Node_Event>(FoundNode))
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
TEXT("Cannot delete event entry node '%s' in graph '%s'. ")
|
|
TEXT("This is the root node of the event handler — removing it would leave an empty, uncompilable graph."),
|
|
*NodeTitle, *GraphName));
|
|
}
|
|
if (Cast<UK2Node_CustomEvent>(FoundNode))
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
TEXT("Cannot delete CustomEvent entry node '%s' in graph '%s'. ")
|
|
TEXT("This is the root node of the custom event — removing it would leave an empty, uncompilable graph."),
|
|
*NodeTitle, *GraphName));
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting node '%s' (%s) from graph '%s' in '%s'"),
|
|
*Node, *NodeTitle, *GraphName, *Blueprint);
|
|
|
|
FoundNode->BreakAllNodeLinks();
|
|
Graph->RemoveNode(FoundNode);
|
|
|
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Node deleted"));
|
|
|
|
Result->SetStringField(TEXT("nodeClass"), NodeClass);
|
|
Result->SetStringField(TEXT("nodeTitle"), NodeTitle);
|
|
Result->SetStringField(TEXT("graph"), GraphName);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="replace_function_calls_in_blueprint"))
|
|
class UMCPHandler_ReplaceFunctionCallsInBlueprint : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
FString Blueprint;
|
|
|
|
UPROPERTY(meta=(Description="Old class name to match"))
|
|
FString OldClass;
|
|
|
|
UPROPERTY(meta=(Description="New class name to redirect to"))
|
|
FString NewClass;
|
|
|
|
UPROPERTY(meta=(Optional, Description="If true, report what would change without modifying"))
|
|
bool DryRun = false;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Redirect function call nodes from one class to another. "
|
|
"Supports dry-run to preview impact before applying.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
|
|
// Load Blueprint
|
|
MCPAssets<UBlueprint> Assets;
|
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
UBlueprint* BP = Assets.Object();
|
|
|
|
// Find the new class — try several search strategies
|
|
UClass* NewClassPtr = nullptr;
|
|
|
|
// Try finding the class across all loaded modules
|
|
NewClassPtr = FindFirstObject<UClass>(*NewClass);
|
|
|
|
// Try with U prefix stripped/added
|
|
if (!NewClassPtr && NewClass.StartsWith(TEXT("U")))
|
|
{
|
|
NewClassPtr = FindFirstObject<UClass>(*NewClass.Mid(1));
|
|
}
|
|
if (!NewClassPtr && !NewClass.StartsWith(TEXT("U")))
|
|
{
|
|
NewClassPtr = FindFirstObject<UClass>(*FString::Printf(TEXT("U%s"), *NewClass));
|
|
}
|
|
|
|
// Broader search across all modules
|
|
if (!NewClassPtr)
|
|
{
|
|
for (TObjectIterator<UClass> It; It; ++It)
|
|
{
|
|
if (It->GetName() == NewClass || It->GetName() == FString::Printf(TEXT("U%s"), *NewClass))
|
|
{
|
|
NewClassPtr = *It;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!NewClassPtr)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not find class '%s'"), *NewClass));
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s function calls in '%s': %s -> %s (%s)"),
|
|
DryRun ? TEXT("[DRY RUN] Analyzing replacement of") : TEXT("Replacing"),
|
|
*Blueprint, *OldClass, *NewClass, *NewClassPtr->GetPathName());
|
|
|
|
// Find all CallFunction nodes
|
|
TArray<UK2Node_CallFunction*> AllCallNodes;
|
|
FBlueprintEditorUtils::GetAllNodesOfClass<UK2Node_CallFunction>(BP, AllCallNodes);
|
|
|
|
int32 ReplacedCount = 0;
|
|
TArray<TSharedPtr<FJsonValue>> BrokenConnections;
|
|
|
|
for (UK2Node_CallFunction* CallNode : AllCallNodes)
|
|
{
|
|
UClass* ParentClass = CallNode->FunctionReference.GetMemberParentClass();
|
|
if (!ParentClass)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Match by class name (with or without U prefix, and _C suffix for BP classes)
|
|
FString ParentName = ParentClass->GetName();
|
|
bool bMatch = (ParentName == OldClass) ||
|
|
(ParentName == FString::Printf(TEXT("%s_C"), *OldClass)) ||
|
|
(ParentName == FString::Printf(TEXT("U%s"), *OldClass)) ||
|
|
(OldClass.StartsWith(TEXT("U")) && (ParentName == OldClass.Mid(1))) ||
|
|
(OldClass.EndsWith(TEXT("_C")) && (ParentName == OldClass.LeftChop(2)));
|
|
|
|
if (!bMatch)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FName FuncName = CallNode->FunctionReference.GetMemberName();
|
|
|
|
// Find the matching function in the new class
|
|
UFunction* NewFunc = NewClassPtr->FindFunctionByName(FuncName);
|
|
if (!NewFunc)
|
|
{
|
|
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Function '%s' not found in '%s', skipping node"),
|
|
*FuncName.ToString(), *NewClass);
|
|
|
|
TSharedRef<FJsonObject> Warning = MakeShared<FJsonObject>();
|
|
Warning->SetStringField(TEXT("type"), TEXT("functionNotFound"));
|
|
Warning->SetStringField(TEXT("functionName"), FuncName.ToString());
|
|
Warning->SetStringField(TEXT("nodeId"), CallNode->NodeGuid.ToString());
|
|
BrokenConnections.Add(MakeShared<FJsonValueObject>(Warning));
|
|
continue;
|
|
}
|
|
|
|
if (DryRun)
|
|
{
|
|
// In dry run mode: report what would be affected without modifying
|
|
ReplacedCount++;
|
|
|
|
// Check which pins have connections that might break
|
|
for (UEdGraphPin* Pin : CallNode->Pins)
|
|
{
|
|
if (!Pin || Pin->LinkedTo.Num() == 0) continue;
|
|
|
|
// Check if the new function has a matching parameter
|
|
bool bPinExistsInNew = false;
|
|
for (TFieldIterator<FProperty> PropIt(NewFunc); PropIt; ++PropIt)
|
|
{
|
|
if (PropIt->GetFName() == Pin->PinName ||
|
|
Pin->PinName == UEdGraphSchema_K2::PN_Execute ||
|
|
Pin->PinName == UEdGraphSchema_K2::PN_Then ||
|
|
Pin->PinName == UEdGraphSchema_K2::PN_Self ||
|
|
Pin->PinName == UEdGraphSchema_K2::PN_ReturnValue)
|
|
{
|
|
bPinExistsInNew = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!bPinExistsInNew)
|
|
{
|
|
for (UEdGraphPin* Linked : Pin->LinkedTo)
|
|
{
|
|
if (Linked && Linked->GetOwningNode())
|
|
{
|
|
TSharedRef<FJsonObject> AtRisk = MakeShared<FJsonObject>();
|
|
AtRisk->SetStringField(TEXT("type"), TEXT("connectionAtRisk"));
|
|
AtRisk->SetStringField(TEXT("functionName"), FuncName.ToString());
|
|
AtRisk->SetStringField(TEXT("nodeId"), CallNode->NodeGuid.ToString());
|
|
AtRisk->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
|
|
AtRisk->SetStringField(TEXT("connectedToNode"), Linked->GetOwningNode()->NodeGuid.ToString());
|
|
AtRisk->SetStringField(TEXT("connectedToPin"), Linked->PinName.ToString());
|
|
BrokenConnections.Add(MakeShared<FJsonValueObject>(AtRisk));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Record existing pin connections before replacement
|
|
TMap<FString, TArray<TPair<FString, FString>>> OldPinConnections;
|
|
for (UEdGraphPin* Pin : CallNode->Pins)
|
|
{
|
|
if (Pin->LinkedTo.Num() > 0)
|
|
{
|
|
TArray<TPair<FString, FString>> Links;
|
|
for (UEdGraphPin* Linked : Pin->LinkedTo)
|
|
{
|
|
if (Linked && Linked->GetOwningNode())
|
|
{
|
|
Links.Add(TPair<FString, FString>(
|
|
Linked->GetOwningNode()->NodeGuid.ToString(),
|
|
Linked->PinName.ToString()));
|
|
}
|
|
}
|
|
OldPinConnections.Add(Pin->PinName.ToString(), Links);
|
|
}
|
|
}
|
|
|
|
// Replace the function reference
|
|
CallNode->SetFromFunction(NewFunc);
|
|
ReplacedCount++;
|
|
|
|
// Check which connections survived
|
|
for (auto& Pair : OldPinConnections)
|
|
{
|
|
const FString& PinName = Pair.Key;
|
|
const TArray<TPair<FString, FString>>& OldLinks = Pair.Value;
|
|
|
|
UEdGraphPin* NewPin = CallNode->FindPin(FName(*PinName));
|
|
for (auto& Link : OldLinks)
|
|
{
|
|
bool bStillConnected = false;
|
|
if (NewPin)
|
|
{
|
|
for (UEdGraphPin* L : NewPin->LinkedTo)
|
|
{
|
|
if (L && L->GetOwningNode() &&
|
|
L->GetOwningNode()->NodeGuid.ToString() == Link.Key &&
|
|
L->PinName.ToString() == Link.Value)
|
|
{
|
|
bStillConnected = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!bStillConnected)
|
|
{
|
|
TSharedRef<FJsonObject> Broken = MakeShared<FJsonObject>();
|
|
Broken->SetStringField(TEXT("type"), TEXT("connectionLost"));
|
|
Broken->SetStringField(TEXT("functionName"), FuncName.ToString());
|
|
Broken->SetStringField(TEXT("nodeId"), CallNode->NodeGuid.ToString());
|
|
Broken->SetStringField(TEXT("pinName"), PinName);
|
|
Broken->SetStringField(TEXT("wasConnectedToNode"), Link.Key);
|
|
Broken->SetStringField(TEXT("wasConnectedToPin"), Link.Value);
|
|
BrokenConnections.Add(MakeShared<FJsonValueObject>(Broken));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (DryRun)
|
|
{
|
|
Result->SetNumberField(TEXT("wouldReplaceCount"), ReplacedCount);
|
|
Result->SetNumberField(TEXT("connectionsAtRisk"), BrokenConnections.Num());
|
|
Result->SetArrayField(TEXT("connectionsAtRisk"), BrokenConnections);
|
|
return;
|
|
}
|
|
|
|
if (ReplacedCount > 0)
|
|
{
|
|
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Replaced %d function call(s)"), ReplacedCount);
|
|
|
|
Result->SetNumberField(TEXT("replacedCount"), ReplacedCount);
|
|
Result->SetNumberField(TEXT("brokenConnectionCount"), BrokenConnections.Num());
|
|
Result->SetArrayField(TEXT("brokenConnections"), BrokenConnections);
|
|
return;
|
|
}
|
|
|
|
Result->SetNumberField(TEXT("replacedCount"), 0);
|
|
Result->SetStringField(TEXT("message"), FString::Printf(
|
|
TEXT("No function call nodes found targeting class '%s'"), *OldClass));
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="refresh_all_nodes_in_graph"))
|
|
class UMCPHandler_RefreshAllNodesInGraph : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
FString Blueprint;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Refresh all nodes in a Blueprint, removing orphaned pins. "
|
|
"Reports compiler warnings and errors.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
|
|
// Load Blueprint
|
|
MCPAssets<UBlueprint> Assets;
|
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
UBlueprint* BP = Assets.Object();
|
|
|
|
// Count graphs and nodes before refresh
|
|
TArray<UEdGraph*> AllGraphs;
|
|
BP->GetAllGraphs(AllGraphs);
|
|
int32 GraphCount = AllGraphs.Num();
|
|
int32 NodeCount = 0;
|
|
for (UEdGraph* G : AllGraphs)
|
|
{
|
|
if (G) NodeCount += G->Nodes.Num();
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Refreshing all nodes in '%s' (%d graphs, %d nodes)"),
|
|
*Blueprint, GraphCount, NodeCount);
|
|
|
|
// Refresh all nodes
|
|
FBlueprintEditorUtils::RefreshAllNodes(BP);
|
|
|
|
// Remove orphaned pins from all nodes
|
|
int32 OrphanedPinsRemoved = 0;
|
|
for (UEdGraph* G : AllGraphs)
|
|
{
|
|
if (!G) continue;
|
|
for (UEdGraphNode* Node : G->Nodes)
|
|
{
|
|
if (!Node) continue;
|
|
for (int32 i = Node->Pins.Num() - 1; i >= 0; --i)
|
|
{
|
|
UEdGraphPin* Pin = Node->Pins[i];
|
|
if (Pin && Pin->bOrphanedPin)
|
|
{
|
|
Pin->BreakAllPinLinks();
|
|
Node->Pins.RemoveAt(i);
|
|
OrphanedPinsRemoved++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (OrphanedPinsRemoved > 0)
|
|
{
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed %d orphaned pins"), OrphanedPinsRemoved);
|
|
}
|
|
|
|
// Mark as modified and recompile after orphan removal
|
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: RefreshAllNodes complete"));
|
|
|
|
// Collect compiler warnings and errors from the blueprint status
|
|
TArray<TSharedPtr<FJsonValue>> WarningsArr;
|
|
TArray<TSharedPtr<FJsonValue>> ErrorsArr;
|
|
|
|
if (BP->Status == BS_Error)
|
|
{
|
|
ErrorsArr.Add(MakeShared<FJsonValueString>(TEXT("Blueprint has compiler errors after refresh")));
|
|
}
|
|
|
|
// Check each graph for nodes with error/warning status
|
|
AllGraphs.Empty();
|
|
BP->GetAllGraphs(AllGraphs);
|
|
for (UEdGraph* G : AllGraphs)
|
|
{
|
|
if (!G) continue;
|
|
for (UEdGraphNode* Node : G->Nodes)
|
|
{
|
|
if (!Node) continue;
|
|
if (Node->bHasCompilerMessage)
|
|
{
|
|
FString NodeTitle = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
|
FString NodeMsg = FString::Printf(TEXT("[%s] %s: %s"),
|
|
*G->GetName(), *NodeTitle, *Node->ErrorMsg);
|
|
if (Node->ErrorType == EMessageSeverity::Error)
|
|
{
|
|
ErrorsArr.Add(MakeShared<FJsonValueString>(NodeMsg));
|
|
}
|
|
else
|
|
{
|
|
WarningsArr.Add(MakeShared<FJsonValueString>(NodeMsg));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Result->SetNumberField(TEXT("graphCount"), GraphCount);
|
|
Result->SetNumberField(TEXT("nodeCount"), NodeCount);
|
|
Result->SetNumberField(TEXT("orphanedPinsRemoved"), OrphanedPinsRemoved);
|
|
Result->SetArrayField(TEXT("warnings"), WarningsArr);
|
|
Result->SetArrayField(TEXT("errors"), ErrorsArr);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="change_struct_node_type"))
|
|
class UMCPHandler_ChangeStructNodeType : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
FString Blueprint;
|
|
|
|
UPROPERTY(meta=(Description="Node GUID of the BreakStruct or MakeStruct node"))
|
|
FString Node;
|
|
|
|
UPROPERTY(meta=(Description="New struct type name (e.g. 'FVector', 'Vector')"))
|
|
FString NewType;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Change the struct type on a BreakStruct or MakeStruct node. "
|
|
"Attempts to reconnect matching pins after the type change.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
|
|
// Load Blueprint
|
|
MCPAssets<UBlueprint> Assets;
|
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
UBlueprint* BP = Assets.Object();
|
|
|
|
// Find node
|
|
UEdGraph* Graph = nullptr;
|
|
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node, &Graph);
|
|
if (!FoundNode)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node));
|
|
}
|
|
|
|
// Determine what kind of struct node this is
|
|
UK2Node_BreakStruct* BreakNode = Cast<UK2Node_BreakStruct>(FoundNode);
|
|
UK2Node_MakeStruct* MakeNode = Cast<UK2Node_MakeStruct>(FoundNode);
|
|
|
|
if (!BreakNode && !MakeNode)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' is not a BreakStruct or MakeStruct node (class: %s)"),
|
|
*Node, *FoundNode->GetClass()->GetName()));
|
|
}
|
|
|
|
// Find the new struct type
|
|
FString SearchName = NewType;
|
|
if (SearchName.StartsWith(TEXT("F")))
|
|
{
|
|
SearchName = SearchName.Mid(1);
|
|
}
|
|
|
|
UScriptStruct* NewStruct = FindFirstObject<UScriptStruct>(*SearchName);
|
|
if (!NewStruct)
|
|
{
|
|
// Try with full name including F prefix
|
|
NewStruct = FindFirstObject<UScriptStruct>(*NewType);
|
|
}
|
|
if (!NewStruct)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Struct type '%s' not found"), *NewType));
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Changing struct node '%s' to type '%s'"),
|
|
*Node, *NewStruct->GetName());
|
|
|
|
// Helper: extract property base name from a BreakStruct pin name
|
|
auto ExtractPropertyBaseName = [](const FString& PinName) -> FString
|
|
{
|
|
// Find the last underscore before 32 hex chars (GUID)
|
|
int32 LastUnderscore;
|
|
if (PinName.FindLastChar(TEXT('_'), LastUnderscore) && (LastUnderscore > 0))
|
|
{
|
|
FString Suffix = PinName.Mid(LastUnderscore + 1);
|
|
if (Suffix.Len() == 32)
|
|
{
|
|
FString WithoutGuid = PinName.Left(LastUnderscore);
|
|
// Strip _Index
|
|
int32 SecondUnderscore;
|
|
if (WithoutGuid.FindLastChar(TEXT('_'), SecondUnderscore) && (SecondUnderscore > 0))
|
|
{
|
|
FString IndexStr = WithoutGuid.Mid(SecondUnderscore + 1);
|
|
if (IndexStr.IsNumeric())
|
|
{
|
|
return WithoutGuid.Left(SecondUnderscore);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return PinName;
|
|
};
|
|
|
|
// Remember existing connections keyed by property base name
|
|
struct FPinConnection
|
|
{
|
|
EEdGraphPinDirection Direction;
|
|
TArray<UEdGraphPin*> LinkedPins;
|
|
};
|
|
TMap<FString, FPinConnection> ConnectionsByBaseName;
|
|
|
|
for (UEdGraphPin* Pin : FoundNode->Pins)
|
|
{
|
|
if (!Pin || Pin->LinkedTo.Num() == 0) continue;
|
|
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue;
|
|
|
|
FString BaseName = ExtractPropertyBaseName(Pin->PinName.ToString());
|
|
FPinConnection& Conn = ConnectionsByBaseName.FindOrAdd(BaseName);
|
|
Conn.Direction = Pin->Direction;
|
|
Conn.LinkedPins = Pin->LinkedTo;
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Saved %d pin connections to reconnect"), ConnectionsByBaseName.Num());
|
|
|
|
// Change the struct type and reconstruct
|
|
if (BreakNode)
|
|
{
|
|
BreakNode->StructType = NewStruct;
|
|
}
|
|
else if (MakeNode)
|
|
{
|
|
MakeNode->StructType = NewStruct;
|
|
}
|
|
|
|
// Break all existing links before reconstruction
|
|
FoundNode->BreakAllNodeLinks();
|
|
|
|
// Reconnect pins by matching property base names
|
|
const UEdGraphSchema* Schema = Graph->GetSchema();
|
|
if (!Schema)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Graph schema not found"));
|
|
}
|
|
|
|
// Reconstruct to rebuild pins for the new struct type (use schema version for MinimalAPI compat)
|
|
Schema->ReconstructNode(*FoundNode);
|
|
|
|
int32 Reconnected = 0;
|
|
int32 Failed = 0;
|
|
TArray<TSharedPtr<FJsonValue>> ReconnectDetails;
|
|
|
|
for (auto& Pair : ConnectionsByBaseName)
|
|
{
|
|
const FString& BaseName = Pair.Key;
|
|
const FPinConnection& OldConn = Pair.Value;
|
|
|
|
// Find matching new pin
|
|
UEdGraphPin* NewPin = nullptr;
|
|
for (UEdGraphPin* Pin : FoundNode->Pins)
|
|
{
|
|
if (!Pin || Pin->Direction != OldConn.Direction) continue;
|
|
FString NewBaseName = ExtractPropertyBaseName(Pin->PinName.ToString());
|
|
if (NewBaseName.Equals(BaseName, ESearchCase::IgnoreCase))
|
|
{
|
|
NewPin = Pin;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Also try matching the struct input/output pin (single struct pin)
|
|
if (!NewPin)
|
|
{
|
|
for (UEdGraphPin* Pin : FoundNode->Pins)
|
|
{
|
|
if (!Pin || Pin->Direction != OldConn.Direction) continue;
|
|
if ((Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct) &&
|
|
(Pin->PinType.PinSubCategoryObject == NewStruct))
|
|
{
|
|
NewPin = Pin;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (NewPin)
|
|
{
|
|
for (UEdGraphPin* Target : OldConn.LinkedPins)
|
|
{
|
|
bool bOK = Schema->TryCreateConnection(NewPin, Target);
|
|
if (bOK)
|
|
{
|
|
Reconnected++;
|
|
}
|
|
else
|
|
{
|
|
Failed++;
|
|
}
|
|
|
|
TSharedPtr<FJsonObject> Detail = MakeShared<FJsonObject>();
|
|
Detail->SetStringField(TEXT("property"), BaseName);
|
|
Detail->SetBoolField(TEXT("connected"), bOK);
|
|
ReconnectDetails.Add(MakeShared<FJsonValueObject>(Detail));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Failed += OldConn.LinkedPins.Num();
|
|
TSharedPtr<FJsonObject> Detail = MakeShared<FJsonObject>();
|
|
Detail->SetStringField(TEXT("property"), BaseName);
|
|
Detail->SetBoolField(TEXT("connected"), false);
|
|
Detail->SetStringField(TEXT("reason"), TEXT("No matching pin found on new struct"));
|
|
ReconnectDetails.Add(MakeShared<FJsonValueObject>(Detail));
|
|
}
|
|
}
|
|
|
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
|
|
// Return updated node state
|
|
TSharedPtr<FJsonObject> UpdatedNodeState = MCPUtils::SerializeNode(FoundNode);
|
|
|
|
Result->SetStringField(TEXT("newStructType"), NewStruct->GetName());
|
|
Result->SetStringField(TEXT("nodeClass"), FoundNode->GetClass()->GetName());
|
|
Result->SetNumberField(TEXT("reconnected"), Reconnected);
|
|
Result->SetNumberField(TEXT("failed"), Failed);
|
|
Result->SetArrayField(TEXT("reconnectDetails"), ReconnectDetails);
|
|
if (UpdatedNodeState.IsValid())
|
|
{
|
|
Result->SetObjectField(TEXT("updatedNode"), UpdatedNodeState);
|
|
}
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="set_class_default_value"))
|
|
class UMCPHandler_SetClassDefaultValue : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
FString Blueprint;
|
|
|
|
UPROPERTY(meta=(Description="Property name on the Class Default Object"))
|
|
FString Property;
|
|
|
|
UPROPERTY(meta=(Description="New value (parsed according to property type)"))
|
|
FString Value;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Set a default property value on a Blueprint's Class Default Object. "
|
|
"Handles class references, object references, and simple types.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
|
|
// Load Blueprint
|
|
MCPAssets<UBlueprint> Assets;
|
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
UBlueprint* BP = Assets.Object();
|
|
|
|
if (!BP->GeneratedClass)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Blueprint has no GeneratedClass"));
|
|
}
|
|
|
|
UObject* CDO = BP->GeneratedClass->GetDefaultObject();
|
|
if (!CDO)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Could not get Class Default Object"));
|
|
}
|
|
|
|
FProperty* Prop = BP->GeneratedClass->FindPropertyByName(*Property);
|
|
if (!Prop)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Property '%s' not found on '%s'"), *Property, *Blueprint));
|
|
}
|
|
|
|
FString OldValue;
|
|
Prop->ExportTextItem_Direct(OldValue, Prop->ContainerPtrToValuePtr<void>(CDO), nullptr, CDO, PPF_None);
|
|
|
|
bool bSuccess = false;
|
|
FString ActualNewValue;
|
|
|
|
// Handle class/soft-class properties (TSubclassOf, TSoftClassPtr)
|
|
FClassProperty* ClassProp = CastField<FClassProperty>(Prop);
|
|
FSoftClassProperty* SoftClassProp = CastField<FSoftClassProperty>(Prop);
|
|
|
|
if (ClassProp || SoftClassProp)
|
|
{
|
|
// Resolve the value to a UClass*
|
|
UClass* ResolvedClass = nullptr;
|
|
|
|
// Try as a C++ class name first
|
|
for (TObjectIterator<UClass> It; It; ++It)
|
|
{
|
|
if (It->GetName() == Value || It->GetName() == Value + TEXT("_C"))
|
|
{
|
|
ResolvedClass = *It;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Try loading as a Blueprint asset
|
|
if (!ResolvedClass)
|
|
{
|
|
MCPAssets<UBlueprint> ValueAssets;
|
|
if (!ValueAssets.Exact(Value).AllContent().Errors(Result).ETwo().Load()) return;
|
|
if (!ValueAssets.Objects().IsEmpty())
|
|
{
|
|
if (ValueAssets.Object()->GeneratedClass)
|
|
ResolvedClass = ValueAssets.Object()->GeneratedClass;
|
|
}
|
|
}
|
|
|
|
if (!ResolvedClass)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not resolve '%s' to a class"), *Value));
|
|
}
|
|
|
|
// Validate meta class compatibility
|
|
if (ClassProp)
|
|
{
|
|
UClass* MetaClass = ClassProp->MetaClass;
|
|
if (MetaClass && !ResolvedClass->IsChildOf(MetaClass))
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
TEXT("'%s' is not a subclass of '%s' (required by property '%s')"),
|
|
*ResolvedClass->GetName(), *MetaClass->GetName(), *Property));
|
|
}
|
|
ClassProp->SetPropertyValue_InContainer(CDO, ResolvedClass);
|
|
}
|
|
else
|
|
{
|
|
FSoftObjectPtr SoftPtr(ResolvedClass);
|
|
SoftClassProp->SetPropertyValue_InContainer(CDO, SoftPtr);
|
|
}
|
|
ActualNewValue = ResolvedClass->GetName();
|
|
bSuccess = true;
|
|
}
|
|
// Handle object properties (TObjectPtr, UObject*)
|
|
else if (FObjectProperty* ObjProp = CastField<FObjectProperty>(Prop))
|
|
{
|
|
// Try finding an existing object/asset by name
|
|
UObject* ResolvedObj = nullptr;
|
|
|
|
// Try loading as a Blueprint asset
|
|
MCPAssets<UBlueprint> ValueAssets;
|
|
if (!ValueAssets.Exact(Value).AllContent().Errors(Result).ENone().ETwo().Load()) return;
|
|
if (ValueAssets.Object()->GeneratedClass)
|
|
ResolvedObj = ValueAssets.Object()->GeneratedClass->GetDefaultObject();
|
|
|
|
ObjProp->SetPropertyValue_InContainer(CDO, ResolvedObj);
|
|
ActualNewValue = ResolvedObj->GetName();
|
|
bSuccess = true;
|
|
}
|
|
// Handle simple types via ImportText
|
|
else
|
|
{
|
|
const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, Prop->ContainerPtrToValuePtr<void>(CDO), CDO, PPF_None);
|
|
if (ImportResult)
|
|
{
|
|
Prop->ExportTextItem_Direct(ActualNewValue, Prop->ContainerPtrToValuePtr<void>(CDO), nullptr, CDO, PPF_None);
|
|
bSuccess = true;
|
|
}
|
|
else
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
TEXT("Failed to set property '%s' to '%s' — value could not be parsed for type '%s'"),
|
|
*Property, *Value, *Prop->GetCPPType()));
|
|
}
|
|
}
|
|
|
|
if (!bSuccess)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to set property value"));
|
|
}
|
|
|
|
// Mark modified and save
|
|
CDO->MarkPackageDirty();
|
|
BP->Modify();
|
|
|
|
FKismetEditorUtilities::CompileBlueprint(BP);
|
|
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set '%s.%s' from '%s' to '%s' (saved: %s)"),
|
|
*Blueprint, *Property, *OldValue, *ActualNewValue, bSaved ? TEXT("true") : TEXT("false"));
|
|
|
|
Result->SetStringField(TEXT("oldValue"), OldValue);
|
|
Result->SetStringField(TEXT("newValue"), ActualNewValue);
|
|
Result->SetStringField(TEXT("propertyType"), Prop->GetCPPType());
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="search_spawnable_node_types"))
|
|
class UMCPHandler_SearchSpawnableNodeTypes : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Search query string"))
|
|
FString Query;
|
|
|
|
UPROPERTY(meta=(Optional, Description="Maximum number of results (default 50, max 500)"))
|
|
int32 MaxResults = 50;
|
|
|
|
UPROPERTY(meta=(Optional, Description="Blueprint name or path. If specified with graph, only returns nodes compatible with that graph."))
|
|
FString Blueprint;
|
|
|
|
UPROPERTY(meta=(Optional, Description="Graph name to filter by compatibility. Requires blueprint."))
|
|
FString Graph;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Search the Blueprint action database for node spawners matching a query. "
|
|
"Returns full action names for use with spawn_node. "
|
|
"Optionally filter by blueprint+graph to only show compatible node types.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
int32 ClampedMax = FMath::Clamp(MaxResults, 1, 500);
|
|
|
|
// Optionally resolve a graph to filter by compatibility
|
|
UEdGraph* GraphFilter = nullptr;
|
|
if (!Blueprint.IsEmpty() && !Graph.IsEmpty())
|
|
{
|
|
MCPAssets<UBlueprint> BPAssets;
|
|
if (!BPAssets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
UBlueprint* BP = BPAssets.Object();
|
|
|
|
FString DecodedGraphName = MCPUtils::UrlDecode(Graph);
|
|
TArray<UEdGraph*> AllGraphs;
|
|
BP->GetAllGraphs(AllGraphs);
|
|
for (UEdGraph* G : AllGraphs)
|
|
{
|
|
if (G && G->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
|
|
{
|
|
GraphFilter = G;
|
|
break;
|
|
}
|
|
}
|
|
if (!GraphFilter)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
|
|
}
|
|
}
|
|
|
|
TArray<UBlueprintNodeSpawner*> Spawners = MCPUtils::SearchNodeSpawners(Query, ClampedMax, /*ExactMatch=*/false, GraphFilter);
|
|
|
|
TArray<TSharedPtr<FJsonValue>> ResultArray;
|
|
for (UBlueprintNodeSpawner* Spawner : Spawners)
|
|
{
|
|
ResultArray.Add(MakeShared<FJsonValueString>(MCPUtils::NodeSpawnerFullName(Spawner)));
|
|
}
|
|
|
|
Result->SetNumberField(TEXT("count"), ResultArray.Num());
|
|
Result->SetArrayField(TEXT("results"), ResultArray);
|
|
}
|
|
};
|