From 8cf847280cc482edb5d9367c38d98f402ff7725a Mon Sep 17 00:00:00 2001 From: jyelon Date: Thu, 12 Mar 2026 17:20:20 -0400 Subject: [PATCH] More work on handlers --- .../AnimBlueprint_SetBlendSpaceSamples.h | 8 +- .../BlueprintMCP/Handlers/Material_Create.h | 5 +- .../Handlers/Material_DumpProperties.h | 48 +++++++++ .../Handlers/Material_SetProperty.h | 101 +++++++----------- .../Handlers/StateMachine_AddTransition.h | 15 +-- .../Handlers/StateMachine_SetTransitionRule.h | 44 ++------ .../Source/BlueprintMCP/Public/MCPUtils.h | 8 ++ 7 files changed, 109 insertions(+), 120 deletions(-) create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_DumpProperties.h diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/AnimBlueprint_SetBlendSpaceSamples.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/AnimBlueprint_SetBlendSpaceSamples.h index dc91cdae..82a1c48f 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/AnimBlueprint_SetBlendSpaceSamples.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/AnimBlueprint_SetBlendSpaceSamples.h @@ -83,12 +83,12 @@ public: FBlendParameter& MutableParamY = const_cast(ParamY); if (!AxisXName.IsEmpty()) MutableParamX.DisplayName = AxisXName; - if (Json->HasField(TEXT("axisXMin"))) MutableParamX.Min = AxisXMin; - if (Json->HasField(TEXT("axisXMax"))) MutableParamX.Max = AxisXMax; + if (AxisXMin != 0.0f) MutableParamX.Min = AxisXMin; + if (AxisXMax != 0.0f) MutableParamX.Max = AxisXMax; if (!AxisYName.IsEmpty()) MutableParamY.DisplayName = AxisYName; - if (Json->HasField(TEXT("axisYMin"))) MutableParamY.Min = AxisYMin; - if (Json->HasField(TEXT("axisYMax"))) MutableParamY.Max = AxisYMax; + if (AxisYMin != 0.0f) MutableParamY.Min = AxisYMin; + if (AxisYMax != 0.0f) MutableParamY.Max = AxisYMax; // Clear existing samples (delete from end to start) int32 NumExisting = BS->GetNumberOfBlendSamples(); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_Create.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_Create.h index 6daaa768..976f893e 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_Create.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_Create.h @@ -82,8 +82,6 @@ public: } // Apply optional properties. - bool bHasTwoSided = Json->HasField(TEXT("twoSided")); - TArray Chain = { MaterialObj }; MCPUtils::PreEdit(Chain); @@ -93,8 +91,7 @@ public: if (!BlendMode.IsEmpty()) MaterialObj->BlendMode = ParsedBlendMode; - if (bHasTwoSided) - MaterialObj->TwoSided = TwoSided; + MaterialObj->TwoSided = TwoSided ? 1 : 0; MCPUtils::PostEdit(Chain); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_DumpProperties.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_DumpProperties.h new file mode 100644 index 00000000..d46bddfc --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_DumpProperties.h @@ -0,0 +1,48 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPFetcher.h" +#include "MCPUtils.h" +#include "Materials/Material.h" +#include "MaterialDomain.h" +#include "Material_DumpProperties.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCP_Material_DumpProperties : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Material path")) + FString Path; + + virtual FString GetDescription() const override + { + return TEXT("List top-level material properties such as domain, blend mode, shading model, and usage flags."); + } + + virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override + { + MCPFetcher F(Result); + UMaterial* Mat = F.Asset(Path).Cast(); + if (!Mat) return; + + Result.Appendf(TEXT("Material: %s\n"), *MCPUtils::FormatName(Mat)); + Result.Appendf(TEXT(" domain = %s\n"), *MCPUtils::EnumToString(Mat->MaterialDomain, TEXT("MD_"))); + Result.Appendf(TEXT(" blendMode = %s\n"), *MCPUtils::EnumToString(Mat->BlendMode, TEXT("BLEND_"))); + Result.Appendf(TEXT(" shadingModel = %s\n"), *MCPUtils::EnumToString(Mat->GetShadingModels().GetFirstShadingModel(), TEXT("MSM_"))); + Result.Appendf(TEXT(" opacityMaskClipValue = %g\n"), Mat->OpacityMaskClipValue); + Result.Appendf(TEXT(" twoSided = %s\n"), Mat->TwoSided ? TEXT("true") : TEXT("false")); + Result.Appendf(TEXT(" bUsedWithSkeletalMesh = %s\n"), Mat->bUsedWithSkeletalMesh ? TEXT("true") : TEXT("false")); + Result.Appendf(TEXT(" bUsedWithMorphTargets = %s\n"), Mat->bUsedWithMorphTargets ? TEXT("true") : TEXT("false")); + Result.Appendf(TEXT(" bUsedWithNiagaraSprites = %s\n"), Mat->bUsedWithNiagaraSprites ? TEXT("true") : TEXT("false")); + Result.Appendf(TEXT(" ditheredLODTransition = %s\n"), Mat->DitheredLODTransition ? TEXT("true") : TEXT("false")); + Result.Appendf(TEXT(" bAllowNegativeEmissiveColor = %s\n"), Mat->bAllowNegativeEmissiveColor ? TEXT("true") : TEXT("false")); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_SetProperty.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_SetProperty.h index 61ce53ff..3288ad4b 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_SetProperty.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_SetProperty.h @@ -2,7 +2,7 @@ #include "CoreMinimal.h" #include "MCPHandler.h" -#include "MCPAssetFinder.h" +#include "MCPFetcher.h" #include "MCPUtils.h" #include "Materials/Material.h" #include "MaterialDomain.h" @@ -19,102 +19,81 @@ class UMCP_Material_SetProperty : public UObject, public IMCPHandler GENERATED_BODY() public: - UPROPERTY(meta=(Description="Material name or package path")) - FString Material; + UPROPERTY(meta=(Description="Material path")) + FString Path; UPROPERTY(meta=(Description="Property name to set (domain, blendMode, twoSided, shadingModel, opacity, opacityMaskClipValue, bUsedWithSkeletalMesh, bUsedWithMorphTargets, bUsedWithNiagaraSprites, ditheredLODTransition, bAllowNegativeEmissiveColor)")) FString Property; - UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it")) - bool DryRun = false; + UPROPERTY(meta=(Description="Value to set")) + FString Value; virtual FString GetDescription() const override { - return TEXT("Set a top-level material property such as domain, blend mode, shading model, or usage flags. " - "The 'value' field in the JSON payload provides the new value."); + return TEXT("Set a top-level material property such as domain, blend mode, shading model, or usage flags."); } virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override { - if (!Json->HasField(TEXT("value"))) - { - Result.Append(TEXT("ERROR: Missing required field: value\n")); - return; - } + MCPFetcher F(Result); + UMaterial* Mat = F.Asset(Path).Cast(); + if (!Mat) return; - // Load material - MCPAssets Assets; - if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; - UMaterial* Mat = Assets.Object(); - - FString OldValue; - FString NewValue; - - // Helper: apply a bool property change. - auto SetBool = [&](auto Getter, auto Setter) { - bool bValue = Json->GetBoolField(TEXT("value")); - OldValue = Getter() ? TEXT("true") : TEXT("false"); - NewValue = bValue ? TEXT("true") : TEXT("false"); - if (!DryRun) Setter(bValue); - }; + // Parse value and build setter. Validation happens here so we can bail before PreEdit. + TFunction Setter; if (Property == TEXT("domain")) { - FString ValueStr = Json->GetStringField(TEXT("value")); - OldValue = MCPUtils::EnumToString(Mat->MaterialDomain, TEXT("MD_")); EMaterialDomain NewDomain; - if (!MCPUtils::StringToEnum(ValueStr, NewDomain, Result, TEXT("MD_"))) return; - NewValue = MCPUtils::EnumToString(NewDomain, TEXT("MD_")); - if (!DryRun) Mat->MaterialDomain = NewDomain; + if (!MCPUtils::StringToEnum(Value, NewDomain, Result, TEXT("MD_"))) return; + Setter = [Mat, NewDomain]() { Mat->MaterialDomain = NewDomain; }; } else if (Property == TEXT("blendMode")) { - FString ValueStr = Json->GetStringField(TEXT("value")); - OldValue = MCPUtils::EnumToString(Mat->BlendMode, TEXT("BLEND_")); EBlendMode NewBlend; - if (!MCPUtils::StringToEnum(ValueStr, NewBlend, Result, TEXT("BLEND_"))) return; - NewValue = MCPUtils::EnumToString(NewBlend, TEXT("BLEND_")); - if (!DryRun) Mat->BlendMode = NewBlend; + if (!MCPUtils::StringToEnum(Value, NewBlend, Result, TEXT("BLEND_"))) return; + Setter = [Mat, NewBlend]() { Mat->BlendMode = NewBlend; }; } else if (Property == TEXT("shadingModel")) { - FString ValueStr = Json->GetStringField(TEXT("value")); - OldValue = MCPUtils::EnumToString(Mat->GetShadingModels().GetFirstShadingModel(), TEXT("MSM_")); EMaterialShadingModel NewModel; - if (!MCPUtils::StringToEnum(ValueStr, NewModel, Result, TEXT("MSM_"))) return; - NewValue = MCPUtils::EnumToString(NewModel, TEXT("MSM_")); - if (!DryRun) Mat->SetShadingModel(NewModel); + if (!MCPUtils::StringToEnum(Value, NewModel, Result, TEXT("MSM_"))) return; + Setter = [Mat, NewModel]() { Mat->SetShadingModel(NewModel); }; } else if (Property == TEXT("opacity") || Property == TEXT("opacityMaskClipValue")) { - double OpacityValue = Json->GetNumberField(TEXT("value")); - OldValue = FString::Printf(TEXT("%g"), Mat->OpacityMaskClipValue); - NewValue = FString::Printf(TEXT("%g"), OpacityValue); - if (!DryRun) Mat->OpacityMaskClipValue = (float)OpacityValue; + float FloatVal = FCString::Atof(*Value); + Setter = [Mat, FloatVal]() { Mat->OpacityMaskClipValue = FloatVal; }; } else if (Property == TEXT("twoSided")) { - SetBool([&]{ return Mat->TwoSided; }, [&](bool v){ Mat->TwoSided = v ? 1 : 0; }); + bool bVal; if (!MCPUtils::StringToBool(Value, bVal, Result)) return; + Setter = [Mat, bVal]() { Mat->TwoSided = bVal ? 1 : 0; }; } else if (Property == TEXT("bUsedWithSkeletalMesh")) { - SetBool([&]{ return Mat->bUsedWithSkeletalMesh; }, [&](bool v){ Mat->bUsedWithSkeletalMesh = v ? 1 : 0; }); + bool bVal; if (!MCPUtils::StringToBool(Value, bVal, Result)) return; + Setter = [Mat, bVal]() { Mat->bUsedWithSkeletalMesh = bVal ? 1 : 0; }; } else if (Property == TEXT("bUsedWithMorphTargets")) { - SetBool([&]{ return Mat->bUsedWithMorphTargets; }, [&](bool v){ Mat->bUsedWithMorphTargets = v ? 1 : 0; }); + bool bVal; if (!MCPUtils::StringToBool(Value, bVal, Result)) return; + Setter = [Mat, bVal]() { Mat->bUsedWithMorphTargets = bVal ? 1 : 0; }; } else if (Property == TEXT("bUsedWithNiagaraSprites")) { - SetBool([&]{ return Mat->bUsedWithNiagaraSprites; }, [&](bool v){ Mat->bUsedWithNiagaraSprites = v ? 1 : 0; }); + bool bVal; if (!MCPUtils::StringToBool(Value, bVal, Result)) return; + Setter = [Mat, bVal]() { Mat->bUsedWithNiagaraSprites = bVal ? 1 : 0; }; } else if (Property == TEXT("ditheredLODTransition") || Property == TEXT("DitheredLODTransition")) { - SetBool([&]{ return Mat->DitheredLODTransition; }, [&](bool v){ Mat->DitheredLODTransition = v ? 1 : 0; }); + bool bVal; if (!MCPUtils::StringToBool(Value, bVal, Result)) return; + Setter = [Mat, bVal]() { Mat->DitheredLODTransition = bVal ? 1 : 0; }; } else if (Property == TEXT("bAllowNegativeEmissiveColor")) { - SetBool([&]{ return Mat->bAllowNegativeEmissiveColor; }, [&](bool v){ Mat->bAllowNegativeEmissiveColor = v ? 1 : 0; }); + bool bVal; if (!MCPUtils::StringToBool(Value, bVal, Result)) return; + Setter = [Mat, bVal]() { Mat->bAllowNegativeEmissiveColor = bVal ? 1 : 0; }; } else { @@ -124,18 +103,12 @@ public: return; } - // Notify and save if not dry run - if (!DryRun) - { - TArray Chain = { Mat }; - MCPUtils::PreEdit(Chain); - MCPUtils::PostEdit(Chain); - if (!MCPUtils::SaveMaterialPackage(Mat)) - Result.Append(TEXT("WARNING: Package save failed\n")); - } + // Apply the change between PreEdit/PostEdit. + F.PreEdit(); + Setter(); + F.PostEdit(); + MCPUtils::SaveMaterialPackage(Mat); - Result.Appendf(TEXT("%s%s: %s -> %s\n"), - DryRun ? TEXT("[DRY RUN] ") : TEXT(""), - *Property, *OldValue, *NewValue); + Result.Appendf(TEXT("Set %s on %s\n"), *Property, *MCPUtils::FormatName(Mat)); } }; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/StateMachine_AddTransition.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/StateMachine_AddTransition.h index 995b4ffb..667bff07 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/StateMachine_AddTransition.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/StateMachine_AddTransition.h @@ -84,18 +84,9 @@ public: TransNode->CreateConnections(FromStateNode, ToStateNode); // Set optional properties - if (Json->HasField(TEXT("crossfadeDuration"))) - { - TransNode->CrossfadeDuration = CrossfadeDuration; - } - if (Json->HasField(TEXT("priority"))) - { - TransNode->PriorityOrder = Priority; - } - if (Json->HasField(TEXT("bBidirectional"))) - { - TransNode->Bidirectional = BBidirectional; - } + TransNode->CrossfadeDuration = CrossfadeDuration; + TransNode->PriorityOrder = Priority; + TransNode->Bidirectional = BBidirectional; // Compile and save FKismetEditorUtilities::CompileBlueprint(AnimBP); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/StateMachine_SetTransitionRule.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/StateMachine_SetTransitionRule.h index e05fc24d..68cf38b4 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/StateMachine_SetTransitionRule.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/StateMachine_SetTransitionRule.h @@ -74,40 +74,12 @@ public: return; } - // Count and apply property changes - int32 ChangedCount = 0; - - if (Json->HasField(TEXT("crossfadeDuration"))) - { - TransNode->CrossfadeDuration = CrossfadeDuration; - ChangedCount++; - } - if (Json->HasField(TEXT("blendMode"))) - { - TransNode->BlendMode = (EAlphaBlendOption)BlendMode; - ChangedCount++; - } - if (Json->HasField(TEXT("priorityOrder"))) - { - TransNode->PriorityOrder = PriorityOrder; - ChangedCount++; - } - if (Json->HasField(TEXT("logicType"))) - { - TransNode->LogicType = (ETransitionLogicType::Type)LogicType; - ChangedCount++; - } - if (Json->HasField(TEXT("bBidirectional"))) - { - TransNode->Bidirectional = BBidirectional; - ChangedCount++; - } - - if (ChangedCount == 0) - { - Result.Append(TEXT("ERROR: No properties to update. Provide at least one of: crossfadeDuration, blendMode, priorityOrder, logicType, bBidirectional\n")); - return; - } + // Apply properties + TransNode->CrossfadeDuration = CrossfadeDuration; + TransNode->BlendMode = (EAlphaBlendOption)BlendMode; + TransNode->PriorityOrder = PriorityOrder; + TransNode->LogicType = (ETransitionLogicType::Type)LogicType; + TransNode->Bidirectional = BBidirectional; TArray Chain = { TransNode }; MCPUtils::PreEdit(Chain); @@ -117,7 +89,7 @@ public: FKismetEditorUtilities::CompileBlueprint(AnimBP); MCPUtils::SaveBlueprintPackage(AnimBP); - Result.Appendf(TEXT("Updated %d properties on transition %s -> %s: %s\n"), - ChangedCount, *FromState, *ToState, *MCPUtils::FormatName(TransNode)); + Result.Appendf(TEXT("Updated transition %s -> %s: %s\n"), + *FromState, *ToState, *MCPUtils::FormatName(TransNode)); } }; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h index ed19a750..2506d261 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h @@ -218,6 +218,14 @@ public: return true; } + static bool StringToBool(const FString& Str, bool& OutValue, MCPErrorCallback Error) + { + if (Str.Equals(TEXT("true"), ESearchCase::IgnoreCase)) { OutValue = true; return true; } + if (Str.Equals(TEXT("false"), ESearchCase::IgnoreCase)) { OutValue = false; return true; } + Error.SetError(FString::Printf(TEXT("Invalid bool value '%s' (expected 'true' or 'false')"), *Str)); + return false; + } + // ----- Blueprint helpers ----- static TArray AllGraphs(UBlueprint* BP); static TArray AllGraphsNamed(UBlueprint* BP, const FString& Name);