Better parsing of enums, and Blueprint_Create

This commit is contained in:
2026-03-16 19:22:59 -04:00
parent 46ea58423b
commit 00a2a932b7
8 changed files with 161 additions and 52 deletions

View File

@@ -5,8 +5,10 @@
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "MCPTypes.h"
#include "MCPPackageMaker.h"
#include "Engine/Blueprint.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Blueprint_Create.generated.h"
@@ -21,14 +23,14 @@ class UMCP_Blueprint_Create : public UObject, public IMCPHandler
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Full asset path for the new Blueprint (e.g. '/Game/Blueprints/BP_MyActor')"))
UPROPERTY(meta=(Description="Full asset path for the new Blueprint"))
FString AssetPath;
UPROPERTY(meta=(Description="Parent class: C++ class name or Blueprint package path"))
UPROPERTY(meta=(Description="Parent class, expressed as a type"))
FString ParentClass;
UPROPERTY(meta=(Optional, Description="Blueprint type: Normal, Interface, FunctionLibrary, or MacroLibrary (default: Normal)"))
FString BlueprintType;
UPROPERTY(meta=(Optional, Description="Normal, Interface, FunctionLibrary, or MacroLibrary"))
TEnumAsByte<EBlueprintType> BlueprintType = BPTYPE_Normal;
virtual FString GetDescription() const override
{
@@ -40,35 +42,27 @@ public:
MCPPackageMaker Maker(AssetPath);
if (!Maker.Ok()) return;
// Resolve parent class — try C++ class first, then Blueprint package path
UClass* ParentClassObj = MCPUtils::FindClassByName(ParentClass);
if (!ParentClassObj)
{
MCPFetcher F;
UBlueprint* ParentBP = F.Asset(ParentClass).Cast<UBlueprint>();
if (ParentBP && ParentBP->GeneratedClass)
ParentClassObj = ParentBP->GeneratedClass;
}
if (!ParentClassObj)
{
UMCPServer::Printf(TEXT("ERROR: Could not find parent class '%s'. Provide a C++ class name (e.g. 'Actor', 'Pawn') or Blueprint package path.\n"),
*ParentClass);
return;
}
// Map blueprintType string to EBlueprintType
EBlueprintType BlueprintTypeEnum = BPTYPE_Normal;
if (!BlueprintType.IsEmpty())
{
if (!MCPUtils::StringToEnum(BlueprintType, BlueprintTypeEnum, TEXT("BPTYPE_"))) return;
}
// For Interface type, parent must be UInterface
if ((BlueprintTypeEnum == BPTYPE_Interface) && !ParentClassObj->IsChildOf(UInterface::StaticClass()))
// Resolve parent class based on blueprint type
UClass* ParentClassObj = nullptr;
switch (BlueprintType)
{
case BPTYPE_Normal:
ParentClassObj = UMCPTypes::TextToOneObjectType(ParentClass);
if (!ParentClassObj) return;
break;
case BPTYPE_MacroLibrary:
ParentClassObj = UMCPTypes::TextToOneObjectType(ParentClass);
if (!ParentClassObj) return;
break;
case BPTYPE_Interface:
ParentClassObj = UInterface::StaticClass();
break;
case BPTYPE_FunctionLibrary:
ParentClassObj = UBlueprintFunctionLibrary::StaticClass();
break;
default:
UMCPServer::Print(TEXT("ERROR: BlueprintType must be Normal, Interface, FunctionLibrary, or MacroLibrary\n"));
return;
}
// Create the package and Blueprint
@@ -78,7 +72,7 @@ public:
ParentClassObj,
Maker.Package(),
FName(*Maker.Name()),
BlueprintTypeEnum,
BlueprintType,
UBlueprint::StaticClass(),
UBlueprintGeneratedClass::StaticClass()
);
@@ -95,10 +89,6 @@ public:
// Report result
UMCPServer::Printf(TEXT("Created: %s\n"), *MCPUtils::FormatName(NewBP));
UMCPServer::Printf(TEXT("Parent: %s\n"), *MCPUtils::FormatName(ParentClassObj));
if (!bSaved)
UMCPServer::Print(TEXT("Warning: save failed\n"));
for (UEdGraph* Graph : MCPUtils::AllGraphs(NewBP))
UMCPServer::Printf(TEXT("Graph: %s\n"), *MCPUtils::FormatName(Graph));
if (!bSaved) UMCPServer::Print(TEXT("Warning: save failed\n"));
}
};

View File

@@ -82,5 +82,6 @@ public:
EmitCommandList(true);
UMCPServer::Printf(TEXT("\n"));
MCPFetcher::PrintDocs();
UMCPTypes::PrintDocs();
}
};

View File

@@ -27,15 +27,23 @@ MCPFetcher::WalkFunc MCPFetcher::GetWalker(const FString& Step)
void MCPFetcher::PrintDocs()
{
UMCPServer::Print(TEXT("Some commands take a Path parameter. A Path starts with an asset\n"));
UMCPServer::Print(TEXT("package path (e.g. /Game/Widgets/WB_Hotkeys), followed by zero or\n"));
UMCPServer::Print(TEXT("more comma-separated steps that navigate into the asset:\n\n"));
UMCPServer::Print(TEXT(" graph — Find a named UEdGraph (blank name for material graphs)\n"));
UMCPServer::Print(TEXT(" node — Find a named UEdGraphNode within a graph or blueprint\n"));
UMCPServer::Print(TEXT(" pin — Find a named UEdGraphPin on a node\n"));
UMCPServer::Print(TEXT(" component — Find a named component in a Blueprint's SCS\n"));
UMCPServer::Print(TEXT(" levelblueprint — Get the level blueprint from a UWorld\n"));
UMCPServer::Print(TEXT("\nExample: /Game/Widgets/WB_Hotkeys,graph:EventGraph,node:Self_Reference_03,pin:Result\n"));
UMCPServer::Print(TEXT("Most commands require you to specify a path. A path starts\n"));
UMCPServer::Print(TEXT("with an asset name, followed by comma-separated steps that\n"));
UMCPServer::Print(TEXT("navigate into the asset. Example:\n"));
UMCPServer::Print(TEXT("\n"));
UMCPServer::Print(TEXT(" /Game/Widgets/WB_Hotkeys,graph:EventGraph,node:Self03,pin:Result\n"));
UMCPServer::Print(TEXT("\n"));
UMCPServer::Print(TEXT("The navigation steps supported are:\n"));
UMCPServer::Print(TEXT("\n"));
UMCPServer::Print(TEXT(" graph — move from a blueprint or material to a graph.\n"));
UMCPServer::Print(TEXT(" node — move from a graph to a graph node\n"));
UMCPServer::Print(TEXT(" pin — move from a graph node to a pin\n"));
UMCPServer::Print(TEXT(" component — move from a blueprint to a component\n"));
UMCPServer::Print(TEXT(" levelblueprint — move from a world to a blueprint\n"));
UMCPServer::Print(TEXT("\n"));
UMCPServer::Print(TEXT("It is often useful to use 'dump' commands to see the contents\n"));
UMCPServer::Print(TEXT("of an asset, so you know what objects exist to navigate into.\n"));
UMCPServer::Print(TEXT("\n"));
}

View File

@@ -6,6 +6,7 @@
#include "Materials/MaterialExpression.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "UObject/EnumProperty.h"
static bool IsPinTypeProperty(FProperty* Prop)
{
@@ -26,18 +27,74 @@ FString MCPProperty::GetText() const
return Result;
}
bool MCPProperty::SetText(const FString& Value)
bool MCPProperty::TryParseEnum(UEnum* Enum, const FString& Text, int64 &OutValue)
{
int Index = Enum->GetIndexByNameString(Text);
if (Index == INDEX_NONE)
{
FString Prefix = Enum->GenerateEnumPrefix();
if (!Prefix.IsEmpty())
{
Index = Enum->GetIndexByNameString(Prefix + TEXT("_") + Text);
}
}
if (Index == INDEX_NONE)
{
UMCPServer::Printf(TEXT("ERROR: '%s' is not a valid value for %s\n"),
*Text, *Enum->GetName());
OutValue = 0;
return false;
}
else
{
OutValue = Enum->GetValueByIndex(Index);
return true;
}
}
bool MCPProperty::TrySetText(const FString &Value)
{
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
// Pin types get parsed by UMCPTypes.
if (IsPinTypeProperty(Prop))
return UMCPTypes::TextToType(Value, *static_cast<FEdGraphPinType*>(ValuePtr));
const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, ValuePtr, nullptr, PPF_None);
if (!ImportResult)
// Byte Enum types get parsed by TryParseEnum, above.
if (FByteProperty* ByteProp = CastField<FByteProperty>(Prop))
{
if (UEnum* Enum = ByteProp->Enum)
{
int64 EnumValue;
if (!TryParseEnum(Enum, Value, EnumValue)) return false;
ByteProp->SetPropertyValue(ValuePtr, (uint8)EnumValue);
return true;
}
}
// Regular Enum types get parsed by TryParseEnum, above.
if (FEnumProperty* EnumProp = CastField<FEnumProperty>(Prop))
{
int64 EnumValue;
if (!TryParseEnum(EnumProp->GetEnum(), Value, EnumValue)) return false;
EnumProp->GetUnderlyingProperty()->SetIntPropertyValue(ValuePtr, EnumValue);
return true;
}
// Non-enum properties use ImportText
const TCHAR* Result = Prop->ImportText_Direct(*Value, ValuePtr, nullptr, PPF_None);
if (!Result)
{
UMCPServer::Printf(TEXT("ERROR: Failed to parse '%s' for property '%s' (type: %s)\n"),
*Value, *MCPUtils::FormatName(Prop), *Prop->GetCPPType());
return false;
}
return true;
}
bool MCPProperty::SetText(const FString& Value)
{
if (!TrySetText(Value)) return false;
if (Prop->GetOwnerClass()->IsChildOf(UMaterialExpression::StaticClass()))
{

View File

@@ -494,4 +494,47 @@ bool UMCPTypes::TextToType(const FString& Text, FEdGraphPinType& OutPinType)
return false;
}
UClass* UMCPTypes::TextToOneObjectType(const FString& Text)
{
FEdGraphPinType PinType;
if (!TextToType(Text, PinType)) return nullptr;
UClass* Class = Cast<UClass>(PinType.PinSubCategoryObject.Get());
if ((!Class) || (PinType.PinCategory != UEdGraphSchema_K2::PC_Object) ||
(PinType.IsContainer()))
{
UMCPServer::Printf(TEXT("ERROR: '%s' is not a plain object class\n"), *Text);
return nullptr;
}
return Class;
}
UClass* UMCPTypes::TextToOneInterfaceType(const FString& Text)
{
FEdGraphPinType PinType;
if (!TextToType(Text, PinType)) return nullptr;
UClass* Class = Cast<UClass>(PinType.PinSubCategoryObject.Get());
if ((!Class) || (PinType.PinCategory != UEdGraphSchema_K2::PC_Interface) ||
(PinType.IsContainer()))
{
UMCPServer::Printf(TEXT("ERROR: '%s' is not an interface class\n"), *Text);
return nullptr;
}
return Class;
}
void UMCPTypes::PrintDocs()
{
UMCPServer::Printf(TEXT("To express types, use case-insensitive short names:\n"));
UMCPServer::Printf(TEXT("\n"));
UMCPServer::Printf(TEXT(" boolean, int64, double, string, etc\n"));
UMCPServer::Printf(TEXT(" vector, rotator, hitresult, etc\n"));
UMCPServer::Printf(TEXT(" actor, character, playercontroller, etc\n"));
UMCPServer::Printf(TEXT(" eblendmode, emovementmode, etc\n"));
UMCPServer::Printf(TEXT("\n"));
UMCPServer::Printf(TEXT("Notice that it's 'actor', not 'AActor'.\n"));
UMCPServer::Printf(TEXT("You can use the following notations for complex types:\n"));
UMCPServer::Printf(TEXT("\n"));
UMCPServer::Printf(TEXT(" soft<abp_manny>, class<pawn>, softclass<pawn>\n"));
UMCPServer::Printf(TEXT(" array<int>, set<string>, map<int, string>\n"));
UMCPServer::Printf(TEXT("\n"));
}

View File

@@ -28,5 +28,7 @@ public:
static MCPProperty GetOneExactMatch(UObject* Obj, EPropertyFlags Flags, const FString& Name);
private:
bool TryParseEnum(UEnum* Enum, const FString& Text, int64 &OutValue);
bool TrySetText(const FString &Text);
static void Collect(UStruct* StructType, void* Container, TArray<MCPProperty> &Props, EPropertyFlags Flags);
};

View File

@@ -48,9 +48,6 @@ public:
/** Track an object that has been modified by the current handler. */
static void AddTouchedObject(UObject* Obj) { GMCPServer->Notifier.AddTouchedObject(Obj); }
/** Send notifications now (also called automatically after the handler returns). */
static void SendNotifications() { GMCPServer->Notifier.SendNotifications(); }
/** Print text to the handler output buffer. */
static void Print(const TCHAR* Text) { GMCPServer->HandlerOutput.Append(Text); }
static void Print(const FString& Text) { GMCPServer->HandlerOutput.Append(Text); }

View File

@@ -34,6 +34,17 @@ public:
// via UMCPServer and returns false.
static bool TextToType(const FString& Text, FEdGraphPinType& OutPinType);
// Parse a type string and verify it's a single object class (PC_Object,
// no container, no wrapper). Returns nullptr and prints error on failure.
static UClass* TextToOneObjectType(const FString& Text);
// Parse a type string and verify it's a single interface class (PC_Interface,
// no container, no wrapper). Returns nullptr and prints error on failure.
static UClass* TextToOneInterfaceType(const FString& Text);
// Print the documentation for how types are expressed, for the LLM.
static void PrintDocs();
private:
FString TypeToTextInner(FName Category, FName SubCategory, UObject* SubCategoryObject);