More progress on MCP

This commit is contained in:
2026-03-13 05:34:19 -04:00
parent 303c3fb03a
commit c35cfcd70c
14 changed files with 309 additions and 78 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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<UEdGraphNode*>& 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<UEdGraphPin>();
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,34 +89,28 @@ public:
// -----------------------------------------------------------------------
void HandleMaterialEntry(const FSetNodeDefaultEntry& Entry, UEdGraph* GraphObj,
TSet<UEdGraphNode*>& ModifiedNodes, FStringBuilderBase& Result)
MCPNotifier& N, FStringBuilderBase& Result)
{
MCPFetcher F(Result, GraphObj);
UMaterialGraphNode* MatNode = F.Node(Entry.Node).Cast<UMaterialGraphNode>();
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<UEdGraphNode>();
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);
}
// -----------------------------------------------------------------------
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<UEdGraph>();
if (!GraphObj) return;
@@ -130,9 +124,7 @@ public:
return;
}
TSet<UEdGraphNode*> ModifiedNodes;
GraphFetcher.PreEdit();
N.PreEdit();
for (const TSharedPtr<FJsonValue>& 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"));
}

View File

@@ -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<UObject>();
if (!Template) return;
TArray<FProperty*> Props = MCPUtils::SearchProperties(Template, Query, CPF_Edit, Local);
TArray<MCPProperty> 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<FString, TArray<FProperty*>> ByCategory;
for (FProperty* Prop : Props)
TMap<FString, TArray<MCPProperty>> 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);
}

View File

@@ -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<UObject>();
if (!Template) return;
MCPProperty P = MCPProperty::GetOneExactMatch(Template, CPF_Edit, Property, Result);
if (!P) return;
Result.Append(P.GetText());
Result.Append(TEXT("\n"));
}
};

View File

@@ -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<TPair<MCPProperty, FString>> 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++;
}

View File

@@ -330,3 +330,4 @@ MCPFetcher& MCPFetcher::ToGraph()
return TypeMismatch(TEXT("ToGraph"), TEXT("Graph or Material"));
}

View File

@@ -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<UEdGraphNode*> Nodes;
TSet<UEdGraph*> Graphs;
TSet<UMaterial*> Materials;
TSet<UBlueprint*> Blueprints;
for (int32 i = TouchedArray.Num() - 1; i >= 0; --i)
{
UObject* Obj = TouchedArray[i];
Obj->PostEditChange();
Obj->MarkPackageDirty();
if (UEdGraphNode* Node = ::Cast<UEdGraphNode>(Obj))
Nodes.Add(Node);
if (UEdGraph* Graph = ::Cast<UEdGraph>(Obj))
Graphs.Add(Graph);
if (UBlueprint* BP = ::Cast<UBlueprint>(Obj))
Blueprints.Add(BP);
if (UMaterialInterface* MatIface = ::Cast<UMaterialInterface>(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;
}

View File

@@ -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<void>(Container) : nullptr) {}
MCPProperty::MCPProperty(FProperty* InProp, void* InContainer)
: Prop(InProp), Container(InContainer) {}
FString MCPProperty::GetText() const
{
FString Result;
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
Prop->ExportTextItem_Direct(Result, ValuePtr, nullptr, nullptr, PPF_None);
return Result;
}
bool MCPProperty::SetText(const FString& Value, MCPErrorCallback Error)
{
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(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<UMaterialExpression*>(Container);
Expr->ForcePropertyValueChanged(Prop);
}
return true;
}
TArray<MCPProperty> MCPProperty::GetAll(UObject* Obj, EPropertyFlags Flags)
{
TArray<MCPProperty> Result;
if (!Obj) return Result;
for (TFieldIterator<FProperty> It(Obj->GetClass()); It; ++It)
{
if (Flags != 0 && !It->HasAnyPropertyFlags(Flags)) continue;
Result.Emplace(*It, Obj);
}
if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Obj))
{
if (UMaterialExpression* Expr = MatNode->MaterialExpression)
{
for (TFieldIterator<FProperty> It(Expr->GetClass()); It; ++It)
{
if (Flags != 0 && !It->HasAnyPropertyFlags(Flags)) continue;
Result.Emplace(*It, Expr);
}
}
}
return Result;
}
TArray<MCPProperty> MCPProperty::GetAllSubstring(UObject* Obj, EPropertyFlags Flags, const FString& Substring)
{
TArray<MCPProperty> All = GetAll(Obj, Flags);
if (Substring.IsEmpty()) return All;
TArray<MCPProperty> Result;
for (const MCPProperty& P : All)
{
if (MCPUtils::FormatName(P.Prop).Contains(Substring, ESearchCase::IgnoreCase))
Result.Add(P);
}
return Result;
}
TArray<MCPProperty> MCPProperty::GetAllExactMatch(UObject* Obj, EPropertyFlags Flags, const FString& Name)
{
TArray<MCPProperty> All = GetAll(Obj, Flags);
TArray<MCPProperty> 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<MCPProperty> 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];
}

View File

@@ -810,6 +810,7 @@ void MCPUtils::PreEdit(const TArray<UObject*>& Objects)
void MCPUtils::PostEdit(const TArray<UObject*>& Objects)
{
TSet<UEdGraphNode*> Nodes;
TSet<UEdGraph*> Graphs;
TSet<UMaterial*> Materials;
TSet<UBlueprint*> Blueprints;
@@ -819,6 +820,9 @@ void MCPUtils::PostEdit(const TArray<UObject*>& Objects)
Obj->PostEditChange();
Obj->MarkPackageDirty();
if (UEdGraphNode* Node = Cast<UEdGraphNode>(Obj))
Nodes.Add(Node);
if (UEdGraph* Graph = Cast<UEdGraph>(Obj))
Graphs.Add(Graph);
@@ -829,6 +833,8 @@ void MCPUtils::PostEdit(const TArray<UObject*>& 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)

View File

@@ -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<EditorType*>(Editor);
}
const TArray<UObject*>& Visited() const { return Chain; }
void PreEdit() { MCPUtils::PreEdit(Chain); }
void PostEdit() { MCPUtils::PostEdit(Chain); }
template<class T> 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<UObject*> 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);

View File

@@ -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<UObject*> TouchedSet;
TArray<UObject*> TouchedArray;
};

View File

@@ -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<MCPProperty> GetAll(UObject* Obj, EPropertyFlags Flags);
static TArray<MCPProperty> GetAllSubstring(UObject* Obj, EPropertyFlags Flags, const FString& Substring);
static TArray<MCPProperty> GetAllExactMatch(UObject* Obj, EPropertyFlags Flags, const FString& Name);
static MCPProperty GetOneExactMatch(UObject* Obj, EPropertyFlags Flags, const FString& Name, MCPErrorCallback Error);
};