Files
unreal-engine-mcp-system-pl…/Source/UEBlueprintMCPEditor/Private/MCPVoxelGraphLibrary.cpp
Bonchellon eba71c4ca8 Unified, portable Unreal Engine MCP system plugin
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>
2026-06-24 01:07:24 +03:00

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