From 72139e9be5029e521a0b4239ffb4927c96957d2f Mon Sep 17 00:00:00 2001 From: jyelon Date: Sun, 15 Mar 2026 05:30:40 -0400 Subject: [PATCH] Finished implementing the type parser --- .../Source/BlueprintMCP/Private/MCPTypes.cpp | 296 ++++++++++++++++++ .../Source/BlueprintMCP/Public/MCPTypes.h | 50 +-- 2 files changed, 326 insertions(+), 20 deletions(-) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPTypes.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPTypes.cpp index 71d9e648..c1ac1b1a 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPTypes.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPTypes.cpp @@ -1,12 +1,20 @@ #include "MCPTypes.h" +#include "MCPServer.h" #include "Editor.h" #include "EdGraphSchema_K2.h" +#include "Engine/Blueprint.h" #include "UObject/UObjectIterator.h" // --------------------------------------------------------------------------- // Choose Short Name // --------------------------------------------------------------------------- +void UMCPTypes::ReserveShortName(FName Name) +{ + FString NameStr = Name.ToString(); + ShortToPath[NameStr.ToLower()] = TEXT("PRIMITIVE"); +} + FString UMCPTypes::ChooseShortName(UObject* Obj) { if (!Cast(Obj) && !Cast(Obj) && !Cast(Obj)) @@ -134,6 +142,7 @@ FString UMCPTypes::TypeToText(const FEdGraphPinType& PinType) return Inner; } + // --------------------------------------------------------------------------- // Subsystem lifecycle // --------------------------------------------------------------------------- @@ -148,11 +157,23 @@ void UMCPTypes::Initialize(FSubsystemCollectionBase& Collection) Packages.Add(*It); Packages.Sort([](const UPackage& A, const UPackage& B) { return A.GetName() < B.GetName(); }); + // Reserve the short names of the primitives. + ReserveShortName(UEdGraphSchema_K2::PC_Boolean); + ReserveShortName(UEdGraphSchema_K2::PC_Int); + ReserveShortName(UEdGraphSchema_K2::PC_Int64); + ReserveShortName(UEdGraphSchema_K2::PC_Float); + ReserveShortName(UEdGraphSchema_K2::PC_Double); + ReserveShortName(UEdGraphSchema_K2::PC_Byte); + ReserveShortName(UEdGraphSchema_K2::PC_Name); + ReserveShortName(UEdGraphSchema_K2::PC_String); + ReserveShortName(UEdGraphSchema_K2::PC_Text); + // Scan priority packages first, then everything else in sorted order. ChooseShortNames(FindPackage(nullptr, TEXT("/Script/CoreUObject"))); ChooseShortNames(FindPackage(nullptr, TEXT("/Script/Engine"))); ChooseShortNames(FindPackage(nullptr, TEXT("/Script/Integration"))); + // Choose short names for everything else. for (UPackage* Pkg : Packages) ChooseShortNames(Pkg); @@ -164,3 +185,278 @@ void UMCPTypes::Deinitialize() Super::Deinitialize(); } +// --------------------------------------------------------------------------- +// Tokenizer +// --------------------------------------------------------------------------- + +void UMCPTypes::Tokenize(const FString& Input) +{ + Tokens.Empty(); + Cursor = 0; + + int32 i = 0; + while (i < Input.Len()) + { + TCHAR Ch = Input[i]; + + // Skip whitespace. + if (FChar::IsWhitespace(Ch)) + { + ++i; + continue; + } + + // Try to parse an identifier. + int32 Start = i; + while (i < Input.Len() && (FChar::IsAlnum(Input[i]) || Input[i] == '_')) + ++i; + if (i > Start) + { + Tokens.Add(Input.Mid(Start, i - Start)); + continue; + } + + // Anything that's not an identifier or whitespace + // gets classified as a single-character token. + Tokens.Add(FString(1, &Ch)); + ++i; + } +} + +// --------------------------------------------------------------------------- +// Path to Object Conversion +// --------------------------------------------------------------------------- + +bool UMCPTypes::ResolvePath(const FString &Name, const FString &Path, FEdGraphPinType &OutType) +{ + // Load the object. + UObject* Obj = LoadObject(nullptr, *Path); + if (!Obj) + { + Error = FString::Printf(TEXT("Failed to load type '%s' at path '%s'"), *Name, *Path); + return false; + } + + // If it's a blueprint, use its generated class. + if (UBlueprint* BP = Cast(Obj)) + { + Obj = BP->GeneratedClass; + if (!Obj) + { + Error = FString::Printf(TEXT("Blueprint '%s' has no generated class"), *Name); + return false; + } + } + + // Determine the category from the object type. + if (Cast(Obj)) + { + OutType.PinCategory = UEdGraphSchema_K2::PC_Struct; + } + else if (UClass* Class = Cast(Obj)) + { + if (Class->IsChildOf(UInterface::StaticClass())) + OutType.PinCategory = UEdGraphSchema_K2::PC_Interface; + else + OutType.PinCategory = UEdGraphSchema_K2::PC_Object; + } + else if (Cast(Obj)) + { + OutType.PinCategory = UEdGraphSchema_K2::PC_Byte; + } + else + { + // This really shouldn't happen. + Error = FString::Printf(TEXT("'%s' is not a struct, class, enum, or interface"), *Name); + return false; + } + + OutType.PinSubCategoryObject = Obj; + return true; +} + +// --------------------------------------------------------------------------- +// Parsing Types +// --------------------------------------------------------------------------- + +bool UMCPTypes::TokenIs(const TCHAR* Text) const +{ + if (Cursor >= Tokens.Num()) return false; + return Tokens[Cursor].Equals(Text, ESearchCase::IgnoreCase); +} + +bool UMCPTypes::TokenIs(const TCHAR* Text, TCHAR Next) const +{ + if (Cursor >= Tokens.Num() - 1) return false; + return (Tokens[Cursor].Equals(Text, ESearchCase::IgnoreCase)) && + (Tokens[Cursor+1].Len() == 1) && + (Tokens[Cursor+1][0] == Next); +} + +bool UMCPTypes::TokenIs(TCHAR Next) const +{ + if (Cursor >= Tokens.Num()) return false; + return (Tokens[Cursor].Len() == 1) && + (Tokens[Cursor][0] == Next); +} + +bool UMCPTypes::TokenIsID() const +{ + if (Cursor >= Tokens.Num()) return false; + return FChar::IsAlnum(Tokens[Cursor][0]); +} + +bool UMCPTypes::ParseEOF() +{ + if (Cursor != Tokens.Num()) + { + Error = TEXT("Extra tokens at end of input"); + return false; + } + return true; +} + +bool UMCPTypes::ParseChar(TCHAR c) +{ + if (!TokenIs(c)) + { + Error = FString::Printf(TEXT("Expected %c"), c); + return false; + } + Cursor++; + return true; +} + +bool UMCPTypes::ParsePlainIdentifier(FEdGraphPinType& OutType) +{ + if (!TokenIsID()) + { + Error = TEXT("Expected Identifier"); + return false; + } + FString Name = Tokens[Cursor++]; + FString *Path = ShortToPath.Find(Name.ToLower()); + if (Path == nullptr) + { + Error = TEXT("Unrecognized Type"); + return false; + } + + if (*Path == TEXT("PRIMITIVE")) + { + OutType.PinCategory = FName(*Name); + if ((OutType.PinCategory == UEdGraphSchema_K2::PC_Double) || + (OutType.PinCategory == UEdGraphSchema_K2::PC_Float)) + { + OutType.PinSubCategory = OutType.PinCategory; + OutType.PinCategory = UEdGraphSchema_K2::PC_Real; + } + return true; + } + else + { + return ResolvePath(Name, *Path, OutType); + } +} + +bool UMCPTypes::ParseWrapped(FName Wrapper, FEdGraphPinType& OutType) +{ + Cursor++; + if (!ParseChar('<')) return false; + if (!ParsePlainIdentifier(OutType)) return false; + if (!Cast(OutType.PinSubCategoryObject)) + { + Error = FString::Printf(TEXT("%s is not a Class"), *OutType.PinSubCategoryObject->GetName()); + return false; + } + if (!ParseChar('>')) return false; + OutType.PinCategory = Wrapper; + return true; +} + +bool UMCPTypes::ParseMaybeWrapped(FEdGraphPinType& OutType) +{ + if (TokenIs(TEXT("Soft"), '<')) + { + return ParseWrapped(UEdGraphSchema_K2::PC_SoftObject, OutType); + } + else if (TokenIs(TEXT("Class"), '<')) + { + return ParseWrapped(UEdGraphSchema_K2::PC_Class, OutType); + } + else if (TokenIs(TEXT("SoftClass"), '<')) + { + return ParseWrapped(UEdGraphSchema_K2::PC_SoftClass, OutType); + } + else return ParsePlainIdentifier(OutType); +} + +bool UMCPTypes::ParseArrayOrSet(FEdGraphPinType& OutType) +{ + Cursor++; + if (!ParseChar('<')) return false; + if (!ParseMaybeWrapped(OutType)) return false; + if (!ParseChar('>')) return false; + return true; +} + +bool UMCPTypes::ParseMap(FEdGraphPinType& OutType) +{ + Cursor++; + if (!ParseChar('<')) return false; + if (!ParsePlainIdentifier(OutType)) return false; + if (!ParseChar(',')) return false; + FEdGraphPinType ValueType; + if (!ParseMaybeWrapped(ValueType)) return false; + OutType.PinValueType.TerminalCategory = ValueType.PinCategory; + OutType.PinValueType.TerminalSubCategory = ValueType.PinSubCategory; + OutType.PinValueType.TerminalSubCategoryObject = ValueType.PinSubCategoryObject; + if (!ParseChar('>')) return false; + return true; +} + +bool UMCPTypes::ParseType(FEdGraphPinType& OutType) +{ + if (TokenIs(TEXT("Array"), '<')) + { + OutType.ContainerType = EPinContainerType::Array; + if (!ParseArrayOrSet(OutType)) return false; + } + else if (TokenIs(TEXT("Set"), '<')) + { + OutType.ContainerType = EPinContainerType::Set; + if (!ParseArrayOrSet(OutType)) return false; + } + else if (TokenIs(TEXT("Map"), '<')) + { + OutType.ContainerType = EPinContainerType::Map; + if (!ParseMap(OutType)) return false; + } + else + { + if (!ParseMaybeWrapped(OutType)) return false; + } + if (!ParseEOF()) return false; + return true; +} + +FString UMCPTypes::TryTextToType(const FString& Text, FEdGraphPinType& OutPinType) +{ + UMCPTypes* Types = GEditor->GetEditorSubsystem(); + check(Types); + Types->Error.Empty(); + Types->Tokenize(Text); + OutPinType = FEdGraphPinType(); + if (!Types->ParseType(OutPinType)) return Types->Error; + return FString(); +} + +bool UMCPTypes::TextToType(const FString& Text, FEdGraphPinType& OutPinType) +{ + FString Error = TryTextToType(Text, OutPinType); + if (Error.IsEmpty()) return true; + UMCPServer::Print(Error); + return false; +} + + diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPTypes.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPTypes.h index 5bf3088a..a34eb67a 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPTypes.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPTypes.h @@ -7,19 +7,6 @@ // --------------------------------------------------------------------------- // UMCPTypes — converts between FEdGraphPinType and a concise C++-like string. -// -// The text format mirrors GetCPPType where possible: -// bool, uint8, int32, int64, float, double, FName, FString, FText -// FVector, FRotator, FTransform, ... (structs) -// EBlendMode, ... (enums) -// AActor*, UStaticMesh*, ... (object references) -// TSubclassOf (class references) -// TSoftObjectPtr (soft object references) -// TSoftClassPtr (soft class references) -// TScriptInterface (interfaces) -// TArray, TSet, TMap (containers) -// -// TypeToText and TextToType are inverses of each other. // --------------------------------------------------------------------------- UCLASS() @@ -34,22 +21,29 @@ public: // Convert a pin type to a type string. Returns empty string on failure. static FString TypeToText(const FEdGraphPinType& PinType); - // Convert a concise string back to an FEdGraphPinType. - // Returns true on success. Prints error via UMCPServer on failure. - static bool TextToType(const FString& Text, FEdGraphPinType& OutPinType); + // Try to parse a type. If there's a problem, returns an error + // message. If all goes well, returns empty string. + static FString TryTextToType(const FString& Text, FEdGraphPinType& OutPinType); - // Same as TextToType, but does not print an error message on failure. - static bool TryTextToType(const FString& Text, FEdGraphPinType& OutPinType); + // Try to parse a type. If there's a problem, prints an error + // via UMCPServer and returns false. + static bool TextToType(const FString& Text, FEdGraphPinType& OutPinType); private: FString TypeToTextInner(FName Category, FName SubCategory, UObject* SubCategoryObject); + bool ResolvePath(const FString &Name, const FString &Path, FEdGraphPinType &OutType); + + // Reserve the short name for a primitive type. + // The value stored in the map is just "PRIMITIVE". + void ReserveShortName(FName Name); + // Chooses a short name for the specified type, making sure not to // choose the same name for two classes. If the object already has // a short name, return it. If it's not a class, struct, enum, or // interface, return empty string. FString ChooseShortName(UObject* Obj); - + // Choose short names for every type in the package. void ChooseShortNames(UPackage* Package); @@ -57,7 +51,23 @@ private: TMap ShortToPath; // e.g. "vector" -> "/Script/CoreUObject.Vector" TMap PathToShort; // e.g. "/Script/CoreUObject.Vector" -> "Vector" - // Parser state (used by TextToType) + // Tokenizer and parser (used by TextToType) + void Tokenize(const FString& Input); + bool TokenIs(const TCHAR* Text) const; + bool TokenIs(const TCHAR* Text, TCHAR ch) const; + bool TokenIs(TCHAR Ch) const; + bool TokenIsID() const; + + bool ParseEOF(); + bool ParseChar(TCHAR c); + bool ParsePlainIdentifier(FEdGraphPinType& OutType); + bool ParseWrapped(FName Wrapper, FEdGraphPinType& OutType); + bool ParseMaybeWrapped(FEdGraphPinType& OutType); + bool ParseArrayOrSet(FEdGraphPinType& OutType); + bool ParseMap(FEdGraphPinType& OutType); + bool ParseType(FEdGraphPinType& OutType); + TArray Tokens; int32 Cursor = 0; + FString Error; };