Fix a lot of broken menu stuff in MCP

This commit is contained in:
2026-03-20 14:56:40 -04:00
parent acc5bafe34
commit 0ac9a01859
7 changed files with 142 additions and 167 deletions

View File

@@ -59,6 +59,7 @@ public:
int32 SuccessCount = 0; int32 SuccessCount = 0;
int32 TotalCount = Nodes.Array.Num(); int32 TotalCount = Nodes.Array.Num();
FWingGraphActions GraphActions(TargetGraph);
for (const TSharedPtr<FJsonValue>& NodeVal : Nodes.Array) for (const TSharedPtr<FJsonValue>& NodeVal : Nodes.Array)
{ {
@@ -67,22 +68,21 @@ public:
continue; continue;
// Find the action by exact full name // Find the action by exact full name
FWingGraphActions GA(TargetGraph, Entry.ActionName, 2, /*ExactMatch=*/true); TArray<FWingGraphAction*> Results = GraphActions.Search(Entry.ActionName, 2, true);
if (GA.Count() == 0) if (Results.Num() == 0)
{ {
UWingServer::Printf(TEXT("ERROR: No action found matching '%s'. Use GraphNodeSearchTypes to find available actions.\n"), UWingServer::Printf(TEXT("ERROR: No action found matching '%s'. Use GraphNode_SearchTypes to find available actions.\n"),
*Entry.ActionName); *Entry.ActionName);
continue; continue;
} }
if (GA.Count() > 1) if (Results.Num() > 1)
{ {
UWingServer::Printf(TEXT("ERROR: Ambiguous: %d actions match '%s'.\n"), UWingServer::Printf(TEXT("ERROR: More than one action matches '%s'.\n"), *Entry.ActionName);
GA.Count(), *Entry.ActionName);
continue; continue;
} }
// Perform the action // Perform the action
UEdGraphNode* NewNode = GA.Execute(0, Entry.PosX, Entry.PosY); UEdGraphNode* NewNode = Results[0]->Execute(FVector2D(Entry.PosX, Entry.PosY));
if (!NewNode) if (!NewNode)
{ {
UWingServer::Printf(TEXT("ERROR: Execute returned null for '%s'.\n"), *Entry.ActionName); UWingServer::Printf(TEXT("ERROR: Execute returned null for '%s'.\n"), *Entry.ActionName);

View File

@@ -21,44 +21,42 @@ class UWing_GraphNode_SearchTypes : public UObject, public IWingHandler
GENERATED_BODY() GENERATED_BODY()
public: public:
UPROPERTY(meta=(Description="Search query string")) UPROPERTY(meta=(Description="Query string, can contain *"))
FString Query; FString Query;
UPROPERTY(meta=(Optional, Description="Maximum number of results (default 50, max 500)")) UPROPERTY(meta=(Optional, Description="Maximum number of results (default 50)"))
int32 MaxResults = 50; int32 MaxResults = 50;
UPROPERTY(meta=(Description="Target graph (needed for context-sensitive results)")) UPROPERTY(meta=(Description="Target graph"))
FString Graph; FString Graph;
virtual FString GetDescription() const override virtual FString GetDescription() const override
{ {
return TEXT("Search the action database for node types that can be spawned in a graph. " return TEXT("Search the action database for node types that can be spawned in a graph. "
"Works with any graph type (Blueprint, Material, etc.). " "Works with any graph type (Blueprint, Material, etc.). "
"Returns full action names for use with GraphNodeCreate."); "Returns full action names for use with GraphNode_Create.");
} }
virtual void Handle() override virtual void Handle() override
{ {
int32 ClampedMax = FMath::Clamp(MaxResults, 1, 500);
WingFetcher F; WingFetcher F;
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>(); UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return; if (!TargetGraph) return;
FWingGraphActions GA(TargetGraph, Query, ClampedMax, /*ExactMatch=*/false); FWingGraphActions GraphActions(TargetGraph);
TArray<FWingGraphAction*> Results = GraphActions.Search(Query, MaxResults, false);
for (int32 i = 0; i < GA.Count(); i++) for (const FWingGraphAction* Action : Results)
{ {
UWingServer::Printf(TEXT("%s\n"), *GA.GetFullName(i)); UWingServer::Printf(TEXT("%s\n"), *Action->Name);
} }
if (GA.Count() == 0) if (Results.Num() == 0)
{ {
UWingServer::Print(TEXT("No matching node types found.\n")); UWingServer::Print(TEXT("No matching node types found.\n"));
} }
else if (GA.Count() >= ClampedMax) else if (Results.Num() >= MaxResults)
{ {
UWingServer::Printf(TEXT("WARNING: Reached limit of %d results. Refine your query or increase MaxResults.\n"), ClampedMax); UWingServer::Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults);
} }
} }
}; };

View File

@@ -3,136 +3,101 @@
#include "BlueprintActionDatabase.h" #include "BlueprintActionDatabase.h"
#include "BlueprintNodeSpawner.h" #include "BlueprintNodeSpawner.h"
#include "EdGraphSchema_K2.h" #include "EdGraphSchema_K2.h"
#include "WingUtils.h"
#include "Kismet2/BlueprintEditorUtils.h" #include "Kismet2/BlueprintEditorUtils.h"
FString FWingGraphActions::GetFullName(const FEdGraphSchemaAction& Action) FWingGraphAction::FWingGraphAction(TSharedPtr<FEdGraphSchemaAction> &iAction, UEdGraph *iGraph)
{ {
FString Category = Action.GetCategory().ToString(); Action = iAction;
FString MenuName = Action.GetMenuDescription().ToString(); Graph = iGraph;
return Category + TEXT("|") + MenuName; FString Category = Action->GetCategory().ToString();
FString MenuName = Action->GetMenuDescription().ToString();
Name = WingUtils::StandardizeMenuItem(Category + TEXT("|") + MenuName);
Keywords = Action->GetKeywords().ToString();
} }
FString FWingGraphActions::GetFullName(const UBlueprintNodeSpawner* Spawner) FWingGraphAction::FWingGraphAction(UBlueprintNodeSpawner *iSpawner, UEdGraph *iGraph)
{ {
Spawner = iSpawner;
Graph = iGraph;
const FBlueprintActionUiSpec& UiSpec = Spawner->PrimeDefaultUiSpec(); const FBlueprintActionUiSpec& UiSpec = Spawner->PrimeDefaultUiSpec();
FString Category = UiSpec.Category.ToString(); FString Category = UiSpec.Category.ToString();
FString MenuName = UiSpec.MenuName.ToString(); FString MenuName = UiSpec.MenuName.ToString();
return Category + TEXT("|") + MenuName; Name = WingUtils::StandardizeMenuItem(Category + TEXT("|") + MenuName);
Keywords = Spawner->PrimeDefaultUiSpec().Keywords.ToString();
} }
bool FWingGraphActions::IsMatch(const FEdGraphSchemaAction& Action, const FString &QueryLower, bool Exact) UEdGraphNode* FWingGraphAction::Execute(const FVector2D &Location) const
{ {
FString FullName = GetFullName(Action).ToLower(); if (Spawner)
if (FullName.Len() < 3) return false; {
if (FullName == QueryLower) return true; return Spawner->Invoke(Graph, IBlueprintNodeBinder::FBindingSet(), Location);
if (Exact) return false; }
if (FullName.Contains(QueryLower)) return true; else
FString Keywords = Action.GetKeywords().ToString(); {
if (Keywords.ToLower().Contains(QueryLower)) return true; return Action->PerformAction(Graph, nullptr, Location, /*bSelectNewNode=*/false);
return false; }
} }
bool FWingGraphActions::IsMatch(const UBlueprintNodeSpawner* Spawner, const FString &QueryLower, bool Exact) TArray<FWingGraphAction*> FWingGraphActions::Search(const FString &Query, int32 MaxResults, bool Exact)
{ {
FString FullName = GetFullName(Spawner).ToLower(); FString ExtQuery = FString::Printf(TEXT("*%s*"), *Query);
if (FullName.Len() < 3) return false; TArray<FWingGraphAction*> Results;
if (FullName == QueryLower) return true; for (FWingGraphAction &Result : Actions)
if (Exact) return false; {
if (FullName.Contains(QueryLower)) return true; if (Results.Num() == MaxResults) break;
FString Keywords = Spawner->PrimeDefaultUiSpec().Keywords.ToString(); if (Exact)
if (Keywords.ToLower().Contains(QueryLower)) return true; {
return false; if (Result.Name.Equals(Query, ESearchCase::IgnoreCase))
Results.Emplace(&Result);
}
else
{
if (Result.Name.MatchesWildcard(ExtQuery, ESearchCase::IgnoreCase) ||
Result.Keywords.MatchesWildcard(ExtQuery, ESearchCase::IgnoreCase))
Results.Emplace(&Result);
}
}
return Results;
} }
UEdGraphNode* FWingGraphActions::Execute(FEdGraphSchemaAction& Action, int32 X, int32 Y) void FWingGraphActions::CollectActions()
{ {
FVector2D Location(X, Y);
UEdGraphNode* NewNode = Action.PerformAction(Graph, nullptr, Location, /*bSelectNewNode=*/false);
return NewNode;
}
UEdGraphNode* FWingGraphActions::Execute(UBlueprintNodeSpawner* Spawner, int32 X, int32 Y)
{
FVector2D Location(X, Y);
IBlueprintNodeBinder::FBindingSet Bindings;
return Spawner->Invoke(Graph, Bindings, Location);
}
void FWingGraphActions::CollectActions(UEdGraph* GraphP, const FString& Query, int32 MaxResults, bool ExactMatch)
{
Graph = GraphP;
FString QueryLower = Query.ToLower();
FGraphContextMenuBuilder ContextMenuBuilder(Graph); FGraphContextMenuBuilder ContextMenuBuilder(Graph);
Graph->GetSchema()->GetGraphContextActions(ContextMenuBuilder); Graph->GetSchema()->GetGraphContextActions(ContextMenuBuilder);
for (int32 i = 0; i < ContextMenuBuilder.GetNumActions(); i++) for (int32 i = 0; i < ContextMenuBuilder.GetNumActions(); i++)
{ {
if ((MaxResults > 0) && (Actions.Num() >= MaxResults)) break; Actions.Emplace(ContextMenuBuilder.GetSchemaAction(i), Graph);
TSharedPtr<FEdGraphSchemaAction> Action = ContextMenuBuilder.GetSchemaAction(i);
if (IsMatch(*Action, QueryLower, ExactMatch)) Actions.Add(Action);
} }
} }
void FWingGraphActions::CollectSpawners(UEdGraph* GraphP, const FString& Query, int32 MaxResults, bool ExactMatch) void FWingGraphActions::CollectSpawners()
{ {
Graph = GraphP;
FString QueryLower = Query.ToLower();
for (const auto& Pair : FBlueprintActionDatabase::Get().GetAllActions()) for (const auto& Pair : FBlueprintActionDatabase::Get().GetAllActions())
{ {
for (UBlueprintNodeSpawner* Spawner : Pair.Value) for (UBlueprintNodeSpawner* Spawner : Pair.Value)
{ {
if ((MaxResults > 0) && (Spawners.Num() >= MaxResults)) break;
if (!Spawner) continue;
// Filter by graph compatibility if a graph was provided
if (Spawner->NodeClass) if (Spawner->NodeClass)
{ {
UEdGraphNode* NodeCDO = CastChecked<UEdGraphNode>(Spawner->NodeClass->ClassDefaultObject); UEdGraphNode* NodeCDO = CastChecked<UEdGraphNode>(Spawner->NodeClass->ClassDefaultObject);
if (!NodeCDO->IsCompatibleWithGraph(Graph)) if (NodeCDO->IsCompatibleWithGraph(Graph))
{ {
continue; Actions.Emplace(Spawner, Graph);
} }
} }
if (IsMatch(Spawner, QueryLower, ExactMatch)) Spawners.Add(Spawner);
} }
} }
} }
FString FWingGraphActions::GetFullName(int N) FWingGraphActions::FWingGraphActions(UEdGraph *iGraph)
{
if (N < Actions.Num())
{
return GetFullName(*Actions[N]);
}
else
{
return GetFullName(Spawners[N - Actions.Num()]);
}
}
UEdGraphNode* FWingGraphActions::Execute(int32 N, int32 PosX, int32 PosY)
{
if (N < Actions.Num())
{
return Execute(*Actions[N], PosX, PosY);
}
else
{
return Execute(Spawners[N - Actions.Num()], PosX, PosY);
}
}
FWingGraphActions::FWingGraphActions(UEdGraph *Graph, const FString& Query, int32 MaxResults, bool ExactMatch)
{ {
Graph = iGraph;
if (Cast<UEdGraphSchema_K2>(Graph->GetSchema())) if (Cast<UEdGraphSchema_K2>(Graph->GetSchema()))
{ {
CollectSpawners(Graph, Query, MaxResults, ExactMatch); CollectSpawners();
} }
else else
{ {
CollectActions(Graph, Query, MaxResults, ExactMatch); CollectActions();
} }
} }

View File

@@ -18,32 +18,13 @@
FText WingToolMenu::MakeBetterLabel(const UEdGraphPin *Pin, const FText &EntryLabel) FText WingToolMenu::MakeBetterLabel(const UEdGraphPin *Pin, const FText &EntryLabel)
{ {
FString Sanitized = EntryLabel.ToString(); FString Standardized = WingUtils::StandardizeMenuItem(EntryLabel.ToString());
int32 Dst = 0;
bool Upper = true;
for (int32 Src = 0; Src < Sanitized.Len(); Src++)
{
TCHAR c = Sanitized[Src];
if (FChar::IsAlnum(c))
{
if (Upper) c = FChar::ToUpper(c);
Sanitized[Dst++] = c;
Upper = false;
}
else
{
Upper = true;
if ((c <= 0x20)||(c == 0x7F)) continue;
if (c == ':') c = '-';
Sanitized[Dst++] = c;
}
}
Sanitized.LeftInline(Dst);
if (Pin) if (Pin)
{ {
Sanitized = FString::Printf(TEXT("Pin:%s:%s"), *WingUtils::FormatName(Pin), *Sanitized); Standardized = FString::Printf(TEXT("Pin %s %s"),
*WingUtils::FormatName(Pin), *Standardized);
} }
return FText::FromString(Sanitized); return FText::FromString(Standardized);
} }
// ============================================================ // ============================================================

View File

@@ -87,6 +87,32 @@ FString WingUtils::SanitizeName(FName Name)
return SanitizeName(Name.ToString()); return SanitizeName(Name.ToString());
} }
FString WingUtils::StandardizeMenuItem(const FString &Item)
{
FString Sanitized = Item;
int32 Dst = 0;
bool Upper = true;
for (int32 Src = 0; Src < Sanitized.Len(); Src++)
{
TCHAR c = Sanitized[Src];
if (FChar::IsAlnum(c))
{
if (Upper) c = FChar::ToUpper(c);
Sanitized[Dst++] = c;
Upper = false;
}
else
{
Upper = true;
if ((c <= 0x20)||(c == 0x7F)) continue;
if (c == ':') c = L'';
Sanitized[Dst++] = c;
}
}
Sanitized.LeftInline(Dst);
return Sanitized;
}
// ============================================================ // ============================================================
// Name Lookup // Name Lookup
// ============================================================ // ============================================================

View File

@@ -6,46 +6,40 @@
class UBlueprintNodeSpawner; class UBlueprintNodeSpawner;
struct FEdGraphSchemaAction; struct FEdGraphSchemaAction;
// Holds a collection of graph actions, populated from either the
// BlueprintActionDatabase (for K2 graphs) or GetGraphContextActions struct FWingGraphAction
// (for everything else). {
TSharedPtr<FEdGraphSchemaAction> Action;
UBlueprintNodeSpawner *Spawner = nullptr;
UEdGraph *Graph;
FString Name;
FString Keywords;
FWingGraphAction(TSharedPtr<FEdGraphSchemaAction> &iAction, UEdGraph *iGraph);
FWingGraphAction(UBlueprintNodeSpawner *iSpawner, UEdGraph *iGraph);
UEdGraphNode *Execute(const FVector2D &Location) const;
};
struct FWingGraphActions struct FWingGraphActions
{ {
public: public:
// Constructor populates the list of actions. // Constructor populates the list of actions.
FWingGraphActions(UEdGraph* Graph, const FString& Query, int32 MaxResults = 0, bool ExactMatch=false); FWingGraphActions(UEdGraph* iGraph);
// Get the number of results. // Choose a subset of the actions.
int Count() const { return Spawners.Num() + Actions.Num(); } TArray<FWingGraphAction*> Search(const FString& Query, int32 MaxResults = 0, bool Exact=false);
// Get the name of the nth result.
FString GetFullName(int32 N);
// Execute the nth result, which should create a graph node.
// If it can't, it will print an error and return nullptr.
UEdGraphNode *Execute(int32 N, int32 PosX, int32 PosY);
private: private:
// The Graph that we're generating Actions for. // The Graph that we're generating Actions for.
UEdGraph *Graph; UEdGraph *Graph;
// One of these two will be populated, depending on graph type. // The Array of Actions
TArray<TSharedPtr<FEdGraphSchemaAction>> Actions; TArray<FWingGraphAction> Actions;
TArray<UBlueprintNodeSpawner*> Spawners;
// Get the full name of an action.
FString GetFullName(const FEdGraphSchemaAction& Action);
FString GetFullName(const UBlueprintNodeSpawner* Spawner);
// Compare an action against a query.
bool IsMatch(const FEdGraphSchemaAction& Action, const FString &QueryLower, bool Exact);
bool IsMatch(const UBlueprintNodeSpawner* Spawner, const FString &QueryLower, bool Exact);
// Execute an action
UEdGraphNode* Execute(FEdGraphSchemaAction& Action, int32 X, int32 Y);
UEdGraphNode* Execute(UBlueprintNodeSpawner* Spawner, int32 X, int32 Y);
// Routines that collect actions and spawners. // Routines that collect actions and spawners.
void CollectActions(UEdGraph* GraphP, const FString& Query, int32 MaxResults, bool ExactMatch); void CollectActions();
void CollectSpawners(UEdGraph* GraphP, const FString& Query, int32 MaxResults, bool ExactMatch); void CollectSpawners();
}; };

View File

@@ -165,6 +165,7 @@ public:
static FString SanitizeName(const FString& Name); static FString SanitizeName(const FString& Name);
static FString SanitizeName(FName Name); static FString SanitizeName(FName Name);
//////////////////////////////////////////////////////// ////////////////////////////////////////////////////////
// Our name sanitization routine, above, will turn names // Our name sanitization routine, above, will turn names
// with spaces into names like "Post·Initiate·Action" // with spaces into names like "Post·Initiate·Action"
@@ -178,6 +179,16 @@ public:
//////////////////////////////////////////////////////// ////////////////////////////////////////////////////////
static FString UnsanitizeName(const FString& Name); static FString UnsanitizeName(const FString& Name);
////////////////////////////////////////////////////////
// In Unreal, Menu items tend to be an unpredictable
// mix of CamelCase without spaces, and with spaces.
// In order to make it so that the LLM doesn't have to remember
// which ones have spaces and which ones don't, we standardize
// them all to camelcase without spaces.
////////////////////////////////////////////////////////
static FString StandardizeMenuItem(const FString &Item);
static FString FormatNodeTitle(const UEdGraphNode *Node); static FString FormatNodeTitle(const UEdGraphNode *Node);
// ----- Enum helpers ----- // ----- Enum helpers -----