diff --git a/Content/Characters/Mannequins/Animations/ABP_Manny.uasset b/Content/Characters/Mannequins/Animations/ABP_Manny.uasset index 20afb75b..8c3063a2 100644 --- a/Content/Characters/Mannequins/Animations/ABP_Manny.uasset +++ b/Content/Characters/Mannequins/Animations/ABP_Manny.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1fda14c5cfa75caa7285421412ca22b1283e867a48b7f54da516997dd86b2eb1 -size 401593 +oid sha256:2050194ecf29a0d8857eb5b957eed0fd4b1044a82d12ac9b5c68b1ffbfb5bb58 +size 401925 diff --git a/Content/Luprex/lxPlayerController.uasset b/Content/Luprex/lxPlayerController.uasset index fba3b21c..e54a9fdf 100644 --- a/Content/Luprex/lxPlayerController.uasset +++ b/Content/Luprex/lxPlayerController.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f50c814a9725926c0a54e4e060463ceb1ffcce72c1769886e31669a782c507cb -size 155591 +oid sha256:ea9d4dfcf0c66054ad8a52cb8b7f57aecd27e3bb07550b3988d82289e1da944f +size 152560 diff --git a/Content/Tangibles/TAN_Character.uasset b/Content/Tangibles/TAN_Character.uasset index 497c6e0d..fe08bcdf 100644 --- a/Content/Tangibles/TAN_Character.uasset +++ b/Content/Tangibles/TAN_Character.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6b9c67d63af89f0ddb1624557c85f25bf6ce097e286f8f52f1e558469549581 -size 372097 +oid sha256:06a10b11ff76bacafd20aed5b54ffbd8ef6c1471ac411c5f56bddd617be1a913 +size 371974 diff --git a/Content/Widgets/WB_Hotkeys.uasset b/Content/Widgets/WB_Hotkeys.uasset index 8865beeb..a39c86bf 100644 --- a/Content/Widgets/WB_Hotkeys.uasset +++ b/Content/Widgets/WB_Hotkeys.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27eced69df36e89b5136a4ce2a32de506296e05d5194af0bbc051064a59aaa6f -size 253523 +oid sha256:d1f49b0aee588e2675b41d866fd454b2e03b17d92e220ecd3502228c952594a5 +size 254912 diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp index 4abd7d6b..2013faa6 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp @@ -1,4 +1,5 @@ #include "BlueprintExporter.h" +#include "MCPUtils.h" #include "Engine/Blueprint.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphNode.h" @@ -26,37 +27,6 @@ FlxBlueprintExporter::FlxBlueprintExporter(UEdGraph* InGraph) // //////////////////////////////////////////////////////// -FString FlxBlueprintExporter::SanitizeName(const FString& Title) -{ - FString Result = Title.TrimStartAndEnd().Replace(TEXT(" "), TEXT("_")); - return Result.IsEmpty() ? TEXT("_") : Result; -} - -FString FlxBlueprintExporter::FormatPinType(const FEdGraphPinType& PinType) -{ - if (UObject* SubObj = PinType.PinSubCategoryObject.Get()) - { - return SubObj->GetName(); - } - FString Type = PinType.PinCategory.ToString(); - Type[0] = FChar::ToUpper(Type[0]); - return Type; -} - -FString FlxBlueprintExporter::FormatPinType(UEdGraphPin* Pin) -{ - return FormatPinType(Pin->PinType); -} - -FString FlxBlueprintExporter::FormatNodeBaseName(UEdGraphNode* Node) -{ - FString Title = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(); - int32 NewlineIdx; - if (Title.FindChar(TEXT('\n'), NewlineIdx)) - Title.LeftInline(NewlineIdx); - return SanitizeName(Title); -} - UEdGraphPin* FlxBlueprintExporter::GetLinkedTo(UEdGraphPin* Pin) { while (true) @@ -114,60 +84,6 @@ UEdGraphPin* FlxBlueprintExporter::FindFirstPin(UEdGraphNode* Node, EEdGraphPinD return nullptr; } -UEdGraphPin* FlxBlueprintExporter::BestMatchPin(UEdGraphNode* Node, EEdGraphPinDirection Direction, bool Exec, const FString& Name) -{ - if (Name == TEXT("_")) return nullptr; - - UEdGraphPin* DisplayMatch = nullptr; - int32 DisplayCount = 0; - UEdGraphPin* RawMatch = nullptr; - int32 RawCount = 0; - - for (UEdGraphPin* Pin : Node->Pins) - { - if (Pin->Direction != Direction) continue; - bool PinIsExec = (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec); - if (PinIsExec != Exec) continue; - - if (Name == SanitizeName(Node->GetPinDisplayName(Pin).ToString())) - { - DisplayMatch = Pin; - DisplayCount++; - } - - if (Name == SanitizeName(Pin->PinName.ToString())) - { - RawMatch = Pin; - RawCount++; - } - } - - if (DisplayCount == 1) return DisplayMatch; - if (RawCount == 1) return RawMatch; - return nullptr; -} - -FString FlxBlueprintExporter::FormatPinName(UEdGraphPin *Pin) -{ - UEdGraphNode* Node = Pin->GetOwningNode(); - bool Exec = (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec); - - // Try sanitized display name first. - FString SanitizedDisplay = SanitizeName(Node->GetPinDisplayName(Pin).ToString()); - if (BestMatchPin(Node, Pin->Direction, Exec, SanitizedDisplay) == Pin) - return SanitizedDisplay; - - // Try sanitized raw name. - FString SanitizedRaw = SanitizeName(Pin->PinName.ToString()); - if (BestMatchPin(Node, Pin->Direction, Exec, SanitizedRaw) == Pin) - return SanitizedRaw; - - // No unambiguous name found. - UE_LOG(LogTemp, Warning, TEXT("Blueprint export: ambiguous pin name '%s' on node '%s'"), - *Pin->PinName.ToString(), *Node->GetNodeTitle(ENodeTitleType::ListView).ToString()); - return FString::Printf(TEXT("?%s"), *SanitizedRaw); -} - //////////////////////////////////////////////////////// // @@ -175,12 +91,6 @@ FString FlxBlueprintExporter::FormatPinName(UEdGraphPin *Pin) // //////////////////////////////////////////////////////// -FString FlxBlueprintExporter::FormatNodeName(UEdGraphNode* Node) -{ - FString* Name = NodeNames.Find(Node); - return Name ? *Name : FormatNodeBaseName(Node); -} - FString FlxBlueprintExporter::FormatPinSource(UEdGraphPin* Pin) { // If connected, show source node.pin @@ -191,10 +101,10 @@ FString FlxBlueprintExporter::FormatPinSource(UEdGraphPin* Pin) // For variable get nodes, just show the variable name. if (UK2Node_VariableGet* VarGet = Cast(LinkedToNode)) - return SanitizeName(VarGet->GetVarNameString()); + return MCPUtils::FormatName(VarGet->VariableReference); - FString PinLabel = FormatPinName(LinkedTo); - return FString::Printf(TEXT("%s.%s"), *FormatNodeName(LinkedToNode), *PinLabel); + FString PinLabel = MCPUtils::FormatName(LinkedTo); + return FString::Printf(TEXT("%s.%s"), *MCPUtils::FormatName(LinkedToNode), *PinLabel); } // String pins: always show in quotes (even if empty). @@ -286,16 +196,7 @@ void FlxBlueprintExporter::SortNodes() void FlxBlueprintExporter::AssignNodeNames() { - TMap NextIndex; - - for (UEdGraphNode* Node : SortedNodes) - { - FString Base = FormatNodeBaseName(Node); - int32& Idx = NextIndex.FindOrAdd(Base, 0); - FString Name = (Idx == 0) ? Base : FString::Printf(TEXT("%s_%d"), *Base, Idx + 1); - NodeNames.Add(Node, Name); - Idx++; - } + // Node names are now computed on the fly by FormatNodeName. } void FlxBlueprintExporter::EmitNode(UEdGraphNode* Node) @@ -306,7 +207,7 @@ void FlxBlueprintExporter::EmitNode(UEdGraphNode* Node) return; } - Output.Appendf(TEXT("\nnode %s\n"), *FormatNodeName(Node)); + Output.Appendf(TEXT("\nnode %s\n"), *MCPUtils::FormatName(Node)); // Emit input data pins. for (UEdGraphPin* Pin : FilterPins(Node, EGPD_Input)) @@ -315,8 +216,8 @@ void FlxBlueprintExporter::EmitNode(UEdGraphNode* Node) if (Pin->bHidden) continue; Output.Appendf(TEXT(" input %s %s = %s\n"), - *FormatPinType(Pin), - *FormatPinName(Pin), + *MCPUtils::FormatPinType(Pin), + *MCPUtils::FormatName(Pin), *FormatPinSource(Pin)); } @@ -327,7 +228,7 @@ void FlxBlueprintExporter::EmitNode(UEdGraphNode* Node) if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue; if (Pin->bHidden) continue; if (!ReturnPins.IsEmpty()) ReturnPins += TEXT(", "); - ReturnPins += FormatPinName(Pin); + ReturnPins += MCPUtils::FormatName(Pin); } if (!ReturnPins.IsEmpty()) { @@ -341,12 +242,12 @@ void FlxBlueprintExporter::EmitNode(UEdGraphNode* Node) UEdGraphPin* LinkedTo = GetLinkedTo(Pin); if (!LinkedTo) continue; - FString Target = FormatNodeName(LinkedTo->GetOwningNode()); + FString Target = MCPUtils::FormatName(LinkedTo->GetOwningNode()); if (ExecOuts.Num() == 1) Output.Appendf(TEXT(" goto %s\n"), *Target); else - Output.Appendf(TEXT(" goto-if %s %s\n"), *FormatPinName(Pin), *Target); + Output.Appendf(TEXT(" goto-if %s %s\n"), *MCPUtils::FormatName(Pin), *Target); } } @@ -361,8 +262,8 @@ void FlxBlueprintExporter::EmitLocalVariables() { FString Default = Var.DefaultValue.IsEmpty() ? TEXT("") : Var.DefaultValue; Output.Appendf(TEXT("local %s %s = %s\n"), - *FormatPinType(Var.VarType), - *SanitizeName(Var.VarName.ToString()), + *MCPUtils::FormatPinType(Var.VarType), + *MCPUtils::FormatName(Var), *Default); } break; @@ -387,6 +288,6 @@ void FlxBlueprintExporter::EmitNodeList() if (Node->IsA()) continue; if (Node->IsA()) continue; Details.Appendf(TEXT("%s = %s\n"), - *FormatNodeName(Node), *Node->NodeGuid.ToString()); + *MCPUtils::FormatName(Node), *Node->NodeGuid.ToString()); } } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_GetPinDetails.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_GetPinDetails.h index 53d6ab36..8b994b41 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_GetPinDetails.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_GetPinDetails.h @@ -2,14 +2,10 @@ #include "CoreMinimal.h" #include "MCPHandler.h" -#include "MCPAssetFinder.h" +#include "MCPFetcher.h" #include "MCPUtils.h" -#include "Engine/Blueprint.h" -#include "EdGraph/EdGraph.h" -#include "EdGraph/EdGraphNode.h" #include "EdGraph/EdGraphPin.h" #include "EdGraphSchema_K2.h" -#include "UObject/UObjectIterator.h" #include "UMCPHandler_GetPinDetails.generated.h" @@ -27,14 +23,8 @@ class UMCPHandler_GetPinDetails : public UObject, public IMCPHandler GENERATED_BODY() public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Node to look up (GUID)")) - FString Node; - - UPROPERTY(meta=(Description="Pin name on the node")) - FString PinName; + UPROPERTY(meta=(Description="Path to the pin, e.g. /Game/Widgets/WB_Hotkeys,node:MyNode,pin:Result")) + FString Pin; virtual FString GetDescription() const override { @@ -44,75 +34,47 @@ public: virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override { - MCPAssets Assets; - if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; - UBlueprint* BP = Assets.Object(); + MCPFetcher F(Result); + UEdGraphPin* P = F.Walk(Pin).Cast(); + if (!P) return; - UEdGraph* Graph = nullptr; - UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node, &Graph); - if (!FoundNode) + Result->SetStringField(TEXT("pinName"), P->PinName.ToString()); + Result->SetStringField(TEXT("direction"), P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")); + Result->SetStringField(TEXT("type"), P->PinType.PinCategory.ToString()); + + if (!P->PinType.PinSubCategory.IsNone()) { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node)); + Result->SetStringField(TEXT("subCategory"), P->PinType.PinSubCategory.ToString()); + } + if (P->PinType.PinSubCategoryObject.IsValid()) + { + Result->SetStringField(TEXT("subtype"), P->PinType.PinSubCategoryObject->GetName()); } - UEdGraphPin* Pin = FoundNode->FindPin(FName(*PinName)); - if (!Pin) - { - // List available pins - TArray> AvailPins; - for (UEdGraphPin* P : FoundNode->Pins) - { - if (P) - { - TSharedRef PinObj = MakeShared(); - PinObj->SetStringField(TEXT("name"), P->PinName.ToString()); - PinObj->SetStringField(TEXT("direction"), P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")); - PinObj->SetStringField(TEXT("type"), P->PinType.PinCategory.ToString()); - AvailPins.Add(MakeShared(PinObj)); - } - } - MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *PinName, *Node)); - Result->SetArrayField(TEXT("availablePins"), AvailPins); - return; - } + Result->SetBoolField(TEXT("isArray"), P->PinType.IsArray()); + Result->SetBoolField(TEXT("isSet"), P->PinType.IsSet()); + Result->SetBoolField(TEXT("isMap"), P->PinType.IsMap()); + Result->SetBoolField(TEXT("isReference"), P->PinType.bIsReference); + Result->SetBoolField(TEXT("isConst"), P->PinType.bIsConst); - Result->SetStringField(TEXT("pinName"), Pin->PinName.ToString()); - Result->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")); - Result->SetStringField(TEXT("type"), Pin->PinType.PinCategory.ToString()); - - if (!Pin->PinType.PinSubCategory.IsNone()) + if (!P->DefaultValue.IsEmpty()) { - Result->SetStringField(TEXT("subCategory"), Pin->PinType.PinSubCategory.ToString()); + Result->SetStringField(TEXT("defaultValue"), P->DefaultValue); } - if (Pin->PinType.PinSubCategoryObject.IsValid()) + if (!P->DefaultTextValue.IsEmpty()) { - Result->SetStringField(TEXT("subtype"), Pin->PinType.PinSubCategoryObject->GetName()); + Result->SetStringField(TEXT("defaultTextValue"), P->DefaultTextValue.ToString()); } - - Result->SetBoolField(TEXT("isArray"), Pin->PinType.IsArray()); - Result->SetBoolField(TEXT("isSet"), Pin->PinType.IsSet()); - Result->SetBoolField(TEXT("isMap"), Pin->PinType.IsMap()); - Result->SetBoolField(TEXT("isReference"), Pin->PinType.bIsReference); - Result->SetBoolField(TEXT("isConst"), Pin->PinType.bIsConst); - - if (!Pin->DefaultValue.IsEmpty()) + if (P->DefaultObject) { - Result->SetStringField(TEXT("defaultValue"), Pin->DefaultValue); - } - if (!Pin->DefaultTextValue.IsEmpty()) - { - Result->SetStringField(TEXT("defaultTextValue"), Pin->DefaultTextValue.ToString()); - } - if (Pin->DefaultObject) - { - Result->SetStringField(TEXT("defaultObject"), Pin->DefaultObject->GetPathName()); + Result->SetStringField(TEXT("defaultObject"), P->DefaultObject->GetPathName()); } // Connected pins - if (Pin->LinkedTo.Num() > 0) + if (P->LinkedTo.Num() > 0) { TArray> Conns; - for (UEdGraphPin* Linked : Pin->LinkedTo) + for (UEdGraphPin* Linked : P->LinkedTo) { if (!Linked || !Linked->GetOwningNode()) continue; TSharedRef CJ = MakeShared(); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp new file mode 100644 index 00000000..16c5455e --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp @@ -0,0 +1,239 @@ +#include "MCPFetcher.h" +#include "MCPUtils.h" +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "UObject/PropertyAccessUtil.h" +#include "Engine/SimpleConstructionScript.h" +#include "Engine/SCS_Node.h" +#include "Engine/World.h" +#include "Engine/LevelScriptBlueprint.h" + +MCPFetcher& MCPFetcher::SetError(const FString& Msg) +{ + bError = true; + ErrorCB.SetError(Msg); + return *this; +} + +MCPFetcher& MCPFetcher::TypeMismatch(const TCHAR* Walker, const TCHAR* Expected) +{ + bError = true; + if (ResultPin) + ErrorCB.SetError(FString::Printf(TEXT("Input to '%s' is a pin, expected %s"), Walker, Expected)); + else if (Obj) + ErrorCB.SetError(FString::Printf(TEXT("Input to '%s' is %s, expected %s"), Walker, *Obj->GetClass()->GetName(), Expected)); + else + ErrorCB.SetError(FString::Printf(TEXT("Input to '%s' is null, expected %s"), Walker, Expected)); + return *this; +} + +MCPFetcher& MCPFetcher::Walk(const FString& Path) +{ + if (bError) return *this; + + // Split on commas + TArray Segments; + Path.ParseIntoArray(Segments, TEXT(",")); + if (Segments.Num() == 0) + { + SetError(TEXT("Empty path")); + return *this; + } + + // If no object yet, first segment is an asset path + int32 Start = 0; + if (!Obj && !ResultPin) + { + LoadUAsset(Segments[0]); + if (bError) return *this; + Start = 1; + } + + // Walk each subsequent segment + for (int32 i = Start; i < Segments.Num(); i++) + { + FString Key, Value; + Segments[i].Split(TEXT(":"), &Key, &Value); + + if (StrEq(Key, TEXT("graph"))) Graph(Value); + else if (StrEq(Key, TEXT("node"))) Node(Value); + else if (StrEq(Key, TEXT("pin"))) Pin(Value); + else if (StrEq(Key, TEXT("component"))) Component(Value); + else if (StrEq(Key, TEXT("property"))) Property(Value); + else if (StrEq(Key, TEXT("levelblueprint"))) LevelBlueprint(); + else + { + SetError(FString::Printf(TEXT("Unknown walker '%s'"), *Key)); + return *this; + } + + if (bError) return *this; + } + + return *this; +} + +void MCPFetcher::LoadUAsset(const FString& PackagePath) +{ + SetObj(LoadObject(nullptr, *PackagePath)); + if (!Obj) + SetError(FString::Printf(TEXT("Could not load asset '%s'"), *PackagePath)); +} + +MCPFetcher& MCPFetcher::Graph(const FString& Value) +{ + if (bError) return *this; + + UBlueprint* BP = ::Cast(Obj); + if (!BP) + return TypeMismatch(TEXT("graph"), TEXT("Blueprint")); + + TArray Matches = MCPUtils::AllGraphsNamed(BP, Value); + if (Matches.Num() == 0) + return SetError(FString::Printf(TEXT("Graph '%s' not found in %s"), *Value, *BP->GetName())); + if (Matches.Num() > 1) + return SetError(FString::Printf(TEXT("Ambiguous graph '%s' in %s — %d matches"), *Value, *BP->GetName(), Matches.Num())); + + SetObj(Matches[0]); + return *this; +} + +MCPFetcher& MCPFetcher::Node(const FString& Value) +{ + if (bError) return *this; + + // If current object is a graph, search that graph + if (UEdGraph* G = ::Cast(Obj)) + { + UEdGraphNode* Found = nullptr; + for (UEdGraphNode* N : G->Nodes) + { + if (!N || !MCPUtils::Identifies(Value, N)) + continue; + if (Found) + return SetError(FString::Printf(TEXT("Ambiguous node '%s' in graph %s"), *Value, *G->GetName())); + Found = N; + } + if (!Found) + return SetError(FString::Printf(TEXT("Node '%s' not found in graph %s"), *Value, *G->GetName())); + SetObj(Found); + return *this; + } + + // If current object is a blueprint, search all graphs + if (UBlueprint* BP = ::Cast(Obj)) + { + UEdGraphNode* Found = nullptr; + for (UEdGraph* G : MCPUtils::AllGraphs(BP)) + { + for (UEdGraphNode* N : G->Nodes) + { + if (!N || !MCPUtils::Identifies(Value, N)) + continue; + if (Found) + return SetError(FString::Printf(TEXT("Ambiguous node '%s' in %s"), *Value, *BP->GetName())); + Found = N; + } + } + if (!Found) + return SetError(FString::Printf(TEXT("Node '%s' not found in %s"), *Value, *BP->GetName())); + SetObj(Found); + return *this; + } + + return TypeMismatch(TEXT("node"), TEXT("graph or Blueprint")); +} + +MCPFetcher& MCPFetcher::Pin(const FString& Value) +{ + if (bError) return *this; + + UEdGraphNode* N = ::Cast(Obj); + if (!N) + return TypeMismatch(TEXT("pin"), TEXT("node")); + UEdGraphPin* Found = nullptr; + for (UEdGraphPin *P : N->Pins) + { + if (!MCPUtils::Identifies(Value, P)) + continue; + if (Found) + return SetError(FString::Printf(TEXT("Ambiguous pin '%s' on node %s"), + *Value, *MCPUtils::FormatName(N))); + Found = P; + } + if (!Found) + return SetError(FString::Printf(TEXT("Pin '%s' not found on node %s"), + *Value, *MCPUtils::FormatName(N))); + + SetPin(Found); + return *this; +} + +MCPFetcher& MCPFetcher::Component(const FString& Value) +{ + if (bError) return *this; + + UBlueprint* BP = ::Cast(Obj); + if (!BP) + return TypeMismatch(TEXT("component"), TEXT("Blueprint")); + + USimpleConstructionScript* SCS = BP->SimpleConstructionScript; + if (!SCS) + return SetError(FString::Printf(TEXT("Blueprint %s has no SimpleConstructionScript (not an Actor Blueprint)"), *BP->GetName())); + + FName SearchName(*Value); + for (USCS_Node* SCSNode : SCS->GetAllNodes()) + { + if (SCSNode && SCSNode->GetVariableName() == SearchName) + { + SetObj(SCSNode->ComponentTemplate); + return *this; + } + } + + return SetError(FString::Printf(TEXT("Component '%s' not found in %s"), *Value, *BP->GetName())); +} + +MCPFetcher& MCPFetcher::Property(const FString& Value) +{ + if (bError) return *this; + + if (!Obj) + return TypeMismatch(TEXT("property"), TEXT("UObject")); + + FProperty* Prop = Obj->GetClass()->FindPropertyByName(FName(*Value)); + if (!Prop) + return SetError(FString::Printf(TEXT("Property '%s' not found on %s"), *Value, *Obj->GetClass()->GetName())); + + FObjectProperty* ObjProp = CastField(Prop); + if (!ObjProp) + return SetError(FString::Printf(TEXT("Property '%s' is not an object property (type: %s)"), *Value, *Prop->GetCPPType())); + + UObject* PropValue = ObjProp->GetObjectPropertyValue(Prop->ContainerPtrToValuePtr(Obj)); + if (!PropValue) + return SetError(FString::Printf(TEXT("Property '%s' is null on %s"), *Value, *Obj->GetName())); + + SetObj(PropValue); + return *this; +} + +MCPFetcher& MCPFetcher::LevelBlueprint() +{ + if (bError) return *this; + + UWorld* World = ::Cast(Obj); + if (!World) + return TypeMismatch(TEXT("levelblueprint"), TEXT("World")); + + if (!World->PersistentLevel) + return SetError(TEXT("World has no PersistentLevel")); + + ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(true); + if (!LevelBP) + return SetError(TEXT("World has no level blueprint")); + + SetObj(LevelBP); + return *this; +} diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp index 6e664ebb..3411388e 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp @@ -7,6 +7,9 @@ #include "Serialization/JsonWriter.h" #include "Serialization/JsonSerializer.h" #include "Engine/Blueprint.h" +#include "Engine/MemberReference.h" +#include "Engine/World.h" +#include "Components/ActorComponent.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphNode.h" #include "EdGraph/EdGraphPin.h" @@ -98,6 +101,157 @@ MCPErrorCallback::MCPErrorCallback(FStringBuilderBase& OutResult) : Func([&OutResult](const FString& Msg) { OutResult.Reset(); OutResult.Appendf(TEXT("ERROR: %s\n"), *Msg); }) {} +// ============================================================ +// Name Formatting +// ============================================================ + +void MCPUtils::SanitizeNameInPlace(FString &Name) +{ + int32 Dst = 0; + for (int32 Src = 0; Src < Name.Len(); Src++) + { + TCHAR c = Name[Src]; + if (c <= 0x20 || c == '_' || c == 0x7F) continue; + if (c >= 0x21 && c <= 0x7E && !FChar::IsAlnum(c)) + Name[Dst++] = '_'; + else + Name[Dst++] = c; + } + Name.LeftInline(Dst); + if (Name.IsEmpty()) Name = TEXT("_"); +} + +void MCPUtils::AppendNumericSuffix(FString &Name, int32 N) +{ + if (N > 0) + Name += FString::Printf(TEXT("_%d"), N - 1); +} + +FString MCPUtils::FormatName(UWorld *World) +{ + return World->GetPathName(); +} + +FString MCPUtils::FormatName(UBlueprint *BP) +{ + return BP->GetPathName(); +} + +FString MCPUtils::FormatName(UActorComponent *C) +{ + return C->GetName(); +} + +FString MCPUtils::FormatName(UEdGraph *Graph) +{ + FString Name = Graph->GetName(); + SanitizeNameInPlace(Name); + return Name; +} + +FString MCPUtils::FormatName(UEdGraphNode* Node) +{ + // Sanitized first line of the node title. + FString Title = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(); + int32 NewlineIdx; + if (Title.FindChar(TEXT('\n'), NewlineIdx)) + Title.LeftInline(NewlineIdx); + SanitizeNameInPlace(Title); + AppendNumericSuffix(Title, Node->GetFName().GetNumber()); + return Title; +} + +FString MCPUtils::FormatName(UEdGraphPin *Pin) +{ + FString Name = Pin->PinName.ToString(); + SanitizeNameInPlace(Name); + return Name; +} + +FString MCPUtils::FormatName(const FMemberReference &Ref) +{ + FString Name = Ref.GetMemberName().ToString(); + SanitizeNameInPlace(Name); + return Name; +} + +FString MCPUtils::FormatName(const FBPVariableDescription &Var) +{ + FString Name = Var.VarName.ToString(); + SanitizeNameInPlace(Name); + return Name; +} + +FString MCPUtils::FormatName(const UClass *Class) +{ + FString Name = Class->GetName(); + SanitizeNameInPlace(Name); + return Name; +} + +bool MCPUtils::Identifies(const FString &Name, const UClass *Class) +{ + return FormatName(Class).Equals(Name, ESearchCase::IgnoreCase); +} + +// ============================================================ +// Identifies +// ============================================================ + +bool MCPUtils::Identifies(const FString &Name, UWorld *World) +{ + return FormatName(World).Equals(Name, ESearchCase::IgnoreCase); +} + +bool MCPUtils::Identifies(const FString &Name, UBlueprint *BP) +{ + return FormatName(BP).Equals(Name, ESearchCase::IgnoreCase); +} + +bool MCPUtils::Identifies(const FString &Name, UActorComponent *C) +{ + return FormatName(C).Equals(Name, ESearchCase::IgnoreCase); +} + +bool MCPUtils::Identifies(const FString &Name, UEdGraph *Graph) +{ + return FormatName(Graph).Equals(Name, ESearchCase::IgnoreCase); +} + +bool MCPUtils::Identifies(const FString &Name, UEdGraphNode* Node) +{ + if (Node->NodeGuid.ToString().Equals(Name, ESearchCase::IgnoreCase)) + return true; + return FormatName(Node).Equals(Name, ESearchCase::IgnoreCase); +} + +bool MCPUtils::Identifies(const FString &Name, UEdGraphPin *Pin) +{ + return FormatName(Pin).Equals(Name, ESearchCase::IgnoreCase); +} + +bool MCPUtils::Identifies(const FString &Name, const FMemberReference &Ref) +{ + return FormatName(Ref).Equals(Name, ESearchCase::IgnoreCase); +} + +FString MCPUtils::FormatPinType(const FEdGraphPinType& PinType) +{ + if (UObject* SubObj = PinType.PinSubCategoryObject.Get()) + { + return SubObj->GetName(); + } + FString Type = PinType.PinCategory.ToString(); + Type[0] = FChar::ToUpper(Type[0]); + return Type; +} + +FString MCPUtils::FormatPinType(UEdGraphPin* Pin) +{ + return FormatPinType(Pin->PinType); +} + + // ============================================================ // JSON helpers // ============================================================ @@ -204,7 +358,7 @@ TArray MCPUtils::AllGraphsNamed(UBlueprint* BP, const FString& Name) { TArray Result; for (UEdGraph* Graph : AllGraphs(BP)) - if (Graph->GetName().Equals(Name, ESearchCase::IgnoreCase)) + if (Identifies(Name, Graph)) Result.Add(Graph); return Result; } @@ -221,7 +375,7 @@ TArray> MCPUtils::AllGraphNamesJson(UBlueprint* BP) { TArray> Result; for (UEdGraph* Graph : AllGraphs(BP)) - Result.Add(MakeShared(Graph->GetName())); + Result.Add(MakeShared(FormatName(Graph))); return Result; } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/paths.md b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/paths.md new file mode 100644 index 00000000..5dc31637 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/paths.md @@ -0,0 +1,11 @@ + +Fetches a graph pin: + +/Game/Widgets/WB_Hotkeys,graph:ReadLuaConfiguration,node:Self_Reference_03,pin:Result + +Fetches an skeletal mesh: + +/Game/Tangibles/TAN_Character,component:CharacterMesh0,property:SkeletalMeshAsset + + + diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintExporter.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintExporter.h index ac31de3b..b90256ce 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintExporter.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintExporter.h @@ -21,25 +21,6 @@ private: // //////////////////////////////////////////////////////// - // Sanitize a name: trim whitespace, and replace space - // with underscores. - // - static FString SanitizeName(const FString& Title); - - // Get the pin type as a string. This is lossy, - // we don't differentiate between object reference - // or struct, for example. But that's OK, we don't - // need precise type info, we just need readability. - // - static FString FormatPinType(const FEdGraphPinType& PinType); - static FString FormatPinType(UEdGraphPin* Pin); - - // Get the node base name as a sanitized string. - // Later, we may append a number to this base name - // in order to turn it into a unique string. - // - static FString FormatNodeBaseName(UEdGraphNode* Node); - // Get the pin that this pin is linked to. If the // pin is linked to multiple pins, returns the first. // If the pin is linked to a knot node, follow the @@ -69,32 +50,12 @@ private: // static UEdGraphPin* FindFirstPin(UEdGraphNode* Node, EEdGraphPinDirection Direction); - // Given a sanitized pin display name or a sanitized pin - // name, find the one pin that matches. If the string - // provided doesn't match any pin, or if it matches - // multiple pins ambiguously, returns nullptr. If the - // string is the sanitized empty string, returns nullptr - // even if that matches a pin. - // - static UEdGraphPin* BestMatchPin(UEdGraphNode* Node, EEdGraphPinDirection Direction, bool Exec, const FString& Name); - - // Returns either the sanitized display name or - // sanitized pin name. Chooses the one that - // unambiguously identifies the pin. If neither is - // ambiguous, prefers the display name. If both are - // ambiguous, returns the display name with a question - // mark prepended to indicate that it doesn't uniquely - // identify the pin. - // - static FString FormatPinName(UEdGraphPin *Pin); - //////////////////////////////////////////////////////// // // Traverse and Emit the Nodes. // //////////////////////////////////////////////////////// - FString FormatNodeName(UEdGraphNode* Node); FString FormatPinSource(UEdGraphPin* Pin); void Traverse(UEdGraphNode* Node); void SortNodes(); @@ -114,7 +75,6 @@ private: UEdGraph* Graph; // Data populated by passes. - TMap NodeNames; TArray SortedNodes; TSet Visited; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h new file mode 100644 index 00000000..8bd3b672 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h @@ -0,0 +1,80 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPUtils.h" + +class UEdGraphPin; + +// Resolves a path string into a UObject or UEdGraphPin. +// +// A path starts with a package path (e.g. "/Game/Widgets/WB_Hotkeys"), +// followed by zero or more comma-separated walker segments of the form +// "key:value". Each segment navigates from the current object to a child. +// +// Supported walkers: +// graph:Name - find a named UEdGraph within a UBlueprint +// node:Name - find a named UEdGraphNode within a UEdGraph +// pin:Name - find a named UEdGraphPin on a UEdGraphNode +// component:Name - find a named component in a Blueprint's SCS +// property:Name - follow an object-valued UProperty to its value +// levelblueprint - get the level blueprint from a UWorld +// +// Example paths: +// /Game/Widgets/WB_Hotkeys,graph:ReadLuaConfiguration,node:Self_Reference_03,pin:Result +// /Game/Tangibles/TAN_Character,component:CharacterMesh0,property:SkeletalMeshAsset +// +// Builder-style usage: +// MCPFetcher F(cb); +// if (!F.Walk(Path).Ok()) return; +// +// MCPFetcher F(cb, ExistingObj); +// if (!F.Graph("EventGraph").Node("MyNode").Ok()) return; +// +class MCPFetcher +{ +public: + bool bError = false; + UObject* Obj = nullptr; + UEdGraphPin* ResultPin = nullptr; + MCPErrorCallback ErrorCB = nullptr; + + MCPFetcher(MCPErrorCallback CB) : ErrorCB(CB) {} + MCPFetcher(MCPErrorCallback CB, UObject* O) : Obj(O), ErrorCB(CB) {} + + // Walk one step. + MCPFetcher& Graph(const FString& Value); + MCPFetcher& Node(const FString& Value); + MCPFetcher& Pin(const FString& Value); + MCPFetcher& Component(const FString& Value); + MCPFetcher& Property(const FString& Value); + MCPFetcher& LevelBlueprint(); + + // Parse string and walk multiple steps. + MCPFetcher& Walk(const FString& Path); + + bool Ok() const { return !bError; } + template T *Cast() + { + if (bError) return nullptr; + T* Result = ::Cast(Obj); + if (Result == nullptr) + TypeMismatch(TEXT("Cast"), *T::StaticClass()->GetName()); + return Result; + } + +private: + static bool StrEq(const FString& A, const TCHAR* B) { return A.Equals(B, ESearchCase::IgnoreCase); } + void SetObj(UObject* 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); + void LoadUAsset(const FString& PackagePath); +}; + +template<> inline UEdGraphPin* MCPFetcher::Cast() +{ + if (bError) return nullptr; + if (!ResultPin) + TypeMismatch(TEXT("Cast"), TEXT("pin")); + return ResultPin; +} diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h index a888a831..bcef53e7 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h @@ -14,6 +14,10 @@ class UBlueprintNodeSpawner; class UAnimationStateMachineGraph; class UAnimStateNode; class UAnimStateTransitionNode; +class UActorComponent; +class UWorld; +struct FMemberReference; +struct FBPVariableDescription; // ----- Log capture ----- @@ -87,6 +91,55 @@ struct MCPErrorCallback class MCPUtils { public: + //////////////////////////////////////////////////////// + // + // Name Formatting + // + // The goal here is to centralize the code that outputs + // names, and have everybody use it, so that names are + // used consistently. The secondary goal is to choose + // names that are as uniquely-identifying as is practical. + // It's not always 100% possible to get perfectly unique + // names, though, so our code needs to check for ambiguity. + // + //////////////////////////////////////////////////////// + + static FString FormatName(UWorld *World); + static FString FormatName(UBlueprint *BP); + static FString FormatName(UActorComponent *C); + static FString FormatName(UEdGraph *Graph); + static FString FormatName(UEdGraphNode* Node); + static FString FormatName(UEdGraphPin *Pin); + static FString FormatName(const FMemberReference &Ref); + static FString FormatName(const FBPVariableDescription &Var); + static FString FormatName(const UClass *Class); + + //////////////////////////////////////////////////////// + // + // Identifies + // + // Return true if the name identifies the object. The + // FormatName functions, above, always return names that + // identify the object. However, there may be other + // names that also identify the object. Identifying names + // aren't 100% guaranteed to be unique, but very likely. + // + //////////////////////////////////////////////////////// + + static bool Identifies(const FString &Name, UWorld *World); + static bool Identifies(const FString &Name, UBlueprint *BP); + static bool Identifies(const FString &Name, UActorComponent *C); + static bool Identifies(const FString &Name, UEdGraph *Graph); + static bool Identifies(const FString &Name, UEdGraphNode* Node); + static bool Identifies(const FString &Name, UEdGraphPin *Pin); + static bool Identifies(const FString &Name, const FMemberReference &Ref); + static bool Identifies(const FString &Name, const UClass *Class); + + //////////////////////////////////////////////////////// + + static FString FormatPinType(const FEdGraphPinType& PinType); + static FString FormatPinType(UEdGraphPin* Pin); + // ----- Asset path helpers ----- // Splits "/Game/Foo/Bar" into PackagePath="/Game/Foo" and AssetName="Bar". // Returns false if the path has no slash or the asset name is empty. @@ -210,5 +263,7 @@ public: static void FormatCommandHelp(UClass* HandlerClass, FStringBuilderBase& Result); private: + static void SanitizeNameInPlace(FString& Name); + static void AppendNumericSuffix(FString &Name, int32 N); static FString SetPropertyFromJson(void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json); };