More work on MCP
This commit is contained in:
@@ -54,3 +54,11 @@ is actually fine — but it's a one-way door.
|
||||
recreated during the replacement
|
||||
- **DumpMaterialInstanceParameters** — parent chain lost class
|
||||
type info (Material vs MaterialInstance)
|
||||
|
||||
## Design changes
|
||||
|
||||
- Saving assets is being done at somewhat unpredictable
|
||||
points. It's not entirely clear that we *should* be
|
||||
saving things every time an edit is made. It might be
|
||||
better to have an explicit "Save" MCP command.
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "BlueprintExporter.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "Materials/Material.h"
|
||||
#include "MaterialGraph/MaterialGraph.h"
|
||||
#include "UMCPHandler_DumpGraphs.generated.h"
|
||||
|
||||
|
||||
@@ -20,13 +22,13 @@ class UMCPHandler_DumpGraphs : public UObject, public IMCPHandler
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Path to a blueprint or graph, e.g. /Game/Foo or /Game/Foo,graph:EventGraph"))
|
||||
UPROPERTY(meta=(Description="Path to a blueprint, material, or graph, e.g. /Game/Foo or /Game/Foo,graph:EventGraph"))
|
||||
FString Path;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Dump blueprint graphs as readable text. "
|
||||
"If given a blueprint, dumps all graphs. If given a specific graph, dumps only that one.");
|
||||
return TEXT("Dump blueprint or material graphs as readable text. "
|
||||
"If given a blueprint or material, dumps all graphs. If given a specific graph, dumps only that one.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
||||
@@ -52,7 +54,19 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
Result.Appendf(TEXT("ERROR: Expected a blueprint or graph, got %s\n"),
|
||||
if (UMaterial* Mat = Cast<UMaterial>(F.Obj))
|
||||
{
|
||||
MCPUtils::EnsureMaterialGraph(Mat);
|
||||
if (!Mat->MaterialGraph)
|
||||
{
|
||||
Result.Append(TEXT("ERROR: Could not build MaterialGraph for this material\n"));
|
||||
return;
|
||||
}
|
||||
EmitGraph(Mat->MaterialGraph, Result);
|
||||
return;
|
||||
}
|
||||
|
||||
Result.Appendf(TEXT("ERROR: Expected a blueprint, material, or graph, got %s\n"),
|
||||
*MCPUtils::FormatName(F.Obj->GetClass()));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "Materials/Material.h"
|
||||
#include "Materials/MaterialExpression.h"
|
||||
#include "MaterialGraph/MaterialGraph.h"
|
||||
#include "MaterialGraph/MaterialGraphNode.h"
|
||||
#include "MaterialGraph/MaterialGraphNode_Root.h"
|
||||
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphNode.h"
|
||||
#include "UMCPHandler_DumpMaterialExpressionGraph.generated.h"
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS()
|
||||
class UMCPHandler_DumpMaterialExpressionGraph : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Material name or package path"))
|
||||
FString Material;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Dump the expression graph for a material, showing all nodes and connections as readable text.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
||||
{
|
||||
MCPAssets<UMaterial> Assets;
|
||||
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
||||
UMaterial* Mat = Assets.Object();
|
||||
|
||||
// Ensure the material graph is built (it's created lazily by the material editor)
|
||||
MCPUtils::EnsureMaterialGraph(Mat);
|
||||
if (!Mat->MaterialGraph)
|
||||
{
|
||||
Result.Append(TEXT("ERROR: Could not build MaterialGraph for this material\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
Result.Appendf(TEXT("Material: %s\n"), *MCPUtils::FormatName(Mat));
|
||||
Result.Appendf(TEXT("Expressions: %d\n\n"), Mat->GetExpressions().Num());
|
||||
|
||||
UMaterialGraph* Graph = Mat->MaterialGraph;
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
Result.Appendf(TEXT("--- %s ---\n"), *MCPUtils::FormatName(Node));
|
||||
|
||||
// Show the material expression type for material graph nodes
|
||||
if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Node))
|
||||
{
|
||||
if (MatNode->MaterialExpression)
|
||||
Result.Appendf(TEXT(" Expression: %s\n"), *MCPUtils::FormatName(MatNode->MaterialExpression));
|
||||
}
|
||||
|
||||
// Output pins and their connections
|
||||
for (UEdGraphPin* Pin : Node->Pins)
|
||||
{
|
||||
if (Pin->Direction == EGPD_Output && Pin->LinkedTo.Num() > 0)
|
||||
{
|
||||
for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
|
||||
{
|
||||
Result.Appendf(TEXT(" %s -> %s.%s\n"),
|
||||
*MCPUtils::FormatName(Pin),
|
||||
*MCPUtils::FormatName(LinkedPin->GetOwningNode()),
|
||||
*MCPUtils::FormatName(LinkedPin));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPFetcher.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "UMCPHandler_DumpProperties.generated.h"
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS()
|
||||
class UMCPHandler_DumpProperties : 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=(Optional, Description="Substring filter for property names"))
|
||||
FString Query;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Truncate values to 80 characters (default true)"))
|
||||
bool Truncate = true;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("List all blueprint-visible properties on an object resolved via MCPFetcher path, "
|
||||
"showing current values and which properties are editable.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
||||
{
|
||||
// Resolve the path to an object.
|
||||
MCPFetcher F(Result);
|
||||
UObject* Obj = F.Walk(Path).Cast<UObject>();
|
||||
if (!Obj) return;
|
||||
|
||||
// Get the editable template (e.g. Blueprint → CDO).
|
||||
UObject* Template = MCPUtils::GetEditableTemplate(Obj, Result);
|
||||
if (!Template) return;
|
||||
|
||||
TArray<FProperty*> Props = MCPUtils::BlueprintVisibleProperties(Template, Query);
|
||||
|
||||
UStruct* CurrentOwner = nullptr;
|
||||
for (FProperty* Prop : Props)
|
||||
{
|
||||
FString PropName = MCPUtils::FormatName(Prop);
|
||||
|
||||
// Print section heading when the owning class changes.
|
||||
UStruct* Owner = Prop->GetOwnerStruct();
|
||||
if (Owner != CurrentOwner)
|
||||
{
|
||||
CurrentOwner = Owner;
|
||||
Result.Appendf(TEXT("\nFrom %s:\n\n"), *MCPUtils::FormatName(Owner));
|
||||
}
|
||||
|
||||
FString ValueStr = MCPUtils::GetPropertyValueText(Template, Prop);
|
||||
|
||||
if (Truncate && (ValueStr.Len() > 80))
|
||||
ValueStr = ValueStr.Left(80) + TEXT("...");
|
||||
|
||||
bool bEditable = Prop->HasAnyPropertyFlags(CPF_Edit);
|
||||
Result.Appendf(TEXT(" %s %s %s = %s\n"),
|
||||
bEditable ? TEXT("editable") : TEXT("readonly"),
|
||||
*MCPUtils::FormatPropertyType(Prop),
|
||||
*PropName,
|
||||
*ValueStr);
|
||||
}
|
||||
|
||||
if (Props.IsEmpty())
|
||||
Result.Append(TEXT(" (no blueprint-visible properties found)\n"));
|
||||
}
|
||||
};
|
||||
@@ -20,7 +20,7 @@ class UMCPHandler_ListBlueprintAssets : public UObject, public IMCPHandler
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Optional, Description="Substring filter for blueprint name or path"))
|
||||
FString Filter;
|
||||
FString Query;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Filter by parent class name (exact match, case-insensitive)"))
|
||||
FString ParentClass;
|
||||
@@ -39,7 +39,7 @@ public:
|
||||
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
||||
{
|
||||
MCPAssets<UObject> Assets;
|
||||
Assets.NoScans().Substring(Filter).Limit(500).Errors(Result);
|
||||
Assets.NoScans().Substring(Query).Limit(500).Errors(Result);
|
||||
if (IncludeRegular) Assets.Scan<UBlueprint>();
|
||||
if (IncludeLevel) Assets.Scan<UWorld>();
|
||||
Assets.Info();
|
||||
|
||||
@@ -20,7 +20,7 @@ public:
|
||||
FString ClassName;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Substring filter for property names"))
|
||||
FString Filter;
|
||||
FString Query;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
@@ -46,7 +46,7 @@ public:
|
||||
|
||||
FString PropName = Prop->GetName();
|
||||
|
||||
if (!Filter.IsEmpty() && !PropName.Contains(Filter, ESearchCase::IgnoreCase))
|
||||
if (!Query.IsEmpty() && !PropName.Contains(Query, ESearchCase::IgnoreCase))
|
||||
continue;
|
||||
|
||||
// Build compact flags string
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "Subsystems/AssetEditorSubsystem.h"
|
||||
#include "UMCPHandler_ListOpenAssetEditors.generated.h"
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS()
|
||||
class UMCPHandler_ListOpenAssetEditors : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("List all currently open asset editors, showing which has focus and whether they have unsaved changes.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
||||
{
|
||||
UAssetEditorSubsystem* Sub = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
|
||||
if (!Sub)
|
||||
{
|
||||
Result.Append(TEXT("Error: AssetEditorSubsystem not available\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<UObject*> EditedAssets = Sub->GetAllEditedAssets();
|
||||
if (EditedAssets.IsEmpty())
|
||||
{
|
||||
Result.Append(TEXT("No asset editors are open.\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (UObject* Asset : EditedAssets)
|
||||
{
|
||||
bool bDirty = Asset->GetOutermost()->IsDirty();
|
||||
|
||||
Result.Appendf(TEXT(" %s%s\n"),
|
||||
bDirty ? TEXT("[unsaved] ") : TEXT(""),
|
||||
*Asset->GetPathName());
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPFetcher.h"
|
||||
#include "Subsystems/AssetEditorSubsystem.h"
|
||||
#include "UMCPHandler_OpenAssetEditor.generated.h"
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS()
|
||||
class UMCPHandler_OpenAssetEditor : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="MCPFetcher path to the asset to open (e.g. /Game/Materials/M_Gold)"))
|
||||
FString Path;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Open an asset in its editor and bring it to focus.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
||||
{
|
||||
MCPFetcher F(Result);
|
||||
UObject* Obj = F.Walk(Path).Cast<UObject>();
|
||||
if (!Obj) return;
|
||||
|
||||
UAssetEditorSubsystem* Sub = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
|
||||
if (!Sub)
|
||||
{
|
||||
Result.Append(TEXT("Error: AssetEditorSubsystem not available\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Sub->OpenEditorForAsset(Obj))
|
||||
Result.Appendf(TEXT("Opened editor for %s\n"), *Obj->GetPathName());
|
||||
else
|
||||
Result.Appendf(TEXT("Error: Could not open editor for %s\n"), *Obj->GetPathName());
|
||||
}
|
||||
};
|
||||
@@ -18,7 +18,7 @@ class UMCPHandler_SearchAssets : public UObject, public IMCPHandler
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Optional, Description="Substring to match against asset package paths"))
|
||||
FString Search;
|
||||
FString Query;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Asset class name to filter by, e.g. Blueprint, Material, StaticMesh"))
|
||||
FString Type;
|
||||
@@ -28,14 +28,14 @@ public:
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Search for assets by name and/or type. At least one of Search or Type must be specified.");
|
||||
return TEXT("Search for assets by name and/or type. At least one of Query or Type must be specified.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
||||
{
|
||||
if (Search.IsEmpty() && Type.IsEmpty())
|
||||
if (Query.IsEmpty() && Type.IsEmpty())
|
||||
{
|
||||
Result.Append(TEXT("ERROR: At least one of Search or Type must be specified\n"));
|
||||
Result.Append(TEXT("ERROR: At least one of Query or Type must be specified\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,9 +53,9 @@ public:
|
||||
Assets.NoScans().Scan(TypeClass);
|
||||
}
|
||||
|
||||
if (!Search.IsEmpty())
|
||||
if (!Query.IsEmpty())
|
||||
{
|
||||
Assets.Substring(Search);
|
||||
Assets.Substring(Query);
|
||||
}
|
||||
|
||||
Assets.AllContent().Limit(Limit).Errors(Result).Info();
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPFetcher.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphSchema.h"
|
||||
#include "UMCPHandler_SearchSpawnableNodeTypes.generated.h"
|
||||
|
||||
|
||||
@@ -25,50 +25,36 @@ public:
|
||||
UPROPERTY(meta=(Optional, Description="Maximum number of results (default 50, max 500)"))
|
||||
int32 MaxResults = 50;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Blueprint path. If specified with Graph, only returns nodes compatible with that graph."))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Graph name to filter by compatibility. Requires Blueprint."))
|
||||
UPROPERTY(meta=(Description="MCPFetcher path to a graph, e.g. /Game/Foo,graph:EventGraph or /Game/Materials/M_Gold,graph:"))
|
||||
FString Graph;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Search the Blueprint action database for node spawners matching a query. "
|
||||
"Returns full action names for use with spawn_node. "
|
||||
"Optionally filter by blueprint+graph to only show compatible node types.");
|
||||
return TEXT("Search the action database for node types that can be spawned in a graph. "
|
||||
"Works with any graph type (Blueprint, Material, etc.). "
|
||||
"Returns full action names for use with SpawnNodesInGraph.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
||||
{
|
||||
int32 ClampedMax = FMath::Clamp(MaxResults, 1, 500);
|
||||
|
||||
// Optionally resolve a graph to filter by compatibility
|
||||
UEdGraph* GraphFilter = nullptr;
|
||||
if (!Blueprint.IsEmpty() && !Graph.IsEmpty())
|
||||
MCPFetcher F(Result);
|
||||
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
|
||||
if (!TargetGraph) return;
|
||||
|
||||
TArray<TSharedPtr<FEdGraphSchemaAction>> Actions = MCPUtils::SearchGraphActions(TargetGraph, Query, ClampedMax, /*ExactMatch=*/false);
|
||||
|
||||
for (const TSharedPtr<FEdGraphSchemaAction>& Action : Actions)
|
||||
{
|
||||
MCPFetcher F(Result);
|
||||
F.Walk(Blueprint).Graph(Graph);
|
||||
if (!F.Ok()) return;
|
||||
GraphFilter = Cast<UEdGraph>(F.Obj);
|
||||
if (!GraphFilter)
|
||||
{
|
||||
Result.Appendf(TEXT("ERROR: '%s' is not a graph\n"), *Graph);
|
||||
return;
|
||||
}
|
||||
Result.Appendf(TEXT("%s\n"), *MCPUtils::ActionFullName(Action));
|
||||
}
|
||||
|
||||
TArray<UBlueprintNodeSpawner*> Spawners = MCPUtils::SearchNodeSpawners(Query, ClampedMax, /*ExactMatch=*/false, GraphFilter);
|
||||
|
||||
for (UBlueprintNodeSpawner* Spawner : Spawners)
|
||||
{
|
||||
Result.Appendf(TEXT("%s\n"), *MCPUtils::NodeSpawnerFullName(Spawner));
|
||||
}
|
||||
|
||||
if (Spawners.Num() == 0)
|
||||
if (Actions.Num() == 0)
|
||||
{
|
||||
Result.Append(TEXT("No matching node types found.\n"));
|
||||
}
|
||||
else if (Spawners.Num() >= ClampedMax)
|
||||
else if (Actions.Num() >= ClampedMax)
|
||||
{
|
||||
Result.Appendf(TEXT("WARNING: Reached limit of %d results. Refine your query or increase MaxResults.\n"), ClampedMax);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public:
|
||||
FString TypeName;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Filter to blueprints whose name or path contains this substring"))
|
||||
FString Filter;
|
||||
FString Query;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 200, max 500)"))
|
||||
int32 MaxResults = 0;
|
||||
@@ -54,7 +54,7 @@ public:
|
||||
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
||||
{
|
||||
FString DecodedTypeName = MCPUtils::UrlDecode(TypeName);
|
||||
FString FilterStr = Filter.IsEmpty() ? FString() : MCPUtils::UrlDecode(Filter);
|
||||
FString FilterStr = Query.IsEmpty() ? FString() : MCPUtils::UrlDecode(Query);
|
||||
|
||||
int32 EffectiveMaxResults = (MaxResults > 0) ? FMath::Clamp(MaxResults, 1, 500) : 200;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class UMCPHandler_SearchUnrealClasses : public UObject, public IMCPHandler
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Optional, Description="Substring filter for class names"))
|
||||
FString Filter;
|
||||
FString Query;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Parent class name to restrict results to subclasses"))
|
||||
FString ParentClass;
|
||||
@@ -69,7 +69,7 @@ public:
|
||||
if (ParentClassObj && !Class->IsChildOf(ParentClassObj)) continue;
|
||||
|
||||
FString ClassName = MCPUtils::FormatName(Class);
|
||||
if (!Filter.IsEmpty() && !ClassName.Contains(Filter, ESearchCase::IgnoreCase)) continue;
|
||||
if (!Query.IsEmpty() && !ClassName.Contains(Query, ESearchCase::IgnoreCase)) continue;
|
||||
|
||||
TotalMatched++;
|
||||
if (Matches.Num() < Limit)
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPFetcher.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "UMCPHandler_SetProperties.generated.h"
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS()
|
||||
class UMCPHandler_SetProperties : 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="Object mapping property names to new values in Unreal text format"))
|
||||
FMCPJsonObject Properties;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Set one or more editable properties on an object resolved via MCPFetcher path. "
|
||||
"Properties is a JSON object like {\"TwoSided\": \"true\", \"BlendMode\": \"BLEND_Translucent\"}.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
||||
{
|
||||
// Resolve the path to an object.
|
||||
MCPFetcher F(Result);
|
||||
UObject* Obj = F.Walk(Path).Cast<UObject>();
|
||||
if (!Obj) return;
|
||||
|
||||
// Get the editable template (e.g. Blueprint → CDO).
|
||||
UObject* Template = MCPUtils::GetEditableTemplate(Obj, Result);
|
||||
if (!Template) return;
|
||||
|
||||
if (!Properties.Json || Properties.Json->Values.Num() == 0)
|
||||
{
|
||||
Result.Append(TEXT("Error: No properties specified\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation pass — check all properties before modifying anything.
|
||||
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;
|
||||
}
|
||||
|
||||
FString ValueStr;
|
||||
if (!Pair.Value->TryGetString(ValueStr))
|
||||
{
|
||||
Result.Appendf(TEXT("Error: Value for '%s' must be a string\n"), *Pair.Key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply all changes in a single Pre/PostEditChange bracket.
|
||||
Template->PreEditChange(nullptr);
|
||||
|
||||
int32 SuccessCount = 0;
|
||||
for (const auto& Pair : Properties.Json->Values)
|
||||
{
|
||||
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))
|
||||
continue;
|
||||
|
||||
FString NewValue = MCPUtils::GetPropertyValueText(Template, Prop);
|
||||
Result.Appendf(TEXT("%s: %s -> %s\n"), *MCPUtils::FormatName(Prop), *OldValue, *NewValue);
|
||||
SuccessCount++;
|
||||
}
|
||||
|
||||
Template->PostEditChange();
|
||||
|
||||
// Save.
|
||||
Template->MarkPackageDirty();
|
||||
bool bSaved = MCPUtils::SaveGenericPackage(Obj);
|
||||
|
||||
Result.Appendf(TEXT("Set %d/%d properties.\n"), SuccessCount, Properties.Json->Values.Num());
|
||||
if (!bSaved)
|
||||
Result.Append(TEXT("Warning: Save failed\n"));
|
||||
}
|
||||
};
|
||||
@@ -11,6 +11,9 @@ class UMCPHandler_ShowCommands : public UObject, public IMCPHandler
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Optional, Description="Substring filter for command names"))
|
||||
FString Query;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="If true, return full details including parameter types and descriptions"))
|
||||
bool Verbose = false;
|
||||
|
||||
@@ -21,8 +24,14 @@ public:
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
||||
{
|
||||
FString QueryLower = Query.ToLower();
|
||||
|
||||
for (UClass* Class : MCPUtils::CollectHandlerClasses())
|
||||
{
|
||||
FString ToolName = MCPUtils::GetToolName(Class);
|
||||
if (!ToolName.ToLower().Contains(QueryLower))
|
||||
continue;
|
||||
|
||||
if (Verbose)
|
||||
{
|
||||
MCPUtils::FormatCommandHelp(Class, Result);
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphNode.h"
|
||||
#include "BlueprintNodeSpawner.h"
|
||||
#include "EdGraph/EdGraphSchema.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "UMCPHandler_SpawnNodesInGraph.generated.h"
|
||||
#include "UMCPHandler_SpawnNodes.generated.h"
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -33,7 +33,7 @@ struct FSpawnNodeEntry
|
||||
|
||||
|
||||
UCLASS()
|
||||
class UMCPHandler_SpawnNodesInGraph : public UObject, public IMCPHandler
|
||||
class UMCPHandler_SpawnNodes : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
@@ -41,14 +41,14 @@ public:
|
||||
UPROPERTY(meta=(Description="Path to a graph, e.g. /Game/Foo,graph:EventGraph"))
|
||||
FString Graph;
|
||||
|
||||
UPROPERTY(meta=(Description="Array of {actionName, posX, posY} objects. Use search_spawnable_node_types to find action names."))
|
||||
UPROPERTY(meta=(Description="Array of {actionName, posX, posY} objects. Use SearchSpawnableNodeTypes to find action names."))
|
||||
FMCPJsonArray Nodes;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Create nodes in a Blueprint graph using the editor's action database. "
|
||||
"Can create ANY node type that appears in the editor's right-click menu, including custom K2 nodes. "
|
||||
"Use search_spawnable_node_types first to find the exact action name.");
|
||||
return TEXT("Create nodes in any graph (Blueprint, Material, etc.) using the editor's action database. "
|
||||
"Can create ANY node type that appears in the editor's right-click menu. "
|
||||
"Use SearchSpawnableNodeTypes first to find the exact action name.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
||||
@@ -66,28 +66,27 @@ public:
|
||||
if (!MCPUtils::PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal, Result))
|
||||
continue;
|
||||
|
||||
// Find the spawner by exact full name
|
||||
TArray<UBlueprintNodeSpawner*> Matches = MCPUtils::SearchNodeSpawners(Entry.ActionName, 0, /*ExactMatch=*/true, TargetGraph);
|
||||
// Find the action by exact full name
|
||||
TArray<TSharedPtr<FEdGraphSchemaAction>> Matches = MCPUtils::SearchGraphActions(TargetGraph, Entry.ActionName, 0, /*ExactMatch=*/true);
|
||||
if (Matches.Num() == 0)
|
||||
{
|
||||
Result.Appendf(TEXT("ERROR: No action found matching '%s'. Use search_spawnable_node_types to find available actions.\n"),
|
||||
Result.Appendf(TEXT("ERROR: No action found matching '%s'. Use SearchSpawnableNodeTypes to find available actions.\n"),
|
||||
*Entry.ActionName);
|
||||
continue;
|
||||
}
|
||||
if (Matches.Num() > 1)
|
||||
{
|
||||
Result.Appendf(TEXT("ERROR: Ambiguous: %d spawners match '%s'.\n"),
|
||||
Result.Appendf(TEXT("ERROR: Ambiguous: %d actions match '%s'.\n"),
|
||||
Matches.Num(), *Entry.ActionName);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Invoke the spawner
|
||||
// Perform the action
|
||||
FVector2D Location(Entry.PosX, Entry.PosY);
|
||||
IBlueprintNodeBinder::FBindingSet Bindings;
|
||||
UEdGraphNode* NewNode = Matches[0]->Invoke(TargetGraph, Bindings, Location);
|
||||
UEdGraphNode* NewNode = Matches[0]->PerformAction(TargetGraph, nullptr, Location, /*bSelectNewNode=*/false);
|
||||
if (!NewNode)
|
||||
{
|
||||
Result.Appendf(TEXT("ERROR: Spawner Invoke() returned null for '%s'.\n"), *Entry.ActionName);
|
||||
Result.Appendf(TEXT("ERROR: PerformAction returned null for '%s'.\n"), *Entry.ActionName);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -99,10 +98,17 @@ public:
|
||||
SuccessCount++;
|
||||
}
|
||||
|
||||
// Mark the owning asset as modified
|
||||
UBlueprint* BP = Cast<UBlueprint>(TargetGraph->GetOuter());
|
||||
if (BP)
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
|
||||
TargetGraph->NotifyGraphChanged();
|
||||
|
||||
UObject* Outer = TargetGraph->GetOuter();
|
||||
if (Outer)
|
||||
Outer->MarkPackageDirty();
|
||||
|
||||
Result.Appendf(TEXT("Spawned %d/%d nodes.\n"), SuccessCount, TotalCount);
|
||||
}
|
||||
};
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "Engine/World.h"
|
||||
#include "Engine/Level.h"
|
||||
#include "Engine/LevelScriptBlueprint.h"
|
||||
#include "Materials/Material.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "AssetRegistry/IAssetRegistry.h"
|
||||
@@ -101,11 +102,14 @@ bool MCPAssetsBase::Load()
|
||||
for (const FAssetData &Asset : AssetsToLoad)
|
||||
{
|
||||
UObject *Obj = TryLoadAsset(Asset);
|
||||
if (Obj != nullptr)
|
||||
{
|
||||
AssetResults.Add(Asset);
|
||||
UObjectResults.Add(Obj);
|
||||
}
|
||||
if (!Obj) continue;
|
||||
|
||||
// If this is a material open in the editor, use the editor's transient copy.
|
||||
if (UMaterial* Mat = Cast<UMaterial>(Obj))
|
||||
Obj = MCPUtils::ReplaceMaterialWithTransientCopy(Mat);
|
||||
|
||||
AssetResults.Add(Asset);
|
||||
UObjectResults.Add(Obj);
|
||||
}
|
||||
if (bErrorIfNone && AssetResults.IsEmpty())
|
||||
{
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "Engine/SimpleConstructionScript.h"
|
||||
#include "Engine/SCS_Node.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Materials/Material.h"
|
||||
#include "MaterialGraph/MaterialGraph.h"
|
||||
#include "Engine/LevelScriptBlueprint.h"
|
||||
|
||||
MCPFetcher& MCPFetcher::SetError(const FString& Msg)
|
||||
@@ -55,7 +57,8 @@ MCPFetcher& MCPFetcher::Walk(const FString& Path)
|
||||
for (int32 i = Start; i < Segments.Num(); i++)
|
||||
{
|
||||
FString Key, Value;
|
||||
Segments[i].Split(TEXT(":"), &Key, &Value);
|
||||
if (!Segments[i].Split(TEXT(":"), &Key, &Value))
|
||||
Key = Segments[i];
|
||||
|
||||
if (StrEq(Key, TEXT("graph"))) Graph(Value);
|
||||
else if (StrEq(Key, TEXT("node"))) Node(Value);
|
||||
@@ -80,15 +83,31 @@ void MCPFetcher::LoadUAsset(const FString& PackagePath)
|
||||
SetObj(LoadObject<UObject>(nullptr, *PackagePath));
|
||||
if (!Obj)
|
||||
SetError(FString::Printf(TEXT("Could not load asset '%s'"), *PackagePath));
|
||||
|
||||
// If this is a material open in the editor, use the editor's transient copy.
|
||||
if (UMaterial* Mat = ::Cast<UMaterial>(Obj))
|
||||
SetObj(MCPUtils::ReplaceMaterialWithTransientCopy(Mat));
|
||||
}
|
||||
|
||||
MCPFetcher& MCPFetcher::Graph(const FString& Value)
|
||||
{
|
||||
if (bError) return *this;
|
||||
|
||||
// Material with blank graph name → navigate to the material graph.
|
||||
if (UMaterial* Mat = ::Cast<UMaterial>(Obj))
|
||||
{
|
||||
if (!Value.IsEmpty())
|
||||
return SetError(FString::Printf(TEXT("Materials do not have named graphs (got '%s')"), *Value));
|
||||
MCPUtils::EnsureMaterialGraph(Mat);
|
||||
if (!Mat->MaterialGraph)
|
||||
return SetError(FString::Printf(TEXT("Material '%s' has no material graph"), *Mat->GetName()));
|
||||
SetObj(Mat->MaterialGraph);
|
||||
return *this;
|
||||
}
|
||||
|
||||
UBlueprint* BP = ::Cast<UBlueprint>(Obj);
|
||||
if (!BP)
|
||||
return TypeMismatch(TEXT("graph"), TEXT("Blueprint"));
|
||||
return TypeMismatch(TEXT("graph"), TEXT("Blueprint or Material"));
|
||||
|
||||
TArray<UEdGraph*> Matches = MCPUtils::AllGraphsNamed(BP, Value);
|
||||
if (Matches.Num() == 0)
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
#include "Handlers/UMCPHandler_DisconnectPins.h"
|
||||
#include "Handlers/UMCPHandler_DisconnectMaterialExpressionPin.h"
|
||||
#include "Handlers/UMCPHandler_DumpBlueprint.h"
|
||||
#include "Handlers/UMCPHandler_DumpProperties.h"
|
||||
#include "Handlers/UMCPHandler_DumpGraphs.h"
|
||||
#include "Handlers/UMCPHandler_DumpMaterial.h"
|
||||
#include "Handlers/UMCPHandler_DumpMaterialExpressionGraph.h"
|
||||
#include "Handlers/UMCPHandler_DumpMaterialFunction.h"
|
||||
#include "Handlers/UMCPHandler_DumpMaterialInstanceParameters.h"
|
||||
#include "Handlers/UMCPHandler_DuplicateNodesInGraph.h"
|
||||
@@ -47,10 +47,12 @@
|
||||
#include "Handlers/UMCPHandler_ListAnimSlotNames.h"
|
||||
#include "Handlers/UMCPHandler_ListAnimSyncGroups.h"
|
||||
#include "Handlers/UMCPHandler_ListBlueprintAssets.h"
|
||||
#include "Handlers/UMCPHandler_ListOpenAssetEditors.h"
|
||||
#include "Handlers/UMCPHandler_ListBlueprintComponents.h"
|
||||
#include "Handlers/UMCPHandler_ListBlueprintInterfaces.h"
|
||||
#include "Handlers/UMCPHandler_ListClassProperties.h"
|
||||
#include "Handlers/UMCPHandler_ListEventDispatchers.h"
|
||||
#include "Handlers/UMCPHandler_OpenAssetEditor.h"
|
||||
#include "Handlers/UMCPHandler_RefreshAllNodesInGraph.h"
|
||||
#include "Handlers/UMCPHandler_RemoveAnimStateFromMachine.h"
|
||||
#include "Handlers/UMCPHandler_RemoveBlueprintComponent.h"
|
||||
@@ -81,7 +83,8 @@
|
||||
#include "Handlers/UMCPHandler_SetMaterialInstanceParameter.h"
|
||||
#include "Handlers/UMCPHandler_SetMaterialProperty.h"
|
||||
#include "Handlers/UMCPHandler_SetNodeComment.h"
|
||||
#include "Handlers/UMCPHandler_SetProperties.h"
|
||||
#include "Handlers/UMCPHandler_SetNodePositions.h"
|
||||
#include "Handlers/UMCPHandler_SetPinDefaultValues.h"
|
||||
#include "Handlers/UMCPHandler_ShowCommands.h"
|
||||
#include "Handlers/UMCPHandler_SpawnNodesInGraph.h"
|
||||
#include "Handlers/UMCPHandler_SpawnNodes.h"
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "MCPUtils.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "BlueprintActionDatabase.h"
|
||||
#include "BlueprintNodeSpawner.h"
|
||||
#include "Dom/JsonValue.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
@@ -73,6 +71,8 @@
|
||||
#include "MaterialGraph/MaterialGraph.h"
|
||||
#include "MaterialGraph/MaterialGraphNode.h"
|
||||
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||
#include "IMaterialEditor.h"
|
||||
#include "Subsystems/AssetEditorSubsystem.h"
|
||||
|
||||
// Mesh, animation, texture support
|
||||
#include "Engine/StaticMesh.h"
|
||||
@@ -102,7 +102,7 @@ MCPErrorCallback::MCPErrorCallback(FString& OutError)
|
||||
{}
|
||||
|
||||
MCPErrorCallback::MCPErrorCallback(FStringBuilderBase& OutResult)
|
||||
: Func([&OutResult](const FString& Msg) { OutResult.Reset(); OutResult.Appendf(TEXT("ERROR: %s\n"), *Msg); })
|
||||
: Func([&OutResult](const FString& Msg) { OutResult.Appendf(TEXT("ERROR: %s\n"), *Msg); })
|
||||
{}
|
||||
|
||||
// ============================================================
|
||||
@@ -186,9 +186,9 @@ FString MCPUtils::FormatName(const FBPVariableDescription &Var)
|
||||
return Name;
|
||||
}
|
||||
|
||||
FString MCPUtils::FormatName(const UClass *Class)
|
||||
FString MCPUtils::FormatName(const UStruct *Struct)
|
||||
{
|
||||
FString Name = Class->GetName();
|
||||
FString Name = Struct->GetName();
|
||||
SanitizeNameInPlace(Name);
|
||||
return Name;
|
||||
}
|
||||
@@ -254,9 +254,19 @@ FString MCPUtils::FormatName(const UEnum *Enum)
|
||||
return Name;
|
||||
}
|
||||
|
||||
bool MCPUtils::Identifies(const FString &Name, const UClass *Class)
|
||||
FString MCPUtils::FormatName(const FProperty *Prop)
|
||||
{
|
||||
return FormatName(Class).Equals(Name, ESearchCase::IgnoreCase);
|
||||
return Prop->GetName();
|
||||
}
|
||||
|
||||
bool MCPUtils::Identifies(const FString &Name, const FBPVariableDescription &Var)
|
||||
{
|
||||
return FormatName(Var).Equals(Name, ESearchCase::IgnoreCase);
|
||||
}
|
||||
|
||||
bool MCPUtils::Identifies(const FString &Name, const UStruct *Struct)
|
||||
{
|
||||
return FormatName(Struct).Equals(Name, ESearchCase::IgnoreCase);
|
||||
}
|
||||
|
||||
bool MCPUtils::Identifies(const FString &Name, const UMaterial *Material)
|
||||
@@ -314,6 +324,11 @@ bool MCPUtils::Identifies(const FString &Name, const UEnum *Enum)
|
||||
return FormatName(Enum).Equals(Name, ESearchCase::IgnoreCase);
|
||||
}
|
||||
|
||||
bool MCPUtils::Identifies(const FString &Name, const FProperty *Prop)
|
||||
{
|
||||
return FormatName(Prop).Equals(Name, ESearchCase::IgnoreCase);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Identifies
|
||||
// ============================================================
|
||||
@@ -1167,6 +1182,34 @@ void MCPUtils::EnsureMaterialGraph(UMaterial* Material)
|
||||
}
|
||||
}
|
||||
|
||||
UMaterial* MCPUtils::ReplaceMaterialWithTransientCopy(UMaterial* Material)
|
||||
{
|
||||
if (!Material) return nullptr;
|
||||
|
||||
// Already a preview material — nothing to do.
|
||||
if (Material->GetOutermost() == GetTransientPackage())
|
||||
return Material;
|
||||
|
||||
// If the material editor has a transient preview copy open, get it
|
||||
// via the editor API. This follows the same pattern as Epic's
|
||||
// MaterialEditingLibrary (FindMaterialEditorForAsset).
|
||||
UAssetEditorSubsystem* Sub = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
|
||||
IAssetEditorInstance* EditorInstance = Sub ? Sub->FindEditorForAsset(Material, false) : nullptr;
|
||||
if (EditorInstance)
|
||||
{
|
||||
// This is a weird hack. We know that the IAssetEditorInstance for a material
|
||||
// is always going to be an FMaterialEditor, which conforms to IMaterialEditor.
|
||||
// If that weren't the case, this unsafe code would crash hard. However,
|
||||
// lots of places in unreal use this same unsafe pattern.
|
||||
IMaterialEditor* MatEditor = static_cast<IMaterialEditor*>(EditorInstance);
|
||||
UMaterialInterface* Edited = MatEditor->GetMaterialInterface();
|
||||
if (UMaterial* EditedMat = Cast<UMaterial>(Edited))
|
||||
return EditedMat;
|
||||
}
|
||||
|
||||
return Material;
|
||||
}
|
||||
|
||||
bool MCPUtils::SaveMaterialPackage(UMaterial* Material)
|
||||
{
|
||||
if (!Material) return false;
|
||||
@@ -1392,70 +1435,53 @@ UAnimStateTransitionNode* MCPUtils::FindTransition(UAnimationStateMachineGraph*
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Node spawners
|
||||
// Graph actions (node spawning)
|
||||
// ============================================================
|
||||
|
||||
FString MCPUtils::NodeSpawnerFullName(UBlueprintNodeSpawner* Spawner)
|
||||
#include "EdGraph/EdGraphSchema.h"
|
||||
|
||||
FString MCPUtils::ActionFullName(const TSharedPtr<FEdGraphSchemaAction>& Action)
|
||||
{
|
||||
const FBlueprintActionUiSpec& UiSpec = Spawner->PrimeDefaultUiSpec();
|
||||
FString Category = UiSpec.Category.ToString();
|
||||
FString MenuName = UiSpec.MenuName.ToString();
|
||||
FString Category = Action->GetCategory().ToString();
|
||||
FString MenuName = Action->GetMenuDescription().ToString();
|
||||
if (Category.IsEmpty())
|
||||
{
|
||||
return MenuName;
|
||||
}
|
||||
return Category + TEXT("|") + MenuName;
|
||||
}
|
||||
|
||||
TArray<UBlueprintNodeSpawner*> MCPUtils::SearchNodeSpawners(const FString& Query, int32 MaxResults, bool ExactMatch, UEdGraph* GraphFilter)
|
||||
TArray<TSharedPtr<FEdGraphSchemaAction>> MCPUtils::SearchGraphActions(UEdGraph* Graph, const FString& Query, int32 MaxResults, bool ExactMatch)
|
||||
{
|
||||
FString QueryLower = Query.ToLower();
|
||||
TArray<UBlueprintNodeSpawner*> Result;
|
||||
TArray<TSharedPtr<FEdGraphSchemaAction>> Result;
|
||||
|
||||
for (const auto& Pair : FBlueprintActionDatabase::Get().GetAllActions())
|
||||
FGraphContextMenuBuilder ContextMenuBuilder(Graph);
|
||||
Graph->GetSchema()->GetGraphContextActions(ContextMenuBuilder);
|
||||
|
||||
for (int32 i = 0; i < ContextMenuBuilder.GetNumActions(); i++)
|
||||
{
|
||||
for (UBlueprintNodeSpawner* Spawner : Pair.Value)
|
||||
TSharedPtr<FEdGraphSchemaAction> Action = ContextMenuBuilder.GetSchemaAction(i);
|
||||
if (!Action.IsValid()) continue;
|
||||
|
||||
FString FullName = ActionFullName(Action);
|
||||
if (FullName.IsEmpty()) continue;
|
||||
|
||||
if (ExactMatch)
|
||||
{
|
||||
if (!Spawner) continue;
|
||||
if (Spawner->PrimeDefaultUiSpec().MenuName.IsEmpty()) continue;
|
||||
|
||||
// Filter by graph compatibility if a graph was provided
|
||||
if (GraphFilter && Spawner->NodeClass)
|
||||
{
|
||||
UEdGraphNode* NodeCDO = CastChecked<UEdGraphNode>(Spawner->NodeClass->ClassDefaultObject);
|
||||
if (!NodeCDO->IsCompatibleWithGraph(GraphFilter))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
FString FullName = NodeSpawnerFullName(Spawner);
|
||||
|
||||
if (ExactMatch)
|
||||
{
|
||||
if (FullName.ToLower() != QueryLower)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FString Keywords = Spawner->PrimeDefaultUiSpec().Keywords.ToString();
|
||||
if (!FullName.ToLower().Contains(QueryLower)
|
||||
&& !Keywords.ToLower().Contains(QueryLower))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Result.Add(Spawner);
|
||||
|
||||
if ((MaxResults > 0) && (Result.Num() >= MaxResults))
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (FullName.ToLower() != QueryLower)
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
FString Keywords = Action->GetKeywords().ToString();
|
||||
if (!FullName.ToLower().Contains(QueryLower) && !Keywords.ToLower().Contains(QueryLower))
|
||||
continue;
|
||||
}
|
||||
|
||||
Result.Add(Action);
|
||||
if ((MaxResults > 0) && (Result.Num() >= MaxResults))
|
||||
break;
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
@@ -1734,6 +1760,121 @@ FString MCPUtils::FormatPropertyType(FProperty* Prop)
|
||||
return TEXT("string");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GetEditableTemplate
|
||||
// ============================================================
|
||||
|
||||
UObject* MCPUtils::GetEditableTemplate(UObject* Obj, MCPErrorCallback Error)
|
||||
{
|
||||
if (!Obj)
|
||||
{
|
||||
Error.SetError(TEXT("Object is null"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Blueprint → operate on the Class Default Object.
|
||||
if (UBlueprint* BP = Cast<UBlueprint>(Obj))
|
||||
{
|
||||
if (!BP->GeneratedClass)
|
||||
{
|
||||
Error.SetError(FString::Printf(TEXT("Blueprint '%s' has no GeneratedClass"), *Obj->GetName()));
|
||||
return nullptr;
|
||||
}
|
||||
return BP->GeneratedClass->GetDefaultObject();
|
||||
}
|
||||
|
||||
// These asset types are safe to edit directly.
|
||||
if (Cast<UMaterial>(Obj)) return Obj;
|
||||
if (Cast<UMaterialInstance>(Obj)) return Obj;
|
||||
if (Cast<UStaticMesh>(Obj)) return Obj;
|
||||
if (Cast<USkeletalMesh>(Obj)) return Obj;
|
||||
if (Cast<UTexture>(Obj)) return Obj;
|
||||
if (Cast<UActorComponent>(Obj)) return Obj;
|
||||
|
||||
// Unknown type — refuse for safety.
|
||||
Error.SetError(FString::Printf(TEXT("Object type '%s' is not supported for generic property editing"), *Obj->GetClass()->GetName()));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FindPropertyByName
|
||||
// ============================================================
|
||||
|
||||
FProperty* MCPUtils::FindPropertyByName(UObject* Obj, const FString& Name, MCPErrorCallback Error)
|
||||
{
|
||||
if (!Obj)
|
||||
{
|
||||
Error.SetError(TEXT("Object is null"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FProperty* Found = nullptr;
|
||||
for (TFieldIterator<FProperty> PropIt(Obj->GetClass()); PropIt; ++PropIt)
|
||||
{
|
||||
if (!Identifies(Name, *PropIt)) continue;
|
||||
if (Found)
|
||||
{
|
||||
Error.SetError(FString::Printf(TEXT("Ambiguous property '%s' on %s"), *Name, *FormatName(Obj->GetClass())));
|
||||
return nullptr;
|
||||
}
|
||||
Found = *PropIt;
|
||||
}
|
||||
|
||||
if (!Found)
|
||||
Error.SetError(FString::Printf(TEXT("Property '%s' not found on %s"), *Name, *FormatName(Obj->GetClass())));
|
||||
|
||||
return Found;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GetPropertyValueText
|
||||
// ============================================================
|
||||
|
||||
FString MCPUtils::GetPropertyValueText(UObject* Container, FProperty* Prop)
|
||||
{
|
||||
FString Result;
|
||||
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
|
||||
Prop->ExportTextItem_Direct(Result, ValuePtr, nullptr, Container, PPF_None);
|
||||
return Result;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SetPropertyValueText
|
||||
// ============================================================
|
||||
|
||||
bool MCPUtils::SetPropertyValueText(UObject* Container, FProperty* Prop, const FString& Value, MCPErrorCallback Error)
|
||||
{
|
||||
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
|
||||
const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, ValuePtr, Container, 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;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// BlueprintVisibleProperties
|
||||
// ============================================================
|
||||
|
||||
TArray<FProperty*> MCPUtils::BlueprintVisibleProperties(UObject* Obj, const FString& Query)
|
||||
{
|
||||
TArray<FProperty*> Result;
|
||||
if (!Obj) return Result;
|
||||
for (TFieldIterator<FProperty> PropIt(Obj->GetClass()); PropIt; ++PropIt)
|
||||
{
|
||||
FProperty* Prop = *PropIt;
|
||||
if (!Prop) continue;
|
||||
if (!Prop->HasAnyPropertyFlags(CPF_BlueprintVisible)) continue;
|
||||
if (!Query.IsEmpty() && !FormatName(Prop).Contains(Query, ESearchCase::IgnoreCase))
|
||||
continue;
|
||||
Result.Add(Prop);
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FormatCommandHelp — verbose description of one handler command
|
||||
// ============================================================
|
||||
|
||||
@@ -12,7 +12,7 @@ class UMaterial;
|
||||
class UMaterialInstance;
|
||||
class UMaterialFunction;
|
||||
class UMaterialExpression;
|
||||
class UBlueprintNodeSpawner;
|
||||
struct FEdGraphSchemaAction;
|
||||
class UAnimationStateMachineGraph;
|
||||
class UAnimStateNode;
|
||||
class UAnimStateTransitionNode;
|
||||
@@ -122,7 +122,7 @@ public:
|
||||
static FString FormatName(const UEdGraphPin *Pin);
|
||||
static FString FormatName(const FMemberReference &Ref);
|
||||
static FString FormatName(const FBPVariableDescription &Var);
|
||||
static FString FormatName(const UClass *Class);
|
||||
static FString FormatName(const UStruct *Struct);
|
||||
static FString FormatName(const UMaterial *Material);
|
||||
static FString FormatName(const UMaterialInstance *MaterialInstance);
|
||||
static FString FormatName(const UMaterialFunction *MaterialFunction);
|
||||
@@ -134,6 +134,7 @@ public:
|
||||
static FString FormatName(const UTexture *Texture);
|
||||
static FString FormatName(const UScriptStruct *Struct);
|
||||
static FString FormatName(const UEnum *Enum);
|
||||
static FString FormatName(const FProperty *Prop);
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
//
|
||||
@@ -154,7 +155,8 @@ public:
|
||||
static bool Identifies(const FString &Name, const UEdGraphNode* Node);
|
||||
static bool Identifies(const FString &Name, const UEdGraphPin *Pin);
|
||||
static bool Identifies(const FString &Name, const FMemberReference &Ref);
|
||||
static bool Identifies(const FString &Name, const UClass *Class);
|
||||
static bool Identifies(const FString &Name, const FBPVariableDescription &Var);
|
||||
static bool Identifies(const FString &Name, const UStruct *Struct);
|
||||
static bool Identifies(const FString &Name, const UMaterial *Material);
|
||||
static bool Identifies(const FString &Name, const UMaterialInstance *MaterialInstance);
|
||||
static bool Identifies(const FString &Name, const UMaterialFunction *MaterialFunction);
|
||||
@@ -166,6 +168,7 @@ public:
|
||||
static bool Identifies(const FString &Name, const UTexture *Texture);
|
||||
static bool Identifies(const FString &Name, const UScriptStruct *Struct);
|
||||
static bool Identifies(const FString &Name, const UEnum *Enum);
|
||||
static bool Identifies(const FString &Name, const FProperty *Prop);
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
@@ -265,14 +268,30 @@ public:
|
||||
static bool SaveMaterialPackage(UMaterial* Material);
|
||||
static bool SaveGenericPackage(UObject* Asset);
|
||||
|
||||
// If the material editor has a transient preview copy of this material,
|
||||
// return that copy (which is what the editor is actually working on).
|
||||
// Otherwise return the original.
|
||||
static UMaterial* ReplaceMaterialWithTransientCopy(UMaterial* Material);
|
||||
|
||||
// ----- Anim blueprint helpers -----
|
||||
static UAnimationStateMachineGraph* FindStateMachineGraph(UBlueprint* BP, const FString& GraphName);
|
||||
static UAnimStateNode* FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName, MCPErrorCallback Error);
|
||||
static UAnimStateTransitionNode* FindTransition(UAnimationStateMachineGraph* SMGraph, const FString& FromStateName, const FString& ToStateName);
|
||||
|
||||
// ----- Node spawners -----
|
||||
static FString NodeSpawnerFullName(UBlueprintNodeSpawner* Spawner);
|
||||
static TArray<UBlueprintNodeSpawner*> SearchNodeSpawners(const FString& Query, int32 MaxResults = 0, bool ExactMatch = false, UEdGraph* GraphFilter = nullptr);
|
||||
// ----- Graph actions (node spawning) -----
|
||||
static FString ActionFullName(const TSharedPtr<FEdGraphSchemaAction>& Action);
|
||||
static TArray<TSharedPtr<FEdGraphSchemaAction>> SearchGraphActions(UEdGraph* Graph, const FString& Query, int32 MaxResults = 0, bool ExactMatch = false);
|
||||
|
||||
// ----- Editable template -----
|
||||
// Given an object, returns the appropriate template object for generic
|
||||
// property editing, or nullptr if the type isn't whitelisted.
|
||||
// UBlueprint → CDO; UMaterial, UActorComponent, etc. → as-is.
|
||||
static UObject* GetEditableTemplate(UObject* Obj, MCPErrorCallback Error);
|
||||
static TArray<FProperty*> BlueprintVisibleProperties(UObject* Obj, const FString& Query = FString());
|
||||
|
||||
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);
|
||||
|
||||
// ----- Property population -----
|
||||
static FString PropertyNameToJsonKey(const FString& PropName);
|
||||
|
||||
Reference in New Issue
Block a user