diff --git a/Content/Testing/M_Test.uasset b/Content/Testing/M_Test.uasset index 67b665f4..c782a435 100644 --- a/Content/Testing/M_Test.uasset +++ b/Content/Testing/M_Test.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ff518f26fa64b769892a751f3018a1cc7f8f69a99ff5893b09d876ac9dcdd82 -size 14276 +oid sha256:2ed02979e67523f6aefb439525a4a58d5b4a2984a544a34e99fc6d134b4aeba4 +size 26812 diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_Delete.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_Delete.h index 60ee255f..1bf49ff5 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_Delete.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_Delete.h @@ -6,6 +6,9 @@ #include "MCPUtils.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphNode.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "Materials/Material.h" +#include "IMaterialEditor.h" #include "GraphNode_Delete.generated.h" @@ -47,8 +50,21 @@ public: } F.PreEdit(); - FoundNode->BreakAllNodeLinks(); - Graph->RemoveNode(FoundNode); + + if (Cast(FoundNode)) + { + // Use the material editor's DeleteNodes to properly remove + // both the graph node and the underlying material expression. + IMaterialEditor* MatEditor = F.CastEditor(); + if (!MatEditor) return; + MatEditor->DeleteNodes({FoundNode}); + } + else + { + FoundNode->BreakAllNodeLinks(); + Graph->RemoveNode(FoundNode); + } + F.PostEdit(); Result.Appendf(TEXT("Deleted node '%s' from graph '%s'.\n"), *NodeTitle, *GraphName); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_SetDefaults.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_SetDefaults.h new file mode 100644 index 00000000..dd26fe68 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_SetDefaults.h @@ -0,0 +1,156 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPFetcher.h" +#include "MCPUtils.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "MaterialGraph/MaterialGraphSchema.h" +#include "MaterialGraph/MaterialGraphNode.h" +#include "GraphNode_SetDefaults.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +USTRUCT() +struct FSetNodeDefaultEntry +{ + GENERATED_BODY() + + UPROPERTY() + FString Node; + + UPROPERTY() + FString Name; + + UPROPERTY() + FString Value; +}; + + +UCLASS() +class UMCP_GraphNode_SetDefaults : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Graph path, e.g. /Game/Foo,graph:EventGraph")) + FString Graph; + + UPROPERTY(meta=(Description="Array of {node, name, value} objects")) + FMCPJsonArray Pins; + + virtual FString GetDescription() const override + { + return TEXT("Set the default value of input pins or material expression properties on nodes."); + } + + // ----------------------------------------------------------------------- + // K2 graphs: set pin default values. + // ----------------------------------------------------------------------- + + void HandleK2Entry(const FSetNodeDefaultEntry& Entry, UEdGraph* GraphObj, const UEdGraphSchema_K2* K2Schema, + TSet& ModifiedNodes, FStringBuilderBase& Result) + { + MCPFetcher F(Result, GraphObj); + UEdGraphPin* Pin = F.Node(Entry.Node).Pin(Entry.Name).Cast(); + if (!Pin) return; + + UEdGraphNode* Node = Pin->GetOwningNode(); + + if (Pin->Direction != EGPD_Input) + { + Result.Appendf(TEXT("error: %s is an output pin\n"), *MCPUtils::FormatName(Pin)); + return; + } + + Pin->Modify(); + + FString UseDefaultValue; + TObjectPtr UseDefaultObject = nullptr; + FText UseDefaultText; + K2Schema->GetPinDefaultValuesFromString(Pin->PinType, Node, Entry.Value, UseDefaultValue, UseDefaultObject, UseDefaultText, false); + FString Error = K2Schema->IsPinDefaultValid(Pin, UseDefaultValue, UseDefaultObject, UseDefaultText); + if (!Error.IsEmpty()) + { + Result.Appendf(TEXT("error: %s: %s\n"), *MCPUtils::FormatName(Pin), *Error); + return; + } + K2Schema->TrySetDefaultValue(*Pin, Entry.Value); + + ModifiedNodes.Add(Node); + } + + // ----------------------------------------------------------------------- + // Material graphs: set material expression properties. + // ----------------------------------------------------------------------- + + void HandleMaterialEntry(const FSetNodeDefaultEntry& Entry, UEdGraph* GraphObj, + TSet& ModifiedNodes, FStringBuilderBase& Result) + { + MCPFetcher F(Result, GraphObj); + UMaterialGraphNode* MatNode = F.Node(Entry.Node).Cast(); + if (!MatNode) return; + if (!MatNode->MaterialExpression) + { + Result.Appendf(TEXT("error: %s has no material expression\n"), *MCPUtils::FormatName(MatNode)); + return; + } + + UMaterialExpression* Expression = MatNode->MaterialExpression; + FProperty* Prop = MCPUtils::FindPropertyByName(Expression, Entry.Name, Result); + if (!Prop) return; + + if (!MCPUtils::SetPropertyValueText(Expression, Prop, Entry.Value, Result)) + return; + + Expression->ForcePropertyValueChanged(Prop); + ModifiedNodes.Add(MatNode); + } + + // ----------------------------------------------------------------------- + + virtual void Handle(FStringBuilderBase& Result) override + { + // Fetch the graph once. + MCPFetcher GraphFetcher(Result); + UEdGraph* GraphObj = GraphFetcher.Walk(Graph).Cast(); + if (!GraphObj) return; + + const UEdGraphSchema* Schema = GraphObj->GetSchema(); + const UEdGraphSchema_K2* K2Schema = Cast(Schema); + const UMaterialGraphSchema* MGSchema = Cast(Schema); + + if (!K2Schema && !MGSchema) + { + Result.Appendf(TEXT("error: unsupported graph schema %s\n"), *Schema->GetClass()->GetName()); + return; + } + + TSet ModifiedNodes; + + GraphFetcher.PreEdit(); + for (const TSharedPtr& PinVal : Pins.Array) + { + FSetNodeDefaultEntry Entry; + if (!MCPUtils::PopulateFromJson(FSetNodeDefaultEntry::StaticStruct(), &Entry, PinVal, Result)) + continue; + + if (K2Schema) + HandleK2Entry(Entry, GraphObj, K2Schema, ModifiedNodes, Result); + else if (MGSchema) + HandleMaterialEntry(Entry, GraphObj, ModifiedNodes, Result); + } + + for (UEdGraphNode* Node : ModifiedNodes) + { + Node->ReconstructNode(); + } + GraphFetcher.PostEdit(); + + Result.Appendf(TEXT("Done.\n")); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphPin_SetDefault.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphPin_SetDefault.h deleted file mode 100644 index 10a9fa22..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphPin_SetDefault.h +++ /dev/null @@ -1,114 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "MCPHandler.h" -#include "MCPFetcher.h" -#include "MCPUtils.h" -#include "EdGraph/EdGraphPin.h" -#include "EdGraphSchema_K2.h" -#include "GraphPin_SetDefault.generated.h" - - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -USTRUCT() -struct FSetPinDefaultEntry -{ - GENERATED_BODY() - - UPROPERTY() - FString Node; - - UPROPERTY() - FString Pin; - - UPROPERTY() - FString Value; -}; - - -UCLASS() -class UMCP_GraphPin_SetDefault : public UObject, public IMCPHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Graph path, e.g. /Game/Foo,graph:EventGraph")) - FString Graph; - - UPROPERTY(meta=(Description="Array of {node, pin, value} objects")) - FMCPJsonArray Pins; - - virtual FString GetDescription() const override - { - return TEXT("Set the default value of input pins on nodes."); - } - - virtual void Handle(FStringBuilderBase& Result) override - { - // Fetch the graph once. - MCPFetcher GraphFetcher(Result); - UEdGraph* GraphObj = GraphFetcher.Walk(Graph).Cast(); - if (!GraphObj) return; - - int32 SuccessCount = 0; - int32 TotalCount = Pins.Array.Num(); - TSet ModifiedNodes; - - GraphFetcher.PreEdit(); - for (const TSharedPtr& PinVal : Pins.Array) - { - FSetPinDefaultEntry Entry; - if (!MCPUtils::PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal, Result)) - continue; - - MCPFetcher F(Result, GraphObj); - UEdGraphPin* Pin = F.Node(Entry.Node).Pin(Entry.Pin).Cast(); - if (!Pin) continue; - - UEdGraphNode* Node = Pin->GetOwningNode(); - - if (Pin->Direction != EGPD_Input) - { - Result.Appendf(TEXT("error: %s is an output pin\n"), *MCPUtils::FormatName(Pin)); - continue; - } - - Pin->Modify(); - - // K2 schemas support validation before setting; other schemas (e.g. material) don't. - const UEdGraphSchema_K2* K2Schema = Cast(Node->GetGraph()->GetSchema()); - if (K2Schema) - { - FString UseDefaultValue; - TObjectPtr UseDefaultObject = nullptr; - FText UseDefaultText; - K2Schema->GetPinDefaultValuesFromString(Pin->PinType, Node, Entry.Value, UseDefaultValue, UseDefaultObject, UseDefaultText, false); - FString Error = K2Schema->IsPinDefaultValid(Pin, UseDefaultValue, UseDefaultObject, UseDefaultText); - if (!Error.IsEmpty()) - { - Result.Appendf(TEXT("error: %s: %s\n"), *MCPUtils::FormatName(Pin), *Error); - continue; - } - K2Schema->TrySetDefaultValue(*Pin, Entry.Value); - } - else - { - Node->GetGraph()->GetSchema()->TrySetDefaultValue(*Pin, Entry.Value); - } - - SuccessCount++; - ModifiedNodes.Add(Node); - } - - for (UEdGraphNode* Node : ModifiedNodes) - { - Node->ReconstructNode(); - } - GraphFetcher.PostEdit(); - - Result.Appendf(TEXT("Set %d/%d pin defaults.\n"), SuccessCount, TotalCount); - } -}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp index 5cf15259..36b81282 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp @@ -288,7 +288,7 @@ void FlxBlueprintExporter::EmitMaterialProperty(UMaterialExpression* Expression, bool bEditable = !Prop->HasAnyPropertyFlags(CPF_EditConst); Out.Appendf(TEXT(" %s %s %s = %s\n"), - bEditable ? TEXT("editable") : TEXT("readonly"), + bEditable ? TEXT("mxeditable") : TEXT("mxreadonly"), *MCPUtils::FormatPropertyType(Prop), *MCPUtils::FormatName(Prop), *ValueStr); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp index 156e06e0..1d95ffe3 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp @@ -11,6 +11,7 @@ #include "MaterialGraph/MaterialGraph.h" #include "MaterialGraph/MaterialGraphNode.h" #include "Engine/LevelScriptBlueprint.h" +#include "Subsystems/AssetEditorSubsystem.h" MCPFetcher& MCPFetcher::SetError(const FString& Msg) { @@ -86,7 +87,18 @@ MCPFetcher& MCPFetcher::Asset(const FString& PackagePath) { SetObj(LoadObject(nullptr, *PackagePath)); if (!Obj) - SetError(FString::Printf(TEXT("Could not load asset '%s'"), *PackagePath)); + return SetError(FString::Printf(TEXT("Could not load asset '%s'"), *PackagePath)); + + OriginalAsset = Obj; + + // Open the editor for this asset (or bring it to front if already open). + UAssetEditorSubsystem* Sub = GEditor->GetEditorSubsystem(); + if (!Sub || !Sub->OpenEditorForAsset(Obj)) + return SetError(FString::Printf(TEXT("Could not open editor for '%s'"), *PackagePath)); + + Editor = Sub->FindEditorForAsset(OriginalAsset, false); + if (!Editor) + return SetError(FString::Printf(TEXT("Could not find editor instance for '%s'"), *PackagePath)); // If this is a material open in the editor, use the editor's transient copy. if (UMaterial* Mat = ::Cast(Obj)) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp index ea4e5df3..a37f67a3 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp @@ -1126,6 +1126,7 @@ void MCPUtils::PreEdit(const TArray& Objects) void MCPUtils::PostEdit(const TArray& Objects) { + TSet Graphs; TSet Materials; TSet Blueprints; for (int32 i = Objects.Num() - 1; i >= 0; --i) @@ -1134,6 +1135,9 @@ void MCPUtils::PostEdit(const TArray& Objects) Obj->PostEditChange(); Obj->MarkPackageDirty(); + if (UEdGraph* Graph = Cast(Obj)) + Graphs.Add(Graph); + if (UBlueprint* BP = Cast(Obj)) Blueprints.Add(BP); @@ -1141,6 +1145,8 @@ void MCPUtils::PostEdit(const TArray& Objects) if (UMaterial* BaseMat = MatIface->GetMaterial()) Materials.Add(BaseMat); } + for (UEdGraph* Graph : Graphs) + Graph->NotifyGraphChanged(); for (UMaterial *Material : Materials) UMaterialEditingLibrary::RebuildMaterialInstanceEditors(Material); for (UBlueprint *Blueprint : Blueprints) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h index 46989eff..d83275e0 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h @@ -4,6 +4,7 @@ #include "MCPUtils.h" class UEdGraphPin; +class IAssetEditorInstance; // Resolves a path string into a UObject or UEdGraphPin. // @@ -29,6 +30,8 @@ class MCPFetcher public: bool bError = false; UObject* Obj = nullptr; + UObject* OriginalAsset = nullptr; + IAssetEditorInstance* Editor = nullptr; UEdGraphPin* ResultPin = nullptr; MCPErrorCallback ErrorCB = nullptr; @@ -69,6 +72,30 @@ public: static const TArray& GetWalkerTable(); bool Ok() const { return !bError; } + UObject* GetAsset() const { return OriginalAsset; } + template T* CastAsset() + { + if (bError) return nullptr; + T* Result = ::Cast(OriginalAsset); + if (!Result) + SetError(FString::Printf(TEXT("Asset is %s, expected %s"), + OriginalAsset ? *OriginalAsset->GetClass()->GetName() : TEXT("null"), + *T::StaticClass()->GetName())); + return Result; + } + template + EditorType* CastEditor() + { + if (bError) return nullptr; + if (!OriginalAsset || !OriginalAsset->IsA()) + { + SetError(FString::Printf(TEXT("Asset is %s, expected %s"), + OriginalAsset ? *OriginalAsset->GetClass()->GetName() : TEXT("null"), + *AssetType::StaticClass()->GetName())); + return nullptr; + } + return static_cast(Editor); + } const TArray& Visited() const { return Chain; } void PreEdit() { MCPUtils::PreEdit(Chain); } void PostEdit() { MCPUtils::PostEdit(Chain); }