From c35cfcd70cf400b0132edef0bab4480f4b542e0f Mon Sep 17 00:00:00 2001 From: jyelon Date: Fri, 13 Mar 2026 05:34:19 -0400 Subject: [PATCH] More progress on MCP --- Content/Testing/M_Kaleidoscope.uasset | 3 + Content/Testing/M_Mandelbrot.uasset | 3 + Content/Testing/M_Test.uasset | 4 +- .../Handlers/GraphNode_SetDefaults.h | 56 +++++-------- .../BlueprintMCP/Handlers/Property_Dump.h | 26 +++--- .../BlueprintMCP/Handlers/Property_Get.h | 44 ++++++++++ .../BlueprintMCP/Handlers/Property_Set.h | 28 ++----- .../BlueprintMCP/Private/MCPFetcher.cpp | 1 + .../BlueprintMCP/Private/MCPNotifier.cpp | 65 +++++++++++++++ .../BlueprintMCP/Private/MCPProperty.cpp | 81 ++++++++++++++++++- .../Source/BlueprintMCP/Private/MCPUtils.cpp | 6 ++ .../Source/BlueprintMCP/Public/MCPFetcher.h | 35 +++++--- .../Source/BlueprintMCP/Public/MCPNotifier.h | 28 +++++++ .../Source/BlueprintMCP/Public/MCPProperty.h | 7 +- 14 files changed, 309 insertions(+), 78 deletions(-) create mode 100644 Content/Testing/M_Kaleidoscope.uasset create mode 100644 Content/Testing/M_Mandelbrot.uasset create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Get.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPNotifier.cpp create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPNotifier.h diff --git a/Content/Testing/M_Kaleidoscope.uasset b/Content/Testing/M_Kaleidoscope.uasset new file mode 100644 index 00000000..bd0ad18e --- /dev/null +++ b/Content/Testing/M_Kaleidoscope.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78b6d2cbe8c2e3fbd2183df29f8d9200a632f74c94586cbd4845ffe1b256e411 +size 15193 diff --git a/Content/Testing/M_Mandelbrot.uasset b/Content/Testing/M_Mandelbrot.uasset new file mode 100644 index 00000000..9c499aec --- /dev/null +++ b/Content/Testing/M_Mandelbrot.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:778f068b12af2526ddc893443170d4aeeebae2b397699c88126e9b7ca78d1d47 +size 15349 diff --git a/Content/Testing/M_Test.uasset b/Content/Testing/M_Test.uasset index 60bd062b..c60ba42d 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:e77ffbce63d1d578a6ee0ae763e3d1d59309511455a8592f472c1a5310649a7e -size 13010 +oid sha256:316f5800bdf14dfc6902bf8c7196b8c3d5be1339ae169ef2b78114651aaef053 +size 16646 diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_SetDefaults.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_SetDefaults.h index dd26fe68..9f664da9 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_SetDefaults.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_SetDefaults.h @@ -3,11 +3,12 @@ #include "CoreMinimal.h" #include "MCPHandler.h" #include "MCPFetcher.h" +#include "MCPNotifier.h" +#include "MCPProperty.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" @@ -53,9 +54,9 @@ public: // ----------------------------------------------------------------------- void HandleK2Entry(const FSetNodeDefaultEntry& Entry, UEdGraph* GraphObj, const UEdGraphSchema_K2* K2Schema, - TSet& ModifiedNodes, FStringBuilderBase& Result) + MCPNotifier& N, FStringBuilderBase& Result) { - MCPFetcher F(Result, GraphObj); + MCPFetcher F(Result, N, GraphObj); UEdGraphPin* Pin = F.Node(Entry.Node).Pin(Entry.Name).Cast(); if (!Pin) return; @@ -79,9 +80,8 @@ public: Result.Appendf(TEXT("error: %s: %s\n"), *MCPUtils::FormatName(Pin), *Error); return; } + N.PreEditAddObject(Node); K2Schema->TrySetDefaultValue(*Pin, Entry.Value); - - ModifiedNodes.Add(Node); } // ----------------------------------------------------------------------- @@ -89,26 +89,19 @@ public: // ----------------------------------------------------------------------- void HandleMaterialEntry(const FSetNodeDefaultEntry& Entry, UEdGraph* GraphObj, - TSet& ModifiedNodes, FStringBuilderBase& Result) + MCPNotifier& N, 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)); + MCPFetcher F(Result, N, GraphObj); + UEdGraphNode* Node = F.Node(Entry.Node).Cast(); + if (!Node) return; + + MCPProperty P = MCPProperty::GetOneExactMatch(Node, CPF_Edit, Entry.Name, Result); + if (!P) return; + + N.PreEditAddObject(Node); + + if (!P.SetText(Entry.Value, Result)) 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); } // ----------------------------------------------------------------------- @@ -116,7 +109,8 @@ public: virtual void Handle(FStringBuilderBase& Result) override { // Fetch the graph once. - MCPFetcher GraphFetcher(Result); + MCPNotifier N; + MCPFetcher GraphFetcher(Result, N); UEdGraph* GraphObj = GraphFetcher.Walk(Graph).Cast(); if (!GraphObj) return; @@ -130,9 +124,7 @@ public: return; } - TSet ModifiedNodes; - - GraphFetcher.PreEdit(); + N.PreEdit(); for (const TSharedPtr& PinVal : Pins.Array) { FSetNodeDefaultEntry Entry; @@ -140,16 +132,12 @@ public: continue; if (K2Schema) - HandleK2Entry(Entry, GraphObj, K2Schema, ModifiedNodes, Result); + HandleK2Entry(Entry, GraphObj, K2Schema, N, Result); else if (MGSchema) - HandleMaterialEntry(Entry, GraphObj, ModifiedNodes, Result); + HandleMaterialEntry(Entry, GraphObj, N, Result); } - for (UEdGraphNode* Node : ModifiedNodes) - { - Node->ReconstructNode(); - } - GraphFetcher.PostEdit(); + N.PostEdit(); Result.Appendf(TEXT("Done.\n")); } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Dump.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Dump.h index 46da8ba7..b58666cb 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Dump.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Dump.h @@ -3,6 +3,7 @@ #include "CoreMinimal.h" #include "MCPHandler.h" #include "MCPFetcher.h" +#include "MCPProperty.h" #include "MCPUtils.h" #include "Property_Dump.generated.h" @@ -42,14 +43,19 @@ public: UObject* Template = F.Walk(Path).Template().Cast(); if (!Template) return; - TArray Props = MCPUtils::SearchProperties(Template, Query, CPF_Edit, Local); + TArray Props = MCPProperty::GetAllSubstring(Template, CPF_Edit, Query); + if (Local) + { + UClass* ObjClass = Template->GetClass(); + Props.RemoveAll([ObjClass](const MCPProperty& P) { return P->GetOwnerStruct() != ObjClass; }); + } // Group properties by category. - TMap> ByCategory; - for (FProperty* Prop : Props) + TMap> ByCategory; + for (MCPProperty& P : Props) { - FString Category = Prop->HasMetaData(TEXT("Category")) ? Prop->GetMetaData(TEXT("Category")) : FString(); - ByCategory.FindOrAdd(Category).Add(Prop); + FString Category = P->HasMetaData(TEXT("Category")) ? P->GetMetaData(TEXT("Category")) : FString(); + ByCategory.FindOrAdd(Category).Add(P); } // Sort category names, putting empty category last. @@ -68,18 +74,18 @@ public: else Result.Appendf(TEXT("\n%s:\n"), *Category); - for (FProperty* Prop : ByCategory[Category]) + for (MCPProperty& P : ByCategory[Category]) { - FString PropName = MCPUtils::FormatName(Prop); - FString ValueStr = MCPUtils::GetPropertyValueText(Template, Prop); + FString PropName = MCPUtils::FormatName(P.Prop); + FString ValueStr = P.GetText(); if (Truncate && (ValueStr.Len() > 80)) ValueStr = ValueStr.Left(80) + TEXT("..."); - bool bEditable = !Prop->HasAnyPropertyFlags(CPF_EditConst); + bool bEditable = !P->HasAnyPropertyFlags(CPF_EditConst); Result.Appendf(TEXT(" %s %s %s = %s\n"), bEditable ? TEXT("editable") : TEXT("readonly"), - *MCPUtils::FormatPropertyType(Prop), + *MCPUtils::FormatPropertyType(P.Prop), *PropName, *ValueStr); } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Get.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Get.h new file mode 100644 index 00000000..24420234 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Get.h @@ -0,0 +1,44 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPFetcher.h" +#include "MCPProperty.h" +#include "MCPUtils.h" +#include "Property_Get.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCP_Property_Get : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="MCPFetcher path to the object (e.g. /Game/Materials/M_Gold or /Game/Tangibles/TAN_Char,component:Mesh0)")) + FString Path; + + UPROPERTY(meta=(Description="Property name")) + FString Property; + + virtual FString GetDescription() const override + { + return TEXT("Get the value of a single property on an object resolved via MCPFetcher path."); + } + + virtual void Handle(FStringBuilderBase& Result) override + { + MCPFetcher F(Result); + UObject* Template = F.Walk(Path).Template().Cast(); + if (!Template) return; + + MCPProperty P = MCPProperty::GetOneExactMatch(Template, CPF_Edit, Property, Result); + if (!P) return; + + Result.Append(P.GetText()); + Result.Append(TEXT("\n")); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Set.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Set.h index 3ad71fa7..ca019854 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Set.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Set.h @@ -3,6 +3,7 @@ #include "CoreMinimal.h" #include "MCPHandler.h" #include "MCPFetcher.h" +#include "MCPProperty.h" #include "MCPUtils.h" #include "Property_Set.generated.h" @@ -42,17 +43,12 @@ public: return; } - // Validation pass — check all properties before modifying anything. + // Validation pass — resolve all properties and values before modifying anything. + TArray> Resolved; for (const auto& Pair : Properties.Json->Values) { - FProperty* Prop = MCPUtils::FindPropertyByName(Template, Pair.Key, Result); - if (!Prop) return; - - if (!Prop->HasAnyPropertyFlags(CPF_Edit)) - { - Result.Appendf(TEXT("Error: Property '%s' is not editable (no Edit flag)\n"), *Pair.Key); - return; - } + MCPProperty P = MCPProperty::GetOneExactMatch(Template, CPF_Edit, Pair.Key, Result); + if (!P) return; FString ValueStr; if (!Pair.Value->TryGetString(ValueStr)) @@ -60,25 +56,17 @@ public: Result.Appendf(TEXT("Error: Value for '%s' must be a string\n"), *Pair.Key); return; } + Resolved.Emplace(P, ValueStr); } // Apply all changes in a single Pre/PostEditChange bracket. F.PreEdit(); int32 SuccessCount = 0; - for (const auto& Pair : Properties.Json->Values) + for (auto& [P, ValueStr] : Resolved) { - FProperty* Prop = MCPUtils::FindPropertyByName(Template, Pair.Key); - FString ValueStr; - Pair.Value->TryGetString(ValueStr); - - FString OldValue = MCPUtils::GetPropertyValueText(Template, Prop); - - if (!MCPUtils::SetPropertyValueText(Template, Prop, ValueStr, Result)) + if (!P.SetText(ValueStr, Result)) continue; - - FString NewValue = MCPUtils::GetPropertyValueText(Template, Prop); - Result.Appendf(TEXT("%s: %s -> %s\n"), *MCPUtils::FormatName(Prop), *OldValue, *NewValue); SuccessCount++; } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp index 55e20545..3ae96d99 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp @@ -330,3 +330,4 @@ MCPFetcher& MCPFetcher::ToGraph() return TypeMismatch(TEXT("ToGraph"), TEXT("Graph or Material")); } + diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPNotifier.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPNotifier.cpp new file mode 100644 index 00000000..0c44a723 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPNotifier.cpp @@ -0,0 +1,65 @@ +#include "MCPNotifier.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraph.h" +#include "Engine/Blueprint.h" +#include "Materials/Material.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "MaterialEditingLibrary.h" + +void MCPNotifier::PreEditAddObject(UObject* Obj) +{ + if (!Obj) return; + bool bAlreadyInSet = false; + TouchedSet.Add(Obj, &bAlreadyInSet); + if (bAlreadyInSet) return; + TouchedArray.Add(Obj); + if (bInsidePrePost) + Obj->PreEditChange(nullptr); +} + +void MCPNotifier::PreEdit() +{ + bInsidePrePost = true; + for (UObject* Obj : TouchedArray) + Obj->PreEditChange(nullptr); +} + +void MCPNotifier::PostEdit() +{ + TSet Nodes; + TSet Graphs; + TSet Materials; + TSet Blueprints; + for (int32 i = TouchedArray.Num() - 1; i >= 0; --i) + { + UObject* Obj = TouchedArray[i]; + Obj->PostEditChange(); + Obj->MarkPackageDirty(); + + if (UEdGraphNode* Node = ::Cast(Obj)) + Nodes.Add(Node); + + if (UEdGraph* Graph = ::Cast(Obj)) + Graphs.Add(Graph); + + if (UBlueprint* BP = ::Cast(Obj)) + Blueprints.Add(BP); + + if (UMaterialInterface* MatIface = ::Cast(Obj)) + if (UMaterial* BaseMat = MatIface->GetMaterial()) + Materials.Add(BaseMat); + } + for (UEdGraphNode* Node : Nodes) + Node->ReconstructNode(); + for (UEdGraph* Graph : Graphs) + Graph->NotifyGraphChanged(); + for (UMaterial *Material : Materials) + UMaterialEditingLibrary::RebuildMaterialInstanceEditors(Material); + for (UBlueprint *Blueprint : Blueprints) + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint); + + if (GEditor) + GEditor->RedrawAllViewports(); + + bInsidePrePost = false; +} diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPProperty.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPProperty.cpp index 94e77799..f7ecd99e 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPProperty.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPProperty.cpp @@ -1,18 +1,22 @@ #include "MCPProperty.h" #include "MCPUtils.h" +#include "Materials/MaterialExpression.h" +#include "MaterialGraph/MaterialGraphNode.h" -MCPProperty::MCPProperty(FProperty* InProp, void* Container) - : Prop(InProp), ValuePtr(InProp ? InProp->ContainerPtrToValuePtr(Container) : nullptr) {} +MCPProperty::MCPProperty(FProperty* InProp, void* InContainer) + : Prop(InProp), Container(InContainer) {} FString MCPProperty::GetText() const { FString Result; + void* ValuePtr = Prop->ContainerPtrToValuePtr(Container); Prop->ExportTextItem_Direct(Result, ValuePtr, nullptr, nullptr, PPF_None); return Result; } bool MCPProperty::SetText(const FString& Value, MCPErrorCallback Error) { + void* ValuePtr = Prop->ContainerPtrToValuePtr(Container); const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, ValuePtr, nullptr, PPF_None); if (!ImportResult) { @@ -20,5 +24,78 @@ bool MCPProperty::SetText(const FString& Value, MCPErrorCallback Error) *Value, *MCPUtils::FormatName(Prop), *Prop->GetCPPType())); return false; } + + if (Prop->GetOwnerClass()->IsChildOf(UMaterialExpression::StaticClass())) + { + UMaterialExpression* Expr = static_cast(Container); + Expr->ForcePropertyValueChanged(Prop); + } + return true; } + +TArray MCPProperty::GetAll(UObject* Obj, EPropertyFlags Flags) +{ + TArray Result; + if (!Obj) return Result; + for (TFieldIterator It(Obj->GetClass()); It; ++It) + { + if (Flags != 0 && !It->HasAnyPropertyFlags(Flags)) continue; + Result.Emplace(*It, Obj); + } + if (UMaterialGraphNode* MatNode = Cast(Obj)) + { + if (UMaterialExpression* Expr = MatNode->MaterialExpression) + { + for (TFieldIterator It(Expr->GetClass()); It; ++It) + { + if (Flags != 0 && !It->HasAnyPropertyFlags(Flags)) continue; + Result.Emplace(*It, Expr); + } + } + } + return Result; +} + +TArray MCPProperty::GetAllSubstring(UObject* Obj, EPropertyFlags Flags, const FString& Substring) +{ + TArray All = GetAll(Obj, Flags); + if (Substring.IsEmpty()) return All; + TArray Result; + for (const MCPProperty& P : All) + { + if (MCPUtils::FormatName(P.Prop).Contains(Substring, ESearchCase::IgnoreCase)) + Result.Add(P); + } + return Result; +} + +TArray MCPProperty::GetAllExactMatch(UObject* Obj, EPropertyFlags Flags, const FString& Name) +{ + TArray All = GetAll(Obj, Flags); + TArray Result; + for (const MCPProperty& P : All) + { + if (MCPUtils::Identifies(Name, P.Prop)) + Result.Add(P); + } + return Result; +} + +MCPProperty MCPProperty::GetOneExactMatch(UObject* Obj, EPropertyFlags Flags, const FString& Name, MCPErrorCallback Error) +{ + TArray Matches = GetAllExactMatch(Obj, Flags, Name); + if (Matches.Num() == 0) + { + Error.SetError(FString::Printf(TEXT("Property '%s' not found on %s"), + *Name, *MCPUtils::FormatName(Obj->GetClass()))); + return MCPProperty(); + } + if (Matches.Num() > 1) + { + Error.SetError(FString::Printf(TEXT("Ambiguous property '%s' on %s"), + *Name, *MCPUtils::FormatName(Obj->GetClass()))); + return MCPProperty(); + } + return Matches[0]; +} diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp index 8378194b..e5ae8a6f 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp @@ -810,6 +810,7 @@ void MCPUtils::PreEdit(const TArray& Objects) void MCPUtils::PostEdit(const TArray& Objects) { + TSet Nodes; TSet Graphs; TSet Materials; TSet Blueprints; @@ -819,6 +820,9 @@ void MCPUtils::PostEdit(const TArray& Objects) Obj->PostEditChange(); Obj->MarkPackageDirty(); + if (UEdGraphNode* Node = Cast(Obj)) + Nodes.Add(Node); + if (UEdGraph* Graph = Cast(Obj)) Graphs.Add(Graph); @@ -829,6 +833,8 @@ void MCPUtils::PostEdit(const TArray& Objects) if (UMaterial* BaseMat = MatIface->GetMaterial()) Materials.Add(BaseMat); } + for (UEdGraphNode* Node : Nodes) + Node->ReconstructNode(); for (UEdGraph* Graph : Graphs) Graph->NotifyGraphChanged(); for (UMaterial *Material : Materials) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h index 6f62924a..b295f0a5 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h @@ -2,6 +2,7 @@ #include "CoreMinimal.h" #include "MCPUtils.h" +#include "MCPNotifier.h" class UEdGraphPin; class IAssetEditorInstance; @@ -29,7 +30,9 @@ class MCPFetcher { public: MCPFetcher(MCPErrorCallback CB) : ErrorCB(CB) {} - MCPFetcher(MCPErrorCallback CB, UObject* O) : Obj(O), ErrorCB(CB) {} + MCPFetcher(MCPErrorCallback CB, UObject* O) : ErrorCB(CB), Obj(O) {} + MCPFetcher(MCPErrorCallback CB, MCPNotifier& N) : ErrorCB(CB), Notifier(&N) {} + MCPFetcher(MCPErrorCallback CB, MCPNotifier& N, UObject* O) : ErrorCB(CB), Obj(O), Notifier(&N) {} // Starting point is always Asset. MCPFetcher& Asset(const FString& PackagePath); @@ -77,9 +80,7 @@ public: if (!CheckAssetIsA(AssetType::StaticClass())) return nullptr; return static_cast(Editor); } - const TArray& Visited() const { return Chain; } - void PreEdit() { MCPUtils::PreEdit(Chain); } - void PostEdit() { MCPUtils::PostEdit(Chain); } + template T *Cast() { if (bError) return nullptr; @@ -89,17 +90,33 @@ public: return Result; } + void PreEditAddObject(UObject* Obj) { Notifier->PreEditAddObject(Obj); } + void PreEdit() { Notifier->PreEdit(); } + void PostEdit() { Notifier->PostEdit(); } + private: - bool bError = false; + // The error callback is invoked whenever an error is detected. + MCPErrorCallback ErrorCB = nullptr; + + // The Current Object or Pin UObject* Obj = nullptr; + UEdGraphPin* ResultPin = nullptr; + + // The Starting Asset and the Editor we Opened UObject* OriginalAsset = nullptr; IAssetEditorInstance* Editor = nullptr; - UEdGraphPin* ResultPin = nullptr; - MCPErrorCallback ErrorCB = nullptr; - TArray Chain; + + // True if an error has occurred. + bool bError = false; + + // Notifier for tracking touched objects. + MCPNotifier OwnedNotifier; + MCPNotifier* Notifier = &OwnedNotifier; + + // Internal methods. static bool StrEq(const FString& A, const TCHAR* B) { return A.Equals(B, ESearchCase::IgnoreCase); } - void SetObj(UObject* InObj) { if (InObj) Chain.AddUnique(InObj); Obj = InObj; ResultPin = nullptr; } + void SetObj(UObject* InObj) { Notifier->PreEditAddObject(InObj); Obj = InObj; ResultPin = nullptr; } void SetPin(UEdGraphPin* InPin) { ResultPin = InPin; Obj = nullptr; } MCPFetcher& SetError(const FString& Msg); MCPFetcher& TypeMismatch(const TCHAR* Walker, const TCHAR* Expected); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPNotifier.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPNotifier.h new file mode 100644 index 00000000..b4cb5b89 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPNotifier.h @@ -0,0 +1,28 @@ +#pragma once + +#include "CoreMinimal.h" + +// Tracks objects that have been touched during an editing operation. +// Handles PreEditChange/PostEditChange, ReconstructNode, and other +// notifications that need to happen after modifications. +// +// Usage: +// MCPNotifier N; +// MCPFetcher F(Result, N); +// F.Walk(Path)... +// N.PreEdit(); +// // modify stuff +// N.PostEdit(); +// +class MCPNotifier +{ +public: + void PreEditAddObject(UObject* Obj); + void PreEdit(); + void PostEdit(); + +private: + bool bInsidePrePost = false; + TSet TouchedSet; + TArray TouchedArray; +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPProperty.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPProperty.h index 15c1199b..9244f6d7 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPProperty.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPProperty.h @@ -8,7 +8,7 @@ struct MCPProperty { FProperty* Prop = nullptr; - void* ValuePtr = nullptr; + void* Container = nullptr; MCPProperty() = default; MCPProperty(FProperty* InProp, void* Container); @@ -18,4 +18,9 @@ struct MCPProperty explicit operator bool() const { return Prop != nullptr; } FProperty* operator->() const { return Prop; } + + static TArray GetAll(UObject* Obj, EPropertyFlags Flags); + static TArray GetAllSubstring(UObject* Obj, EPropertyFlags Flags, const FString& Substring); + static TArray GetAllExactMatch(UObject* Obj, EPropertyFlags Flags, const FString& Name); + static MCPProperty GetOneExactMatch(UObject* Obj, EPropertyFlags Flags, const FString& Name, MCPErrorCallback Error); };