Merge of the two project copies into one self-contained plugin (the superset: variable_op + variables.py, full pcg_op runtime/declarative/preset ops, the CreateWidgetFloatAnimation widget tool, and full Voxel graph authoring). Made fully project- and machine-agnostic — no hardcoded paths: - New src/projectPaths.js auto-detects the host .uproject (walk-up), project name, Editor build target, log file, and engine install (EngineAssociation via launcher manifest/registry, else installed-engine scan). All overridable via UE_* env vars. - Rewired buildOrchestrator/insights/launcher/insightsExporter/server.js and the Python workers (cpp_scaffold, live_coding, apply_graph, console) off the old C:/Github/ihy, IHY*, E:/UE_Versions and UE_5.7 literals onto the resolver. - Voxel made optional: Build.cs auto-detects the Voxel plugin (env UE_MCP_WITH_VOXEL override) and the C++ compiles to stubs under WITH_MCP_VOXEL, so the module builds in projects without Voxel; .uplugin marks Voxel optional. - De-branded the agent-gateway and docs; scrubbed a leaked API key; excluded node_modules/Binaries/Intermediate/__pycache__/secrets from the repo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
949 lines
32 KiB
C++
949 lines
32 KiB
C++
#include "MCPVoxelGraphLibrary.h"
|
|
|
|
#include "MCPGraphLibrary.h"
|
|
|
|
#include "EdGraph/EdGraph.h"
|
|
#include "EdGraph/EdGraphNode.h"
|
|
#include "EdGraph/EdGraphPin.h"
|
|
#include "EdGraphNode_Comment.h"
|
|
#include "Subsystems/AssetEditorSubsystem.h"
|
|
#include "Editor.h"
|
|
#include "Dom/JsonObject.h"
|
|
#include "Dom/JsonValue.h"
|
|
#include "Serialization/JsonSerializer.h"
|
|
#include "Serialization/JsonWriter.h"
|
|
|
|
// WITH_MCP_VOXEL is set by UEBlueprintMCPEditor.Build.cs: 1 when the Voxel plugin
|
|
// is present (auto-detected, or forced via env UE_MCP_WITH_VOXEL), 0 otherwise.
|
|
// When 0 the whole Voxel integration compiles down to harmless stubs so the
|
|
// plugin builds in any project — with or without Voxel installed.
|
|
#ifndef WITH_MCP_VOXEL
|
|
#define WITH_MCP_VOXEL 0
|
|
#endif
|
|
|
|
#if WITH_MCP_VOXEL
|
|
|
|
#include "VoxelGraph.h"
|
|
#include "VoxelGraphToolkit.h"
|
|
#include "VoxelGraphTracker.h"
|
|
#include "VoxelTerminalGraph.h"
|
|
#include "VoxelTerminalGraphRuntime.h"
|
|
|
|
#include "VoxelMinimal.h"
|
|
#include "VoxelNode.h"
|
|
#include "VoxelFunctionLibrary.h"
|
|
#include "Nodes/VoxelNode_UFunction.h"
|
|
#include "VoxelMinimal/VoxelInstancedStruct.h"
|
|
#include "VoxelMinimal/VoxelObjectHelpers.h"
|
|
#include "VoxelParameter.h"
|
|
#include "VoxelPinType.h"
|
|
|
|
// Private headers of the VoxelGraphEditor module (reachable via PrivateIncludePaths in Build.cs).
|
|
// We only use inline/virtual members from these, so no cross-module link symbols are required.
|
|
#include "Nodes/VoxelGraphNode_Struct.h"
|
|
|
|
namespace
|
|
{
|
|
static FString VoxelJsonStringify(const TSharedRef<FJsonObject>& Object)
|
|
{
|
|
FString Out;
|
|
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Out);
|
|
FJsonSerializer::Serialize(Object, Writer);
|
|
return Out;
|
|
}
|
|
|
|
static UVoxelGraph* AsVoxelGraph(UObject* Asset)
|
|
{
|
|
return Cast<UVoxelGraph>(Asset);
|
|
}
|
|
|
|
static void FillTerminalJson(TSharedRef<FJsonObject> Json, const UVoxelTerminalGraph& TerminalGraph)
|
|
{
|
|
Json->SetStringField(TEXT("guid"), TerminalGraph.GetGuid().ToString());
|
|
Json->SetStringField(TEXT("name"), TerminalGraph.GetDisplayName());
|
|
Json->SetBoolField(TEXT("is_main"), TerminalGraph.IsMainTerminalGraph());
|
|
Json->SetBoolField(TEXT("is_editor"), TerminalGraph.IsEditorTerminalGraph());
|
|
|
|
const UEdGraph& EdGraph = TerminalGraph.GetEdGraph();
|
|
Json->SetStringField(TEXT("ed_graph"), EdGraph.GetName());
|
|
Json->SetNumberField(TEXT("node_count"), EdGraph.Nodes.Num());
|
|
}
|
|
|
|
static UVoxelTerminalGraph* FindTerminalGraph(UVoxelGraph* Graph, const FString& Terminal)
|
|
{
|
|
if (!Graph)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
if (Terminal.IsEmpty() ||
|
|
Terminal.Equals(TEXT("main"), ESearchCase::IgnoreCase) ||
|
|
Terminal.Equals(TEXT("Main"), ESearchCase::IgnoreCase))
|
|
{
|
|
return &Graph->GetMainTerminalGraph();
|
|
}
|
|
|
|
if (Terminal.Equals(TEXT("editor"), ESearchCase::IgnoreCase))
|
|
{
|
|
return &Graph->GetEditorTerminalGraph();
|
|
}
|
|
|
|
FGuid Guid;
|
|
if (FGuid::Parse(Terminal, Guid))
|
|
{
|
|
return Graph->FindTerminalGraph(Guid);
|
|
}
|
|
|
|
UVoxelTerminalGraph* Result = nullptr;
|
|
Graph->ForeachTerminalGraph_NoInheritance([&](UVoxelTerminalGraph& Candidate)
|
|
{
|
|
if (!Result &&
|
|
(Candidate.GetDisplayName().Equals(Terminal, ESearchCase::IgnoreCase) ||
|
|
Candidate.GetEdGraph().GetName().Equals(Terminal, ESearchCase::IgnoreCase)))
|
|
{
|
|
Result = &Candidate;
|
|
}
|
|
});
|
|
return Result;
|
|
}
|
|
|
|
static TSharedRef<FJsonObject> VoxelNodeToJson(UEdGraphNode* Node)
|
|
{
|
|
TSharedRef<FJsonObject> Json = MakeShared<FJsonObject>();
|
|
Json->SetStringField(TEXT("guid"), Node->NodeGuid.ToString());
|
|
Json->SetStringField(TEXT("class"), Node->GetClass()->GetName());
|
|
Json->SetNumberField(TEXT("x"), Node->NodePosX);
|
|
Json->SetNumberField(TEXT("y"), Node->NodePosY);
|
|
Json->SetStringField(TEXT("title"), Node->GetNodeTitle(ENodeTitleType::ListView).ToString());
|
|
if (!Node->NodeComment.IsEmpty())
|
|
{
|
|
Json->SetStringField(TEXT("comment"), Node->NodeComment);
|
|
}
|
|
if (const UEdGraphNode_Comment* CommentNode = Cast<UEdGraphNode_Comment>(Node))
|
|
{
|
|
Json->SetNumberField(TEXT("w"), CommentNode->NodeWidth);
|
|
Json->SetNumberField(TEXT("h"), CommentNode->NodeHeight);
|
|
}
|
|
|
|
TArray<TSharedPtr<FJsonValue>> Pins;
|
|
for (UEdGraphPin* Pin : Node->Pins)
|
|
{
|
|
if (!Pin)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
TSharedRef<FJsonObject> PinJson = MakeShared<FJsonObject>();
|
|
PinJson->SetStringField(TEXT("name"), Pin->PinName.ToString());
|
|
PinJson->SetStringField(TEXT("dir"), Pin->Direction == EGPD_Input ? TEXT("in") : TEXT("out"));
|
|
PinJson->SetStringField(TEXT("type"), Pin->PinType.PinCategory.ToString());
|
|
if (!Pin->PinType.PinSubCategory.IsNone())
|
|
{
|
|
PinJson->SetStringField(TEXT("subtype"), Pin->PinType.PinSubCategory.ToString());
|
|
}
|
|
if (Pin->PinType.PinSubCategoryObject.IsValid())
|
|
{
|
|
PinJson->SetStringField(TEXT("subobj"), Pin->PinType.PinSubCategoryObject->GetPathName());
|
|
}
|
|
if (!Pin->DefaultValue.IsEmpty())
|
|
{
|
|
PinJson->SetStringField(TEXT("default"), Pin->DefaultValue);
|
|
}
|
|
|
|
TArray<TSharedPtr<FJsonValue>> Links;
|
|
for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
|
|
{
|
|
if (!LinkedPin || !LinkedPin->GetOwningNode())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
TSharedRef<FJsonObject> LinkJson = MakeShared<FJsonObject>();
|
|
LinkJson->SetStringField(TEXT("node"), LinkedPin->GetOwningNode()->NodeGuid.ToString());
|
|
LinkJson->SetStringField(TEXT("pin"), LinkedPin->PinName.ToString());
|
|
Links.Add(MakeShared<FJsonValueObject>(LinkJson));
|
|
}
|
|
if (Links.Num() > 0)
|
|
{
|
|
PinJson->SetArrayField(TEXT("links"), Links);
|
|
}
|
|
Pins.Add(MakeShared<FJsonValueObject>(PinJson));
|
|
}
|
|
Json->SetArrayField(TEXT("pins"), Pins);
|
|
return Json;
|
|
}
|
|
|
|
// Lazily-built catalogue of every spawnable Voxel node. Mirrors FVoxelNodeLibrary's
|
|
// construction (which lives in VoxelGraphEditor/Private and is not exported), so we
|
|
// rebuild it here from the exported reflection helpers.
|
|
static const TArray<TSharedRef<const FVoxelNode>>& GetVoxelNodeCatalogue()
|
|
{
|
|
static TArray<TSharedRef<const FVoxelNode>> Nodes;
|
|
static bool bBuilt = false;
|
|
if (bBuilt)
|
|
{
|
|
return Nodes;
|
|
}
|
|
bBuilt = true;
|
|
|
|
// Struct nodes (FVoxelNode subclasses): Add, Multiply, noise, output nodes, etc.
|
|
for (UScriptStruct* Struct : GetDerivedStructs<FVoxelNode>())
|
|
{
|
|
if (!Struct)
|
|
{
|
|
continue;
|
|
}
|
|
if (Struct->HasMetaData(TEXT("Abstract")) || Struct->HasMetaData(TEXT("Internal")))
|
|
{
|
|
continue;
|
|
}
|
|
Nodes.Add(MakeSharedStruct<FVoxelNode>(Struct));
|
|
}
|
|
|
|
// Function-library nodes (UVoxelFunctionLibrary UFUNCTIONs wrapped as FVoxelNode_UFunction):
|
|
// most math / position / curve nodes live here.
|
|
for (const TSubclassOf<UVoxelFunctionLibrary>& Class : GetDerivedClasses<UVoxelFunctionLibrary>())
|
|
{
|
|
if (!Class)
|
|
{
|
|
continue;
|
|
}
|
|
for (UFunction* Function : GetClassFunctions(Class))
|
|
{
|
|
if (!Function || Function->HasMetaData(TEXT("Internal")))
|
|
{
|
|
continue;
|
|
}
|
|
Nodes.Add(FVoxelNode_UFunction::Make(Function));
|
|
}
|
|
}
|
|
|
|
return Nodes;
|
|
}
|
|
|
|
// The UFunction backing a function-library node, or nullptr for plain struct nodes.
|
|
static UFunction* GetNodeFunction(const FVoxelNode& Node)
|
|
{
|
|
if (Node.GetStruct() &&
|
|
Node.GetStruct()->IsChildOf(FVoxelNode_UFunction::StaticStruct()))
|
|
{
|
|
return static_cast<const FVoxelNode_UFunction&>(Node).GetFunction();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Primary, stable identifier used to spawn a node.
|
|
static FString GetNodeId(const FVoxelNode& Node)
|
|
{
|
|
if (const UFunction* Function = GetNodeFunction(Node))
|
|
{
|
|
const UClass* Owner = Function->GetOwnerClass();
|
|
return FString::Printf(TEXT("%s.%s"),
|
|
Owner ? *Owner->GetName() : TEXT("?"),
|
|
*Function->GetName());
|
|
}
|
|
return Node.GetStruct() ? Node.GetStruct()->GetName() : FString();
|
|
}
|
|
|
|
// Returns true if NodeId matches the node by any of its accepted aliases.
|
|
static bool NodeMatchesId(const FVoxelNode& Node, const FString& NodeId)
|
|
{
|
|
if (GetNodeId(Node).Equals(NodeId, ESearchCase::IgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
if (Node.GetDisplayName().Equals(NodeId, ESearchCase::IgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
if (const UFunction* Function = GetNodeFunction(Node))
|
|
{
|
|
if (Function->GetName().Equals(NodeId, ESearchCase::IgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
const UClass* Owner = Function->GetOwnerClass();
|
|
if (Owner &&
|
|
FString::Printf(TEXT("%s::%s"), *Owner->GetName(), *Function->GetName()).Equals(NodeId, ESearchCase::IgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
else if (const UScriptStruct* Struct = Node.GetStruct())
|
|
{
|
|
if (Struct->GetPathName().Equals(NodeId, ESearchCase::IgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static TSharedPtr<const FVoxelNode> FindCatalogueNode(const FString& NodeId)
|
|
{
|
|
for (const TSharedRef<const FVoxelNode>& Node : GetVoxelNodeCatalogue())
|
|
{
|
|
if (NodeMatchesId(*Node, NodeId))
|
|
{
|
|
return Node;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Map a friendly string to a Voxel pin type. Defaults to float for unknown input.
|
|
static FVoxelPinType PinTypeFromString(const FString& TypeStr)
|
|
{
|
|
const FString T = TypeStr.ToLower();
|
|
if (T == TEXT("double")) return FVoxelPinType::Make<double>();
|
|
if (T == TEXT("int") || T == TEXT("int32")) return FVoxelPinType::Make<int32>();
|
|
if (T == TEXT("bool")) return FVoxelPinType::Make<bool>();
|
|
if (T == TEXT("vector2d") || T == TEXT("vector2"))return FVoxelPinType::Make<FVector2D>();
|
|
if (T == TEXT("vector") || T == TEXT("vector3d")) return FVoxelPinType::Make<FVector>();
|
|
if (T == TEXT("color") || T == TEXT("linearcolor"))return FVoxelPinType::Make<FLinearColor>();
|
|
if (T == TEXT("name")) return FVoxelPinType::Make<FName>();
|
|
return FVoxelPinType::Make<float>();
|
|
}
|
|
|
|
// Reflection-based spawn of a (private, non-exported) Voxel graph node class by its script path,
|
|
// setting its FGuid "Guid" UPROPERTY. Only inline/virtual calls -> no cross-module link symbols.
|
|
static UEdGraphNode* SpawnGuidNodeReflect(UEdGraph* Graph, const TCHAR* ClassPath, const FGuid& Guid, FVector2D Position)
|
|
{
|
|
UClass* NodeClass = FindObject<UClass>(nullptr, ClassPath);
|
|
if (!Graph || !NodeClass)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
Graph->Modify();
|
|
UEdGraphNode* Node = NewObject<UEdGraphNode>(Graph, NodeClass, NAME_None, RF_Transactional);
|
|
if (!Node)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
if (FStructProperty* GuidProp = FindFProperty<FStructProperty>(NodeClass, TEXT("Guid")))
|
|
{
|
|
*GuidProp->ContainerPtrToValuePtr<FGuid>(Node) = Guid;
|
|
}
|
|
|
|
Node->NodePosX = static_cast<int32>(Position.X);
|
|
Node->NodePosY = static_cast<int32>(Position.Y);
|
|
Node->CreateNewGuid();
|
|
Graph->AddNode(Node, true, false);
|
|
Node->PostPlacedNewNode();
|
|
if (Node->Pins.Num() == 0)
|
|
{
|
|
Node->AllocateDefaultPins();
|
|
}
|
|
return Node;
|
|
}
|
|
|
|
static UVoxelTerminalGraph* OwningTerminal(UEdGraph* Graph)
|
|
{
|
|
return Graph ? Graph->GetTypedOuter<UVoxelTerminalGraph>() : nullptr;
|
|
}
|
|
|
|
static FGuid ResolveParameterGuid(UVoxelGraph* Graph, const FString& Ref)
|
|
{
|
|
if (!Graph)
|
|
{
|
|
return FGuid();
|
|
}
|
|
FGuid Guid;
|
|
if (FGuid::Parse(Ref, Guid) && Graph->FindParameter(Guid))
|
|
{
|
|
return Guid;
|
|
}
|
|
for (const FGuid& Candidate : Graph->GetParameters())
|
|
{
|
|
if (Graph->FindParameterChecked(Candidate).Name.ToString().Equals(Ref, ESearchCase::IgnoreCase))
|
|
{
|
|
return Candidate;
|
|
}
|
|
}
|
|
return FGuid();
|
|
}
|
|
|
|
// Resolve a terminal-graph property (kind: input/output/local) by guid or name.
|
|
static FGuid ResolveTerminalPropGuid(UVoxelTerminalGraph* Terminal, const FString& Kind, const FString& Ref)
|
|
{
|
|
if (!Terminal)
|
|
{
|
|
return FGuid();
|
|
}
|
|
FGuid Guid;
|
|
const bool bIsGuid = FGuid::Parse(Ref, Guid);
|
|
|
|
if (Kind == TEXT("input"))
|
|
{
|
|
if (bIsGuid && Terminal->FindInput(Guid)) return Guid;
|
|
for (const FGuid& G : Terminal->GetFunctionInputs())
|
|
if (Terminal->FindInputChecked(G).Name.ToString().Equals(Ref, ESearchCase::IgnoreCase)) return G;
|
|
}
|
|
else if (Kind == TEXT("output"))
|
|
{
|
|
if (bIsGuid && Terminal->FindOutput(Guid)) return Guid;
|
|
for (const FGuid& G : Terminal->GetFunctionOutputs())
|
|
if (Terminal->FindOutputChecked(G).Name.ToString().Equals(Ref, ESearchCase::IgnoreCase)) return G;
|
|
}
|
|
else if (Kind == TEXT("local"))
|
|
{
|
|
if (bIsGuid && Terminal->FindLocalVariable(Guid)) return Guid;
|
|
for (const FGuid& G : Terminal->GetLocalVariables())
|
|
if (Terminal->FindLocalVariableChecked(G).Name.ToString().Equals(Ref, ESearchCase::IgnoreCase)) return G;
|
|
}
|
|
return FGuid();
|
|
}
|
|
|
|
static TSharedRef<FJsonObject> JsonError(const FString& Message)
|
|
{
|
|
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
|
Root->SetBoolField(TEXT("ok"), false);
|
|
Root->SetStringField(TEXT("error"), Message);
|
|
return Root;
|
|
}
|
|
}
|
|
|
|
bool UMCPVoxelGraphLibrary::IsVoxelGraphAsset(UObject* Asset)
|
|
{
|
|
return AsVoxelGraph(Asset) != nullptr;
|
|
}
|
|
|
|
FString UMCPVoxelGraphLibrary::ListTerminalGraphsJson(UObject* Asset)
|
|
{
|
|
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
|
UVoxelGraph* Graph = AsVoxelGraph(Asset);
|
|
if (!Graph)
|
|
{
|
|
Root->SetBoolField(TEXT("ok"), false);
|
|
Root->SetStringField(TEXT("error"), TEXT("asset is not a UVoxelGraph"));
|
|
return VoxelJsonStringify(Root);
|
|
}
|
|
|
|
Root->SetBoolField(TEXT("ok"), true);
|
|
Root->SetStringField(TEXT("asset"), Graph->GetPathName());
|
|
Root->SetStringField(TEXT("class"), Graph->GetClass()->GetPathName());
|
|
|
|
TArray<TSharedPtr<FJsonValue>> Terminals;
|
|
Graph->ForeachTerminalGraph_NoInheritance([&](UVoxelTerminalGraph& TerminalGraph)
|
|
{
|
|
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
|
|
FillTerminalJson(Item, TerminalGraph);
|
|
Terminals.Add(MakeShared<FJsonValueObject>(Item));
|
|
});
|
|
Root->SetArrayField(TEXT("terminals"), Terminals);
|
|
Root->SetNumberField(TEXT("terminal_count"), Terminals.Num());
|
|
return VoxelJsonStringify(Root);
|
|
}
|
|
|
|
UEdGraph* UMCPVoxelGraphLibrary::FindTerminalEdGraph(UObject* Asset, const FString& Terminal)
|
|
{
|
|
UVoxelTerminalGraph* TerminalGraph = FindTerminalGraph(AsVoxelGraph(Asset), Terminal);
|
|
return TerminalGraph ? &TerminalGraph->GetEdGraph() : nullptr;
|
|
}
|
|
|
|
FString UMCPVoxelGraphLibrary::DumpTerminalGraphJson(UObject* Asset, const FString& Terminal)
|
|
{
|
|
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
|
UVoxelGraph* Graph = AsVoxelGraph(Asset);
|
|
UVoxelTerminalGraph* TerminalGraph = FindTerminalGraph(Graph, Terminal);
|
|
if (!Graph || !TerminalGraph)
|
|
{
|
|
Root->SetBoolField(TEXT("ok"), false);
|
|
Root->SetStringField(TEXT("error"), Graph ? TEXT("terminal graph not found") : TEXT("asset is not a UVoxelGraph"));
|
|
return VoxelJsonStringify(Root);
|
|
}
|
|
|
|
Root->SetBoolField(TEXT("ok"), true);
|
|
Root->SetStringField(TEXT("asset"), Graph->GetPathName());
|
|
|
|
TSharedRef<FJsonObject> TerminalJson = MakeShared<FJsonObject>();
|
|
FillTerminalJson(TerminalJson, *TerminalGraph);
|
|
Root->SetObjectField(TEXT("terminal"), TerminalJson);
|
|
|
|
TArray<TSharedPtr<FJsonValue>> Nodes;
|
|
UEdGraph& EdGraph = TerminalGraph->GetEdGraph();
|
|
for (UEdGraphNode* Node : EdGraph.Nodes)
|
|
{
|
|
if (Node)
|
|
{
|
|
Nodes.Add(MakeShared<FJsonValueObject>(VoxelNodeToJson(Node)));
|
|
}
|
|
}
|
|
Root->SetArrayField(TEXT("nodes"), Nodes);
|
|
return VoxelJsonStringify(Root);
|
|
}
|
|
|
|
FString UMCPVoxelGraphLibrary::GetSelectedNodesJson(UObject* Asset, const FString& Terminal)
|
|
{
|
|
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
|
UEdGraph* EdGraph = FindTerminalEdGraph(Asset, Terminal);
|
|
if (!EdGraph)
|
|
{
|
|
Root->SetBoolField(TEXT("ok"), false);
|
|
Root->SetStringField(TEXT("error"), TEXT("terminal graph not found"));
|
|
return VoxelJsonStringify(Root);
|
|
}
|
|
|
|
const TSharedPtr<FVoxelGraphToolkit> Toolkit = FVoxelGraphToolkit::Get(EdGraph);
|
|
if (!Toolkit.IsValid())
|
|
{
|
|
Root->SetBoolField(TEXT("ok"), false);
|
|
Root->SetStringField(TEXT("error"), TEXT("voxel graph editor is not open"));
|
|
return VoxelJsonStringify(Root);
|
|
}
|
|
|
|
Root->SetBoolField(TEXT("ok"), true);
|
|
Root->SetStringField(TEXT("graph"), EdGraph->GetName());
|
|
TArray<TSharedPtr<FJsonValue>> Nodes;
|
|
for (UEdGraphNode* Node : Toolkit->GetSelectedNodes())
|
|
{
|
|
if (Node)
|
|
{
|
|
Nodes.Add(MakeShared<FJsonValueObject>(VoxelNodeToJson(Node)));
|
|
}
|
|
}
|
|
Root->SetArrayField(TEXT("selected"), Nodes);
|
|
Root->SetNumberField(TEXT("selected_count"), Nodes.Num());
|
|
return VoxelJsonStringify(Root);
|
|
}
|
|
|
|
FString UMCPVoxelGraphLibrary::SpawnCommentNode(UEdGraph* Graph, FVector2D Position, FVector2D Size, const FString& Comment, FLinearColor Color)
|
|
{
|
|
if (!Graph)
|
|
{
|
|
return FString();
|
|
}
|
|
|
|
Graph->Modify();
|
|
UEdGraphNode_Comment* Node = NewObject<UEdGraphNode_Comment>(Graph);
|
|
Node->CreateNewGuid();
|
|
Node->NodePosX = static_cast<int32>(Position.X);
|
|
Node->NodePosY = static_cast<int32>(Position.Y);
|
|
Node->NodeWidth = FMath::Max(64, static_cast<int32>(Size.X));
|
|
Node->NodeHeight = FMath::Max(64, static_cast<int32>(Size.Y));
|
|
Node->NodeComment = Comment;
|
|
Node->CommentColor = Color;
|
|
Node->bCommentBubbleVisible = false;
|
|
Graph->AddNode(Node, true, false);
|
|
Node->PostPlacedNewNode();
|
|
NotifyGraphChanged(Graph, false);
|
|
return Node->NodeGuid.ToString();
|
|
}
|
|
|
|
FString UMCPVoxelGraphLibrary::ListNodeTypesJson(const FString& Filter, const int32 Limit)
|
|
{
|
|
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
|
Root->SetBoolField(TEXT("ok"), true);
|
|
|
|
const FString FilterLower = Filter.ToLower();
|
|
const int32 EffectiveLimit = Limit > 0 ? Limit : MAX_int32;
|
|
|
|
TArray<TSharedPtr<FJsonValue>> Items;
|
|
int32 Total = 0;
|
|
for (const TSharedRef<const FVoxelNode>& Node : GetVoxelNodeCatalogue())
|
|
{
|
|
const FString Id = GetNodeId(*Node);
|
|
const FString DisplayName = Node->GetDisplayName();
|
|
const FString Category = Node->GetCategory();
|
|
|
|
if (!FilterLower.IsEmpty())
|
|
{
|
|
const bool bMatches =
|
|
Id.ToLower().Contains(FilterLower) ||
|
|
DisplayName.ToLower().Contains(FilterLower) ||
|
|
Category.ToLower().Contains(FilterLower);
|
|
if (!bMatches)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
++Total;
|
|
if (Items.Num() >= EffectiveLimit)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
|
|
Item->SetStringField(TEXT("id"), Id);
|
|
Item->SetStringField(TEXT("display_name"), DisplayName);
|
|
Item->SetStringField(TEXT("category"), Category);
|
|
Item->SetBoolField(TEXT("is_pure"), Node->IsPureNode());
|
|
Item->SetStringField(TEXT("kind"), GetNodeFunction(*Node) ? TEXT("function") : TEXT("struct"));
|
|
Items.Add(MakeShared<FJsonValueObject>(Item));
|
|
}
|
|
|
|
Root->SetArrayField(TEXT("nodes"), Items);
|
|
Root->SetNumberField(TEXT("returned"), Items.Num());
|
|
Root->SetNumberField(TEXT("total"), Total);
|
|
Root->SetBoolField(TEXT("truncated"), Total > Items.Num());
|
|
return VoxelJsonStringify(Root);
|
|
}
|
|
|
|
FString UMCPVoxelGraphLibrary::SpawnStructNode(UEdGraph* Graph, const FString& NodeId, FVector2D Position)
|
|
{
|
|
if (!Graph)
|
|
{
|
|
return FString();
|
|
}
|
|
|
|
const TSharedPtr<const FVoxelNode> Chosen = FindCatalogueNode(NodeId);
|
|
if (!Chosen)
|
|
{
|
|
return FString();
|
|
}
|
|
|
|
UClass* NodeClass = FindObject<UClass>(nullptr, TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_Struct"));
|
|
if (!NodeClass)
|
|
{
|
|
return FString();
|
|
}
|
|
|
|
Graph->Modify();
|
|
|
|
// NewObject with the reflected class avoids referencing the (non-exported) StaticClass()
|
|
// symbol of UVoxelGraphNode_Struct; the static_cast is purely compile-time.
|
|
UVoxelGraphNode_Struct* Node = static_cast<UVoxelGraphNode_Struct*>(
|
|
NewObject<UEdGraphNode>(Graph, NodeClass, NAME_None, RF_Transactional));
|
|
if (!Node)
|
|
{
|
|
return FString();
|
|
}
|
|
|
|
// Mirrors FVoxelGraphSchemaAction_NewStructNode::PerformAction: copy the catalogue
|
|
// node into the graph node's instanced Struct, then let it build its pins.
|
|
Node->Struct = FVoxelInstancedStruct::Make(*Chosen);
|
|
Node->NodePosX = static_cast<int32>(Position.X);
|
|
Node->NodePosY = static_cast<int32>(Position.Y);
|
|
Node->CreateNewGuid();
|
|
|
|
Graph->AddNode(Node, true, false);
|
|
|
|
Node->PostPlacedNewNode();
|
|
if (Node->Pins.Num() == 0)
|
|
{
|
|
Node->AllocateDefaultPins();
|
|
}
|
|
|
|
return Node->NodeGuid.ToString();
|
|
}
|
|
|
|
FString UMCPVoxelGraphLibrary::AddGraphPropertyJson(UObject* Asset, const FString& Terminal, const FString& Kind, const FString& Name, const FString& TypeStr, const FString& Category)
|
|
{
|
|
UVoxelGraph* Graph = AsVoxelGraph(Asset);
|
|
if (!Graph)
|
|
{
|
|
return VoxelJsonStringify(JsonError(TEXT("asset is not a UVoxelGraph")));
|
|
}
|
|
|
|
const FString KindLower = Kind.ToLower();
|
|
const FVoxelPinType Type = PinTypeFromString(TypeStr);
|
|
const FName PropName = Name.IsEmpty() ? FName(TEXT("NewProperty")) : FName(*Name);
|
|
const FGuid Guid = FGuid::NewGuid();
|
|
|
|
if (KindLower == TEXT("parameter"))
|
|
{
|
|
FVoxelParameter Parameter;
|
|
Parameter.Name = PropName;
|
|
Parameter.Type = Type;
|
|
#if WITH_EDITORONLY_DATA
|
|
Parameter.Category = Category;
|
|
#endif
|
|
Parameter.Fixup();
|
|
Graph->AddParameter(Guid, Parameter);
|
|
Graph->Fixup();
|
|
}
|
|
else
|
|
{
|
|
UVoxelTerminalGraph* TerminalGraph = FindTerminalGraph(Graph, Terminal);
|
|
if (!TerminalGraph)
|
|
{
|
|
return VoxelJsonStringify(JsonError(TEXT("terminal graph not found")));
|
|
}
|
|
|
|
if (KindLower == TEXT("input"))
|
|
{
|
|
FVoxelGraphFunctionInput Input;
|
|
Input.Name = PropName;
|
|
Input.Type = Type;
|
|
#if WITH_EDITORONLY_DATA
|
|
Input.Category = Category;
|
|
#endif
|
|
Input.Fixup();
|
|
TerminalGraph->AddFunctionInput(Guid, Input);
|
|
}
|
|
else if (KindLower == TEXT("output"))
|
|
{
|
|
FVoxelGraphFunctionOutput Output;
|
|
Output.Name = PropName;
|
|
Output.Type = Type;
|
|
#if WITH_EDITORONLY_DATA
|
|
Output.Category = Category;
|
|
#endif
|
|
TerminalGraph->AddFunctionOutput(Guid, Output);
|
|
}
|
|
else if (KindLower == TEXT("local"))
|
|
{
|
|
FVoxelGraphLocalVariable LocalVariable;
|
|
LocalVariable.Name = PropName;
|
|
LocalVariable.Type = Type;
|
|
#if WITH_EDITORONLY_DATA
|
|
LocalVariable.Category = Category;
|
|
#endif
|
|
TerminalGraph->AddLocalVariable(Guid, LocalVariable);
|
|
}
|
|
else
|
|
{
|
|
return VoxelJsonStringify(JsonError(FString::Printf(TEXT("unknown kind %s (use parameter|input|output|local)"), *Kind)));
|
|
}
|
|
}
|
|
|
|
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
|
Root->SetBoolField(TEXT("ok"), true);
|
|
Root->SetStringField(TEXT("guid"), Guid.ToString());
|
|
Root->SetStringField(TEXT("name"), PropName.ToString());
|
|
Root->SetStringField(TEXT("type"), Type.ToString());
|
|
Root->SetStringField(TEXT("kind"), KindLower);
|
|
return VoxelJsonStringify(Root);
|
|
}
|
|
|
|
FString UMCPVoxelGraphLibrary::ListGraphPropertiesJson(UObject* Asset, const FString& Terminal, const FString& Kind)
|
|
{
|
|
UVoxelGraph* Graph = AsVoxelGraph(Asset);
|
|
if (!Graph)
|
|
{
|
|
return VoxelJsonStringify(JsonError(TEXT("asset is not a UVoxelGraph")));
|
|
}
|
|
|
|
const FString KindLower = Kind.ToLower();
|
|
TArray<TSharedPtr<FJsonValue>> Items;
|
|
|
|
const auto AddItem = [&Items](const FGuid& Guid, const FName& PropName, const FVoxelPinType& Type, const FString& Category)
|
|
{
|
|
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
|
|
Item->SetStringField(TEXT("guid"), Guid.ToString());
|
|
Item->SetStringField(TEXT("name"), PropName.ToString());
|
|
Item->SetStringField(TEXT("type"), Type.ToString());
|
|
Item->SetStringField(TEXT("category"), Category);
|
|
Items.Add(MakeShared<FJsonValueObject>(Item));
|
|
};
|
|
|
|
if (KindLower == TEXT("parameter"))
|
|
{
|
|
for (const FGuid& G : Graph->GetParameters())
|
|
{
|
|
const FVoxelParameter& P = Graph->FindParameterChecked(G);
|
|
#if WITH_EDITORONLY_DATA
|
|
AddItem(G, P.Name, P.Type, P.Category);
|
|
#else
|
|
AddItem(G, P.Name, P.Type, FString());
|
|
#endif
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UVoxelTerminalGraph* TerminalGraph = FindTerminalGraph(Graph, Terminal);
|
|
if (!TerminalGraph)
|
|
{
|
|
return VoxelJsonStringify(JsonError(TEXT("terminal graph not found")));
|
|
}
|
|
|
|
if (KindLower == TEXT("input"))
|
|
{
|
|
for (const FGuid& G : TerminalGraph->GetFunctionInputs())
|
|
{
|
|
const FVoxelGraphFunctionInput& P = TerminalGraph->FindInputChecked(G);
|
|
#if WITH_EDITORONLY_DATA
|
|
AddItem(G, P.Name, P.Type, P.Category);
|
|
#else
|
|
AddItem(G, P.Name, P.Type, FString());
|
|
#endif
|
|
}
|
|
}
|
|
else if (KindLower == TEXT("output"))
|
|
{
|
|
for (const FGuid& G : TerminalGraph->GetFunctionOutputs())
|
|
{
|
|
const FVoxelGraphFunctionOutput& P = TerminalGraph->FindOutputChecked(G);
|
|
#if WITH_EDITORONLY_DATA
|
|
AddItem(G, P.Name, P.Type, P.Category);
|
|
#else
|
|
AddItem(G, P.Name, P.Type, FString());
|
|
#endif
|
|
}
|
|
}
|
|
else if (KindLower == TEXT("local"))
|
|
{
|
|
for (const FGuid& G : TerminalGraph->GetLocalVariables())
|
|
{
|
|
const FVoxelGraphLocalVariable& P = TerminalGraph->FindLocalVariableChecked(G);
|
|
#if WITH_EDITORONLY_DATA
|
|
AddItem(G, P.Name, P.Type, P.Category);
|
|
#else
|
|
AddItem(G, P.Name, P.Type, FString());
|
|
#endif
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return VoxelJsonStringify(JsonError(FString::Printf(TEXT("unknown kind %s (use parameter|input|output|local)"), *Kind)));
|
|
}
|
|
}
|
|
|
|
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
|
Root->SetBoolField(TEXT("ok"), true);
|
|
Root->SetStringField(TEXT("kind"), KindLower);
|
|
Root->SetArrayField(TEXT("properties"), Items);
|
|
Root->SetNumberField(TEXT("count"), Items.Num());
|
|
return VoxelJsonStringify(Root);
|
|
}
|
|
|
|
FString UMCPVoxelGraphLibrary::SpawnPropertyNode(UEdGraph* Graph, const FString& Kind, const FString& Ref, FVector2D Position, bool bDeclaration)
|
|
{
|
|
if (!Graph)
|
|
{
|
|
return FString();
|
|
}
|
|
|
|
const FString KindLower = Kind.ToLower();
|
|
UVoxelTerminalGraph* Terminal = OwningTerminal(Graph);
|
|
|
|
FGuid Guid;
|
|
const TCHAR* ClassPath = nullptr;
|
|
|
|
if (KindLower == TEXT("parameter"))
|
|
{
|
|
UVoxelGraph* OwnerGraph = Terminal ? &Terminal->GetGraph() : nullptr;
|
|
Guid = ResolveParameterGuid(OwnerGraph, Ref);
|
|
ClassPath = TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_Parameter");
|
|
}
|
|
else if (KindLower == TEXT("input"))
|
|
{
|
|
Guid = ResolveTerminalPropGuid(Terminal, KindLower, Ref);
|
|
ClassPath = TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_FunctionInput");
|
|
}
|
|
else if (KindLower == TEXT("output"))
|
|
{
|
|
Guid = ResolveTerminalPropGuid(Terminal, KindLower, Ref);
|
|
ClassPath = TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_FunctionOutput");
|
|
}
|
|
else if (KindLower == TEXT("local"))
|
|
{
|
|
Guid = ResolveTerminalPropGuid(Terminal, KindLower, Ref);
|
|
ClassPath = bDeclaration
|
|
? TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_LocalVariableDeclaration")
|
|
: TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_LocalVariableUsage");
|
|
}
|
|
else
|
|
{
|
|
return FString();
|
|
}
|
|
|
|
if (!Guid.IsValid())
|
|
{
|
|
return FString();
|
|
}
|
|
|
|
UEdGraphNode* Node = SpawnGuidNodeReflect(Graph, ClassPath, Guid, Position);
|
|
return Node ? Node->NodeGuid.ToString() : FString();
|
|
}
|
|
|
|
FString UMCPVoxelGraphLibrary::SpawnCallFunctionNode(UEdGraph* Graph, const FString& FunctionRef, FVector2D Position)
|
|
{
|
|
UVoxelTerminalGraph* Terminal = OwningTerminal(Graph);
|
|
UVoxelGraph* OwnerGraph = Terminal ? &Terminal->GetGraph() : nullptr;
|
|
if (!OwnerGraph)
|
|
{
|
|
return FString();
|
|
}
|
|
|
|
UVoxelTerminalGraph* Function = FindTerminalGraph(OwnerGraph, FunctionRef);
|
|
if (!Function || Function->IsMainTerminalGraph() || Function->IsEditorTerminalGraph())
|
|
{
|
|
return FString();
|
|
}
|
|
|
|
UEdGraphNode* Node = SpawnGuidNodeReflect(
|
|
Graph,
|
|
TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_CallMemberFunction"),
|
|
Function->GetGuid(),
|
|
Position);
|
|
return Node ? Node->NodeGuid.ToString() : FString();
|
|
}
|
|
|
|
bool UMCPVoxelGraphLibrary::NotifyGraphChanged(UEdGraph* Graph, const bool bCompile)
|
|
{
|
|
if (!Graph)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
UVoxelTerminalGraph* TerminalGraph = Graph->GetTypedOuter<UVoxelTerminalGraph>();
|
|
if (!TerminalGraph)
|
|
{
|
|
Graph->MarkPackageDirty();
|
|
return false;
|
|
}
|
|
|
|
TerminalGraph->Modify();
|
|
TerminalGraph->GetGraph().Modify();
|
|
Graph->Modify();
|
|
Graph->MarkPackageDirty();
|
|
TerminalGraph->GetGraph().MarkPackageDirty();
|
|
|
|
if (GVoxelGraphTracker)
|
|
{
|
|
GVoxelGraphTracker->NotifyEdGraphChanged(*Graph);
|
|
GVoxelGraphTracker->NotifyTerminalGraphChanged(TerminalGraph->GetGraph());
|
|
}
|
|
|
|
if (bCompile)
|
|
{
|
|
TerminalGraph->GetRuntime().EnsureIsCompiled(true);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool UMCPVoxelGraphLibrary::OpenVoxelGraphAsset(UObject* Asset)
|
|
{
|
|
if (!Asset || !GEditor)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
|
|
return AssetEditorSubsystem && AssetEditorSubsystem->OpenEditorForAsset(Asset);
|
|
}
|
|
|
|
#else // !WITH_MCP_VOXEL
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Voxel plugin not present in this project. Provide stub implementations so the
|
|
// module always links. voxel_graph_op will report this error to the caller.
|
|
// Rebuild with the Voxel plugin installed (or env UE_MCP_WITH_VOXEL=1) to enable.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static const TCHAR* MCPVoxelDisabledJson()
|
|
{
|
|
return TEXT("{\"ok\":false,\"error\":\"Voxel plugin not enabled in this build. Install the Voxel plugin and rebuild (UE_MCP_WITH_VOXEL=1) to use voxel_graph_op.\"}");
|
|
}
|
|
|
|
bool UMCPVoxelGraphLibrary::IsVoxelGraphAsset(UObject*) { return false; }
|
|
FString UMCPVoxelGraphLibrary::ListTerminalGraphsJson(UObject*) { return MCPVoxelDisabledJson(); }
|
|
UEdGraph* UMCPVoxelGraphLibrary::FindTerminalEdGraph(UObject*, const FString&) { return nullptr; }
|
|
FString UMCPVoxelGraphLibrary::DumpTerminalGraphJson(UObject*, const FString&) { return MCPVoxelDisabledJson(); }
|
|
FString UMCPVoxelGraphLibrary::GetSelectedNodesJson(UObject*, const FString&) { return MCPVoxelDisabledJson(); }
|
|
FString UMCPVoxelGraphLibrary::SpawnCommentNode(UEdGraph*, FVector2D, FVector2D, const FString&, FLinearColor) { return FString(); }
|
|
FString UMCPVoxelGraphLibrary::ListNodeTypesJson(const FString&, int32) { return MCPVoxelDisabledJson(); }
|
|
FString UMCPVoxelGraphLibrary::SpawnStructNode(UEdGraph*, const FString&, FVector2D) { return FString(); }
|
|
FString UMCPVoxelGraphLibrary::AddGraphPropertyJson(UObject*, const FString&, const FString&, const FString&, const FString&, const FString&) { return MCPVoxelDisabledJson(); }
|
|
FString UMCPVoxelGraphLibrary::ListGraphPropertiesJson(UObject*, const FString&, const FString&) { return MCPVoxelDisabledJson(); }
|
|
FString UMCPVoxelGraphLibrary::SpawnPropertyNode(UEdGraph*, const FString&, const FString&, FVector2D, bool) { return FString(); }
|
|
FString UMCPVoxelGraphLibrary::SpawnCallFunctionNode(UEdGraph*, const FString&, FVector2D) { return FString(); }
|
|
bool UMCPVoxelGraphLibrary::NotifyGraphChanged(UEdGraph*, bool) { return false; }
|
|
bool UMCPVoxelGraphLibrary::OpenVoxelGraphAsset(UObject*) { return false; }
|
|
|
|
#endif // WITH_MCP_VOXEL
|