From 73b5d67b963645c75963be5614d9d9cc34f13a87 Mon Sep 17 00:00:00 2001 From: jyelon Date: Thu, 12 Mar 2026 08:26:18 -0400 Subject: [PATCH] Working on MI SetParameter --- Content/Testing/M_Test.uasset | 4 +- Content/Testing/M_Test_Inst.uasset | 3 + Content/Testing/M_Test_Inst_Inst.uasset | 3 + Integration.code-workspace.tpl.json | 5 +- .../Material_ReparentInstance.h | 0 .../Source/BlueprintMCP/BlueprintMCP.Build.cs | 4 +- ...rs.h => MaterialInstance_DumpParameters.h} | 4 +- .../Handlers/MaterialInstance_SetParameter.h | 110 ++++++++++++++++++ .../BlueprintMCP/Private/MCPFetcher.cpp | 49 ++++---- .../Source/BlueprintMCP/Private/MCPUtils.cpp | 46 +++++++- .../Source/BlueprintMCP/Public/MCPFetcher.h | 12 +- .../Source/BlueprintMCP/Public/MCPUtils.h | 12 ++ 12 files changed, 212 insertions(+), 40 deletions(-) create mode 100644 Content/Testing/M_Test_Inst.uasset create mode 100644 Content/Testing/M_Test_Inst_Inst.uasset rename Plugins/BlueprintMCP/{Source/BlueprintMCP/Handlers => Deprecated}/Material_ReparentInstance.h (100%) rename Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/{Material_DumpInstanceParameters.h => MaterialInstance_DumpParameters.h} (98%) create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/MaterialInstance_SetParameter.h diff --git a/Content/Testing/M_Test.uasset b/Content/Testing/M_Test.uasset index 420ac5bb..47cba279 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:c556b7172995c348c76e164c1f818fc84f96f6be9cb9bbf20f41bf6bcddd20e7 -size 14226 +oid sha256:296d949b2dadcc103f32ad1549ea8405259d9744d0023d8de10790d0335f3ffd +size 14297 diff --git a/Content/Testing/M_Test_Inst.uasset b/Content/Testing/M_Test_Inst.uasset new file mode 100644 index 00000000..89918215 --- /dev/null +++ b/Content/Testing/M_Test_Inst.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8025bae950d02457c5095c48aa30cdcdf0b628d6a9f5019120e0f721060c8d03 +size 9315 diff --git a/Content/Testing/M_Test_Inst_Inst.uasset b/Content/Testing/M_Test_Inst_Inst.uasset new file mode 100644 index 00000000..b7a4c91a --- /dev/null +++ b/Content/Testing/M_Test_Inst_Inst.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef57ab84d7e785519670ccd29b3f272fc902c16a2a22fc208a5b835d16027b1c +size 9021 diff --git a/Integration.code-workspace.tpl.json b/Integration.code-workspace.tpl.json index 9a63bd9c..010e4595 100644 --- a/Integration.code-workspace.tpl.json +++ b/Integration.code-workspace.tpl.json @@ -63,10 +63,7 @@ "extensions": { "recommendations": [ "llvm-vs-code-extensions.vscode-clangd", - "vadimcn.vscode-lldb", - "dfarley1.file-picker", - "ms-python.python", - "ms-vscode.mono-debug" + "vadimcn.vscode-lldb" ] }, "tasks": { diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_ReparentInstance.h b/Plugins/BlueprintMCP/Deprecated/Material_ReparentInstance.h similarity index 100% rename from Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_ReparentInstance.h rename to Plugins/BlueprintMCP/Deprecated/Material_ReparentInstance.h diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/BlueprintMCP.Build.cs b/Plugins/BlueprintMCP/Source/BlueprintMCP/BlueprintMCP.Build.cs index ee026262..f8a62504 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/BlueprintMCP.Build.cs +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/BlueprintMCP.Build.cs @@ -29,7 +29,9 @@ public class BlueprintMCP : ModuleRules "MaterialEditor", "AnimGraph", "AnimGraphRuntime", - "RHI" + "RHI", + "Slate", + "SlateCore" }); } } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_DumpInstanceParameters.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/MaterialInstance_DumpParameters.h similarity index 98% rename from Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_DumpInstanceParameters.h rename to Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/MaterialInstance_DumpParameters.h index f7ff935a..4d36421f 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_DumpInstanceParameters.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/MaterialInstance_DumpParameters.h @@ -12,7 +12,7 @@ #include "Materials/MaterialExpressionTextureSampleParameter2D.h" #include "Materials/MaterialExpressionStaticSwitchParameter.h" #include "Engine/Texture.h" -#include "Material_DumpInstanceParameters.generated.h" +#include "MaterialInstance_DumpParameters.generated.h" // --------------------------------------------------------------------------- @@ -20,7 +20,7 @@ // --------------------------------------------------------------------------- UCLASS() -class UMCP_Material_DumpInstanceParameters : public UObject, public IMCPHandler +class UMCP_MaterialInstance_DumpParameters : public UObject, public IMCPHandler { GENERATED_BODY() diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/MaterialInstance_SetParameter.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/MaterialInstance_SetParameter.h new file mode 100644 index 00000000..fc6b8f88 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/MaterialInstance_SetParameter.h @@ -0,0 +1,110 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPFetcher.h" +#include "MCPUtils.h" +#include "Materials/MaterialInstanceConstant.h" +#include "Misc/DefaultValueHelper.h" +#include "MaterialInstance_SetParameter.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCP_MaterialInstance_SetParameter : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Material Instance path")) + FString Path; + + UPROPERTY(meta=(Description="Parameter name to set")) + FString Parameter; + + UPROPERTY(meta=(Description="Value to set (uses Unreal text format, e.g. '0.5' for scalar, '(R=1,G=0,B=0,A=1)' for vector)")) + FString Value; + + virtual FString GetDescription() const override + { + return TEXT("Set a parameter override on a Material Instance."); + } + + virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override + { + MCPFetcher F(Result); + UMaterialInstanceConstant* MI = F.Asset(Path).Cast(); + if (!MI) return; + + auto Parameters = MCPUtils::MaterialParameters(MI); + + // Find a parameter with a matching name. + FName Name(*Parameter); + MCPMaterialParameter* Found = nullptr; + int MatchCount = 0; + for (auto& P : Parameters) + { + if (P.Name == Name) + { + MatchCount++; + Found = &P; + } + } + if (MatchCount > 1) + { + Result.Appendf(TEXT("More than one parameter named '%s'"), *Parameter); + return; + } + if (MatchCount == 0) + { + Result.Appendf(TEXT("No parameter named '%s'"), *Parameter); + return; + } + if (Found->Metadata.PrimitiveDataIndex != INDEX_NONE) + { + Result.Appendf(TEXT("Parameter '%s' uses custom primitive data and cannot be set on a material instance"), *Parameter); + return; + } + + FMaterialParameterInfo Info(*Parameter); + EMaterialParameterType Type = Found->Metadata.Value.Type; + + F.PreEdit(); + switch (Type) + { + case EMaterialParameterType::Scalar: + { + float ScalarValue; + if (!FDefaultValueHelper::ParseFloat(Value, ScalarValue)) + { + Result.Appendf(TEXT("Failed to parse '%s' as a float"), *Value); + return; + } + MI->SetScalarParameterValueEditorOnly(Info, ScalarValue); + break; + } + case EMaterialParameterType::Vector: + { + FLinearColor Color; + if (!Color.InitFromString(Value)) + { + Result.Appendf(TEXT("Failed to parse '%s' as a color/vector (expected format: '(R=1,G=0,B=0,A=1)')"), *Value); + return; + } + MI->SetVectorParameterValueEditorOnly(Info, Color); + break; + } + default: + Result.Appendf(TEXT("Unsupported parameter type for '%s'"), *Parameter); + return; + } + F.PostEdit(); + MCPUtils::SaveGenericPackage(MI); + + Result.Appendf(TEXT("Set '%s' = %s on %s\n"), + *Parameter, *Value, *MCPUtils::FormatName(MI)); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp index 2cf74f30..d49a502b 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp @@ -48,7 +48,6 @@ MCPFetcher& MCPFetcher::Walk(const FString& Path) { if (bError) return *this; - // Split on commas TArray Segments; Path.ParseIntoArray(Segments, TEXT(",")); if (Segments.Num() == 0) @@ -57,46 +56,33 @@ MCPFetcher& MCPFetcher::Walk(const FString& Path) return *this; } - // If no object yet, first segment is an asset path - int32 Start = 0; - if (!Obj && !ResultPin) + for (int32 i = 0; i < Segments.Num(); i++) { - LoadUAsset(Segments[0]); - if (bError) return *this; - Start = 1; - } + if (!Obj && !ResultPin) + { + Asset(Segments[i]); + if (bError) return *this; + continue; + } - const TArray& Walkers = GetWalkerTable(); - - // Walk each subsequent segment - for (int32 i = Start; i < Segments.Num(); i++) - { FString Key, Value; if (!Segments[i].Split(TEXT(":"), &Key, &Value)) Key = Segments[i]; - bool bFound = false; - for (const FWalker& W : Walkers) + const FWalker* W = GetWalker(Key); + if (!W) { - if (!StrEq(Key, W.Key)) continue; - (this->*W.Func)(Value); - bFound = true; - break; - } - - if (!bFound) - { - SetError(FString::Printf(TEXT("Unknown walker '%s'"), *Key)); + SetError(FString::Printf(TEXT("Unknown path step '%s'"), *Key)); return *this; } - + (this->*W->Func)(Value); if (bError) return *this; } return *this; } -void MCPFetcher::LoadUAsset(const FString& PackagePath) +MCPFetcher& MCPFetcher::Asset(const FString& PackagePath) { SetObj(LoadObject(nullptr, *PackagePath)); if (!Obj) @@ -105,6 +91,17 @@ void MCPFetcher::LoadUAsset(const FString& PackagePath) // If this is a material open in the editor, use the editor's transient copy. if (UMaterial* Mat = ::Cast(Obj)) SetObj(MCPUtils::ReplaceMaterialWithTransientCopy(Mat)); + return *this; +} + +const MCPFetcher::FWalker* MCPFetcher::GetWalker(const FString& Key) +{ + for (const FWalker& W : GetWalkerTable()) + { + if (StrEq(Key, W.Key)) + return &W; + } + return nullptr; } MCPFetcher& MCPFetcher::Graph(const FString& Value) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp index d36dde2b..ce2b56da 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp @@ -72,6 +72,7 @@ #include "MaterialGraph/MaterialGraphNode.h" #include "MaterialGraph/MaterialGraphSchema.h" #include "IMaterialEditor.h" +#include "MaterialEditingLibrary.h" #include "Subsystems/AssetEditorSubsystem.h" // Mesh, animation, texture support @@ -1125,6 +1126,8 @@ void MCPUtils::PreEdit(const TArray& Objects) void MCPUtils::PostEdit(const TArray& Objects) { + TSet Materials; + TSet Blueprints; for (int32 i = Objects.Num() - 1; i >= 0; --i) { UObject* Obj = Objects[i]; @@ -1132,8 +1135,36 @@ void MCPUtils::PostEdit(const TArray& Objects) Obj->MarkPackageDirty(); if (UBlueprint* BP = Cast(Obj)) - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); + Blueprints.Add(BP); + + if (UMaterialInterface* MatIface = Cast(Obj)) + if (UMaterial* BaseMat = MatIface->GetMaterial()) + Materials.Add(BaseMat); } + for (UMaterial *Material : Materials) + UMaterialEditingLibrary::RebuildMaterialInstanceEditors(Material); + for (UBlueprint *Blueprint : Blueprints) + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint); + + if (GEditor) + GEditor->RedrawAllViewports(); +} + +TArray MCPUtils::MaterialParameters(UMaterialInstanceConstant *MI) +{ + TArray Results; + UMaterial* BaseMat = MI->GetMaterial(); + if (!BaseMat) return Results; + for (UMaterialExpression* Expr : BaseMat->GetExpressions()) + { + if (!Expr || !Expr->bIsParameterExpression) continue; + MCPMaterialParameter P; + P.Name = Expr->GetParameterName(); + if (P.Name.IsNone()) continue; + if (!Expr->GetParameterValue(P.Metadata)) continue; + Results.Add(P); + } + return Results; } bool MCPUtils::SaveMaterialPackage(UMaterial* Material) @@ -1790,6 +1821,19 @@ bool MCPUtils::SetPropertyValueText(UObject* Container, FProperty* Prop, const F return true; } +bool MCPUtils::SetPropertyValueText(void* Container, FProperty* Prop, const FString& Value, UObject* Owner, MCPErrorCallback Error) +{ + void* ValuePtr = Prop->ContainerPtrToValuePtr(Container); + const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, ValuePtr, Owner, PPF_None); + if (!ImportResult) + { + Error.SetError(FString::Printf(TEXT("Failed to parse '%s' for property '%s' (type: %s)"), + *Value, *FormatName(Prop), *Prop->GetCPPType())); + return false; + } + return true; +} + // ============================================================ // SearchProperties // ============================================================ diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h index 9cff296b..608553cc 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h @@ -35,6 +35,9 @@ public: MCPFetcher(MCPErrorCallback CB) : ErrorCB(CB) {} MCPFetcher(MCPErrorCallback CB, UObject* O) : Obj(O), ErrorCB(CB) {} + // Starting point is always Asset. + MCPFetcher& Asset(const FString& PackagePath); + // Walk one step. MCPFetcher& Graph(const FString& Value); MCPFetcher& Node(const FString& Value); @@ -43,13 +46,13 @@ public: MCPFetcher& LevelBlueprint(const FString& Value); MCPFetcher& MatExp(const FString& Value); + // Parse string and walk multiple steps. + MCPFetcher& Walk(const FString& Path); + // C++-only walk step: resolve to the editable template // (e.g. Blueprint → CDO, everything else → as-is). MCPFetcher& Template(); - // Parse string and walk multiple steps. - MCPFetcher& Walk(const FString& Path); - // Walker table entry. struct FWalker { @@ -74,6 +77,7 @@ public: return Result; } + private: TArray Chain; static bool StrEq(const FString& A, const TCHAR* B) { return A.Equals(B, ESearchCase::IgnoreCase); } @@ -81,7 +85,7 @@ private: void SetPin(UEdGraphPin* InPin) { ResultPin = InPin; Obj = nullptr; } MCPFetcher& SetError(const FString& Msg); MCPFetcher& TypeMismatch(const TCHAR* Walker, const TCHAR* Expected); - void LoadUAsset(const FString& PackagePath); + const FWalker* GetWalker(const FString& Key); }; template<> inline UEdGraphPin* MCPFetcher::Cast() diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h index 6f440dee..8c2a28e9 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h @@ -3,6 +3,8 @@ #include "CoreMinimal.h" #include "Dom/JsonObject.h" #include "EdGraph/EdGraphPin.h" +#include "Materials/MaterialInstanceConstant.h" +#include "MaterialTypes.h" class UBlueprint; class UEdGraph; @@ -96,6 +98,12 @@ struct MCPErrorCallback void SetError(const FString& Msg) const { Func(Msg); } }; +struct MCPMaterialParameter +{ + FName Name; + FMaterialParameterMetadata Metadata; +}; + // Stateless utility functions used by MCP handlers and the MCP server. // This is effectively a namespace — all methods are static. class MCPUtils @@ -277,6 +285,9 @@ public: static void PreEdit(const TArray& Objects); static void PostEdit(const TArray& Objects); + // ----- Material Instance ----- + static TArray MaterialParameters(UMaterialInstanceConstant *MI); + // ----- Editable template ----- // Given an object, returns the appropriate template object for generic // property editing, or nullptr if the type isn't whitelisted. @@ -287,6 +298,7 @@ public: static FProperty* FindPropertyByName(UObject* Obj, const FString& Name, MCPErrorCallback Error = nullptr); static FString GetPropertyValueText(UObject* Container, FProperty* Prop); static bool SetPropertyValueText(UObject* Container, FProperty* Prop, const FString& Value, MCPErrorCallback Error = nullptr); + static bool SetPropertyValueText(void* Container, FProperty* Prop, const FString& Value, UObject* Owner, MCPErrorCallback Error = nullptr); // ----- Property population ----- static FString PropertyNameToJsonKey(const FString& PropName);