|
|
|
|
@@ -804,196 +804,104 @@ void FBlueprintMCPServer::HandleRefreshAllNodes(const FJsonObject* Json, FJsonOb
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// HandleSetPinDefault — set the default value of a pin on a node
|
|
|
|
|
// SetPinDefault — set the default value of a pin on a node
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void FBlueprintMCPServer::HandleSetPinDefault(const FJsonObject* Json, FJsonObject* Result)
|
|
|
|
|
void UMCPHandler_SetPinDefault::Handle(const FJsonObject* Json, FJsonObject* Result)
|
|
|
|
|
{
|
|
|
|
|
// Check for batch mode
|
|
|
|
|
const TArray<TSharedPtr<FJsonValue>>* BatchArray = nullptr;
|
|
|
|
|
if (Json->TryGetArrayField(TEXT("batch"), BatchArray) && BatchArray && BatchArray->Num() > 0)
|
|
|
|
|
MCPHelper* Helper = MCPHelper::Get();
|
|
|
|
|
|
|
|
|
|
TArray<TSharedPtr<FJsonValue>> Results;
|
|
|
|
|
int32 SuccessCount = 0;
|
|
|
|
|
TSet<UEdGraphNode*> ModifiedNodes;
|
|
|
|
|
TSet<UBlueprint*> ModifiedBlueprints;
|
|
|
|
|
|
|
|
|
|
for (const TSharedPtr<FJsonValue>& PinVal : Pins.Array)
|
|
|
|
|
{
|
|
|
|
|
// Batch mode: process multiple pin default operations
|
|
|
|
|
TArray<TSharedPtr<FJsonValue>> Results;
|
|
|
|
|
int32 SuccessCount = 0;
|
|
|
|
|
TSet<UBlueprint*> ModifiedBlueprints;
|
|
|
|
|
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
|
|
|
|
|
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
|
|
|
|
|
|
|
|
|
|
for (const TSharedPtr<FJsonValue>& OpVal : *BatchArray)
|
|
|
|
|
FSetPinDefaultEntry Entry;
|
|
|
|
|
FString PopulateError = Helper->PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal);
|
|
|
|
|
if (!PopulateError.IsEmpty())
|
|
|
|
|
{
|
|
|
|
|
TSharedPtr<FJsonObject> OpObj = OpVal->AsObject();
|
|
|
|
|
if (!OpObj.IsValid())
|
|
|
|
|
{
|
|
|
|
|
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
|
|
|
Entry->SetStringField(TEXT("error"), TEXT("Invalid batch entry"));
|
|
|
|
|
Results.Add(MakeShared<FJsonValueObject>(Entry));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FString OpBlueprint = OpObj->GetStringField(TEXT("blueprint"));
|
|
|
|
|
FString OpNodeId = OpObj->GetStringField(TEXT("nodeId"));
|
|
|
|
|
FString OpPinName = OpObj->GetStringField(TEXT("pinName"));
|
|
|
|
|
FString OpValue = OpObj->GetStringField(TEXT("value"));
|
|
|
|
|
|
|
|
|
|
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
|
|
|
Entry->SetStringField(TEXT("blueprint"), OpBlueprint);
|
|
|
|
|
Entry->SetStringField(TEXT("nodeId"), OpNodeId);
|
|
|
|
|
Entry->SetStringField(TEXT("pinName"), OpPinName);
|
|
|
|
|
|
|
|
|
|
if (OpBlueprint.IsEmpty() || OpNodeId.IsEmpty() || OpPinName.IsEmpty())
|
|
|
|
|
{
|
|
|
|
|
Entry->SetStringField(TEXT("error"), TEXT("Missing required fields: blueprint, nodeId, pinName"));
|
|
|
|
|
Results.Add(MakeShared<FJsonValueObject>(Entry));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FString LoadError;
|
|
|
|
|
UBlueprint* BP = LoadBlueprintByName(OpBlueprint, LoadError);
|
|
|
|
|
if (!BP)
|
|
|
|
|
{
|
|
|
|
|
Entry->SetStringField(TEXT("error"), LoadError);
|
|
|
|
|
Results.Add(MakeShared<FJsonValueObject>(Entry));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UEdGraph* Graph = nullptr;
|
|
|
|
|
UEdGraphNode* Node = FindNodeByGuid(BP, OpNodeId, &Graph);
|
|
|
|
|
if (!Node)
|
|
|
|
|
{
|
|
|
|
|
Entry->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *OpNodeId));
|
|
|
|
|
Results.Add(MakeShared<FJsonValueObject>(Entry));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UEdGraphPin* Pin = Node->FindPin(FName(*OpPinName));
|
|
|
|
|
if (!Pin)
|
|
|
|
|
{
|
|
|
|
|
Entry->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *OpPinName, *OpNodeId));
|
|
|
|
|
Results.Add(MakeShared<FJsonValueObject>(Entry));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Pin->Direction != EGPD_Input)
|
|
|
|
|
{
|
|
|
|
|
Entry->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' is an output pin"), *OpPinName));
|
|
|
|
|
Results.Add(MakeShared<FJsonValueObject>(Entry));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FString OldValue = Pin->DefaultValue;
|
|
|
|
|
|
|
|
|
|
const UEdGraphSchema* Schema = Graph->GetSchema();
|
|
|
|
|
if (Schema)
|
|
|
|
|
{
|
|
|
|
|
Schema->TrySetDefaultValue(*Pin, OpValue);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Pin->DefaultValue = OpValue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Entry->SetBoolField(TEXT("success"), true);
|
|
|
|
|
Entry->SetStringField(TEXT("oldValue"), OldValue);
|
|
|
|
|
Entry->SetStringField(TEXT("newValue"), Pin->DefaultValue);
|
|
|
|
|
Results.Add(MakeShared<FJsonValueObject>(Entry));
|
|
|
|
|
SuccessCount++;
|
|
|
|
|
ModifiedBlueprints.Add(BP);
|
|
|
|
|
EntryResult->SetStringField(TEXT("error"), PopulateError);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Save all modified blueprints
|
|
|
|
|
bool bAllSaved = true;
|
|
|
|
|
for (UBlueprint* BP : ModifiedBlueprints)
|
|
|
|
|
EntryResult->SetStringField(TEXT("blueprint"), Entry.Blueprint);
|
|
|
|
|
EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId);
|
|
|
|
|
EntryResult->SetStringField(TEXT("pinName"), Entry.PinName);
|
|
|
|
|
|
|
|
|
|
FString LoadError;
|
|
|
|
|
UBlueprint* BP = Helper->LoadBlueprintByName(Entry.Blueprint, LoadError);
|
|
|
|
|
if (!BP)
|
|
|
|
|
{
|
|
|
|
|
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
|
|
|
|
FKismetEditorUtilities::CompileBlueprint(BP);
|
|
|
|
|
if (!SaveBlueprintPackage(BP))
|
|
|
|
|
EntryResult->SetStringField(TEXT("error"), LoadError);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UEdGraph* Graph = nullptr;
|
|
|
|
|
UEdGraphNode* Node = Helper->FindNodeByGuid(BP, Entry.NodeId, &Graph);
|
|
|
|
|
if (!Node)
|
|
|
|
|
{
|
|
|
|
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UEdGraphPin* Pin = Node->FindPin(FName(*Entry.PinName));
|
|
|
|
|
if (!Pin)
|
|
|
|
|
{
|
|
|
|
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *Entry.PinName, *Entry.NodeId));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Pin->Direction != EGPD_Input)
|
|
|
|
|
{
|
|
|
|
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' is an output pin"), *Entry.PinName));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const UEdGraphSchema* Schema = Graph->GetSchema();
|
|
|
|
|
if (Schema)
|
|
|
|
|
{
|
|
|
|
|
FString ValidationError = Schema->IsPinDefaultValid(Pin, Entry.Value, nullptr, FText::GetEmpty());
|
|
|
|
|
if (!ValidationError.IsEmpty())
|
|
|
|
|
{
|
|
|
|
|
bAllSaved = false;
|
|
|
|
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(
|
|
|
|
|
TEXT("Invalid value for pin '%s': %s"), *Entry.PinName, *ValidationError));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Batch SetPinDefault — %d/%d succeeded, save %s"),
|
|
|
|
|
SuccessCount, BatchArray->Num(), bAllSaved ? TEXT("true") : TEXT("false"));
|
|
|
|
|
FString OldValue = Pin->DefaultValue;
|
|
|
|
|
Pin->DefaultValue = Entry.Value;
|
|
|
|
|
|
|
|
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
|
|
|
Result->SetNumberField(TEXT("successCount"), SuccessCount);
|
|
|
|
|
Result->SetNumberField(TEXT("totalCount"), BatchArray->Num());
|
|
|
|
|
Result->SetArrayField(TEXT("results"), Results);
|
|
|
|
|
Result->SetBoolField(TEXT("saved"), bAllSaved);
|
|
|
|
|
return;
|
|
|
|
|
EntryResult->SetBoolField(TEXT("success"), true);
|
|
|
|
|
EntryResult->SetStringField(TEXT("oldValue"), OldValue);
|
|
|
|
|
EntryResult->SetStringField(TEXT("newValue"), Pin->DefaultValue);
|
|
|
|
|
SuccessCount++;
|
|
|
|
|
ModifiedNodes.Add(Node);
|
|
|
|
|
ModifiedBlueprints.Add(BP);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Single-pin mode (existing logic)
|
|
|
|
|
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
|
|
|
|
FString NodeId = Json->GetStringField(TEXT("nodeId"));
|
|
|
|
|
FString PinName = Json->GetStringField(TEXT("pinName"));
|
|
|
|
|
FString Value = Json->GetStringField(TEXT("value"));
|
|
|
|
|
|
|
|
|
|
if (BlueprintName.IsEmpty() || NodeId.IsEmpty() || PinName.IsEmpty())
|
|
|
|
|
for (UEdGraphNode* Node : ModifiedNodes)
|
|
|
|
|
{
|
|
|
|
|
return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, nodeId, pinName"));
|
|
|
|
|
Node->ReconstructNode();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load Blueprint
|
|
|
|
|
FString LoadError;
|
|
|
|
|
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
|
|
|
|
|
if (!BP)
|
|
|
|
|
for (UBlueprint* BP : ModifiedBlueprints)
|
|
|
|
|
{
|
|
|
|
|
return MakeErrorJson(Result, LoadError);
|
|
|
|
|
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find node
|
|
|
|
|
UEdGraph* Graph = nullptr;
|
|
|
|
|
UEdGraphNode* Node = FindNodeByGuid(BP, NodeId, &Graph);
|
|
|
|
|
if (!Node)
|
|
|
|
|
{
|
|
|
|
|
return MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find pin
|
|
|
|
|
UEdGraphPin* Pin = Node->FindPin(FName(*PinName));
|
|
|
|
|
if (!Pin)
|
|
|
|
|
{
|
|
|
|
|
return MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *PinName, *NodeId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only allow setting defaults on input pins
|
|
|
|
|
if (Pin->Direction != EGPD_Input)
|
|
|
|
|
{
|
|
|
|
|
return MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' is an output pin — can only set defaults on input pins"), *PinName));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Store old value for reporting
|
|
|
|
|
FString OldValue = Pin->DefaultValue;
|
|
|
|
|
|
|
|
|
|
// Use the schema to set the default value (handles type validation)
|
|
|
|
|
const UEdGraphSchema* Schema = Graph->GetSchema();
|
|
|
|
|
if (Schema)
|
|
|
|
|
{
|
|
|
|
|
Schema->TrySetDefaultValue(*Pin, Value);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Fallback: set directly
|
|
|
|
|
Pin->DefaultValue = Value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SetPinDefault on '%s' pin '%s': '%s' -> '%s'"),
|
|
|
|
|
*Node->GetNodeTitle(ENodeTitleType::ListView).ToString(), *PinName, *OldValue, *Value);
|
|
|
|
|
|
|
|
|
|
// Mark modified and compile
|
|
|
|
|
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
|
|
|
|
FKismetEditorUtilities::CompileBlueprint(BP);
|
|
|
|
|
|
|
|
|
|
// Save
|
|
|
|
|
bool bSaved = SaveBlueprintPackage(BP);
|
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SetPinDefault — %d/%d succeeded"),
|
|
|
|
|
SuccessCount, Pins.Array.Num());
|
|
|
|
|
|
|
|
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
|
|
|
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
|
|
|
|
Result->SetStringField(TEXT("nodeId"), NodeId);
|
|
|
|
|
Result->SetStringField(TEXT("pinName"), PinName);
|
|
|
|
|
Result->SetStringField(TEXT("oldValue"), OldValue);
|
|
|
|
|
Result->SetStringField(TEXT("newValue"), Pin->DefaultValue);
|
|
|
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
|
|
|
Result->SetNumberField(TEXT("successCount"), SuccessCount);
|
|
|
|
|
Result->SetNumberField(TEXT("totalCount"), Pins.Array.Num());
|
|
|
|
|
Result->SetArrayField(TEXT("results"), Results);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
@@ -2109,191 +2017,119 @@ void FBlueprintMCPServer::HandleSetBlueprintDefault(const FJsonObject* Json, FJs
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// HandleMoveNode — reposition one or more nodes in a blueprint graph
|
|
|
|
|
// MoveNode — reposition one or more nodes in a blueprint graph
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void FBlueprintMCPServer::HandleMoveNode(const FJsonObject* Json, FJsonObject* Result)
|
|
|
|
|
void UMCPHandler_MoveNode::Handle(const FJsonObject* Json, FJsonObject* Result)
|
|
|
|
|
{
|
|
|
|
|
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
|
|
|
|
if (BlueprintName.IsEmpty())
|
|
|
|
|
{
|
|
|
|
|
return MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
|
|
|
|
|
}
|
|
|
|
|
MCPHelper* Helper = MCPHelper::Get();
|
|
|
|
|
|
|
|
|
|
// Load Blueprint
|
|
|
|
|
FString LoadError;
|
|
|
|
|
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
|
|
|
|
|
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
|
|
|
|
|
if (!BP)
|
|
|
|
|
{
|
|
|
|
|
return MakeErrorJson(Result, LoadError);
|
|
|
|
|
return Helper->MakeErrorJson(Result, LoadError);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for batch mode
|
|
|
|
|
const TArray<TSharedPtr<FJsonValue>>* NodesArray = nullptr;
|
|
|
|
|
bool bBatchMode = Json->TryGetArrayField(TEXT("nodes"), NodesArray) && NodesArray && NodesArray->Num() > 0;
|
|
|
|
|
TArray<TSharedPtr<FJsonValue>> Results;
|
|
|
|
|
int32 SuccessCount = 0;
|
|
|
|
|
|
|
|
|
|
if (bBatchMode)
|
|
|
|
|
for (const TSharedPtr<FJsonValue>& NodeVal : Nodes.Array)
|
|
|
|
|
{
|
|
|
|
|
TArray<TSharedPtr<FJsonValue>> Results;
|
|
|
|
|
int32 SuccessCount = 0;
|
|
|
|
|
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
|
|
|
|
|
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
|
|
|
|
|
|
|
|
|
|
for (const TSharedPtr<FJsonValue>& NodeVal : *NodesArray)
|
|
|
|
|
FMoveNodeEntry Entry;
|
|
|
|
|
FString PopulateError = Helper->PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal);
|
|
|
|
|
if (!PopulateError.IsEmpty())
|
|
|
|
|
{
|
|
|
|
|
TSharedPtr<FJsonObject> NodeObj = NodeVal->AsObject();
|
|
|
|
|
if (!NodeObj.IsValid()) continue;
|
|
|
|
|
|
|
|
|
|
FString NodeId = NodeObj->GetStringField(TEXT("nodeId"));
|
|
|
|
|
int32 X = (int32)NodeObj->GetNumberField(TEXT("x"));
|
|
|
|
|
int32 Y = (int32)NodeObj->GetNumberField(TEXT("y"));
|
|
|
|
|
|
|
|
|
|
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
|
|
|
|
|
EntryResult->SetStringField(TEXT("nodeId"), NodeId);
|
|
|
|
|
|
|
|
|
|
UEdGraphNode* Node = FindNodeByGuid(BP, NodeId);
|
|
|
|
|
if (!Node)
|
|
|
|
|
{
|
|
|
|
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *NodeId));
|
|
|
|
|
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int32 OldX = Node->NodePosX;
|
|
|
|
|
int32 OldY = Node->NodePosY;
|
|
|
|
|
Node->NodePosX = X;
|
|
|
|
|
Node->NodePosY = Y;
|
|
|
|
|
EntryResult->SetBoolField(TEXT("success"), true);
|
|
|
|
|
EntryResult->SetNumberField(TEXT("oldX"), OldX);
|
|
|
|
|
EntryResult->SetNumberField(TEXT("oldY"), OldY);
|
|
|
|
|
EntryResult->SetNumberField(TEXT("newX"), Node->NodePosX);
|
|
|
|
|
EntryResult->SetNumberField(TEXT("newY"), Node->NodePosY);
|
|
|
|
|
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
|
|
|
|
|
SuccessCount++;
|
|
|
|
|
EntryResult->SetStringField(TEXT("error"), PopulateError);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
|
|
|
|
bool bSaved = SaveBlueprintPackage(BP);
|
|
|
|
|
EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId);
|
|
|
|
|
|
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: MoveNode batch — %d/%d succeeded, save %s"),
|
|
|
|
|
SuccessCount, NodesArray->Num(), bSaved ? TEXT("true") : TEXT("false"));
|
|
|
|
|
UEdGraphNode* Node = Helper->FindNodeByGuid(BP, Entry.NodeId);
|
|
|
|
|
if (!Node)
|
|
|
|
|
{
|
|
|
|
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
|
|
|
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
|
|
|
|
Result->SetNumberField(TEXT("movedCount"), SuccessCount);
|
|
|
|
|
Result->SetNumberField(TEXT("totalRequested"), NodesArray->Num());
|
|
|
|
|
Result->SetArrayField(TEXT("results"), Results);
|
|
|
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
|
|
|
return;
|
|
|
|
|
int32 OldX = Node->NodePosX;
|
|
|
|
|
int32 OldY = Node->NodePosY;
|
|
|
|
|
Node->NodePosX = Entry.X;
|
|
|
|
|
Node->NodePosY = Entry.Y;
|
|
|
|
|
EntryResult->SetBoolField(TEXT("success"), true);
|
|
|
|
|
EntryResult->SetNumberField(TEXT("oldX"), OldX);
|
|
|
|
|
EntryResult->SetNumberField(TEXT("oldY"), OldY);
|
|
|
|
|
EntryResult->SetNumberField(TEXT("newX"), Node->NodePosX);
|
|
|
|
|
EntryResult->SetNumberField(TEXT("newY"), Node->NodePosY);
|
|
|
|
|
SuccessCount++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Single node mode
|
|
|
|
|
FString NodeId = Json->GetStringField(TEXT("nodeId"));
|
|
|
|
|
if (NodeId.IsEmpty())
|
|
|
|
|
{
|
|
|
|
|
return MakeErrorJson(Result, TEXT("Missing required field: nodeId (or use 'nodes' array for batch mode)"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!Json->HasField(TEXT("x")) || !Json->HasField(TEXT("y")))
|
|
|
|
|
{
|
|
|
|
|
return MakeErrorJson(Result, TEXT("Missing required fields: x, y"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int32 X = (int32)Json->GetNumberField(TEXT("x"));
|
|
|
|
|
int32 Y = (int32)Json->GetNumberField(TEXT("y"));
|
|
|
|
|
|
|
|
|
|
UEdGraphNode* Node = FindNodeByGuid(BP, NodeId);
|
|
|
|
|
if (!Node)
|
|
|
|
|
{
|
|
|
|
|
return MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int32 OldX = Node->NodePosX;
|
|
|
|
|
int32 OldY = Node->NodePosY;
|
|
|
|
|
Node->NodePosX = X;
|
|
|
|
|
Node->NodePosY = Y;
|
|
|
|
|
|
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: MoveNode '%s' from (%d,%d) to (%d,%d)"),
|
|
|
|
|
*NodeId, OldX, OldY, X, Y);
|
|
|
|
|
|
|
|
|
|
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
|
|
|
|
bool bSaved = SaveBlueprintPackage(BP);
|
|
|
|
|
|
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: MoveNode — %d/%d succeeded"),
|
|
|
|
|
SuccessCount, Nodes.Array.Num());
|
|
|
|
|
|
|
|
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
|
|
|
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
|
|
|
|
Result->SetStringField(TEXT("nodeId"), NodeId);
|
|
|
|
|
Result->SetNumberField(TEXT("oldX"), OldX);
|
|
|
|
|
Result->SetNumberField(TEXT("oldY"), OldY);
|
|
|
|
|
Result->SetNumberField(TEXT("newX"), Node->NodePosX);
|
|
|
|
|
Result->SetNumberField(TEXT("newY"), Node->NodePosY);
|
|
|
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
|
|
|
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
|
|
|
|
Result->SetNumberField(TEXT("movedCount"), SuccessCount);
|
|
|
|
|
Result->SetNumberField(TEXT("totalRequested"), Nodes.Array.Num());
|
|
|
|
|
Result->SetArrayField(TEXT("results"), Results);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// HandleDuplicateNodes — duplicate one or more nodes in a graph
|
|
|
|
|
// DuplicateNodes — duplicate one or more nodes in a graph
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void FBlueprintMCPServer::HandleDuplicateNodes(const FJsonObject* Json, FJsonObject* Result)
|
|
|
|
|
void UMCPHandler_DuplicateNodes::Handle(const FJsonObject* Json, FJsonObject* Result)
|
|
|
|
|
{
|
|
|
|
|
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
|
|
|
|
FString GraphName = Json->GetStringField(TEXT("graph"));
|
|
|
|
|
MCPHelper* Helper = MCPHelper::Get();
|
|
|
|
|
|
|
|
|
|
if (BlueprintName.IsEmpty() || GraphName.IsEmpty())
|
|
|
|
|
{
|
|
|
|
|
return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get node IDs
|
|
|
|
|
const TArray<TSharedPtr<FJsonValue>>* NodeIdsArray = nullptr;
|
|
|
|
|
if (!Json->TryGetArrayField(TEXT("nodeIds"), NodeIdsArray) || !NodeIdsArray || NodeIdsArray->Num() == 0)
|
|
|
|
|
{
|
|
|
|
|
return MakeErrorJson(Result, TEXT("Missing required field: nodeIds (array of node GUIDs)"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int32 OffsetX = 50;
|
|
|
|
|
int32 OffsetY = 50;
|
|
|
|
|
if (Json->HasField(TEXT("offsetX")))
|
|
|
|
|
OffsetX = (int32)Json->GetNumberField(TEXT("offsetX"));
|
|
|
|
|
if (Json->HasField(TEXT("offsetY")))
|
|
|
|
|
OffsetY = (int32)Json->GetNumberField(TEXT("offsetY"));
|
|
|
|
|
|
|
|
|
|
// Load Blueprint
|
|
|
|
|
FString LoadError;
|
|
|
|
|
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
|
|
|
|
|
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
|
|
|
|
|
if (!BP)
|
|
|
|
|
{
|
|
|
|
|
return MakeErrorJson(Result, LoadError);
|
|
|
|
|
return Helper->MakeErrorJson(Result, LoadError);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find the target graph
|
|
|
|
|
FString DecodedGraphName = UrlDecode(GraphName);
|
|
|
|
|
FString DecodedGraphName = MCPHelper::UrlDecode(Graph);
|
|
|
|
|
UEdGraph* TargetGraph = nullptr;
|
|
|
|
|
TArray<UEdGraph*> AllGraphs;
|
|
|
|
|
BP->GetAllGraphs(AllGraphs);
|
|
|
|
|
|
|
|
|
|
for (UEdGraph* Graph : AllGraphs)
|
|
|
|
|
for (UEdGraph* G : AllGraphs)
|
|
|
|
|
{
|
|
|
|
|
if (Graph && Graph->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
|
|
|
|
|
if (G && G->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
TargetGraph = Graph;
|
|
|
|
|
TargetGraph = G;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!TargetGraph)
|
|
|
|
|
{
|
|
|
|
|
return MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
|
|
|
|
|
return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (NodeIds.Array.Num() == 0)
|
|
|
|
|
{
|
|
|
|
|
return Helper->MakeErrorJson(Result, TEXT("nodeIds array is empty"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find all source nodes
|
|
|
|
|
TArray<UEdGraphNode*> SourceNodes;
|
|
|
|
|
TArray<FString> NotFound;
|
|
|
|
|
|
|
|
|
|
for (const TSharedPtr<FJsonValue>& IdVal : *NodeIdsArray)
|
|
|
|
|
for (const TSharedPtr<FJsonValue>& IdVal : NodeIds.Array)
|
|
|
|
|
{
|
|
|
|
|
FString NodeId = IdVal->AsString();
|
|
|
|
|
UEdGraphNode* Node = FindNodeByGuid(BP, NodeId);
|
|
|
|
|
UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId);
|
|
|
|
|
if (Node)
|
|
|
|
|
{
|
|
|
|
|
// Verify it's in the target graph
|
|
|
|
|
if (Node->GetGraph() == TargetGraph)
|
|
|
|
|
{
|
|
|
|
|
SourceNodes.Add(Node);
|
|
|
|
|
@@ -2311,11 +2147,11 @@ void FBlueprintMCPServer::HandleDuplicateNodes(const FJsonObject* Json, FJsonObj
|
|
|
|
|
|
|
|
|
|
if (SourceNodes.Num() == 0)
|
|
|
|
|
{
|
|
|
|
|
return MakeErrorJson(Result, TEXT("No valid nodes found to duplicate"));
|
|
|
|
|
return Helper->MakeErrorJson(Result, TEXT("No valid nodes found to duplicate"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Duplicating %d node(s) in graph '%s' of '%s'"),
|
|
|
|
|
SourceNodes.Num(), *DecodedGraphName, *BlueprintName);
|
|
|
|
|
SourceNodes.Num(), *DecodedGraphName, *Blueprint);
|
|
|
|
|
|
|
|
|
|
// Duplicate each node
|
|
|
|
|
TArray<TSharedPtr<FJsonValue>> DuplicatedNodes;
|
|
|
|
|
@@ -2323,7 +2159,6 @@ void FBlueprintMCPServer::HandleDuplicateNodes(const FJsonObject* Json, FJsonObj
|
|
|
|
|
|
|
|
|
|
for (UEdGraphNode* SourceNode : SourceNodes)
|
|
|
|
|
{
|
|
|
|
|
// Duplicate the node using DuplicateObject
|
|
|
|
|
UEdGraphNode* NewNode = DuplicateObject<UEdGraphNode>(SourceNode, TargetGraph);
|
|
|
|
|
if (!NewNode)
|
|
|
|
|
{
|
|
|
|
|
@@ -2334,15 +2169,12 @@ void FBlueprintMCPServer::HandleDuplicateNodes(const FJsonObject* Json, FJsonObj
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Assign new GUID
|
|
|
|
|
NewNode->CreateNewGuid();
|
|
|
|
|
OldToNewGuidMap.Add(SourceNode->NodeGuid, NewNode->NodeGuid);
|
|
|
|
|
|
|
|
|
|
// Offset position
|
|
|
|
|
NewNode->NodePosX += OffsetX;
|
|
|
|
|
NewNode->NodePosY += OffsetY;
|
|
|
|
|
|
|
|
|
|
// Break all connections on the duplicate (they point to old pin instances)
|
|
|
|
|
for (UEdGraphPin* Pin : NewNode->Pins)
|
|
|
|
|
{
|
|
|
|
|
if (Pin)
|
|
|
|
|
@@ -2351,7 +2183,6 @@ void FBlueprintMCPServer::HandleDuplicateNodes(const FJsonObject* Json, FJsonObj
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add to graph
|
|
|
|
|
TargetGraph->AddNode(NewNode, false, false);
|
|
|
|
|
|
|
|
|
|
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
|
|
|
@@ -2365,17 +2196,15 @@ void FBlueprintMCPServer::HandleDuplicateNodes(const FJsonObject* Json, FJsonObj
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
|
|
|
bool bSaved = SaveBlueprintPackage(BP);
|
|
|
|
|
|
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Duplicated %d node(s), save %s"),
|
|
|
|
|
DuplicatedNodes.Num(), bSaved ? TEXT("true") : TEXT("false"));
|
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Duplicated %d node(s)"),
|
|
|
|
|
DuplicatedNodes.Num());
|
|
|
|
|
|
|
|
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
|
|
|
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
|
|
|
|
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
|
|
|
|
Result->SetStringField(TEXT("graph"), DecodedGraphName);
|
|
|
|
|
Result->SetNumberField(TEXT("duplicatedCount"), DuplicatedNodes.Num());
|
|
|
|
|
Result->SetArrayField(TEXT("nodes"), DuplicatedNodes);
|
|
|
|
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
|
|
|
|
|
|
|
|
if (NotFound.Num() > 0)
|
|
|
|
|
{
|
|
|
|
|
@@ -2634,61 +2463,90 @@ void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result)
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find the spawner by exact full name
|
|
|
|
|
TArray<UBlueprintNodeSpawner*> Matches = FNodeActionSearch::FindSpawner(ActionName);
|
|
|
|
|
if (Matches.Num() == 0)
|
|
|
|
|
{
|
|
|
|
|
return Helper->MakeErrorJson(Result, FString::Printf(
|
|
|
|
|
TEXT("No action found matching '%s'. Use search_node_actions to find available actions."),
|
|
|
|
|
*ActionName));
|
|
|
|
|
}
|
|
|
|
|
if (Matches.Num() > 1)
|
|
|
|
|
{
|
|
|
|
|
return Helper->MakeErrorJson(Result, FString::Printf(
|
|
|
|
|
TEXT("Ambiguous: %d spawners match '%s'. Cannot determine which one to use."),
|
|
|
|
|
Matches.Num(), *ActionName));
|
|
|
|
|
}
|
|
|
|
|
UBlueprintNodeSpawner* Spawner = Matches[0];
|
|
|
|
|
TArray<TSharedPtr<FJsonValue>> Results;
|
|
|
|
|
int32 SuccessCount = 0;
|
|
|
|
|
|
|
|
|
|
// Invoke the spawner
|
|
|
|
|
FVector2D Location(PosX, PosY);
|
|
|
|
|
IBlueprintNodeBinder::FBindingSet Bindings;
|
|
|
|
|
UEdGraphNode* NewNode = Spawner->Invoke(TargetGraph, Bindings, Location);
|
|
|
|
|
|
|
|
|
|
if (!NewNode)
|
|
|
|
|
for (const TSharedPtr<FJsonValue>& NodeVal : Nodes.Array)
|
|
|
|
|
{
|
|
|
|
|
return Helper->MakeErrorJson(Result, TEXT("Spawner Invoke() returned null — node creation failed."));
|
|
|
|
|
}
|
|
|
|
|
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
|
|
|
|
|
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
|
|
|
|
|
|
|
|
|
|
// Ensure valid GUID
|
|
|
|
|
if (!NewNode->NodeGuid.IsValid())
|
|
|
|
|
{
|
|
|
|
|
NewNode->CreateNewGuid();
|
|
|
|
|
FSpawnNodeEntry Entry;
|
|
|
|
|
FString PopulateError = Helper->PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal);
|
|
|
|
|
if (!PopulateError.IsEmpty())
|
|
|
|
|
{
|
|
|
|
|
EntryResult->SetStringField(TEXT("error"), PopulateError);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EntryResult->SetStringField(TEXT("actionName"), Entry.ActionName);
|
|
|
|
|
|
|
|
|
|
// Find the spawner by exact full name
|
|
|
|
|
TArray<UBlueprintNodeSpawner*> Matches = FNodeActionSearch::FindSpawner(Entry.ActionName);
|
|
|
|
|
if (Matches.Num() == 0)
|
|
|
|
|
{
|
|
|
|
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(
|
|
|
|
|
TEXT("No action found matching '%s'. Use search_node_actions to find available actions."),
|
|
|
|
|
*Entry.ActionName));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (Matches.Num() > 1)
|
|
|
|
|
{
|
|
|
|
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(
|
|
|
|
|
TEXT("Ambiguous: %d spawners match '%s'. Cannot determine which one to use."),
|
|
|
|
|
Matches.Num(), *Entry.ActionName));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
UBlueprintNodeSpawner* Spawner = Matches[0];
|
|
|
|
|
|
|
|
|
|
// Invoke the spawner
|
|
|
|
|
FVector2D Location(Entry.PosX, Entry.PosY);
|
|
|
|
|
IBlueprintNodeBinder::FBindingSet Bindings;
|
|
|
|
|
UEdGraphNode* NewNode = Spawner->Invoke(TargetGraph, Bindings, Location);
|
|
|
|
|
|
|
|
|
|
if (!NewNode)
|
|
|
|
|
{
|
|
|
|
|
EntryResult->SetStringField(TEXT("error"), TEXT("Spawner Invoke() returned null — node creation failed."));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure valid GUID
|
|
|
|
|
if (!NewNode->NodeGuid.IsValid())
|
|
|
|
|
{
|
|
|
|
|
NewNode->CreateNewGuid();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Spawned node '%s' (class %s) via action '%s' in graph '%s' of '%s'"),
|
|
|
|
|
*NewNode->NodeGuid.ToString(),
|
|
|
|
|
*NewNode->GetClass()->GetName(),
|
|
|
|
|
*Entry.ActionName,
|
|
|
|
|
*DecodedGraphName,
|
|
|
|
|
*Blueprint);
|
|
|
|
|
|
|
|
|
|
// Serialize result
|
|
|
|
|
TSharedPtr<FJsonObject> NodeState = Helper->SerializeNode(NewNode);
|
|
|
|
|
|
|
|
|
|
EntryResult->SetBoolField(TEXT("success"), true);
|
|
|
|
|
EntryResult->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString());
|
|
|
|
|
EntryResult->SetStringField(TEXT("nodeClass"), NewNode->GetClass()->GetName());
|
|
|
|
|
EntryResult->SetStringField(TEXT("nodeTitle"), NewNode->GetNodeTitle(ENodeTitleType::ListView).ToString());
|
|
|
|
|
if (NodeState.IsValid())
|
|
|
|
|
{
|
|
|
|
|
EntryResult->SetObjectField(TEXT("node"), NodeState);
|
|
|
|
|
}
|
|
|
|
|
SuccessCount++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
|
|
|
if (Save) Helper->SaveBlueprintPackage(BP);
|
|
|
|
|
|
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Spawned node '%s' (class %s) via action '%s' in graph '%s' of '%s'"),
|
|
|
|
|
*NewNode->NodeGuid.ToString(),
|
|
|
|
|
*NewNode->GetClass()->GetName(),
|
|
|
|
|
*ActionName,
|
|
|
|
|
*DecodedGraphName,
|
|
|
|
|
*Blueprint);
|
|
|
|
|
|
|
|
|
|
// Serialize result
|
|
|
|
|
TSharedPtr<FJsonObject> NodeState = Helper->SerializeNode(NewNode);
|
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SpawnNode — %d/%d succeeded in graph '%s' of '%s'"),
|
|
|
|
|
SuccessCount, Nodes.Array.Num(), *DecodedGraphName, *Blueprint);
|
|
|
|
|
|
|
|
|
|
Result->SetBoolField(TEXT("success"), true);
|
|
|
|
|
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
|
|
|
|
Result->SetStringField(TEXT("graph"), DecodedGraphName);
|
|
|
|
|
Result->SetStringField(TEXT("actionName"), ActionName);
|
|
|
|
|
Result->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString());
|
|
|
|
|
Result->SetStringField(TEXT("nodeClass"), NewNode->GetClass()->GetName());
|
|
|
|
|
Result->SetStringField(TEXT("nodeTitle"), NewNode->GetNodeTitle(ENodeTitleType::ListView).ToString());
|
|
|
|
|
if (NodeState.IsValid())
|
|
|
|
|
{
|
|
|
|
|
Result->SetObjectField(TEXT("node"), NodeState);
|
|
|
|
|
}
|
|
|
|
|
Result->SetNumberField(TEXT("successCount"), SuccessCount);
|
|
|
|
|
Result->SetNumberField(TEXT("totalCount"), Nodes.Array.Num());
|
|
|
|
|
Result->SetArrayField(TEXT("results"), Results);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|