#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& Object) { FString Out; const TSharedRef> Writer = TJsonWriterFactory<>::Create(&Out); FJsonSerializer::Serialize(Object, Writer); return Out; } static UVoxelGraph* AsVoxelGraph(UObject* Asset) { return Cast(Asset); } static void FillTerminalJson(TSharedRef 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 VoxelNodeToJson(UEdGraphNode* Node) { TSharedRef Json = MakeShared(); 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(Node)) { Json->SetNumberField(TEXT("w"), CommentNode->NodeWidth); Json->SetNumberField(TEXT("h"), CommentNode->NodeHeight); } TArray> Pins; for (UEdGraphPin* Pin : Node->Pins) { if (!Pin) { continue; } TSharedRef PinJson = MakeShared(); 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> Links; for (UEdGraphPin* LinkedPin : Pin->LinkedTo) { if (!LinkedPin || !LinkedPin->GetOwningNode()) { continue; } TSharedRef LinkJson = MakeShared(); LinkJson->SetStringField(TEXT("node"), LinkedPin->GetOwningNode()->NodeGuid.ToString()); LinkJson->SetStringField(TEXT("pin"), LinkedPin->PinName.ToString()); Links.Add(MakeShared(LinkJson)); } if (Links.Num() > 0) { PinJson->SetArrayField(TEXT("links"), Links); } Pins.Add(MakeShared(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>& GetVoxelNodeCatalogue() { static TArray> 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()) { if (!Struct) { continue; } if (Struct->HasMetaData(TEXT("Abstract")) || Struct->HasMetaData(TEXT("Internal"))) { continue; } Nodes.Add(MakeSharedStruct(Struct)); } // Function-library nodes (UVoxelFunctionLibrary UFUNCTIONs wrapped as FVoxelNode_UFunction): // most math / position / curve nodes live here. for (const TSubclassOf& Class : GetDerivedClasses()) { 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(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 FindCatalogueNode(const FString& NodeId) { for (const TSharedRef& 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(); if (T == TEXT("int") || T == TEXT("int32")) return FVoxelPinType::Make(); if (T == TEXT("bool")) return FVoxelPinType::Make(); if (T == TEXT("vector2d") || T == TEXT("vector2"))return FVoxelPinType::Make(); if (T == TEXT("vector") || T == TEXT("vector3d")) return FVoxelPinType::Make(); if (T == TEXT("color") || T == TEXT("linearcolor"))return FVoxelPinType::Make(); if (T == TEXT("name")) return FVoxelPinType::Make(); return FVoxelPinType::Make(); } // 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(nullptr, ClassPath); if (!Graph || !NodeClass) { return nullptr; } Graph->Modify(); UEdGraphNode* Node = NewObject(Graph, NodeClass, NAME_None, RF_Transactional); if (!Node) { return nullptr; } if (FStructProperty* GuidProp = FindFProperty(NodeClass, TEXT("Guid"))) { *GuidProp->ContainerPtrToValuePtr(Node) = Guid; } Node->NodePosX = static_cast(Position.X); Node->NodePosY = static_cast(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() : 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 JsonError(const FString& Message) { TSharedRef Root = MakeShared(); 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 Root = MakeShared(); 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> Terminals; Graph->ForeachTerminalGraph_NoInheritance([&](UVoxelTerminalGraph& TerminalGraph) { TSharedRef Item = MakeShared(); FillTerminalJson(Item, TerminalGraph); Terminals.Add(MakeShared(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 Root = MakeShared(); 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 TerminalJson = MakeShared(); FillTerminalJson(TerminalJson, *TerminalGraph); Root->SetObjectField(TEXT("terminal"), TerminalJson); TArray> Nodes; UEdGraph& EdGraph = TerminalGraph->GetEdGraph(); for (UEdGraphNode* Node : EdGraph.Nodes) { if (Node) { Nodes.Add(MakeShared(VoxelNodeToJson(Node))); } } Root->SetArrayField(TEXT("nodes"), Nodes); return VoxelJsonStringify(Root); } FString UMCPVoxelGraphLibrary::GetSelectedNodesJson(UObject* Asset, const FString& Terminal) { TSharedRef Root = MakeShared(); 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 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> Nodes; for (UEdGraphNode* Node : Toolkit->GetSelectedNodes()) { if (Node) { Nodes.Add(MakeShared(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(Graph); Node->CreateNewGuid(); Node->NodePosX = static_cast(Position.X); Node->NodePosY = static_cast(Position.Y); Node->NodeWidth = FMath::Max(64, static_cast(Size.X)); Node->NodeHeight = FMath::Max(64, static_cast(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 Root = MakeShared(); Root->SetBoolField(TEXT("ok"), true); const FString FilterLower = Filter.ToLower(); const int32 EffectiveLimit = Limit > 0 ? Limit : MAX_int32; TArray> Items; int32 Total = 0; for (const TSharedRef& 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 Item = MakeShared(); 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(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 Chosen = FindCatalogueNode(NodeId); if (!Chosen) { return FString(); } UClass* NodeClass = FindObject(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( NewObject(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(Position.X); Node->NodePosY = static_cast(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 Root = MakeShared(); 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> Items; const auto AddItem = [&Items](const FGuid& Guid, const FName& PropName, const FVoxelPinType& Type, const FString& Category) { TSharedRef Item = MakeShared(); 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(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 Root = MakeShared(); 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(); 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(); 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