diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_Create.h b/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_Create.h index 267d20c8..41857514 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_Create.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_Create.h @@ -59,7 +59,8 @@ public: int32 SuccessCount = 0; int32 TotalCount = Nodes.Array.Num(); - + FWingGraphActions GraphActions(TargetGraph); + for (const TSharedPtr& NodeVal : Nodes.Array) { FSpawnNodeEntry Entry; @@ -67,22 +68,21 @@ public: continue; // Find the action by exact full name - FWingGraphActions GA(TargetGraph, Entry.ActionName, 2, /*ExactMatch=*/true); - if (GA.Count() == 0) + TArray Results = GraphActions.Search(Entry.ActionName, 2, true); + 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); continue; } - if (GA.Count() > 1) + if (Results.Num() > 1) { - UWingServer::Printf(TEXT("ERROR: Ambiguous: %d actions match '%s'.\n"), - GA.Count(), *Entry.ActionName); + UWingServer::Printf(TEXT("ERROR: More than one action matches '%s'.\n"), *Entry.ActionName); continue; } // Perform the action - UEdGraphNode* NewNode = GA.Execute(0, Entry.PosX, Entry.PosY); + UEdGraphNode* NewNode = Results[0]->Execute(FVector2D(Entry.PosX, Entry.PosY)); if (!NewNode) { UWingServer::Printf(TEXT("ERROR: Execute returned null for '%s'.\n"), *Entry.ActionName); diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SearchTypes.h b/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SearchTypes.h index 7a8862a8..89121a35 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SearchTypes.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SearchTypes.h @@ -21,44 +21,42 @@ class UWing_GraphNode_SearchTypes : public UObject, public IWingHandler GENERATED_BODY() public: - UPROPERTY(meta=(Description="Search query string")) + UPROPERTY(meta=(Description="Query string, can contain *")) 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; - UPROPERTY(meta=(Description="Target graph (needed for context-sensitive results)")) + UPROPERTY(meta=(Description="Target graph")) FString Graph; virtual FString GetDescription() const override { 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 GraphNodeCreate."); + "Returns full action names for use with GraphNode_Create."); } virtual void Handle() override { - int32 ClampedMax = FMath::Clamp(MaxResults, 1, 500); - WingFetcher F; UEdGraph* TargetGraph = F.Walk(Graph).Cast(); if (!TargetGraph) return; - FWingGraphActions GA(TargetGraph, Query, ClampedMax, /*ExactMatch=*/false); - - for (int32 i = 0; i < GA.Count(); i++) + FWingGraphActions GraphActions(TargetGraph); + TArray Results = GraphActions.Search(Query, MaxResults, false); + 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")); } - 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); } } }; diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingGraphActions.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingGraphActions.cpp index 1925d16b..b26a1eb9 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingGraphActions.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingGraphActions.cpp @@ -3,136 +3,101 @@ #include "BlueprintActionDatabase.h" #include "BlueprintNodeSpawner.h" #include "EdGraphSchema_K2.h" +#include "WingUtils.h" #include "Kismet2/BlueprintEditorUtils.h" -FString FWingGraphActions::GetFullName(const FEdGraphSchemaAction& Action) +FWingGraphAction::FWingGraphAction(TSharedPtr &iAction, UEdGraph *iGraph) { - FString Category = Action.GetCategory().ToString(); - FString MenuName = Action.GetMenuDescription().ToString(); - return Category + TEXT("|") + MenuName; + Action = iAction; + Graph = iGraph; + 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(); FString Category = UiSpec.Category.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 (FullName.Len() < 3) return false; - if (FullName == QueryLower) return true; - if (Exact) return false; - if (FullName.Contains(QueryLower)) return true; - FString Keywords = Action.GetKeywords().ToString(); - if (Keywords.ToLower().Contains(QueryLower)) return true; - return false; -} - -bool FWingGraphActions::IsMatch(const UBlueprintNodeSpawner* Spawner, const FString &QueryLower, bool Exact) -{ - FString FullName = GetFullName(Spawner).ToLower(); - if (FullName.Len() < 3) return false; - if (FullName == QueryLower) return true; - if (Exact) return false; - if (FullName.Contains(QueryLower)) return true; - FString Keywords = Spawner->PrimeDefaultUiSpec().Keywords.ToString(); - if (Keywords.ToLower().Contains(QueryLower)) return true; - return false; -} - -UEdGraphNode* FWingGraphActions::Execute(FEdGraphSchemaAction& Action, int32 X, int32 Y) -{ - 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); - Graph->GetSchema()->GetGraphContextActions(ContextMenuBuilder); - - for (int32 i = 0; i < ContextMenuBuilder.GetNumActions(); i++) + if (Spawner) { - if ((MaxResults > 0) && (Actions.Num() >= MaxResults)) break; - TSharedPtr Action = ContextMenuBuilder.GetSchemaAction(i); - if (IsMatch(*Action, QueryLower, ExactMatch)) Actions.Add(Action); + return Spawner->Invoke(Graph, IBlueprintNodeBinder::FBindingSet(), Location); + } + else + { + return Action->PerformAction(Graph, nullptr, Location, /*bSelectNewNode=*/false); } } -void FWingGraphActions::CollectSpawners(UEdGraph* GraphP, const FString& Query, int32 MaxResults, bool ExactMatch) +TArray FWingGraphActions::Search(const FString &Query, int32 MaxResults, bool Exact) { - Graph = GraphP; - FString QueryLower = Query.ToLower(); + FString ExtQuery = FString::Printf(TEXT("*%s*"), *Query); + TArray Results; + for (FWingGraphAction &Result : Actions) + { + if (Results.Num() == MaxResults) break; + if (Exact) + { + 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; +} +void FWingGraphActions::CollectActions() +{ + FGraphContextMenuBuilder ContextMenuBuilder(Graph); + Graph->GetSchema()->GetGraphContextActions(ContextMenuBuilder); + for (int32 i = 0; i < ContextMenuBuilder.GetNumActions(); i++) + { + Actions.Emplace(ContextMenuBuilder.GetSchemaAction(i), Graph); + } +} + +void FWingGraphActions::CollectSpawners() +{ for (const auto& Pair : FBlueprintActionDatabase::Get().GetAllActions()) { 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) { UEdGraphNode* NodeCDO = CastChecked(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) -{ - 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) +FWingGraphActions::FWingGraphActions(UEdGraph *iGraph) { + Graph = iGraph; if (Cast(Graph->GetSchema())) { - CollectSpawners(Graph, Query, MaxResults, ExactMatch); + CollectSpawners(); } else { - CollectActions(Graph, Query, MaxResults, ExactMatch); + CollectActions(); } } diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingToolMenu.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingToolMenu.cpp index 826c49d5..61345fef 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingToolMenu.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingToolMenu.cpp @@ -18,32 +18,13 @@ FText WingToolMenu::MakeBetterLabel(const UEdGraphPin *Pin, const FText &EntryLabel) { - FString Sanitized = 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); + FString Standardized = WingUtils::StandardizeMenuItem(EntryLabel.ToString()); 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); } // ============================================================ diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp index 1a88d31b..01dabf0d 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp @@ -87,6 +87,32 @@ FString WingUtils::SanitizeName(FName Name) 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 // ============================================================ diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingGraphActions.h b/Plugins/UEWingman/Source/UEWingman/Public/WingGraphActions.h index 4deaaf54..ef2e378d 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingGraphActions.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingGraphActions.h @@ -6,46 +6,40 @@ class UBlueprintNodeSpawner; struct FEdGraphSchemaAction; -// Holds a collection of graph actions, populated from either the -// BlueprintActionDatabase (for K2 graphs) or GetGraphContextActions -// (for everything else). + +struct FWingGraphAction +{ + TSharedPtr Action; + UBlueprintNodeSpawner *Spawner = nullptr; + UEdGraph *Graph; + FString Name; + FString Keywords; + + FWingGraphAction(TSharedPtr &iAction, UEdGraph *iGraph); + FWingGraphAction(UBlueprintNodeSpawner *iSpawner, UEdGraph *iGraph); + + UEdGraphNode *Execute(const FVector2D &Location) const; +}; + + struct FWingGraphActions { public: // Constructor populates the list of actions. - FWingGraphActions(UEdGraph* Graph, const FString& Query, int32 MaxResults = 0, bool ExactMatch=false); + FWingGraphActions(UEdGraph* iGraph); + + // Choose a subset of the actions. + TArray Search(const FString& Query, int32 MaxResults = 0, bool Exact=false); - // Get the number of results. - int Count() const { return Spawners.Num() + Actions.Num(); } - - // 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: // The Graph that we're generating Actions for. UEdGraph *Graph; - // One of these two will be populated, depending on graph type. - TArray> Actions; - TArray 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); + // The Array of Actions + TArray Actions; // Routines that collect actions and spawners. - void CollectActions(UEdGraph* GraphP, const FString& Query, int32 MaxResults, bool ExactMatch); - void CollectSpawners(UEdGraph* GraphP, const FString& Query, int32 MaxResults, bool ExactMatch); + void CollectActions(); + void CollectSpawners(); }; diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h b/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h index 117efdb0..693c7056 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h @@ -165,6 +165,7 @@ public: static FString SanitizeName(const FString& Name); static FString SanitizeName(FName Name); + //////////////////////////////////////////////////////// // Our name sanitization routine, above, will turn names // with spaces into names like "Post·Initiate·Action" @@ -178,6 +179,16 @@ public: //////////////////////////////////////////////////////// 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); // ----- Enum helpers -----