Finished implementing the type parser

This commit is contained in:
2026-03-15 05:30:40 -04:00
parent 6b21fb9674
commit 72139e9be5
2 changed files with 326 additions and 20 deletions

View File

@@ -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<UScriptStruct>(Obj) && !Cast<UClass>(Obj) && !Cast<UEnum>(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<UObject>(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<UBlueprint>(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<UScriptStruct>(Obj))
{
OutType.PinCategory = UEdGraphSchema_K2::PC_Struct;
}
else if (UClass* Class = Cast<UClass>(Obj))
{
if (Class->IsChildOf(UInterface::StaticClass()))
OutType.PinCategory = UEdGraphSchema_K2::PC_Interface;
else
OutType.PinCategory = UEdGraphSchema_K2::PC_Object;
}
else if (Cast<UEnum>(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<UClass>(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<UMCPTypes>();
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;
}

View File

@@ -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<AActor> (class references)
// TSoftObjectPtr<AActor> (soft object references)
// TSoftClassPtr<AActor> (soft class references)
// TScriptInterface<IMyInterface> (interfaces)
// TArray<float>, TSet<FName>, TMap<FString, int32> (containers)
//
// TypeToText and TextToType are inverses of each other.
// ---------------------------------------------------------------------------
UCLASS()
@@ -34,16 +21,23 @@ 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
@@ -57,7 +51,23 @@ private:
TMap<FString, FString> ShortToPath; // e.g. "vector" -> "/Script/CoreUObject.Vector"
TMap<FString, FString> 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<FString> Tokens;
int32 Cursor = 0;
FString Error;
};