Files
integration/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Variables.cpp

588 lines
20 KiB
C++

#include "BlueprintMCPServer.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphPin.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"
#include "UObject/UObjectIterator.h"
// ============================================================
// HandleChangeVariableType — change a Blueprint member variable's type
// ============================================================
void FBlueprintMCPServer::HandleChangeVariableType(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
FString VariableName = Json->GetStringField(TEXT("variable"));
FString NewTypeName = Json->GetStringField(TEXT("newType"));
FString TypeCategory; // now optional
if (Json->HasField(TEXT("typeCategory")))
{
TypeCategory = Json->GetStringField(TEXT("typeCategory"));
}
if (BlueprintName.IsEmpty() || VariableName.IsEmpty() || NewTypeName.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, variable, newType"));
}
// Load Blueprint
FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MakeErrorJson(Result, LoadError);
}
// Verify variable exists
bool bVarFound = false;
for (const FBPVariableDescription& Var : BP->NewVariables)
{
if (Var.VarName.ToString() == VariableName)
{
bVarFound = true;
break;
}
}
if (!bVarFound)
{
return MakeErrorJson(Result, FString::Printf(TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *BlueprintName));
}
// Build the new pin type using shared resolver
FEdGraphPinType NewPinType;
FString ResolveInput = NewTypeName;
// If typeCategory is an object reference variant, use colon syntax for the resolver
if (TypeCategory == TEXT("object") || TypeCategory == TEXT("softobject") ||
TypeCategory == TEXT("class") || TypeCategory == TEXT("softclass") ||
TypeCategory == TEXT("interface"))
{
ResolveInput = TypeCategory + TEXT(":") + NewTypeName;
}
FString TypeError;
if (!ResolveTypeFromString(ResolveInput, NewPinType, TypeError))
{
return MakeErrorJson(Result, TypeError);
}
// Derive typeCategory from the resolved pin type for the response
if (TypeCategory.IsEmpty())
{
if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Struct)
TypeCategory = TEXT("struct");
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Enum || NewPinType.PinCategory == UEdGraphSchema_K2::PC_Byte)
TypeCategory = TEXT("enum");
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Object)
TypeCategory = TEXT("object");
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_SoftObject)
TypeCategory = TEXT("softobject");
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Class)
TypeCategory = TEXT("class");
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_SoftClass)
TypeCategory = TEXT("softclass");
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Interface)
TypeCategory = TEXT("interface");
else
TypeCategory = NewPinType.PinCategory.ToString();
}
// Check for dry run
bool bDryRun = false;
if (Json->HasField(TEXT("dryRun")))
{
bDryRun = Json->GetBoolField(TEXT("dryRun"));
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s variable '%s' in '%s' to %s (%s)"),
bDryRun ? TEXT("[DRY RUN] Analyzing change of") : TEXT("Changing"),
*VariableName, *BlueprintName, *NewTypeName, *TypeCategory);
// Analyze affected nodes (get/set nodes for this variable)
TArray<TSharedPtr<FJsonValue>> AffectedNodes;
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
if (!Graph) continue;
for (UEdGraphNode* Node : Graph->Nodes)
{
if (!Node) continue;
if (auto* VG = Cast<UK2Node_VariableGet>(Node))
{
if (VG->GetVarName().ToString() == VariableName)
{
TSharedRef<FJsonObject> AffNode = MakeShared<FJsonObject>();
AffNode->SetStringField(TEXT("nodeId"), VG->NodeGuid.ToString());
AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableGet"));
AffNode->SetStringField(TEXT("graph"), Graph->GetName());
// Check which pins would be affected
TArray<TSharedPtr<FJsonValue>> AffPins;
for (UEdGraphPin* Pin : VG->Pins)
{
if (Pin && Pin->LinkedTo.Num() > 0 && Pin->Direction == EGPD_Output)
{
AffPins.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("%s (connected to %d pin(s))"),
*Pin->PinName.ToString(), Pin->LinkedTo.Num())));
}
}
AffNode->SetArrayField(TEXT("affectedPins"), AffPins);
AffectedNodes.Add(MakeShared<FJsonValueObject>(AffNode));
}
}
else if (auto* VS = Cast<UK2Node_VariableSet>(Node))
{
if (VS->GetVarName().ToString() == VariableName)
{
TSharedRef<FJsonObject> AffNode = MakeShared<FJsonObject>();
AffNode->SetStringField(TEXT("nodeId"), VS->NodeGuid.ToString());
AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableSet"));
AffNode->SetStringField(TEXT("graph"), Graph->GetName());
TArray<TSharedPtr<FJsonValue>> AffPins;
for (UEdGraphPin* Pin : VS->Pins)
{
if (Pin && Pin->LinkedTo.Num() > 0)
{
AffPins.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("%s (connected to %d pin(s))"),
*Pin->PinName.ToString(), Pin->LinkedTo.Num())));
}
}
AffNode->SetArrayField(TEXT("affectedPins"), AffPins);
AffectedNodes.Add(MakeShared<FJsonValueObject>(AffNode));
}
}
}
}
if (bDryRun)
{
Result->SetBoolField(TEXT("dryRun"), true);
Result->SetStringField(TEXT("blueprint"), BlueprintName);
Result->SetStringField(TEXT("variable"), VariableName);
Result->SetStringField(TEXT("newType"), NewTypeName);
Result->SetStringField(TEXT("typeCategory"), TypeCategory);
Result->SetNumberField(TEXT("affectedNodeCount"), AffectedNodes.Num());
Result->SetArrayField(TEXT("affectedNodes"), AffectedNodes);
return;
}
// Directly modify the variable type in the description array.
for (FBPVariableDescription& Var : BP->NewVariables)
{
if (Var.VarName == FName(*VariableName))
{
Var.VarType = NewPinType;
break;
}
}
// Save
bool bSaved = SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Variable type changed, save %s"),
bSaved ? TEXT("succeeded") : TEXT("failed"));
// Return updated variable state
TSharedRef<FJsonObject> UpdatedVar = MakeShared<FJsonObject>();
for (const FBPVariableDescription& Var : BP->NewVariables)
{
if (Var.VarName == FName(*VariableName))
{
UpdatedVar->SetStringField(TEXT("name"), Var.VarName.ToString());
UpdatedVar->SetStringField(TEXT("type"), Var.VarType.PinCategory.ToString());
if (Var.VarType.PinSubCategoryObject.IsValid())
UpdatedVar->SetStringField(TEXT("subtype"), Var.VarType.PinSubCategoryObject->GetName());
UpdatedVar->SetBoolField(TEXT("isArray"), Var.VarType.IsArray());
break;
}
}
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), BlueprintName);
Result->SetStringField(TEXT("variable"), VariableName);
Result->SetStringField(TEXT("newType"), NewTypeName);
Result->SetStringField(TEXT("typeCategory"), TypeCategory);
Result->SetBoolField(TEXT("saved"), bSaved);
Result->SetObjectField(TEXT("updatedVariable"), UpdatedVar);
Result->SetArrayField(TEXT("affectedNodes"), AffectedNodes);
}
// ============================================================
// HandleAddVariable — add a new member variable to a Blueprint
// ============================================================
void FBlueprintMCPServer::HandleAddVariable(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
FString VariableName = Json->GetStringField(TEXT("variableName"));
FString VariableType = Json->GetStringField(TEXT("variableType"));
if (BlueprintName.IsEmpty() || VariableName.IsEmpty() || VariableType.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, variableName, variableType"));
}
FString Category;
if (Json->HasField(TEXT("category")))
{
Category = Json->GetStringField(TEXT("category"));
}
bool bIsArray = false;
if (Json->HasField(TEXT("isArray")))
{
bIsArray = Json->GetBoolField(TEXT("isArray"));
}
FString DefaultValue;
if (Json->HasField(TEXT("defaultValue")))
{
DefaultValue = Json->GetStringField(TEXT("defaultValue"));
}
// Load Blueprint
FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MakeErrorJson(Result, LoadError);
}
// Check for duplicate variable name
FName VarFName(*VariableName);
for (const FBPVariableDescription& Var : BP->NewVariables)
{
if (Var.VarName == VarFName)
{
return MakeErrorJson(Result, FString::Printf(
TEXT("Variable '%s' already exists in Blueprint '%s'"), *VariableName, *BlueprintName));
}
}
// Resolve the type using the shared helper
FEdGraphPinType PinType;
FString TypeError;
if (!ResolveTypeFromString(VariableType, PinType, TypeError))
{
return MakeErrorJson(Result, TypeError);
}
// Set container type for arrays
if (bIsArray)
{
PinType.ContainerType = EPinContainerType::Array;
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding variable '%s' (type=%s, array=%s) to Blueprint '%s'"),
*VariableName, *VariableType, bIsArray ? TEXT("true") : TEXT("false"), *BlueprintName);
// Add the variable using the editor utility function
bool bSuccess = FBlueprintEditorUtils::AddMemberVariable(BP, VarFName, PinType, DefaultValue);
if (!bSuccess)
{
return MakeErrorJson(Result, FString::Printf(
TEXT("FBlueprintEditorUtils::AddMemberVariable failed for '%s'"), *VariableName));
}
// Set category if provided
if (!Category.IsEmpty())
{
FBlueprintEditorUtils::SetBlueprintVariableCategory(BP, VarFName, nullptr, FText::FromString(Category));
}
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
bool bSaved = SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added variable '%s' to '%s' (saved: %s)"),
*VariableName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false"));
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), BlueprintName);
Result->SetStringField(TEXT("variableName"), VariableName);
Result->SetStringField(TEXT("variableType"), VariableType);
if (!Category.IsEmpty())
{
Result->SetStringField(TEXT("category"), Category);
}
Result->SetBoolField(TEXT("isArray"), bIsArray);
Result->SetBoolField(TEXT("saved"), bSaved);
}
// ============================================================
// HandleRemoveVariable — remove a member variable from a Blueprint
// ============================================================
void FBlueprintMCPServer::HandleRemoveVariable(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
FString VariableName = Json->GetStringField(TEXT("variableName"));
if (BlueprintName.IsEmpty() || VariableName.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, variableName"));
}
// Load Blueprint
FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MakeErrorJson(Result, LoadError);
}
// Find variable by name (case-insensitive)
FName VarFName(*VariableName);
bool bVarFound = false;
for (const FBPVariableDescription& Var : BP->NewVariables)
{
if (Var.VarName.ToString().Equals(VariableName, ESearchCase::IgnoreCase))
{
VarFName = Var.VarName; // Use the exact name found
bVarFound = true;
break;
}
}
if (!bVarFound)
{
// Build available variables list for helpful error message
TArray<TSharedPtr<FJsonValue>> AvailVars;
for (const FBPVariableDescription& Var : BP->NewVariables)
{
AvailVars.Add(MakeShared<FJsonValueString>(Var.VarName.ToString()));
}
MakeErrorJson(Result, FString::Printf(
TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *BlueprintName));
Result->SetArrayField(TEXT("availableVariables"), AvailVars);
return;
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing variable '%s' from Blueprint '%s'"),
*VariableName, *BlueprintName);
// Use the editor utility to remove the variable (also cleans up Get/Set nodes)
FBlueprintEditorUtils::RemoveMemberVariable(BP, VarFName);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
bool bSaved = SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed variable '%s' from '%s' (saved: %s)"),
*VariableName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false"));
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), BlueprintName);
Result->SetStringField(TEXT("variableName"), VariableName);
Result->SetBoolField(TEXT("saved"), bSaved);
}
// ============================================================
// HandleSetVariableMetadata — set variable properties (category, tooltip, replication, etc.)
// ============================================================
void FBlueprintMCPServer::HandleSetVariableMetadata(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
FString VariableName = Json->GetStringField(TEXT("variable"));
if (BlueprintName.IsEmpty() || VariableName.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, variable"));
}
// Load Blueprint
FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MakeErrorJson(Result, LoadError);
}
// Find the variable
FName VarFName(*VariableName);
FBPVariableDescription* VarDesc = nullptr;
for (FBPVariableDescription& Var : BP->NewVariables)
{
if (Var.VarName == VarFName)
{
VarDesc = &Var;
break;
}
}
if (!VarDesc)
{
TArray<TSharedPtr<FJsonValue>> AvailableVars;
for (const FBPVariableDescription& Var : BP->NewVariables)
{
AvailableVars.Add(MakeShared<FJsonValueString>(Var.VarName.ToString()));
}
MakeErrorJson(Result, FString::Printf(
TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *BlueprintName));
Result->SetArrayField(TEXT("availableVariables"), AvailableVars);
return;
}
TArray<TSharedPtr<FJsonValue>> Changes;
// Category
if (Json->HasField(TEXT("category")))
{
FString OldCategory = VarDesc->Category.ToString();
FString NewCategory = Json->GetStringField(TEXT("category"));
VarDesc->Category = FText::FromString(NewCategory);
FBlueprintEditorUtils::SetBlueprintVariableCategory(BP, VarFName, nullptr, FText::FromString(NewCategory));
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
Change->SetStringField(TEXT("field"), TEXT("category"));
Change->SetStringField(TEXT("oldValue"), OldCategory);
Change->SetStringField(TEXT("newValue"), NewCategory);
Changes.Add(MakeShared<FJsonValueObject>(Change));
}
// Tooltip
if (Json->HasField(TEXT("tooltip")))
{
FString OldTooltip;
FBlueprintEditorUtils::GetBlueprintVariableMetaData(BP, VarFName, nullptr, TEXT("tooltip"), OldTooltip);
FString NewTooltip = Json->GetStringField(TEXT("tooltip"));
FBlueprintEditorUtils::SetBlueprintVariableMetaData(BP, VarFName, nullptr, TEXT("tooltip"), NewTooltip);
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
Change->SetStringField(TEXT("field"), TEXT("tooltip"));
Change->SetStringField(TEXT("oldValue"), OldTooltip);
Change->SetStringField(TEXT("newValue"), NewTooltip);
Changes.Add(MakeShared<FJsonValueObject>(Change));
}
// Replication
if (Json->HasField(TEXT("replication")))
{
FString ReplicationStr = Json->GetStringField(TEXT("replication"));
uint64 OldFlags = VarDesc->PropertyFlags;
if (ReplicationStr == TEXT("none"))
{
VarDesc->PropertyFlags &= ~CPF_Net;
VarDesc->PropertyFlags &= ~CPF_RepNotify;
VarDesc->RepNotifyFunc = NAME_None;
}
else if (ReplicationStr == TEXT("replicated"))
{
VarDesc->PropertyFlags |= CPF_Net;
VarDesc->PropertyFlags &= ~CPF_RepNotify;
VarDesc->RepNotifyFunc = NAME_None;
}
else if (ReplicationStr == TEXT("repNotify"))
{
VarDesc->PropertyFlags |= CPF_Net | CPF_RepNotify;
// Auto-generate RepNotify function name
VarDesc->RepNotifyFunc = FName(*FString::Printf(TEXT("OnRep_%s"), *VariableName));
}
else
{
return MakeErrorJson(Result, FString::Printf(
TEXT("Invalid replication value '%s'. Valid: none, replicated, repNotify"), *ReplicationStr));
}
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
Change->SetStringField(TEXT("field"), TEXT("replication"));
Change->SetStringField(TEXT("newValue"), ReplicationStr);
Changes.Add(MakeShared<FJsonValueObject>(Change));
}
// ExposeOnSpawn
if (Json->HasField(TEXT("exposeOnSpawn")))
{
bool bOld = (VarDesc->PropertyFlags & CPF_ExposeOnSpawn) != 0;
bool bNew = Json->GetBoolField(TEXT("exposeOnSpawn"));
if (bNew)
VarDesc->PropertyFlags |= CPF_ExposeOnSpawn;
else
VarDesc->PropertyFlags &= ~CPF_ExposeOnSpawn;
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
Change->SetStringField(TEXT("field"), TEXT("exposeOnSpawn"));
Change->SetStringField(TEXT("oldValue"), bOld ? TEXT("true") : TEXT("false"));
Change->SetStringField(TEXT("newValue"), bNew ? TEXT("true") : TEXT("false"));
Changes.Add(MakeShared<FJsonValueObject>(Change));
}
// isPrivate
if (Json->HasField(TEXT("isPrivate")))
{
bool bOld = (VarDesc->PropertyFlags & CPF_DisableEditOnInstance) != 0;
bool bNew = Json->GetBoolField(TEXT("isPrivate"));
// In UE5, "private" for Blueprint variables is represented via metadata
FBlueprintEditorUtils::SetBlueprintVariableMetaData(BP, VarFName, nullptr,
TEXT("BlueprintPrivate"), bNew ? TEXT("true") : TEXT("false"));
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
Change->SetStringField(TEXT("field"), TEXT("isPrivate"));
Change->SetStringField(TEXT("oldValue"), bOld ? TEXT("true") : TEXT("false"));
Change->SetStringField(TEXT("newValue"), bNew ? TEXT("true") : TEXT("false"));
Changes.Add(MakeShared<FJsonValueObject>(Change));
}
// Editability (EditAnywhere, EditDefaultsOnly, EditInstanceOnly)
if (Json->HasField(TEXT("editability")))
{
FString Editability = Json->GetStringField(TEXT("editability"));
// Clear all edit flags first
VarDesc->PropertyFlags &= ~(CPF_Edit | CPF_DisableEditOnInstance | CPF_DisableEditOnTemplate);
if (Editability == TEXT("editAnywhere"))
{
VarDesc->PropertyFlags |= CPF_Edit;
}
else if (Editability == TEXT("editDefaultsOnly"))
{
VarDesc->PropertyFlags |= CPF_Edit | CPF_DisableEditOnInstance;
}
else if (Editability == TEXT("editInstanceOnly"))
{
VarDesc->PropertyFlags |= CPF_Edit | CPF_DisableEditOnTemplate;
}
else if (Editability == TEXT("none"))
{
// All edit flags already cleared
}
else
{
return MakeErrorJson(Result, FString::Printf(
TEXT("Invalid editability value '%s'. Valid: editAnywhere, editDefaultsOnly, editInstanceOnly, none"),
*Editability));
}
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
Change->SetStringField(TEXT("field"), TEXT("editability"));
Change->SetStringField(TEXT("newValue"), Editability);
Changes.Add(MakeShared<FJsonValueObject>(Change));
}
if (Changes.Num() == 0)
{
return MakeErrorJson(Result, TEXT("No metadata fields specified. Provide at least one of: category, tooltip, replication, exposeOnSpawn, isPrivate, editability"));
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SetVariableMetadata on '%s.%s' — %d field(s) changed"),
*BlueprintName, *VariableName, Changes.Num());
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
bool bSaved = SaveBlueprintPackage(BP);
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), BlueprintName);
Result->SetStringField(TEXT("variable"), VariableName);
Result->SetArrayField(TEXT("changes"), Changes);
Result->SetBoolField(TEXT("saved"), bSaved);
}