BlueprintVariable handlers
This commit is contained in:
@@ -1,93 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPFetcher.h"
|
||||
#include "MCPTypes.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "MCPServer.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "Blueprint_AddVariable.generated.h"
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS()
|
||||
class UMCP_Blueprint_AddVariable : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the new variable"))
|
||||
FString VariableName;
|
||||
|
||||
UPROPERTY(meta=(Description="Type of the new variable"))
|
||||
FString VariableType;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Category to assign the variable to"))
|
||||
FString Category;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="If true, make the variable an array"))
|
||||
bool IsArray = false;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Default value for the variable"))
|
||||
FString DefaultValue;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Add a new member variable to a Blueprint.");
|
||||
}
|
||||
|
||||
virtual void Handle() override
|
||||
{
|
||||
MCPFetcher F;
|
||||
UBlueprint* BP = F.Asset(Blueprint).Cast<UBlueprint>();
|
||||
if (!BP) return;
|
||||
|
||||
// Check for duplicate variable name
|
||||
FName VarFName(*VariableName);
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (Var.VarName == VarFName)
|
||||
{
|
||||
UMCPServer::Printf(TEXT("ERROR: Variable '%s' already exists in %s\n"), *VariableName, *MCPUtils::FormatName(BP));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the type
|
||||
FEdGraphPinType PinType;
|
||||
if (!UMCPTypes::TextToType(VariableType, PinType))
|
||||
return;
|
||||
|
||||
if (IsArray)
|
||||
PinType.ContainerType = EPinContainerType::Array;
|
||||
|
||||
// Add the variable
|
||||
if (!FBlueprintEditorUtils::AddMemberVariable(BP, VarFName, PinType, DefaultValue))
|
||||
{
|
||||
UMCPServer::Printf(TEXT("ERROR: Failed to add variable '%s' to %s\n"), *VariableName, *MCPUtils::FormatName(BP));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Category.IsEmpty())
|
||||
FBlueprintEditorUtils::SetBlueprintVariableCategory(BP, VarFName, nullptr, FText::FromString(Category));
|
||||
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UMCPServer::Printf(TEXT("Added %s %s to %s\n"),
|
||||
*VariableType, *VariableName, *MCPUtils::FormatName(BP));
|
||||
if (IsArray)
|
||||
UMCPServer::Print(TEXT("Container: Array\n"));
|
||||
if (!Category.IsEmpty())
|
||||
UMCPServer::Printf(TEXT("Category: %s\n"), *Category);
|
||||
if (!bSaved)
|
||||
UMCPServer::Print(TEXT("Warning: package save failed\n"));
|
||||
}
|
||||
};
|
||||
@@ -1,147 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPServer.h"
|
||||
#include "MCPTypes.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPFetcher.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "EdGraph/EdGraphPin.h"
|
||||
#include "K2Node_VariableGet.h"
|
||||
#include "K2Node_VariableSet.h"
|
||||
#include "Blueprint_ChangeVariableType.generated.h"
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS()
|
||||
class UMCP_Blueprint_ChangeVariableType : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the variable to change"))
|
||||
FString Variable;
|
||||
|
||||
UPROPERTY(meta=(Description="New type name for the variable"))
|
||||
FString NewType;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Type category: object, softobject, class, softclass, interface, struct, enum"))
|
||||
FString TypeCategory;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="If true, analyze the change without applying it"))
|
||||
bool DryRun = false;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Change the type of a Blueprint member variable. "
|
||||
"Supports dry-run mode to preview affected nodes before committing.");
|
||||
}
|
||||
|
||||
virtual void Handle() override
|
||||
{
|
||||
MCPFetcher F;
|
||||
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
|
||||
if (!BP) return;
|
||||
|
||||
// Find the variable
|
||||
FBPVariableDescription* Found = nullptr;
|
||||
for (FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (MCPUtils::FormatName(Var) == Variable ||
|
||||
Var.VarName.ToString().Equals(Variable, ESearchCase::IgnoreCase))
|
||||
{
|
||||
Found = &Var;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!Found)
|
||||
{
|
||||
UMCPServer::Printf(TEXT("ERROR: Variable '%s' not found in %s.\nAvailable variables:\n"),
|
||||
*Variable, *MCPUtils::FormatName(BP));
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
UMCPServer::Printf(TEXT(" %s\n"), *MCPUtils::FormatName(Var));
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the new pin type using shared resolver
|
||||
FEdGraphPinType NewPinType;
|
||||
FString ResolveInput = NewType;
|
||||
|
||||
// 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(":") + NewType;
|
||||
}
|
||||
|
||||
if (!UMCPTypes::TextToType(ResolveInput, NewPinType))
|
||||
return;
|
||||
|
||||
// List affected nodes (get/set nodes for this variable)
|
||||
FName VarFName = Found->VarName;
|
||||
auto AppendAffectedNodes = [&](const auto& NodeArray, const TCHAR* NodeType)
|
||||
{
|
||||
for (auto* VarNode : NodeArray)
|
||||
{
|
||||
if (VarNode->GetVarName() != VarFName) continue;
|
||||
UMCPServer::Printf(TEXT(" %s %s in %s\n"), NodeType,
|
||||
*MCPUtils::FormatName(static_cast<UEdGraphNode*>(VarNode)),
|
||||
*MCPUtils::FormatName(VarNode->GetGraph()));
|
||||
for (UEdGraphPin* Pin : VarNode->Pins)
|
||||
{
|
||||
if (!Pin || Pin->LinkedTo.Num() == 0) continue;
|
||||
if (NodeType[0] == 'G' && Pin->Direction != EGPD_Output) continue; // Get nodes: only output pins
|
||||
UMCPServer::Printf(TEXT(" %s connected to %d pin(s)\n"),
|
||||
*MCPUtils::FormatName(Pin), Pin->LinkedTo.Num());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto GetNodes = MCPUtils::AllNodes<UK2Node_VariableGet>(BP);
|
||||
auto SetNodes = MCPUtils::AllNodes<UK2Node_VariableSet>(BP);
|
||||
|
||||
bool bHasAffected = false;
|
||||
for (auto* VG : GetNodes) if (VG->GetVarName() == VarFName) { bHasAffected = true; break; }
|
||||
if (!bHasAffected)
|
||||
for (auto* VS : SetNodes) if (VS->GetVarName() == VarFName) { bHasAffected = true; break; }
|
||||
|
||||
if (DryRun)
|
||||
{
|
||||
UMCPServer::Printf(TEXT("Dry run: would change %s from %s to %s\n"),
|
||||
*MCPUtils::FormatName(*Found),
|
||||
*UMCPTypes::TypeToText(Found->VarType),
|
||||
*UMCPTypes::TypeToText(NewPinType));
|
||||
if (bHasAffected)
|
||||
{
|
||||
UMCPServer::Print(TEXT("Affected nodes:\n"));
|
||||
AppendAffectedNodes(GetNodes, TEXT("Get"));
|
||||
AppendAffectedNodes(SetNodes, TEXT("Set"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply the type change
|
||||
Found->VarType = NewPinType;
|
||||
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
UMCPServer::Printf(TEXT("Changed %s to %s.%s\n"),
|
||||
*MCPUtils::FormatName(*Found),
|
||||
*UMCPTypes::TypeToText(NewPinType),
|
||||
bSaved ? TEXT("") : TEXT(" WARNING: save failed."));
|
||||
|
||||
if (bHasAffected)
|
||||
{
|
||||
UMCPServer::Print(TEXT("Affected nodes:\n"));
|
||||
AppendAffectedNodes(GetNodes, TEXT("Get"));
|
||||
AppendAffectedNodes(SetNodes, TEXT("Set"));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPServer.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPFetcher.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "Blueprint_RemoveVariable.generated.h"
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS()
|
||||
class UMCP_Blueprint_RemoveVariable : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the variable to remove"))
|
||||
FString VariableName;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Remove a member variable from a Blueprint.");
|
||||
}
|
||||
|
||||
virtual void Handle() override
|
||||
{
|
||||
MCPFetcher F;
|
||||
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
|
||||
if (!BP) return;
|
||||
|
||||
// Find the variable using Identifies for consistent name matching
|
||||
const FBPVariableDescription* Found = nullptr;
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (MCPUtils::FormatName(Var) == VariableName ||
|
||||
Var.VarName.ToString().Equals(VariableName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
Found = &Var;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Found)
|
||||
{
|
||||
UMCPServer::Printf(TEXT("ERROR: Variable '%s' not found in %s.\nAvailable variables:\n"),
|
||||
*VariableName, *MCPUtils::FormatName(BP));
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
UMCPServer::Printf(TEXT(" %s\n"), *MCPUtils::FormatName(Var));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
FName VarFName = Found->VarName;
|
||||
|
||||
// RemoveMemberVariable also cleans up Get/Set nodes
|
||||
FBlueprintEditorUtils::RemoveMemberVariable(BP, VarFName);
|
||||
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
UMCPServer::Printf(TEXT("Removed variable %s from %s.%s\n"),
|
||||
*VarFName.ToString(), *MCPUtils::FormatName(BP),
|
||||
bSaved ? TEXT("") : TEXT(" WARNING: save failed."));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPServer.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPFetcher.h"
|
||||
#include "MCPJson.h"
|
||||
#include "MCPProperty.h"
|
||||
#include "BPVarEditor.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "MCPTypes.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "EdGraphSchema_K2.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "BlueprintVariable_Create.generated.h"
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS()
|
||||
class UMCP_BlueprintVariable_Create : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the new variable"))
|
||||
FString Name;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Variable configuration: VarType, Category, DefaultValue, InstanceEditable, BlueprintReadOnly, ExposeOnSpawn, Private, ExposeToCinematics, etc."))
|
||||
FMCPJsonObject Config;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Add a new member variable to a Blueprint. Pass Config to set type, category, flags, etc.");
|
||||
}
|
||||
|
||||
virtual void Handle() override
|
||||
{
|
||||
MCPFetcher F;
|
||||
UBlueprint* BP = F.Walk(Blueprint).ToBlueprint().Cast<UBlueprint>();
|
||||
if (!BP) return;
|
||||
|
||||
// Check for duplicate variable name
|
||||
FName VarFName(*Name);
|
||||
if (FBlueprintEditorUtils::FindNewVariableIndex(BP, VarFName) != INDEX_NONE)
|
||||
{
|
||||
UMCPServer::Printf(TEXT("ERROR: Variable '%s' already exists in %s\n"), *Name, *MCPUtils::FormatName(BP));
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the variable with a default type
|
||||
FEdGraphPinType DefaultType;
|
||||
DefaultType.PinCategory = UEdGraphSchema_K2::PC_Int;
|
||||
if (!FBlueprintEditorUtils::AddMemberVariable(BP, VarFName, DefaultType))
|
||||
{
|
||||
UMCPServer::Printf(TEXT("ERROR: Failed to add variable '%s' to %s\n"), *Name, *MCPUtils::FormatName(BP));
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the newly created variable description
|
||||
FBPVarEditor Editor(BP, Name);
|
||||
if (Editor.NotFound()) return;
|
||||
|
||||
// Apply config if provided
|
||||
if (Config.Json && Config.Json->Values.Num() > 0)
|
||||
{
|
||||
if (!Editor.LoadJson(Config.Json.Get()))
|
||||
return;
|
||||
}
|
||||
|
||||
UMCPServer::Printf(TEXT("Created variable %s (%s) in %s\n"),
|
||||
*Name, *UMCPTypes::TypeToText(Editor.Desc->VarType), *MCPUtils::FormatName(BP));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPServer.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPFetcher.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "BPVarEditor.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "BlueprintVariable_Delete.generated.h"
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS()
|
||||
class UMCP_BlueprintVariable_Delete : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the variable to delete"))
|
||||
FString Variable;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Remove a member variable from a Blueprint.");
|
||||
}
|
||||
|
||||
virtual void Handle() override
|
||||
{
|
||||
MCPFetcher F;
|
||||
UBlueprint* BP = F.Walk(Blueprint).ToBlueprint().Cast<UBlueprint>();
|
||||
if (!BP) return;
|
||||
|
||||
FBPVarEditor Editor(BP, Variable);
|
||||
if (Editor.NotFound()) return;
|
||||
|
||||
FBlueprintEditorUtils::RemoveMemberVariable(BP, Editor.Desc->VarName);
|
||||
|
||||
UMCPServer::Printf(TEXT("Removed variable %s from %s\n"),
|
||||
*Variable, *MCPUtils::FormatName(BP));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPServer.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPFetcher.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "BPVarEditor.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "BlueprintVariable_Dump.generated.h"
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS()
|
||||
class UMCP_BlueprintVariable_Dump : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the variable to inspect"))
|
||||
FString Variable;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Show all editable properties of a Blueprint variable.");
|
||||
}
|
||||
|
||||
virtual void Handle() override
|
||||
{
|
||||
MCPFetcher F;
|
||||
UBlueprint* BP = F.Walk(Blueprint).ToBlueprint().Cast<UBlueprint>();
|
||||
if (!BP) return;
|
||||
|
||||
FBPVarEditor Editor(BP, Variable);
|
||||
if (Editor.NotFound()) return;
|
||||
|
||||
UMCPServer::Printf(TEXT("Variable %s in %s:\n"), *Variable, *MCPUtils::FormatName(BP));
|
||||
Editor.Dump();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPServer.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPFetcher.h"
|
||||
#include "MCPJson.h"
|
||||
#include "MCPProperty.h"
|
||||
#include "BPVarEditor.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "MCPTypes.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "BlueprintVariable_Modify.generated.h"
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS()
|
||||
class UMCP_BlueprintVariable_Modify : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the variable to modify"))
|
||||
FString Variable;
|
||||
|
||||
UPROPERTY(meta=(Description="Properties to change: VarType, Category, DefaultValue, InstanceEditable, BlueprintReadOnly, ExposeOnSpawn, Private, ExposeToCinematics, etc."))
|
||||
FMCPJsonObject Properties;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Modify properties of an existing Blueprint variable.");
|
||||
}
|
||||
|
||||
virtual void Handle() override
|
||||
{
|
||||
MCPFetcher F;
|
||||
UBlueprint* BP = F.Walk(Blueprint).ToBlueprint().Cast<UBlueprint>();
|
||||
if (!BP) return;
|
||||
|
||||
FBPVarEditor Editor(BP, Variable);
|
||||
if (Editor.NotFound()) return;
|
||||
|
||||
if (!Properties.Json || Properties.Json->Values.Num() == 0)
|
||||
{
|
||||
UMCPServer::Print(TEXT("ERROR: No properties specified\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Editor.LoadJson(Properties.Json.Get()))
|
||||
return;
|
||||
|
||||
UMCPServer::Printf(TEXT("Modified variable %s (%s) in %s\n"),
|
||||
*Variable, *UMCPTypes::TypeToText(Editor.Desc->VarType), *MCPUtils::FormatName(BP));
|
||||
}
|
||||
};
|
||||
110
Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BPVarEditor.cpp
Normal file
110
Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BPVarEditor.cpp
Normal file
@@ -0,0 +1,110 @@
|
||||
#include "BPVarEditor.h"
|
||||
#include "MCPJson.h"
|
||||
#include "MCPServer.h"
|
||||
#include "MCPTypes.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "EdGraphSchema_K2.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
|
||||
FBPVarEditor::FBPVarEditor(UBlueprint* BP, const FString& VarName)
|
||||
{
|
||||
FName VarFName(*VarName);
|
||||
int32 VarIndex = FBlueprintEditorUtils::FindNewVariableIndex(BP, VarFName);
|
||||
if (VarIndex == INDEX_NONE)
|
||||
{
|
||||
UMCPServer::Printf(TEXT("ERROR: Variable '%s' not found in %s\n"), *VarName, *MCPUtils::FormatName(BP));
|
||||
return;
|
||||
}
|
||||
Desc = &BP->NewVariables[VarIndex];
|
||||
}
|
||||
|
||||
void FBPVarEditor::Dump()
|
||||
{
|
||||
Load();
|
||||
TArray<MCPProperty> Props = MergedProperties();
|
||||
for (MCPProperty& P : Props)
|
||||
{
|
||||
UMCPServer::Printf(TEXT(" %s %s = %s\n"),
|
||||
*UMCPTypes::TypeToText(P.Prop),
|
||||
*MCPUtils::FormatName(P.Prop),
|
||||
*P.GetText());
|
||||
}
|
||||
}
|
||||
|
||||
bool FBPVarEditor::LoadJson(const FJsonObject* Json)
|
||||
{
|
||||
Load();
|
||||
TArray<MCPProperty> Props = MergedProperties();
|
||||
if (!MCPJson::PopulateFromJson(Props, Json, true))
|
||||
return false;
|
||||
Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
void FBPVarEditor::Load()
|
||||
{
|
||||
InstanceEditable = !(Desc->PropertyFlags & CPF_DisableEditOnInstance);
|
||||
BlueprintReadOnly = (Desc->PropertyFlags & CPF_BlueprintReadOnly) != 0;
|
||||
ExposeToCinematics = (Desc->PropertyFlags & CPF_Interp) != 0;
|
||||
ExposeOnSpawn = Desc->HasMetaData(FBlueprintMetadata::MD_ExposeOnSpawn);
|
||||
Private = Desc->HasMetaData(FBlueprintMetadata::MD_Private);
|
||||
|
||||
FString Tooltip;
|
||||
if (Desc->HasMetaData(TEXT("tooltip")))
|
||||
Description = Desc->GetMetaData(TEXT("tooltip"));
|
||||
else
|
||||
Description.Empty();
|
||||
}
|
||||
|
||||
void FBPVarEditor::Save() const
|
||||
{
|
||||
// CPF flags
|
||||
if (InstanceEditable)
|
||||
Desc->PropertyFlags &= ~CPF_DisableEditOnInstance;
|
||||
else
|
||||
Desc->PropertyFlags |= CPF_DisableEditOnInstance;
|
||||
|
||||
if (BlueprintReadOnly)
|
||||
Desc->PropertyFlags |= CPF_BlueprintReadOnly;
|
||||
else
|
||||
Desc->PropertyFlags &= ~CPF_BlueprintReadOnly;
|
||||
|
||||
if (ExposeToCinematics)
|
||||
Desc->PropertyFlags |= CPF_Interp;
|
||||
else
|
||||
Desc->PropertyFlags &= ~CPF_Interp;
|
||||
|
||||
// Metadata flags
|
||||
if (ExposeOnSpawn)
|
||||
Desc->SetMetaData(FBlueprintMetadata::MD_ExposeOnSpawn, TEXT("true"));
|
||||
else
|
||||
Desc->RemoveMetaData(FBlueprintMetadata::MD_ExposeOnSpawn);
|
||||
|
||||
if (Private)
|
||||
Desc->SetMetaData(FBlueprintMetadata::MD_Private, TEXT("true"));
|
||||
else
|
||||
Desc->RemoveMetaData(FBlueprintMetadata::MD_Private);
|
||||
|
||||
// Description/tooltip
|
||||
if (!Description.IsEmpty())
|
||||
Desc->SetMetaData(TEXT("tooltip"), Description);
|
||||
else
|
||||
Desc->RemoveMetaData(TEXT("tooltip"));
|
||||
}
|
||||
|
||||
TArray<MCPProperty> FBPVarEditor::MergedProperties()
|
||||
{
|
||||
TArray<MCPProperty> Props = MCPProperty::GetAll(
|
||||
FBPVariableDescription::StaticStruct(), Desc, CPF_Edit);
|
||||
|
||||
MCPProperty::Remove(Props, TEXT("PropertyFlags"));
|
||||
MCPProperty::Remove(Props, TEXT("MetaDataArray"));
|
||||
MCPProperty::Remove(Props, TEXT("VarName"));
|
||||
MCPProperty::Remove(Props, TEXT("VarGuid"));
|
||||
MCPProperty::Remove(Props, TEXT("DefaultValue"));
|
||||
|
||||
Props.Append(MCPProperty::GetAll(
|
||||
FBPVarEditor::StaticStruct(), this, (EPropertyFlags)0));
|
||||
|
||||
return Props;
|
||||
}
|
||||
@@ -6,46 +6,10 @@
|
||||
#include "Dom/JsonValue.h"
|
||||
|
||||
|
||||
|
||||
bool MCPJson::PopulateFromJson(
|
||||
UStruct* StructType, void* Container, const FJsonObject* Json)
|
||||
bool MCPJson::PopulateFromJson(MCPProperty& P, const FJsonObject* Json, bool AllOptional)
|
||||
{
|
||||
bool Ok = true;
|
||||
|
||||
// Build a set of known property names (as JSON keys) for the unknown-field check.
|
||||
TSet<FString> KnownKeys;
|
||||
TArray<FProperty*> Properties;
|
||||
|
||||
for (TFieldIterator<FProperty> It(StructType, EFieldIterationFlags::None); It; ++It)
|
||||
{
|
||||
FProperty* Prop = *It;
|
||||
Properties.Add(Prop);
|
||||
KnownKeys.Add(Prop->GetName());
|
||||
}
|
||||
|
||||
// Check for unknown fields in the JSON
|
||||
for (const auto& KV : Json->Values)
|
||||
{
|
||||
if (!KnownKeys.Contains(KV.Key))
|
||||
{
|
||||
UMCPServer::Printf(TEXT("ERROR: Unknown parameter '%s'\n"), *KV.Key);
|
||||
Ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate each property from JSON
|
||||
for (FProperty* Property : Properties)
|
||||
{
|
||||
if (!PopulateFromJson(Property, Container, Json)) Ok = false;
|
||||
}
|
||||
return Ok;
|
||||
}
|
||||
|
||||
bool MCPJson::PopulateFromJson(
|
||||
FProperty* Property, void* Container, const FJsonObject* Json)
|
||||
{
|
||||
FString JsonKey = Property->GetName();
|
||||
bool bOptional = Property->HasMetaData(TEXT("Optional"));
|
||||
FString JsonKey = P.Prop->GetName();
|
||||
bool bOptional = AllOptional || P.Prop->HasMetaData(TEXT("Optional"));
|
||||
|
||||
if (!Json->HasField(JsonKey))
|
||||
{
|
||||
@@ -57,10 +21,10 @@ bool MCPJson::PopulateFromJson(
|
||||
return true;
|
||||
}
|
||||
|
||||
void* ValuePtr = Property->ContainerPtrToValuePtr<void>(Container);
|
||||
void* ValuePtr = P.Prop->ContainerPtrToValuePtr<void>(P.Container);
|
||||
|
||||
// Special handling for FMCPJsonObject and FMCPJsonArray
|
||||
if (FStructProperty* StructProp = CastField<FStructProperty>(Property))
|
||||
if (FStructProperty* StructProp = CastField<FStructProperty>(P.Prop))
|
||||
{
|
||||
if (StructProp->Struct == FMCPJsonObject::StaticStruct())
|
||||
{
|
||||
@@ -91,42 +55,70 @@ bool MCPJson::PopulateFromJson(
|
||||
if (JsonValue->Type == EJson::Number)
|
||||
{
|
||||
double D = JsonValue->AsNumber();
|
||||
if (FIntProperty* IntProp = CastField<FIntProperty>(Property))
|
||||
if (FIntProperty* IntProp = CastField<FIntProperty>(P.Prop))
|
||||
{ IntProp->SetPropertyValue(ValuePtr, (int32)D); return true; }
|
||||
if (FFloatProperty* FloatProp = CastField<FFloatProperty>(Property))
|
||||
if (FFloatProperty* FloatProp = CastField<FFloatProperty>(P.Prop))
|
||||
{ FloatProp->SetPropertyValue(ValuePtr, (float)D); return true; }
|
||||
if (FDoubleProperty* DoubleProp = CastField<FDoubleProperty>(Property))
|
||||
if (FDoubleProperty* DoubleProp = CastField<FDoubleProperty>(P.Prop))
|
||||
{ DoubleProp->SetPropertyValue(ValuePtr, D); return true; }
|
||||
if (FByteProperty* ByteProp = CastField<FByteProperty>(Property))
|
||||
if (FByteProperty* ByteProp = CastField<FByteProperty>(P.Prop))
|
||||
{ ByteProp->SetPropertyValue(ValuePtr, (uint8)D); return true; }
|
||||
UMCPServer::Printf(TEXT("ERROR: '%s' received a number but expects %s\n"), *JsonKey, *Property->GetCPPType());
|
||||
UMCPServer::Printf(TEXT("ERROR: '%s' received a number but expects %s\n"), *JsonKey, *P.Prop->GetCPPType());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (JsonValue->Type == EJson::Boolean)
|
||||
{
|
||||
if (FBoolProperty* BoolProp = CastField<FBoolProperty>(Property))
|
||||
if (FBoolProperty* BoolProp = CastField<FBoolProperty>(P.Prop))
|
||||
{ BoolProp->SetPropertyValue(ValuePtr, JsonValue->AsBool()); return true; }
|
||||
UMCPServer::Printf(TEXT("ERROR: '%s' received a boolean but expects %s\n"), *JsonKey, *Property->GetCPPType());
|
||||
UMCPServer::Printf(TEXT("ERROR: '%s' received a boolean but expects %s\n"), *JsonKey, *P.Prop->GetCPPType());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (JsonValue->Type == EJson::String)
|
||||
{
|
||||
FString ValueStr = JsonValue->AsString();
|
||||
const TCHAR* Result = Property->ImportText_Direct(*ValueStr, ValuePtr, nullptr, PPF_None);
|
||||
if (!Result)
|
||||
{
|
||||
UMCPServer::Printf(TEXT("ERROR: Could not parse '%s' for parameter '%s'\n"), *ValueStr, *JsonKey);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return P.SetText(JsonValue->AsString());
|
||||
}
|
||||
|
||||
UMCPServer::Printf(TEXT("ERROR: '%s' must be a string, number, or boolean\n"), *JsonKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MCPJson::PopulateFromJson(
|
||||
TArray<MCPProperty>& Props, const FJsonObject* Json, bool AllOptional)
|
||||
{
|
||||
bool Ok = true;
|
||||
|
||||
// Build a set of known property names for the unknown-field check.
|
||||
TSet<FString> KnownKeys;
|
||||
for (const MCPProperty& P : Props)
|
||||
KnownKeys.Add(P.Prop->GetName());
|
||||
|
||||
// Check for unknown fields in the JSON
|
||||
for (const auto& KV : Json->Values)
|
||||
{
|
||||
if (!KnownKeys.Contains(KV.Key))
|
||||
{
|
||||
UMCPServer::Printf(TEXT("ERROR: Unknown parameter '%s'\n"), *KV.Key);
|
||||
Ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate each property from JSON
|
||||
for (MCPProperty& P : Props)
|
||||
{
|
||||
if (!PopulateFromJson(P, Json, AllOptional)) Ok = false;
|
||||
}
|
||||
return Ok;
|
||||
}
|
||||
|
||||
bool MCPJson::PopulateFromJson(
|
||||
UStruct* StructType, void* Container, const FJsonObject* Json)
|
||||
{
|
||||
TArray<MCPProperty> Props = MCPProperty::GetAll(StructType, Container, (EPropertyFlags)0);
|
||||
return PopulateFromJson(Props, Json);
|
||||
}
|
||||
|
||||
bool MCPJson::PopulateFromJson(
|
||||
UStruct* StructType, void* Container,
|
||||
const TSharedPtr<FJsonValue>& JsonValue)
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
#include "MCPProperty.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "MCPServer.h"
|
||||
#include "MCPTypes.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "Materials/MaterialExpression.h"
|
||||
#include "MaterialGraph/MaterialGraphNode.h"
|
||||
#include "EdGraph/EdGraphPin.h"
|
||||
|
||||
static bool IsPinTypeProperty(FProperty* Prop)
|
||||
{
|
||||
FStructProperty* StructProp = CastField<FStructProperty>(Prop);
|
||||
return StructProp && StructProp->Struct == FEdGraphPinType::StaticStruct();
|
||||
}
|
||||
|
||||
MCPProperty::MCPProperty(FProperty* InProp, void* InContainer)
|
||||
: Prop(InProp), Container(InContainer) {}
|
||||
|
||||
FString MCPProperty::GetText() const
|
||||
{
|
||||
FString Result;
|
||||
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
|
||||
if (IsPinTypeProperty(Prop))
|
||||
return UMCPTypes::TypeToText(*static_cast<FEdGraphPinType*>(ValuePtr));
|
||||
FString Result;
|
||||
Prop->ExportTextItem_Direct(Result, ValuePtr, nullptr, nullptr, PPF_None);
|
||||
return Result;
|
||||
}
|
||||
@@ -18,6 +29,8 @@ FString MCPProperty::GetText() const
|
||||
bool MCPProperty::SetText(const FString& Value)
|
||||
{
|
||||
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
|
||||
if (IsPinTypeProperty(Prop))
|
||||
return UMCPTypes::TextToType(Value, *static_cast<FEdGraphPinType*>(ValuePtr));
|
||||
const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, ValuePtr, nullptr, PPF_None);
|
||||
if (!ImportResult)
|
||||
{
|
||||
@@ -35,15 +48,20 @@ bool MCPProperty::SetText(const FString& Value)
|
||||
return true;
|
||||
}
|
||||
|
||||
void MCPProperty::Collect(UObject *Obj, TArray<MCPProperty> &Props, EPropertyFlags Flags)
|
||||
void MCPProperty::Collect(UStruct* StructType, void* Container, TArray<MCPProperty> &Props, EPropertyFlags Flags)
|
||||
{
|
||||
for (TFieldIterator<FProperty> It(Obj->GetClass()); It; ++It)
|
||||
for (TFieldIterator<FProperty> It(StructType); It; ++It)
|
||||
{
|
||||
if (Flags != 0 && !It->HasAnyPropertyFlags(Flags)) continue;
|
||||
Props.Emplace(*It, Obj);
|
||||
Props.Emplace(*It, Container);
|
||||
}
|
||||
}
|
||||
|
||||
void MCPProperty::Remove(TArray<MCPProperty>& Props, const FString& Name)
|
||||
{
|
||||
Props.RemoveAll([&](const MCPProperty& P) { return P.Prop->GetName() == Name; });
|
||||
}
|
||||
|
||||
TArray<MCPProperty> MCPProperty::GetAll(UObject* Obj, EPropertyFlags Flags)
|
||||
{
|
||||
if (!Obj) return {};
|
||||
@@ -63,7 +81,7 @@ TArray<MCPProperty> MCPProperty::GetAll(UObject* Obj, EPropertyFlags Flags)
|
||||
Obj = BP->GeneratedClass->GetDefaultObject();
|
||||
}
|
||||
|
||||
Collect(Obj, Result, Flags);
|
||||
Collect(Obj->GetClass(), Obj, Result, Flags);
|
||||
|
||||
// If it's a Material Graph node, also collect properties from
|
||||
// the associated material expression.
|
||||
@@ -72,12 +90,19 @@ TArray<MCPProperty> MCPProperty::GetAll(UObject* Obj, EPropertyFlags Flags)
|
||||
{
|
||||
if (UMaterialExpression* Expr = MatNode->MaterialExpression)
|
||||
{
|
||||
Collect(Expr, Result, Flags);
|
||||
Collect(Expr->GetClass(), Expr, Result, Flags);
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
TArray<MCPProperty> MCPProperty::GetAll(UStruct* StructType, void* Container, EPropertyFlags Flags)
|
||||
{
|
||||
TArray<MCPProperty> Result;
|
||||
Collect(StructType, Container, Result, Flags);
|
||||
return Result;
|
||||
}
|
||||
|
||||
TArray<MCPProperty> MCPProperty::GetAllSubstring(UObject* Obj, EPropertyFlags Flags, const FString& Substring)
|
||||
{
|
||||
TArray<MCPProperty> All = GetAll(Obj, Flags);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPProperty.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "BPVarEditor.generated.h"
|
||||
|
||||
// Editor-friendly view of a blueprint variable's properties.
|
||||
// Wraps an FBPVariableDescription, exposing commonly-used flags
|
||||
// and metadata as simple UPROPERTYs that the property system can
|
||||
// populate from JSON.
|
||||
USTRUCT()
|
||||
struct FBPVarEditor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
FBPVariableDescription* Desc = nullptr;
|
||||
|
||||
FBPVarEditor() = default;
|
||||
FBPVarEditor(FBPVariableDescription* InDesc) : Desc(InDesc) {}
|
||||
FBPVarEditor(UBlueprint* BP, const FString& VarName);
|
||||
|
||||
bool NotFound() const { return Desc == nullptr; }
|
||||
|
||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Variable description/tooltip"))
|
||||
FString Description;
|
||||
|
||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Allow editing on instances"))
|
||||
bool InstanceEditable = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Read-only in blueprints"))
|
||||
bool BlueprintReadOnly = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Expose as a pin when spawning"))
|
||||
bool ExposeOnSpawn = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Private to this blueprint"))
|
||||
bool Private = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, meta=(Optional, Description="Expose to cinematics/sequencer"))
|
||||
bool ExposeToCinematics = false;
|
||||
|
||||
// Load from Desc, populate from JSON, save back to Desc.
|
||||
bool LoadJson(const FJsonObject* Json);
|
||||
|
||||
// Print all properties and their current values.
|
||||
void Dump();
|
||||
|
||||
private:
|
||||
void Load();
|
||||
void Save() const;
|
||||
TArray<MCPProperty> MergedProperties();
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPProperty.h"
|
||||
#include "Dom/JsonObject.h"
|
||||
|
||||
// JSON utility functions used by MCP handlers.
|
||||
@@ -9,7 +10,8 @@
|
||||
class MCPJson
|
||||
{
|
||||
public:
|
||||
static bool PopulateFromJson(FProperty* Property, void* Container, const FJsonObject* Json);
|
||||
static bool PopulateFromJson(MCPProperty& Prop, const FJsonObject* Json, bool AllOptional = false);
|
||||
static bool PopulateFromJson(TArray<MCPProperty>& Props, const FJsonObject* Json, bool AllOptional = false);
|
||||
static bool PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr<FJsonValue>& JsonValue);
|
||||
static bool PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json);
|
||||
};
|
||||
|
||||
@@ -20,11 +20,13 @@ public:
|
||||
explicit operator bool() const { return Prop != nullptr; }
|
||||
FProperty* operator->() const { return Prop; }
|
||||
|
||||
static void Remove(TArray<MCPProperty>& Props, const FString& Name);
|
||||
static TArray<MCPProperty> GetAll(UObject* Obj, EPropertyFlags Flags);
|
||||
static TArray<MCPProperty> GetAll(UStruct* StructType, void* Container, EPropertyFlags Flags);
|
||||
static TArray<MCPProperty> GetAllSubstring(UObject* Obj, EPropertyFlags Flags, const FString& Substring);
|
||||
static TArray<MCPProperty> GetAllExactMatch(UObject* Obj, EPropertyFlags Flags, const FString& Name);
|
||||
static MCPProperty GetOneExactMatch(UObject* Obj, EPropertyFlags Flags, const FString& Name);
|
||||
|
||||
private:
|
||||
static void Collect(UObject *Obj, TArray<MCPProperty> &Props, EPropertyFlags Flags);
|
||||
static void Collect(UStruct* StructType, void* Container, TArray<MCPProperty> &Props, EPropertyFlags Flags);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user