BlueprintVariable handlers

This commit is contained in:
2026-03-15 22:24:10 -04:00
parent 2d4ccc6d29
commit 8d9ce48dae
16 changed files with 490 additions and 569 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -1,190 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Blueprint_SetVariableMetadata.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UMCP_Blueprint_SetVariableMetadata : 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=(Optional, Description="Category to assign the variable to"))
FString Category;
UPROPERTY(meta=(Optional, Description="Tooltip text for the variable"))
FString Tooltip;
UPROPERTY(meta=(Optional, Description="Replication mode: none, replicated, or repNotify"))
FString Replication;
UPROPERTY(meta=(Optional, Description="If true, expose this variable on spawn"))
bool ExposeOnSpawn = false;
UPROPERTY(meta=(Optional, Description="If true, mark the variable as private"))
bool IsPrivate = false;
UPROPERTY(meta=(Optional, Description="Editability mode: editAnywhere, editDefaultsOnly, editInstanceOnly, or none"))
FString Editability;
virtual FString GetDescription() const override
{
return TEXT("Set variable metadata properties such as category, tooltip, "
"replication, editability, and visibility flags.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
// Find the variable using Identifies for consistent name matching
FBPVariableDescription* VarDesc = nullptr;
for (FBPVariableDescription& Var : BP->NewVariables)
{
if (MCPUtils::FormatName(Var) == Variable ||
Var.VarName.ToString().Equals(Variable, ESearchCase::IgnoreCase))
{
VarDesc = &Var;
break;
}
}
if (!VarDesc)
{
Result.Appendf(TEXT("ERROR: Variable '%s' not found in %s.\nAvailable variables:\n"),
*Variable, *MCPUtils::FormatName(BP));
for (const FBPVariableDescription& Var : BP->NewVariables)
{
Result.Appendf(TEXT(" %s\n"), *MCPUtils::FormatName(Var));
}
return;
}
FName VarFName = VarDesc->VarName;
int32 ChangeCount = 0;
F.PreEdit();
// Category
if (Json->HasField(TEXT("category")))
{
VarDesc->Category = FText::FromString(Category);
FBlueprintEditorUtils::SetBlueprintVariableCategory(BP, VarFName, nullptr, FText::FromString(Category));
Result.Appendf(TEXT("Set category to '%s'.\n"), *Category);
ChangeCount++;
}
// Tooltip
if (Json->HasField(TEXT("tooltip")))
{
FBlueprintEditorUtils::SetBlueprintVariableMetaData(BP, VarFName, nullptr, TEXT("tooltip"), Tooltip);
Result.Appendf(TEXT("Set tooltip to '%s'.\n"), *Tooltip);
ChangeCount++;
}
// Replication
if (Json->HasField(TEXT("replication")))
{
if (Replication == TEXT("none"))
{
VarDesc->PropertyFlags &= ~CPF_Net;
VarDesc->PropertyFlags &= ~CPF_RepNotify;
VarDesc->RepNotifyFunc = NAME_None;
}
else if (Replication == TEXT("replicated"))
{
VarDesc->PropertyFlags |= CPF_Net;
VarDesc->PropertyFlags &= ~CPF_RepNotify;
VarDesc->RepNotifyFunc = NAME_None;
}
else if (Replication == TEXT("repNotify"))
{
VarDesc->PropertyFlags |= CPF_Net | CPF_RepNotify;
VarDesc->RepNotifyFunc = FName(*FString::Printf(TEXT("OnRep_%s"), *Variable));
}
else
{
Result.Appendf(TEXT("ERROR: Invalid replication value '%s'. Valid: none, replicated, repNotify\n"), *Replication);
return;
}
Result.Appendf(TEXT("Set replication to '%s'.\n"), *Replication);
ChangeCount++;
}
// ExposeOnSpawn
if (Json->HasField(TEXT("exposeOnSpawn")))
{
if (ExposeOnSpawn)
VarDesc->PropertyFlags |= CPF_ExposeOnSpawn;
else
VarDesc->PropertyFlags &= ~CPF_ExposeOnSpawn;
Result.Appendf(TEXT("Set exposeOnSpawn to %s.\n"), ExposeOnSpawn ? TEXT("true") : TEXT("false"));
ChangeCount++;
}
// isPrivate
if (Json->HasField(TEXT("isPrivate")))
{
FBlueprintEditorUtils::SetBlueprintVariableMetaData(BP, VarFName, nullptr,
TEXT("BlueprintPrivate"), IsPrivate ? TEXT("true") : TEXT("false"));
Result.Appendf(TEXT("Set isPrivate to %s.\n"), IsPrivate ? TEXT("true") : TEXT("false"));
ChangeCount++;
}
// Editability
if (Json->HasField(TEXT("editability")))
{
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"))
{
Result.Appendf(TEXT("ERROR: Invalid editability value '%s'. Valid: editAnywhere, editDefaultsOnly, editInstanceOnly, none\n"), *Editability);
return;
}
Result.Appendf(TEXT("Set editability to '%s'.\n"), *Editability);
ChangeCount++;
}
if (ChangeCount == 0)
{
Result.Append(TEXT("ERROR: No metadata fields specified. Provide at least one of: category, tooltip, replication, exposeOnSpawn, isPrivate, editability\n"));
return;
}
F.PostEdit();
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
Result.Appendf(TEXT("Updated %d field(s) on %s in %s.%s\n"),
ChangeCount, *VarFName.ToString(), *MCPUtils::FormatName(BP),
bSaved ? TEXT("") : TEXT(" WARNING: save failed."));
}
};

View File

@@ -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"));
}
};

View File

@@ -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"));
}
}
};

View File

@@ -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."));
}
};

View File

@@ -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));
}
};

View File

@@ -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));
}
};

View File

@@ -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();
}
};

View File

@@ -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));
}
};

View 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;
}

View File

@@ -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)

View File

@@ -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);

View File

@@ -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();
};

View File

@@ -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);
};

View File

@@ -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);
};