489 lines
16 KiB
C++
489 lines
16 KiB
C++
#pragma once
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "MCPHandler.h"
|
|
#include "MCPAssetFinder.h"
|
|
#include "MCPUtils.h"
|
|
#include "Engine/Blueprint.h"
|
|
#include "EdGraph/EdGraph.h"
|
|
#include "EdGraph/EdGraphPin.h"
|
|
#include "K2Node_FunctionEntry.h"
|
|
#include "K2Node_CustomEvent.h"
|
|
#include "K2Node_EditablePinBase.h"
|
|
#include "Kismet2/BlueprintEditorUtils.h"
|
|
#include "MCPHandlers_Params.generated.h"
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="change_function_parameter_type"))
|
|
class UMCPHandler_ChangeFunctionParamType : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
FString Blueprint;
|
|
|
|
UPROPERTY(meta=(Description="Name of the function or custom event"))
|
|
FString FunctionName;
|
|
|
|
UPROPERTY(meta=(Description="Name of the parameter to change"))
|
|
FString ParamName;
|
|
|
|
UPROPERTY(meta=(Description="New type for the parameter (e.g. 'Float', 'Vector', 'MyStruct')"))
|
|
FString NewType;
|
|
|
|
UPROPERTY(meta=(Optional, Description="If true, analyze impact without making changes"))
|
|
bool DryRun = false;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Change the type of an existing parameter on a function or custom event in a Blueprint.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
MCPAssets<UBlueprint> Assets;
|
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
UBlueprint* BP = Assets.Object();
|
|
|
|
// Resolve the new type using the shared resolver (supports primitives, structs, enums, and object references)
|
|
FEdGraphPinType NewPinType;
|
|
if (!MCPUtils::ResolveTypeFromString(NewType, NewPinType, Result))
|
|
return;
|
|
|
|
// Find the entry node: K2Node_FunctionEntry in a function graph,
|
|
// or K2Node_CustomEvent in any graph
|
|
UK2Node_EditablePinBase* EntryNode = nullptr;
|
|
FString FoundNodeType;
|
|
|
|
// Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name
|
|
for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
|
|
{
|
|
if (FuncEntry->GetGraph()->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
|
|
{
|
|
EntryNode = FuncEntry;
|
|
FoundNodeType = TEXT("FunctionEntry");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName
|
|
if (!EntryNode)
|
|
{
|
|
for (UK2Node_CustomEvent* CustomEvent : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
|
{
|
|
if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
|
|
{
|
|
EntryNode = CustomEvent;
|
|
FoundNodeType = TEXT("CustomEvent");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!EntryNode)
|
|
{
|
|
// List available functions/events for debugging
|
|
TArray<TSharedPtr<FJsonValue>> Available;
|
|
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
|
|
{
|
|
Available.Add(MakeShared<FJsonValueString>(
|
|
FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName())));
|
|
}
|
|
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
|
{
|
|
Available.Add(MakeShared<FJsonValueString>(
|
|
FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString())));
|
|
}
|
|
|
|
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
TEXT("Function or custom event '%s' not found in Blueprint '%s'"),
|
|
*FunctionName, *Blueprint));
|
|
Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available);
|
|
return;
|
|
}
|
|
|
|
// Find the UserDefinedPin matching paramName
|
|
bool bPinFound = false;
|
|
for (TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
|
|
{
|
|
if (PinInfo.IsValid() && PinInfo->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase))
|
|
{
|
|
PinInfo->PinType = NewPinType;
|
|
bPinFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!bPinFound)
|
|
{
|
|
// List available params for debugging
|
|
TArray<TSharedPtr<FJsonValue>> ParamNames;
|
|
for (const TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
|
|
{
|
|
if (PinInfo.IsValid())
|
|
{
|
|
ParamNames.Add(MakeShared<FJsonValueString>(PinInfo->PinName.ToString()));
|
|
}
|
|
}
|
|
|
|
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
TEXT("Parameter '%s' not found in %s '%s'"),
|
|
*ParamName, *FoundNodeType, *FunctionName));
|
|
Result->SetArrayField(TEXT("availableParams"), ParamNames);
|
|
return;
|
|
}
|
|
|
|
// Check for dry run
|
|
if (DryRun)
|
|
{
|
|
// Analyze what would change: report connected pins that may disconnect
|
|
TArray<TSharedPtr<FJsonValue>> AffectedPins;
|
|
for (UEdGraphPin* Pin : EntryNode->Pins)
|
|
{
|
|
if (Pin && Pin->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase) && Pin->LinkedTo.Num() > 0)
|
|
{
|
|
for (UEdGraphPin* Linked : Pin->LinkedTo)
|
|
{
|
|
if (Linked && Linked->GetOwningNode())
|
|
{
|
|
TSharedRef<FJsonObject> AffPin = MakeShared<FJsonObject>();
|
|
AffPin->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
|
|
AffPin->SetStringField(TEXT("connectedToNode"), Linked->GetOwningNode()->NodeGuid.ToString());
|
|
AffPin->SetStringField(TEXT("connectedToPin"), Linked->PinName.ToString());
|
|
AffPin->SetStringField(TEXT("currentType"), Pin->PinType.PinCategory.ToString());
|
|
if (Pin->PinType.PinSubCategoryObject.IsValid())
|
|
AffPin->SetStringField(TEXT("currentSubtype"), Pin->PinType.PinSubCategoryObject->GetName());
|
|
AffectedPins.Add(MakeShared<FJsonValueObject>(AffPin));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Result->SetBoolField(TEXT("dryRun"), true);
|
|
Result->SetStringField(TEXT("nodeType"), FoundNodeType);
|
|
Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString());
|
|
Result->SetNumberField(TEXT("connectionsAtRisk"), AffectedPins.Num());
|
|
Result->SetArrayField(TEXT("affectedPins"), AffectedPins);
|
|
return;
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Changing param '%s' in %s '%s' of '%s' to %s"),
|
|
*ParamName, *FoundNodeType, *FunctionName, *Blueprint, *NewType);
|
|
|
|
// Reconstruct the node to update output pins with the new type (use schema for MinimalAPI compat)
|
|
if (UEdGraph* OwningGraph = EntryNode->GetGraph())
|
|
{
|
|
if (const UEdGraphSchema* Schema = OwningGraph->GetSchema())
|
|
{
|
|
Schema->ReconstructNode(*EntryNode);
|
|
}
|
|
}
|
|
|
|
// Save
|
|
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter type changed, save %s"),
|
|
bSaved ? TEXT("succeeded") : TEXT("failed"));
|
|
|
|
// Serialize the updated entry node state
|
|
TSharedPtr<FJsonObject> UpdatedNodeState = MCPUtils::SerializeNode(EntryNode);
|
|
|
|
Result->SetStringField(TEXT("nodeType"), FoundNodeType);
|
|
Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString());
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
if (UpdatedNodeState.IsValid())
|
|
{
|
|
Result->SetObjectField(TEXT("updatedNode"), UpdatedNodeState);
|
|
}
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="remove_function_parameter"))
|
|
class UMCPHandler_RemoveFunctionParameter : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
FString Blueprint;
|
|
|
|
UPROPERTY(meta=(Description="Name of the function or custom event"))
|
|
FString FunctionName;
|
|
|
|
UPROPERTY(meta=(Description="Name of the parameter to remove"))
|
|
FString ParamName;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Remove a parameter from a function or custom event in a Blueprint.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
MCPAssets<UBlueprint> Assets;
|
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
UBlueprint* BP = Assets.Object();
|
|
|
|
// Find the entry node
|
|
UK2Node_EditablePinBase* EntryNode = nullptr;
|
|
FString FoundNodeType;
|
|
|
|
// Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name
|
|
for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
|
|
{
|
|
if (FuncEntry->GetGraph()->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
|
|
{
|
|
EntryNode = FuncEntry;
|
|
FoundNodeType = TEXT("FunctionEntry");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName
|
|
if (!EntryNode)
|
|
{
|
|
for (UK2Node_CustomEvent* CustomEvent : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
|
{
|
|
if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
|
|
{
|
|
EntryNode = CustomEvent;
|
|
FoundNodeType = TEXT("CustomEvent");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!EntryNode)
|
|
{
|
|
// List available functions/events for debugging
|
|
TArray<TSharedPtr<FJsonValue>> Available;
|
|
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
|
|
{
|
|
Available.Add(MakeShared<FJsonValueString>(
|
|
FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName())));
|
|
}
|
|
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
|
{
|
|
Available.Add(MakeShared<FJsonValueString>(
|
|
FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString())));
|
|
}
|
|
|
|
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
TEXT("Function or custom event '%s' not found in Blueprint '%s'"),
|
|
*FunctionName, *Blueprint));
|
|
Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available);
|
|
return;
|
|
}
|
|
|
|
// Find and remove the UserDefinedPin matching paramName
|
|
int32 RemovedIndex = INDEX_NONE;
|
|
for (int32 i = 0; i < EntryNode->UserDefinedPins.Num(); ++i)
|
|
{
|
|
if (EntryNode->UserDefinedPins[i].IsValid() &&
|
|
EntryNode->UserDefinedPins[i]->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase))
|
|
{
|
|
RemovedIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (RemovedIndex == INDEX_NONE)
|
|
{
|
|
// List available params for debugging
|
|
TArray<TSharedPtr<FJsonValue>> ParamNames;
|
|
for (const TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
|
|
{
|
|
if (PinInfo.IsValid())
|
|
{
|
|
ParamNames.Add(MakeShared<FJsonValueString>(PinInfo->PinName.ToString()));
|
|
}
|
|
}
|
|
|
|
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
TEXT("Parameter '%s' not found in %s '%s'"),
|
|
*ParamName, *FoundNodeType, *FunctionName));
|
|
Result->SetArrayField(TEXT("availableParams"), ParamNames);
|
|
return;
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing param '%s' from %s '%s' in '%s'"),
|
|
*ParamName, *FoundNodeType, *FunctionName, *Blueprint);
|
|
|
|
// Remove the pin
|
|
EntryNode->UserDefinedPins.RemoveAt(RemovedIndex);
|
|
|
|
// Reconstruct the node to update output pins (use schema for MinimalAPI compat)
|
|
if (UEdGraph* OwningGraph = EntryNode->GetGraph())
|
|
{
|
|
if (const UEdGraphSchema* Schema = OwningGraph->GetSchema())
|
|
{
|
|
Schema->ReconstructNode(*EntryNode);
|
|
}
|
|
}
|
|
|
|
// Save
|
|
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter removed, save %s"),
|
|
bSaved ? TEXT("succeeded") : TEXT("failed"));
|
|
|
|
Result->SetStringField(TEXT("nodeType"), FoundNodeType);
|
|
Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString());
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="add_function_parameter"))
|
|
class UMCPHandler_AddFunctionParameter : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
FString Blueprint;
|
|
|
|
UPROPERTY(meta=(Description="Name of the function, custom event, or event dispatcher"))
|
|
FString FunctionName;
|
|
|
|
UPROPERTY(meta=(Description="Name for the new parameter"))
|
|
FString ParamName;
|
|
|
|
UPROPERTY(meta=(Description="Type for the new parameter (e.g. 'Float', 'Vector', 'MyStruct')"))
|
|
FString ParamType;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Add a new parameter to a function, custom event, or event dispatcher in a Blueprint.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
MCPAssets<UBlueprint> Assets;
|
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
UBlueprint* BP = Assets.Object();
|
|
|
|
// Resolve param type
|
|
FEdGraphPinType PinType;
|
|
if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, Result))
|
|
return;
|
|
|
|
// Find the entry node using 3 strategies
|
|
UK2Node_EditablePinBase* EntryNode = nullptr;
|
|
FString NodeType;
|
|
|
|
FName FuncFName(*FunctionName);
|
|
|
|
// Strategy 1: K2Node_FunctionEntry in function graphs
|
|
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
|
|
{
|
|
UEdGraph* FEGraph = FE->GetGraph();
|
|
if (!FEGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) continue;
|
|
// Skip delegate signature graphs (handled in Strategy 3)
|
|
if (BP->DelegateSignatureGraphs.Contains(FEGraph)) continue;
|
|
|
|
EntryNode = FE;
|
|
NodeType = TEXT("FunctionEntry");
|
|
break;
|
|
}
|
|
|
|
// Strategy 2: K2Node_CustomEvent with matching CustomFunctionName
|
|
if (!EntryNode)
|
|
{
|
|
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
|
{
|
|
if (CE->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
|
|
{
|
|
EntryNode = CE;
|
|
NodeType = TEXT("CustomEvent");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Strategy 3: K2Node_FunctionEntry in DelegateSignatureGraphs
|
|
if (!EntryNode)
|
|
{
|
|
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
|
|
{
|
|
UEdGraph* FEGraph = FE->GetGraph();
|
|
if (!FEGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) continue;
|
|
if (!BP->DelegateSignatureGraphs.Contains(FEGraph)) continue;
|
|
|
|
EntryNode = FE;
|
|
NodeType = TEXT("EventDispatcher");
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!EntryNode)
|
|
{
|
|
// Build a helpful error listing available functions, events, and dispatchers
|
|
TArray<TSharedPtr<FJsonValue>> AvailFuncs;
|
|
|
|
for (UEdGraph* Graph : BP->FunctionGraphs)
|
|
{
|
|
if (Graph) AvailFuncs.Add(MakeShared<FJsonValueString>(Graph->GetName()));
|
|
}
|
|
|
|
// Custom events
|
|
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
|
{
|
|
AvailFuncs.Add(MakeShared<FJsonValueString>(
|
|
FString::Printf(TEXT("%s (custom event)"), *CE->CustomFunctionName.ToString())));
|
|
}
|
|
|
|
// Dispatchers
|
|
TSet<FName> DelegateNames;
|
|
FBlueprintEditorUtils::GetDelegateNameList(BP, DelegateNames);
|
|
for (const FName& DN : DelegateNames)
|
|
{
|
|
AvailFuncs.Add(MakeShared<FJsonValueString>(
|
|
FString::Printf(TEXT("%s (event dispatcher)"), *DN.ToString())));
|
|
}
|
|
|
|
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
TEXT("Function, custom event, or event dispatcher '%s' not found in Blueprint '%s'"),
|
|
*FunctionName, *Blueprint));
|
|
Result->SetArrayField(TEXT("availableFunctions"), AvailFuncs);
|
|
return;
|
|
}
|
|
|
|
// Check for duplicate parameter name
|
|
for (const TSharedPtr<FUserPinInfo>& Existing : EntryNode->UserDefinedPins)
|
|
{
|
|
if (Existing.IsValid() && Existing->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase))
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
TEXT("Parameter '%s' already exists on '%s'"), *ParamName, *FunctionName));
|
|
}
|
|
}
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding parameter '%s' (type=%s) to %s '%s' in Blueprint '%s'"),
|
|
*ParamName, *ParamType, *NodeType, *FunctionName, *Blueprint);
|
|
|
|
// Add the parameter pin (EGPD_Output on entry = input to callers)
|
|
EntryNode->CreateUserDefinedPin(FName(*ParamName), PinType, EGPD_Output);
|
|
|
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added parameter '%s' to '%s' in '%s' (saved: %s)"),
|
|
*ParamName, *FunctionName, *Blueprint, bSaved ? TEXT("true") : TEXT("false"));
|
|
|
|
Result->SetStringField(TEXT("nodeType"), NodeType);
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
}
|
|
};
|