Harden UnrealMCP coexistence and extend training flow

This commit is contained in:
axiomlogicnexus 2026-04-25 05:58:12 +02:00
parent a05c9d85fe
commit 262f193e05
24 changed files with 5874 additions and 2 deletions

8
.gitignore vendored
View file

@ -47,3 +47,11 @@ UnrealHyperTwist/Saved/
UnrealHyperTwist/Plugins/
UnrealHyperTwist/.vsconfig
UnrealHyperTwist/UnrealHyperTwist.sln
# Track the maintained local Chong fork while keeping the rest of the plugin tree external.
!UnrealHyperTwist/Plugins/
UnrealHyperTwist/Plugins/*
!UnrealHyperTwist/Plugins/UnrealMCPChong/
!UnrealHyperTwist/Plugins/UnrealMCPChong/**
UnrealHyperTwist/Plugins/UnrealMCPChong/Binaries/
UnrealHyperTwist/Plugins/UnrealMCPChong/Intermediate/

View file

@ -0,0 +1,905 @@
#include "Commands/UnrealMCPBlueprintNodeCommands.h"
#include "Commands/UnrealMCPCommonUtils.h"
#include "Engine/Blueprint.h"
#include "Engine/BlueprintGeneratedClass.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "K2Node_Event.h"
#include "K2Node_CallFunction.h"
#include "K2Node_VariableGet.h"
#include "K2Node_InputAction.h"
#include "K2Node_Self.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "GameFramework/InputSettings.h"
#include "Camera/CameraActor.h"
#include "Kismet/GameplayStatics.h"
#include "EdGraphSchema_K2.h"
// Declare the log category
DEFINE_LOG_CATEGORY_STATIC(LogUnrealMCP, Log, All);
FUnrealMCPBlueprintNodeCommands::FUnrealMCPBlueprintNodeCommands()
{
}
TSharedPtr<FJsonObject> FUnrealMCPBlueprintNodeCommands::HandleCommand(const FString& CommandType, const TSharedPtr<FJsonObject>& Params)
{
if (CommandType == TEXT("connect_blueprint_nodes"))
{
return HandleConnectBlueprintNodes(Params);
}
else if (CommandType == TEXT("add_blueprint_get_self_component_reference"))
{
return HandleAddBlueprintGetSelfComponentReference(Params);
}
else if (CommandType == TEXT("add_blueprint_event_node"))
{
return HandleAddBlueprintEvent(Params);
}
else if (CommandType == TEXT("add_blueprint_function_node"))
{
return HandleAddBlueprintFunctionCall(Params);
}
else if (CommandType == TEXT("add_blueprint_variable"))
{
return HandleAddBlueprintVariable(Params);
}
else if (CommandType == TEXT("add_blueprint_input_action_node"))
{
return HandleAddBlueprintInputActionNode(Params);
}
else if (CommandType == TEXT("add_blueprint_self_reference"))
{
return HandleAddBlueprintSelfReference(Params);
}
else if (CommandType == TEXT("find_blueprint_nodes"))
{
return HandleFindBlueprintNodes(Params);
}
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Unknown blueprint node command: %s"), *CommandType));
}
TSharedPtr<FJsonObject> FUnrealMCPBlueprintNodeCommands::HandleConnectBlueprintNodes(const TSharedPtr<FJsonObject>& Params)
{
// Get required parameters
FString BlueprintName;
if (!Params->TryGetStringField(TEXT("blueprint_name"), BlueprintName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'blueprint_name' parameter"));
}
FString SourceNodeId;
if (!Params->TryGetStringField(TEXT("source_node_id"), SourceNodeId))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'source_node_id' parameter"));
}
FString TargetNodeId;
if (!Params->TryGetStringField(TEXT("target_node_id"), TargetNodeId))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'target_node_id' parameter"));
}
FString SourcePinName;
if (!Params->TryGetStringField(TEXT("source_pin"), SourcePinName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'source_pin' parameter"));
}
FString TargetPinName;
if (!Params->TryGetStringField(TEXT("target_pin"), TargetPinName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'target_pin' parameter"));
}
// Find the blueprint
UBlueprint* Blueprint = FUnrealMCPCommonUtils::FindBlueprint(BlueprintName);
if (!Blueprint)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Blueprint not found: %s"), *BlueprintName));
}
// Get the event graph
UEdGraph* EventGraph = FUnrealMCPCommonUtils::FindOrCreateEventGraph(Blueprint);
if (!EventGraph)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get event graph"));
}
// Find the nodes
UEdGraphNode* SourceNode = nullptr;
UEdGraphNode* TargetNode = nullptr;
for (UEdGraphNode* Node : EventGraph->Nodes)
{
if (Node->NodeGuid.ToString() == SourceNodeId)
{
SourceNode = Node;
}
else if (Node->NodeGuid.ToString() == TargetNodeId)
{
TargetNode = Node;
}
}
if (!SourceNode || !TargetNode)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Source or target node not found"));
}
// Connect the nodes
if (FUnrealMCPCommonUtils::ConnectGraphNodes(EventGraph, SourceNode, SourcePinName, TargetNode, TargetPinName))
{
// Mark the blueprint as modified
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
ResultObj->SetStringField(TEXT("source_node_id"), SourceNodeId);
ResultObj->SetStringField(TEXT("target_node_id"), TargetNodeId);
return ResultObj;
}
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to connect nodes"));
}
TSharedPtr<FJsonObject> FUnrealMCPBlueprintNodeCommands::HandleAddBlueprintGetSelfComponentReference(const TSharedPtr<FJsonObject>& Params)
{
// Get required parameters
FString BlueprintName;
if (!Params->TryGetStringField(TEXT("blueprint_name"), BlueprintName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'blueprint_name' parameter"));
}
FString ComponentName;
if (!Params->TryGetStringField(TEXT("component_name"), ComponentName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'component_name' parameter"));
}
// Get position parameters (optional)
FVector2D NodePosition(0.0f, 0.0f);
if (Params->HasField(TEXT("node_position")))
{
NodePosition = FUnrealMCPCommonUtils::GetVector2DFromJson(Params, TEXT("node_position"));
}
// Find the blueprint
UBlueprint* Blueprint = FUnrealMCPCommonUtils::FindBlueprint(BlueprintName);
if (!Blueprint)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Blueprint not found: %s"), *BlueprintName));
}
// Get the event graph
UEdGraph* EventGraph = FUnrealMCPCommonUtils::FindOrCreateEventGraph(Blueprint);
if (!EventGraph)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get event graph"));
}
// We'll skip component verification since the GetAllNodes API may have changed in UE5.5
// Create the variable get node directly
UK2Node_VariableGet* GetComponentNode = NewObject<UK2Node_VariableGet>(EventGraph);
if (!GetComponentNode)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to create get component node"));
}
// Set up the variable reference properly for UE5.5
FMemberReference& VarRef = GetComponentNode->VariableReference;
VarRef.SetSelfMember(FName(*ComponentName));
// Set node position
GetComponentNode->NodePosX = NodePosition.X;
GetComponentNode->NodePosY = NodePosition.Y;
// Add to graph
EventGraph->AddNode(GetComponentNode);
GetComponentNode->CreateNewGuid();
GetComponentNode->PostPlacedNewNode();
GetComponentNode->AllocateDefaultPins();
// Explicitly reconstruct node for UE5.5
GetComponentNode->ReconstructNode();
// Mark the blueprint as modified
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
ResultObj->SetStringField(TEXT("node_id"), GetComponentNode->NodeGuid.ToString());
return ResultObj;
}
TSharedPtr<FJsonObject> FUnrealMCPBlueprintNodeCommands::HandleAddBlueprintEvent(const TSharedPtr<FJsonObject>& Params)
{
// Get required parameters
FString BlueprintName;
if (!Params->TryGetStringField(TEXT("blueprint_name"), BlueprintName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'blueprint_name' parameter"));
}
FString EventName;
if (!Params->TryGetStringField(TEXT("event_name"), EventName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'event_name' parameter"));
}
// Get position parameters (optional)
FVector2D NodePosition(0.0f, 0.0f);
if (Params->HasField(TEXT("node_position")))
{
NodePosition = FUnrealMCPCommonUtils::GetVector2DFromJson(Params, TEXT("node_position"));
}
// Find the blueprint
UBlueprint* Blueprint = FUnrealMCPCommonUtils::FindBlueprint(BlueprintName);
if (!Blueprint)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Blueprint not found: %s"), *BlueprintName));
}
// Get the event graph
UEdGraph* EventGraph = FUnrealMCPCommonUtils::FindOrCreateEventGraph(Blueprint);
if (!EventGraph)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get event graph"));
}
// Create the event node
UK2Node_Event* EventNode = FUnrealMCPCommonUtils::CreateEventNode(EventGraph, EventName, NodePosition);
if (!EventNode)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to create event node"));
}
// Mark the blueprint as modified
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
ResultObj->SetStringField(TEXT("node_id"), EventNode->NodeGuid.ToString());
return ResultObj;
}
TSharedPtr<FJsonObject> FUnrealMCPBlueprintNodeCommands::HandleAddBlueprintFunctionCall(const TSharedPtr<FJsonObject>& Params)
{
// Get required parameters
FString BlueprintName;
if (!Params->TryGetStringField(TEXT("blueprint_name"), BlueprintName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'blueprint_name' parameter"));
}
FString FunctionName;
if (!Params->TryGetStringField(TEXT("function_name"), FunctionName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'function_name' parameter"));
}
// Get position parameters (optional)
FVector2D NodePosition(0.0f, 0.0f);
if (Params->HasField(TEXT("node_position")))
{
NodePosition = FUnrealMCPCommonUtils::GetVector2DFromJson(Params, TEXT("node_position"));
}
// Check for target parameter (optional)
FString Target;
Params->TryGetStringField(TEXT("target"), Target);
// Find the blueprint
UBlueprint* Blueprint = FUnrealMCPCommonUtils::FindBlueprint(BlueprintName);
if (!Blueprint)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Blueprint not found: %s"), *BlueprintName));
}
// Get the event graph
UEdGraph* EventGraph = FUnrealMCPCommonUtils::FindOrCreateEventGraph(Blueprint);
if (!EventGraph)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get event graph"));
}
// Find the function
UFunction* Function = nullptr;
UK2Node_CallFunction* FunctionNode = nullptr;
// Add extensive logging for debugging
UE_LOG(LogTemp, Display, TEXT("Looking for function '%s' in target '%s'"),
*FunctionName, Target.IsEmpty() ? TEXT("Blueprint") : *Target);
// Check if we have a target class specified
if (!Target.IsEmpty())
{
// Try to find the target class
UClass* TargetClass = nullptr;
// First try without a prefix
TargetClass = FUnrealMCPCommonUtils::FindClassByName(Target, TEXT("UnrealMCPBlueprintNodeCommands::HandleAddBlueprintFunctionCall"));
UE_LOG(LogTemp, Display, TEXT("Tried to find class '%s': %s"),
*Target, TargetClass ? TEXT("Found") : TEXT("Not found"));
// If not found, try with U prefix (common convention for UE classes)
if (!TargetClass && !Target.StartsWith(TEXT("U")))
{
FString TargetWithPrefix = FString(TEXT("U")) + Target;
TargetClass = FUnrealMCPCommonUtils::FindClassByName(TargetWithPrefix, TEXT("UnrealMCPBlueprintNodeCommands::HandleAddBlueprintFunctionCall"));
UE_LOG(LogTemp, Display, TEXT("Tried to find class '%s': %s"),
*TargetWithPrefix, TargetClass ? TEXT("Found") : TEXT("Not found"));
}
// If still not found, try with common component names
if (!TargetClass)
{
// Try some common component class names
TArray<FString> PossibleClassNames;
PossibleClassNames.Add(FString(TEXT("U")) + Target + TEXT("Component"));
PossibleClassNames.Add(Target + TEXT("Component"));
for (const FString& ClassName : PossibleClassNames)
{
TargetClass = FUnrealMCPCommonUtils::FindClassByName(ClassName, TEXT("UnrealMCPBlueprintNodeCommands::HandleAddBlueprintFunctionCall"));
if (TargetClass)
{
UE_LOG(LogTemp, Display, TEXT("Found class using alternative name '%s'"), *ClassName);
break;
}
}
}
// Special case handling for common classes like UGameplayStatics
if (!TargetClass && Target == TEXT("UGameplayStatics"))
{
// For UGameplayStatics, use a direct reference to known class
TargetClass = FUnrealMCPCommonUtils::FindClassByName(TEXT("UGameplayStatics"), TEXT("UnrealMCPBlueprintNodeCommands::HandleAddBlueprintFunctionCall"));
UE_LOG(LogTemp, Display, TEXT("Explicitly loading GameplayStatics: %s"),
TargetClass ? TEXT("Success") : TEXT("Failed"));
}
// If we found a target class, look for the function there
if (TargetClass)
{
UE_LOG(LogTemp, Display, TEXT("Looking for function '%s' in class '%s'"),
*FunctionName, *TargetClass->GetName());
// First try exact name
Function = TargetClass->FindFunctionByName(*FunctionName);
// If not found, try class hierarchy
UClass* CurrentClass = TargetClass;
while (!Function && CurrentClass)
{
UE_LOG(LogTemp, Display, TEXT("Searching in class: %s"), *CurrentClass->GetName());
// Try exact match
Function = CurrentClass->FindFunctionByName(*FunctionName);
// Try case-insensitive match
if (!Function)
{
for (TFieldIterator<UFunction> FuncIt(CurrentClass); FuncIt; ++FuncIt)
{
UFunction* AvailableFunc = *FuncIt;
UE_LOG(LogTemp, Display, TEXT(" - Available function: %s"), *AvailableFunc->GetName());
if (AvailableFunc->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
{
UE_LOG(LogTemp, Display, TEXT(" - Found case-insensitive match: %s"), *AvailableFunc->GetName());
Function = AvailableFunc;
break;
}
}
}
// Move to parent class
CurrentClass = CurrentClass->GetSuperClass();
}
// Special handling for known functions
if (!Function)
{
if (TargetClass->GetName() == TEXT("GameplayStatics") &&
(FunctionName == TEXT("GetActorOfClass") || FunctionName.Equals(TEXT("GetActorOfClass"), ESearchCase::IgnoreCase)))
{
UE_LOG(LogTemp, Display, TEXT("Using special case handling for GameplayStatics::GetActorOfClass"));
// Create the function node directly
FunctionNode = NewObject<UK2Node_CallFunction>(EventGraph);
if (FunctionNode)
{
// Direct setup for known function
FunctionNode->FunctionReference.SetExternalMember(
FName(TEXT("GetActorOfClass")),
TargetClass
);
FunctionNode->NodePosX = NodePosition.X;
FunctionNode->NodePosY = NodePosition.Y;
EventGraph->AddNode(FunctionNode);
FunctionNode->CreateNewGuid();
FunctionNode->PostPlacedNewNode();
FunctionNode->AllocateDefaultPins();
UE_LOG(LogTemp, Display, TEXT("Created GetActorOfClass node directly"));
// List all pins
for (UEdGraphPin* Pin : FunctionNode->Pins)
{
UE_LOG(LogTemp, Display, TEXT(" - Pin: %s, Direction: %d, Category: %s"),
*Pin->PinName.ToString(), (int32)Pin->Direction, *Pin->PinType.PinCategory.ToString());
}
}
}
}
}
}
// If we still haven't found the function, try in the blueprint's class
if (!Function && !FunctionNode)
{
UE_LOG(LogTemp, Display, TEXT("Trying to find function in blueprint class"));
Function = Blueprint->GeneratedClass->FindFunctionByName(*FunctionName);
}
// Create the function call node if we found the function
if (Function && !FunctionNode)
{
FunctionNode = FUnrealMCPCommonUtils::CreateFunctionCallNode(EventGraph, Function, NodePosition);
}
if (!FunctionNode)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Function not found: %s in target %s"), *FunctionName, Target.IsEmpty() ? TEXT("Blueprint") : *Target));
}
// Set parameters if provided
if (Params->HasField(TEXT("params")))
{
const TSharedPtr<FJsonObject>* ParamsObj;
if (Params->TryGetObjectField(TEXT("params"), ParamsObj))
{
// Process parameters
for (const TPair<FString, TSharedPtr<FJsonValue>>& Param : (*ParamsObj)->Values)
{
const FString& ParamName = Param.Key;
const TSharedPtr<FJsonValue>& ParamValue = Param.Value;
// Find the parameter pin
UEdGraphPin* ParamPin = FUnrealMCPCommonUtils::FindPin(FunctionNode, ParamName, EGPD_Input);
if (ParamPin)
{
UE_LOG(LogTemp, Display, TEXT("Found parameter pin '%s' of category '%s'"),
*ParamName, *ParamPin->PinType.PinCategory.ToString());
UE_LOG(LogTemp, Display, TEXT(" Current default value: '%s'"), *ParamPin->DefaultValue);
if (ParamPin->PinType.PinSubCategoryObject.IsValid())
{
UE_LOG(LogTemp, Display, TEXT(" Pin subcategory: '%s'"),
*ParamPin->PinType.PinSubCategoryObject->GetName());
}
// Set parameter based on type
if (ParamValue->Type == EJson::String)
{
FString StringVal = ParamValue->AsString();
UE_LOG(LogTemp, Display, TEXT(" Setting string parameter '%s' to: '%s'"),
*ParamName, *StringVal);
// Handle class reference parameters (e.g., ActorClass in GetActorOfClass)
if (ParamPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Class)
{
// For class references, we require the exact class name with proper prefix
// - Actor classes must start with 'A' (e.g., ACameraActor)
// - Non-actor classes must start with 'U' (e.g., UObject)
const FString& ClassName = StringVal;
// TODO: This likely won't work in UE5.5+, so don't rely on it.
UClass* Class = FUnrealMCPCommonUtils::FindClassByName(ClassName, TEXT("UnrealMCPBlueprintNodeCommands::HandleAddBlueprintFunctionCall"));
if (!Class)
{
UE_LOG(LogUnrealMCP, Error, TEXT("Failed to find class '%s'. Make sure to use the exact class name with proper prefix (A for actors, U for non-actors)"), *ClassName);
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Failed to find class '%s'"), *ClassName));
}
const UEdGraphSchema_K2* K2Schema = Cast<const UEdGraphSchema_K2>(EventGraph->GetSchema());
if (!K2Schema)
{
UE_LOG(LogUnrealMCP, Error, TEXT("Failed to get K2Schema"));
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get K2Schema"));
}
K2Schema->TrySetDefaultObject(*ParamPin, Class);
if (ParamPin->DefaultObject != Class)
{
UE_LOG(LogUnrealMCP, Error, TEXT("Failed to set class reference for pin '%s' to '%s'"), *ParamPin->PinName.ToString(), *ClassName);
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Failed to set class reference for pin '%s'"), *ParamPin->PinName.ToString()));
}
UE_LOG(LogUnrealMCP, Log, TEXT("Successfully set class reference for pin '%s' to '%s'"), *ParamPin->PinName.ToString(), *ClassName);
continue;
}
else if (ParamPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Int)
{
// Ensure we're using an integer value (no decimal)
int32 IntValue = FMath::RoundToInt(ParamValue->AsNumber());
ParamPin->DefaultValue = FString::FromInt(IntValue);
UE_LOG(LogTemp, Display, TEXT(" Set integer parameter '%s' to: %d (string: '%s')"),
*ParamName, IntValue, *ParamPin->DefaultValue);
}
else if (ParamPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Float)
{
// For other numeric types
float FloatValue = ParamValue->AsNumber();
ParamPin->DefaultValue = FString::SanitizeFloat(FloatValue);
UE_LOG(LogTemp, Display, TEXT(" Set float parameter '%s' to: %f (string: '%s')"),
*ParamName, FloatValue, *ParamPin->DefaultValue);
}
else if (ParamPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Boolean)
{
bool BoolValue = ParamValue->AsBool();
ParamPin->DefaultValue = BoolValue ? TEXT("true") : TEXT("false");
UE_LOG(LogTemp, Display, TEXT(" Set boolean parameter '%s' to: %s"),
*ParamName, *ParamPin->DefaultValue);
}
else if (ParamPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct && ParamPin->PinType.PinSubCategoryObject == TBaseStructure<FVector>::Get())
{
// Handle array parameters - like Vector parameters
const TArray<TSharedPtr<FJsonValue>>* ArrayValue;
if (ParamValue->TryGetArray(ArrayValue))
{
// Check if this could be a vector (array of 3 numbers)
if (ArrayValue->Num() == 3)
{
// Create a proper vector string: (X=0.0,Y=0.0,Z=1000.0)
float X = (*ArrayValue)[0]->AsNumber();
float Y = (*ArrayValue)[1]->AsNumber();
float Z = (*ArrayValue)[2]->AsNumber();
FString VectorString = FString::Printf(TEXT("(X=%f,Y=%f,Z=%f)"), X, Y, Z);
ParamPin->DefaultValue = VectorString;
UE_LOG(LogTemp, Display, TEXT(" Set vector parameter '%s' to: %s"),
*ParamName, *VectorString);
UE_LOG(LogTemp, Display, TEXT(" Final pin value: '%s'"),
*ParamPin->DefaultValue);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("Array parameter type not fully supported yet"));
}
}
}
}
else if (ParamValue->Type == EJson::Number)
{
// Handle integer vs float parameters correctly
if (ParamPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Int)
{
// Ensure we're using an integer value (no decimal)
int32 IntValue = FMath::RoundToInt(ParamValue->AsNumber());
ParamPin->DefaultValue = FString::FromInt(IntValue);
UE_LOG(LogTemp, Display, TEXT(" Set integer parameter '%s' to: %d (string: '%s')"),
*ParamName, IntValue, *ParamPin->DefaultValue);
}
else
{
// For other numeric types
float FloatValue = ParamValue->AsNumber();
ParamPin->DefaultValue = FString::SanitizeFloat(FloatValue);
UE_LOG(LogTemp, Display, TEXT(" Set float parameter '%s' to: %f (string: '%s')"),
*ParamName, FloatValue, *ParamPin->DefaultValue);
}
}
else if (ParamValue->Type == EJson::Boolean)
{
bool BoolValue = ParamValue->AsBool();
ParamPin->DefaultValue = BoolValue ? TEXT("true") : TEXT("false");
UE_LOG(LogTemp, Display, TEXT(" Set boolean parameter '%s' to: %s"),
*ParamName, *ParamPin->DefaultValue);
}
else if (ParamValue->Type == EJson::Array)
{
UE_LOG(LogTemp, Display, TEXT(" Processing array parameter '%s'"), *ParamName);
// Handle array parameters - like Vector parameters
const TArray<TSharedPtr<FJsonValue>>* ArrayValue;
if (ParamValue->TryGetArray(ArrayValue))
{
// Check if this could be a vector (array of 3 numbers)
if (ArrayValue->Num() == 3 &&
(ParamPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct) &&
(ParamPin->PinType.PinSubCategoryObject == TBaseStructure<FVector>::Get()))
{
// Create a proper vector string: (X=0.0,Y=0.0,Z=1000.0)
float X = (*ArrayValue)[0]->AsNumber();
float Y = (*ArrayValue)[1]->AsNumber();
float Z = (*ArrayValue)[2]->AsNumber();
FString VectorString = FString::Printf(TEXT("(X=%f,Y=%f,Z=%f)"), X, Y, Z);
ParamPin->DefaultValue = VectorString;
UE_LOG(LogTemp, Display, TEXT(" Set vector parameter '%s' to: %s"),
*ParamName, *VectorString);
UE_LOG(LogTemp, Display, TEXT(" Final pin value: '%s'"),
*ParamPin->DefaultValue);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("Array parameter type not fully supported yet"));
}
}
}
// Add handling for other types as needed
}
else
{
UE_LOG(LogTemp, Warning, TEXT("Parameter pin '%s' not found"), *ParamName);
}
}
}
}
// Mark the blueprint as modified
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
ResultObj->SetStringField(TEXT("node_id"), FunctionNode->NodeGuid.ToString());
return ResultObj;
}
TSharedPtr<FJsonObject> FUnrealMCPBlueprintNodeCommands::HandleAddBlueprintVariable(const TSharedPtr<FJsonObject>& Params)
{
// Get required parameters
FString BlueprintName;
if (!Params->TryGetStringField(TEXT("blueprint_name"), BlueprintName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'blueprint_name' parameter"));
}
FString VariableName;
if (!Params->TryGetStringField(TEXT("variable_name"), VariableName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'variable_name' parameter"));
}
FString VariableType;
if (!Params->TryGetStringField(TEXT("variable_type"), VariableType))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'variable_type' parameter"));
}
// Get optional parameters
bool IsExposed = false;
if (Params->HasField(TEXT("is_exposed")))
{
IsExposed = Params->GetBoolField(TEXT("is_exposed"));
}
// Find the blueprint
UBlueprint* Blueprint = FUnrealMCPCommonUtils::FindBlueprint(BlueprintName);
if (!Blueprint)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Blueprint not found: %s"), *BlueprintName));
}
// Create variable based on type
FEdGraphPinType PinType;
// Set up pin type based on variable_type string
if (VariableType == TEXT("Boolean"))
{
PinType.PinCategory = UEdGraphSchema_K2::PC_Boolean;
}
else if (VariableType == TEXT("Integer") || VariableType == TEXT("Int"))
{
PinType.PinCategory = UEdGraphSchema_K2::PC_Int;
}
else if (VariableType == TEXT("Float"))
{
PinType.PinCategory = UEdGraphSchema_K2::PC_Float;
}
else if (VariableType == TEXT("String"))
{
PinType.PinCategory = UEdGraphSchema_K2::PC_String;
}
else if (VariableType == TEXT("Vector"))
{
PinType.PinCategory = UEdGraphSchema_K2::PC_Struct;
PinType.PinSubCategoryObject = TBaseStructure<FVector>::Get();
}
else
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Unsupported variable type: %s"), *VariableType));
}
// Create the variable
FBlueprintEditorUtils::AddMemberVariable(Blueprint, FName(*VariableName), PinType);
// Set variable properties
FBPVariableDescription* NewVar = nullptr;
for (FBPVariableDescription& Variable : Blueprint->NewVariables)
{
if (Variable.VarName == FName(*VariableName))
{
NewVar = &Variable;
break;
}
}
if (NewVar)
{
// Set exposure in editor
if (IsExposed)
{
NewVar->PropertyFlags |= CPF_Edit;
}
}
// Mark the blueprint as modified
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
ResultObj->SetStringField(TEXT("variable_name"), VariableName);
ResultObj->SetStringField(TEXT("variable_type"), VariableType);
return ResultObj;
}
TSharedPtr<FJsonObject> FUnrealMCPBlueprintNodeCommands::HandleAddBlueprintInputActionNode(const TSharedPtr<FJsonObject>& Params)
{
// Get required parameters
FString BlueprintName;
if (!Params->TryGetStringField(TEXT("blueprint_name"), BlueprintName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'blueprint_name' parameter"));
}
FString ActionName;
if (!Params->TryGetStringField(TEXT("action_name"), ActionName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'action_name' parameter"));
}
// Get position parameters (optional)
FVector2D NodePosition(0.0f, 0.0f);
if (Params->HasField(TEXT("node_position")))
{
NodePosition = FUnrealMCPCommonUtils::GetVector2DFromJson(Params, TEXT("node_position"));
}
// Find the blueprint
UBlueprint* Blueprint = FUnrealMCPCommonUtils::FindBlueprint(BlueprintName);
if (!Blueprint)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Blueprint not found: %s"), *BlueprintName));
}
// Get the event graph
UEdGraph* EventGraph = FUnrealMCPCommonUtils::FindOrCreateEventGraph(Blueprint);
if (!EventGraph)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get event graph"));
}
// Create the input action node
UK2Node_InputAction* InputActionNode = FUnrealMCPCommonUtils::CreateInputActionNode(EventGraph, ActionName, NodePosition);
if (!InputActionNode)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to create input action node"));
}
// Mark the blueprint as modified
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
ResultObj->SetStringField(TEXT("node_id"), InputActionNode->NodeGuid.ToString());
return ResultObj;
}
TSharedPtr<FJsonObject> FUnrealMCPBlueprintNodeCommands::HandleAddBlueprintSelfReference(const TSharedPtr<FJsonObject>& Params)
{
// Get required parameters
FString BlueprintName;
if (!Params->TryGetStringField(TEXT("blueprint_name"), BlueprintName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'blueprint_name' parameter"));
}
// Get position parameters (optional)
FVector2D NodePosition(0.0f, 0.0f);
if (Params->HasField(TEXT("node_position")))
{
NodePosition = FUnrealMCPCommonUtils::GetVector2DFromJson(Params, TEXT("node_position"));
}
// Find the blueprint
UBlueprint* Blueprint = FUnrealMCPCommonUtils::FindBlueprint(BlueprintName);
if (!Blueprint)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Blueprint not found: %s"), *BlueprintName));
}
// Get the event graph
UEdGraph* EventGraph = FUnrealMCPCommonUtils::FindOrCreateEventGraph(Blueprint);
if (!EventGraph)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get event graph"));
}
// Create the self node
UK2Node_Self* SelfNode = FUnrealMCPCommonUtils::CreateSelfReferenceNode(EventGraph, NodePosition);
if (!SelfNode)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to create self node"));
}
// Mark the blueprint as modified
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
ResultObj->SetStringField(TEXT("node_id"), SelfNode->NodeGuid.ToString());
return ResultObj;
}
TSharedPtr<FJsonObject> FUnrealMCPBlueprintNodeCommands::HandleFindBlueprintNodes(const TSharedPtr<FJsonObject>& Params)
{
// Get required parameters
FString BlueprintName;
if (!Params->TryGetStringField(TEXT("blueprint_name"), BlueprintName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'blueprint_name' parameter"));
}
FString NodeType;
if (!Params->TryGetStringField(TEXT("node_type"), NodeType))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'node_type' parameter"));
}
// Find the blueprint
UBlueprint* Blueprint = FUnrealMCPCommonUtils::FindBlueprint(BlueprintName);
if (!Blueprint)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Blueprint not found: %s"), *BlueprintName));
}
// Get the event graph
UEdGraph* EventGraph = FUnrealMCPCommonUtils::FindOrCreateEventGraph(Blueprint);
if (!EventGraph)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get event graph"));
}
// Create a JSON array for the node GUIDs
TArray<TSharedPtr<FJsonValue>> NodeGuidArray;
// Filter nodes by the exact requested type
if (NodeType == TEXT("Event"))
{
FString EventName;
if (!Params->TryGetStringField(TEXT("event_name"), EventName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'event_name' parameter for Event node search"));
}
// Look for nodes with exact event name (e.g., ReceiveBeginPlay)
for (UEdGraphNode* Node : EventGraph->Nodes)
{
UK2Node_Event* EventNode = Cast<UK2Node_Event>(Node);
if (EventNode && EventNode->EventReference.GetMemberName() == FName(*EventName))
{
UE_LOG(LogTemp, Display, TEXT("Found event node with name %s: %s"), *EventName, *EventNode->NodeGuid.ToString());
NodeGuidArray.Add(MakeShared<FJsonValueString>(EventNode->NodeGuid.ToString()));
}
}
}
// Add other node types as needed (InputAction, etc.)
TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
ResultObj->SetArrayField(TEXT("node_guids"), NodeGuidArray);
return ResultObj;
}

View file

@ -0,0 +1,761 @@
#include "Commands/UnrealMCPCommonUtils.h"
#include "GameFramework/Actor.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "K2Node_Event.h"
#include "K2Node_CallFunction.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "K2Node_InputAction.h"
#include "K2Node_Self.h"
#include "EdGraphSchema_K2.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Components/StaticMeshComponent.h"
#include "Components/LightComponent.h"
#include "Components/PrimitiveComponent.h"
#include "Components/SceneComponent.h"
#include "UObject/UObjectIterator.h"
#include "Engine/Selection.h"
#include "EditorAssetLibrary.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Engine/BlueprintGeneratedClass.h"
#include "BlueprintNodeSpawner.h"
#include "BlueprintActionDatabase.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
// JSON Utilities
TSharedPtr<FJsonObject> FUnrealMCPCommonUtils::CreateErrorResponse(const FString& Message)
{
TSharedPtr<FJsonObject> ResponseObject = MakeShared<FJsonObject>();
ResponseObject->SetBoolField(TEXT("success"), false);
ResponseObject->SetStringField(TEXT("error"), Message);
return ResponseObject;
}
TSharedPtr<FJsonObject> FUnrealMCPCommonUtils::CreateSuccessResponse(const TSharedPtr<FJsonObject>& Data)
{
TSharedPtr<FJsonObject> ResponseObject = MakeShared<FJsonObject>();
ResponseObject->SetBoolField(TEXT("success"), true);
if (Data.IsValid())
{
ResponseObject->SetObjectField(TEXT("data"), Data);
}
return ResponseObject;
}
void FUnrealMCPCommonUtils::GetIntArrayFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName, TArray<int32>& OutArray)
{
OutArray.Reset();
if (!JsonObject->HasField(FieldName))
{
return;
}
const TArray<TSharedPtr<FJsonValue>>* JsonArray;
if (JsonObject->TryGetArrayField(FieldName, JsonArray))
{
for (const TSharedPtr<FJsonValue>& Value : *JsonArray)
{
OutArray.Add((int32)Value->AsNumber());
}
}
}
void FUnrealMCPCommonUtils::GetFloatArrayFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName, TArray<float>& OutArray)
{
OutArray.Reset();
if (!JsonObject->HasField(FieldName))
{
return;
}
const TArray<TSharedPtr<FJsonValue>>* JsonArray;
if (JsonObject->TryGetArrayField(FieldName, JsonArray))
{
for (const TSharedPtr<FJsonValue>& Value : *JsonArray)
{
OutArray.Add((float)Value->AsNumber());
}
}
}
FVector2D FUnrealMCPCommonUtils::GetVector2DFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName)
{
FVector2D Result(0.0f, 0.0f);
if (!JsonObject->HasField(FieldName))
{
return Result;
}
const TArray<TSharedPtr<FJsonValue>>* JsonArray;
if (JsonObject->TryGetArrayField(FieldName, JsonArray) && JsonArray->Num() >= 2)
{
Result.X = (float)(*JsonArray)[0]->AsNumber();
Result.Y = (float)(*JsonArray)[1]->AsNumber();
}
return Result;
}
FVector FUnrealMCPCommonUtils::GetVectorFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName)
{
FVector Result(0.0f, 0.0f, 0.0f);
if (!JsonObject->HasField(FieldName))
{
return Result;
}
const TArray<TSharedPtr<FJsonValue>>* JsonArray;
if (JsonObject->TryGetArrayField(FieldName, JsonArray) && JsonArray->Num() >= 3)
{
Result.X = (float)(*JsonArray)[0]->AsNumber();
Result.Y = (float)(*JsonArray)[1]->AsNumber();
Result.Z = (float)(*JsonArray)[2]->AsNumber();
}
return Result;
}
FRotator FUnrealMCPCommonUtils::GetRotatorFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName)
{
FRotator Result(0.0f, 0.0f, 0.0f);
if (!JsonObject->HasField(FieldName))
{
return Result;
}
const TArray<TSharedPtr<FJsonValue>>* JsonArray;
if (JsonObject->TryGetArrayField(FieldName, JsonArray) && JsonArray->Num() >= 3)
{
Result.Pitch = (float)(*JsonArray)[0]->AsNumber();
Result.Yaw = (float)(*JsonArray)[1]->AsNumber();
Result.Roll = (float)(*JsonArray)[2]->AsNumber();
}
return Result;
}
// Blueprint Utilities
UBlueprint* FUnrealMCPCommonUtils::FindBlueprint(const FString& BlueprintName)
{
return FindBlueprintByName(BlueprintName);
}
UBlueprint* FUnrealMCPCommonUtils::FindBlueprintByName(const FString& BlueprintName)
{
FString AssetPath = TEXT("/Game/Blueprints/") + BlueprintName;
return LoadObject<UBlueprint>(nullptr, *AssetPath);
}
UClass* FUnrealMCPCommonUtils::FindClassByName(const FString& ClassName, const TCHAR* CurrentOperation)
{
if (ClassName.IsEmpty())
{
return nullptr;
}
TArray<FString> Candidates;
Candidates.Add(ClassName);
if (!ClassName.StartsWith(TEXT("/")) && ClassName.Len() > 1)
{
const TCHAR LeadingChar = ClassName[0];
if ((LeadingChar == TEXT('A') || LeadingChar == TEXT('U') || LeadingChar == TEXT('F') || LeadingChar == TEXT('E')) && FChar::IsUpper(ClassName[1]))
{
Candidates.AddUnique(ClassName.RightChop(1));
}
}
for (const FString& Candidate : Candidates)
{
if (UClass* FoundClass = FindFirstObject<UClass>(*Candidate, EFindFirstObjectOptions::NativeFirst, ELogVerbosity::Warning, CurrentOperation))
{
return FoundClass;
}
if (Candidate.StartsWith(TEXT("/")))
{
if (UClass* LoadedClass = LoadObject<UClass>(nullptr, *Candidate))
{
return LoadedClass;
}
continue;
}
const FString EngineClassPath = FString::Printf(TEXT("/Script/Engine.%s"), *Candidate);
if (UClass* LoadedClass = LoadObject<UClass>(nullptr, *EngineClassPath))
{
return LoadedClass;
}
const FString CoreObjectClassPath = FString::Printf(TEXT("/Script/CoreUObject.%s"), *Candidate);
if (UClass* LoadedClass = LoadObject<UClass>(nullptr, *CoreObjectClassPath))
{
return LoadedClass;
}
}
return nullptr;
}
UEdGraph* FUnrealMCPCommonUtils::FindOrCreateEventGraph(UBlueprint* Blueprint)
{
if (!Blueprint)
{
return nullptr;
}
// Try to find the event graph
for (UEdGraph* Graph : Blueprint->UbergraphPages)
{
if (Graph->GetName().Contains(TEXT("EventGraph")))
{
return Graph;
}
}
// Create a new event graph if none exists
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(Blueprint, FName(TEXT("EventGraph")), UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
FBlueprintEditorUtils::AddUbergraphPage(Blueprint, NewGraph);
return NewGraph;
}
// Blueprint node utilities
UK2Node_Event* FUnrealMCPCommonUtils::CreateEventNode(UEdGraph* Graph, const FString& EventName, const FVector2D& Position)
{
if (!Graph)
{
return nullptr;
}
UBlueprint* Blueprint = FBlueprintEditorUtils::FindBlueprintForGraph(Graph);
if (!Blueprint)
{
return nullptr;
}
// Check for existing event node with this exact name
for (UEdGraphNode* Node : Graph->Nodes)
{
UK2Node_Event* EventNode = Cast<UK2Node_Event>(Node);
if (EventNode && EventNode->EventReference.GetMemberName() == FName(*EventName))
{
UE_LOG(LogTemp, Display, TEXT("Using existing event node with name %s (ID: %s)"),
*EventName, *EventNode->NodeGuid.ToString());
return EventNode;
}
}
// No existing node found, create a new one
UK2Node_Event* EventNode = nullptr;
// Find the function to create the event
UClass* BlueprintClass = Blueprint->GeneratedClass;
UFunction* EventFunction = BlueprintClass->FindFunctionByName(FName(*EventName));
if (EventFunction)
{
EventNode = NewObject<UK2Node_Event>(Graph);
EventNode->EventReference.SetExternalMember(FName(*EventName), BlueprintClass);
EventNode->NodePosX = Position.X;
EventNode->NodePosY = Position.Y;
Graph->AddNode(EventNode, true);
EventNode->PostPlacedNewNode();
EventNode->AllocateDefaultPins();
UE_LOG(LogTemp, Display, TEXT("Created new event node with name %s (ID: %s)"),
*EventName, *EventNode->NodeGuid.ToString());
}
else
{
UE_LOG(LogTemp, Error, TEXT("Failed to find function for event name: %s"), *EventName);
}
return EventNode;
}
UK2Node_CallFunction* FUnrealMCPCommonUtils::CreateFunctionCallNode(UEdGraph* Graph, UFunction* Function, const FVector2D& Position)
{
if (!Graph || !Function)
{
return nullptr;
}
UK2Node_CallFunction* FunctionNode = NewObject<UK2Node_CallFunction>(Graph);
FunctionNode->SetFromFunction(Function);
FunctionNode->NodePosX = Position.X;
FunctionNode->NodePosY = Position.Y;
Graph->AddNode(FunctionNode, true);
FunctionNode->CreateNewGuid();
FunctionNode->PostPlacedNewNode();
FunctionNode->AllocateDefaultPins();
return FunctionNode;
}
UK2Node_VariableGet* FUnrealMCPCommonUtils::CreateVariableGetNode(UEdGraph* Graph, UBlueprint* Blueprint, const FString& VariableName, const FVector2D& Position)
{
if (!Graph || !Blueprint)
{
return nullptr;
}
UK2Node_VariableGet* VariableGetNode = NewObject<UK2Node_VariableGet>(Graph);
FName VarName(*VariableName);
FProperty* Property = FindFProperty<FProperty>(Blueprint->GeneratedClass, VarName);
if (Property)
{
VariableGetNode->VariableReference.SetFromField<FProperty>(Property, false);
VariableGetNode->NodePosX = Position.X;
VariableGetNode->NodePosY = Position.Y;
Graph->AddNode(VariableGetNode, true);
VariableGetNode->PostPlacedNewNode();
VariableGetNode->AllocateDefaultPins();
return VariableGetNode;
}
return nullptr;
}
UK2Node_VariableSet* FUnrealMCPCommonUtils::CreateVariableSetNode(UEdGraph* Graph, UBlueprint* Blueprint, const FString& VariableName, const FVector2D& Position)
{
if (!Graph || !Blueprint)
{
return nullptr;
}
UK2Node_VariableSet* VariableSetNode = NewObject<UK2Node_VariableSet>(Graph);
FName VarName(*VariableName);
FProperty* Property = FindFProperty<FProperty>(Blueprint->GeneratedClass, VarName);
if (Property)
{
VariableSetNode->VariableReference.SetFromField<FProperty>(Property, false);
VariableSetNode->NodePosX = Position.X;
VariableSetNode->NodePosY = Position.Y;
Graph->AddNode(VariableSetNode, true);
VariableSetNode->PostPlacedNewNode();
VariableSetNode->AllocateDefaultPins();
return VariableSetNode;
}
return nullptr;
}
UK2Node_InputAction* FUnrealMCPCommonUtils::CreateInputActionNode(UEdGraph* Graph, const FString& ActionName, const FVector2D& Position)
{
if (!Graph)
{
return nullptr;
}
UK2Node_InputAction* InputActionNode = NewObject<UK2Node_InputAction>(Graph);
InputActionNode->InputActionName = FName(*ActionName);
InputActionNode->NodePosX = Position.X;
InputActionNode->NodePosY = Position.Y;
Graph->AddNode(InputActionNode, true);
InputActionNode->CreateNewGuid();
InputActionNode->PostPlacedNewNode();
InputActionNode->AllocateDefaultPins();
return InputActionNode;
}
UK2Node_Self* FUnrealMCPCommonUtils::CreateSelfReferenceNode(UEdGraph* Graph, const FVector2D& Position)
{
if (!Graph)
{
return nullptr;
}
UK2Node_Self* SelfNode = NewObject<UK2Node_Self>(Graph);
SelfNode->NodePosX = Position.X;
SelfNode->NodePosY = Position.Y;
Graph->AddNode(SelfNode, true);
SelfNode->CreateNewGuid();
SelfNode->PostPlacedNewNode();
SelfNode->AllocateDefaultPins();
return SelfNode;
}
bool FUnrealMCPCommonUtils::ConnectGraphNodes(UEdGraph* Graph, UEdGraphNode* SourceNode, const FString& SourcePinName,
UEdGraphNode* TargetNode, const FString& TargetPinName)
{
if (!Graph || !SourceNode || !TargetNode)
{
return false;
}
UEdGraphPin* SourcePin = FindPin(SourceNode, SourcePinName, EGPD_Output);
UEdGraphPin* TargetPin = FindPin(TargetNode, TargetPinName, EGPD_Input);
if (SourcePin && TargetPin)
{
SourcePin->MakeLinkTo(TargetPin);
return true;
}
return false;
}
UEdGraphPin* FUnrealMCPCommonUtils::FindPin(UEdGraphNode* Node, const FString& PinName, EEdGraphPinDirection Direction)
{
if (!Node)
{
return nullptr;
}
// Log all pins for debugging
UE_LOG(LogTemp, Display, TEXT("FindPin: Looking for pin '%s' (Direction: %d) in node '%s'"),
*PinName, (int32)Direction, *Node->GetName());
for (UEdGraphPin* Pin : Node->Pins)
{
UE_LOG(LogTemp, Display, TEXT(" - Available pin: '%s', Direction: %d, Category: %s"),
*Pin->PinName.ToString(), (int32)Pin->Direction, *Pin->PinType.PinCategory.ToString());
}
// First try exact match
for (UEdGraphPin* Pin : Node->Pins)
{
if (Pin->PinName.ToString() == PinName && (Direction == EGPD_MAX || Pin->Direction == Direction))
{
UE_LOG(LogTemp, Display, TEXT(" - Found exact matching pin: '%s'"), *Pin->PinName.ToString());
return Pin;
}
}
// If no exact match and we're looking for a component reference, try case-insensitive match
for (UEdGraphPin* Pin : Node->Pins)
{
if (Pin->PinName.ToString().Equals(PinName, ESearchCase::IgnoreCase) &&
(Direction == EGPD_MAX || Pin->Direction == Direction))
{
UE_LOG(LogTemp, Display, TEXT(" - Found case-insensitive matching pin: '%s'"), *Pin->PinName.ToString());
return Pin;
}
}
// If we're looking for a component output and didn't find it by name, try to find the first data output pin
if (Direction == EGPD_Output && Cast<UK2Node_VariableGet>(Node) != nullptr)
{
for (UEdGraphPin* Pin : Node->Pins)
{
if (Pin->Direction == EGPD_Output && Pin->PinType.PinCategory != UEdGraphSchema_K2::PC_Exec)
{
UE_LOG(LogTemp, Display, TEXT(" - Found fallback data output pin: '%s'"), *Pin->PinName.ToString());
return Pin;
}
}
}
UE_LOG(LogTemp, Warning, TEXT(" - No matching pin found for '%s'"), *PinName);
return nullptr;
}
// Actor utilities
TSharedPtr<FJsonValue> FUnrealMCPCommonUtils::ActorToJson(AActor* Actor)
{
if (!Actor)
{
return MakeShared<FJsonValueNull>();
}
TSharedPtr<FJsonObject> ActorObject = MakeShared<FJsonObject>();
ActorObject->SetStringField(TEXT("name"), Actor->GetName());
ActorObject->SetStringField(TEXT("class"), Actor->GetClass()->GetName());
FVector Location = Actor->GetActorLocation();
TArray<TSharedPtr<FJsonValue>> LocationArray;
LocationArray.Add(MakeShared<FJsonValueNumber>(Location.X));
LocationArray.Add(MakeShared<FJsonValueNumber>(Location.Y));
LocationArray.Add(MakeShared<FJsonValueNumber>(Location.Z));
ActorObject->SetArrayField(TEXT("location"), LocationArray);
FRotator Rotation = Actor->GetActorRotation();
TArray<TSharedPtr<FJsonValue>> RotationArray;
RotationArray.Add(MakeShared<FJsonValueNumber>(Rotation.Pitch));
RotationArray.Add(MakeShared<FJsonValueNumber>(Rotation.Yaw));
RotationArray.Add(MakeShared<FJsonValueNumber>(Rotation.Roll));
ActorObject->SetArrayField(TEXT("rotation"), RotationArray);
FVector Scale = Actor->GetActorScale3D();
TArray<TSharedPtr<FJsonValue>> ScaleArray;
ScaleArray.Add(MakeShared<FJsonValueNumber>(Scale.X));
ScaleArray.Add(MakeShared<FJsonValueNumber>(Scale.Y));
ScaleArray.Add(MakeShared<FJsonValueNumber>(Scale.Z));
ActorObject->SetArrayField(TEXT("scale"), ScaleArray);
return MakeShared<FJsonValueObject>(ActorObject);
}
TSharedPtr<FJsonObject> FUnrealMCPCommonUtils::ActorToJsonObject(AActor* Actor, bool bDetailed)
{
if (!Actor)
{
return nullptr;
}
TSharedPtr<FJsonObject> ActorObject = MakeShared<FJsonObject>();
ActorObject->SetStringField(TEXT("name"), Actor->GetName());
ActorObject->SetStringField(TEXT("class"), Actor->GetClass()->GetName());
FVector Location = Actor->GetActorLocation();
TArray<TSharedPtr<FJsonValue>> LocationArray;
LocationArray.Add(MakeShared<FJsonValueNumber>(Location.X));
LocationArray.Add(MakeShared<FJsonValueNumber>(Location.Y));
LocationArray.Add(MakeShared<FJsonValueNumber>(Location.Z));
ActorObject->SetArrayField(TEXT("location"), LocationArray);
FRotator Rotation = Actor->GetActorRotation();
TArray<TSharedPtr<FJsonValue>> RotationArray;
RotationArray.Add(MakeShared<FJsonValueNumber>(Rotation.Pitch));
RotationArray.Add(MakeShared<FJsonValueNumber>(Rotation.Yaw));
RotationArray.Add(MakeShared<FJsonValueNumber>(Rotation.Roll));
ActorObject->SetArrayField(TEXT("rotation"), RotationArray);
FVector Scale = Actor->GetActorScale3D();
TArray<TSharedPtr<FJsonValue>> ScaleArray;
ScaleArray.Add(MakeShared<FJsonValueNumber>(Scale.X));
ScaleArray.Add(MakeShared<FJsonValueNumber>(Scale.Y));
ScaleArray.Add(MakeShared<FJsonValueNumber>(Scale.Z));
ActorObject->SetArrayField(TEXT("scale"), ScaleArray);
return ActorObject;
}
UK2Node_Event* FUnrealMCPCommonUtils::FindExistingEventNode(UEdGraph* Graph, const FString& EventName)
{
if (!Graph)
{
return nullptr;
}
// Look for existing event nodes
for (UEdGraphNode* Node : Graph->Nodes)
{
UK2Node_Event* EventNode = Cast<UK2Node_Event>(Node);
if (EventNode && EventNode->EventReference.GetMemberName() == FName(*EventName))
{
UE_LOG(LogTemp, Display, TEXT("Found existing event node with name: %s"), *EventName);
return EventNode;
}
}
return nullptr;
}
bool FUnrealMCPCommonUtils::SetObjectProperty(UObject* Object, const FString& PropertyName,
const TSharedPtr<FJsonValue>& Value, FString& OutErrorMessage)
{
if (!Object)
{
OutErrorMessage = TEXT("Invalid object");
return false;
}
FProperty* Property = Object->GetClass()->FindPropertyByName(*PropertyName);
if (!Property)
{
OutErrorMessage = FString::Printf(TEXT("Property not found: %s"), *PropertyName);
return false;
}
void* PropertyAddr = Property->ContainerPtrToValuePtr<void>(Object);
// Handle different property types
if (Property->IsA<FBoolProperty>())
{
((FBoolProperty*)Property)->SetPropertyValue(PropertyAddr, Value->AsBool());
return true;
}
else if (Property->IsA<FIntProperty>())
{
int32 IntValue = static_cast<int32>(Value->AsNumber());
FIntProperty* IntProperty = CastField<FIntProperty>(Property);
if (IntProperty)
{
IntProperty->SetPropertyValue_InContainer(Object, IntValue);
return true;
}
}
else if (Property->IsA<FFloatProperty>())
{
((FFloatProperty*)Property)->SetPropertyValue(PropertyAddr, Value->AsNumber());
return true;
}
else if (Property->IsA<FStrProperty>())
{
((FStrProperty*)Property)->SetPropertyValue(PropertyAddr, Value->AsString());
return true;
}
else if (Property->IsA<FByteProperty>())
{
FByteProperty* ByteProp = CastField<FByteProperty>(Property);
UEnum* EnumDef = ByteProp ? ByteProp->GetIntPropertyEnum() : nullptr;
// If this is a TEnumAsByte property (has associated enum)
if (EnumDef)
{
// Handle numeric value
if (Value->Type == EJson::Number)
{
uint8 ByteValue = static_cast<uint8>(Value->AsNumber());
ByteProp->SetPropertyValue(PropertyAddr, ByteValue);
UE_LOG(LogTemp, Display, TEXT("Setting enum property %s to numeric value: %d"),
*PropertyName, ByteValue);
return true;
}
// Handle string enum value
else if (Value->Type == EJson::String)
{
FString EnumValueName = Value->AsString();
// Try to convert numeric string to number first
if (EnumValueName.IsNumeric())
{
uint8 ByteValue = FCString::Atoi(*EnumValueName);
ByteProp->SetPropertyValue(PropertyAddr, ByteValue);
UE_LOG(LogTemp, Display, TEXT("Setting enum property %s to numeric string value: %s -> %d"),
*PropertyName, *EnumValueName, ByteValue);
return true;
}
// Handle qualified enum names (e.g., "Player0" or "EAutoReceiveInput::Player0")
if (EnumValueName.Contains(TEXT("::")))
{
EnumValueName.Split(TEXT("::"), nullptr, &EnumValueName);
}
int64 EnumValue = EnumDef->GetValueByNameString(EnumValueName);
if (EnumValue == INDEX_NONE)
{
// Try with full name as fallback
EnumValue = EnumDef->GetValueByNameString(Value->AsString());
}
if (EnumValue != INDEX_NONE)
{
ByteProp->SetPropertyValue(PropertyAddr, static_cast<uint8>(EnumValue));
UE_LOG(LogTemp, Display, TEXT("Setting enum property %s to name value: %s -> %lld"),
*PropertyName, *EnumValueName, EnumValue);
return true;
}
else
{
// Log all possible enum values for debugging
UE_LOG(LogTemp, Warning, TEXT("Could not find enum value for '%s'. Available options:"), *EnumValueName);
for (int32 i = 0; i < EnumDef->NumEnums(); i++)
{
UE_LOG(LogTemp, Warning, TEXT(" - %s (value: %d)"),
*EnumDef->GetNameStringByIndex(i), EnumDef->GetValueByIndex(i));
}
OutErrorMessage = FString::Printf(TEXT("Could not find enum value for '%s'"), *EnumValueName);
return false;
}
}
}
else
{
// Regular byte property
uint8 ByteValue = static_cast<uint8>(Value->AsNumber());
ByteProp->SetPropertyValue(PropertyAddr, ByteValue);
return true;
}
}
else if (Property->IsA<FEnumProperty>())
{
FEnumProperty* EnumProp = CastField<FEnumProperty>(Property);
UEnum* EnumDef = EnumProp ? EnumProp->GetEnum() : nullptr;
FNumericProperty* UnderlyingNumericProp = EnumProp ? EnumProp->GetUnderlyingProperty() : nullptr;
if (EnumDef && UnderlyingNumericProp)
{
// Handle numeric value
if (Value->Type == EJson::Number)
{
int64 EnumValue = static_cast<int64>(Value->AsNumber());
UnderlyingNumericProp->SetIntPropertyValue(PropertyAddr, EnumValue);
UE_LOG(LogTemp, Display, TEXT("Setting enum property %s to numeric value: %lld"),
*PropertyName, EnumValue);
return true;
}
// Handle string enum value
else if (Value->Type == EJson::String)
{
FString EnumValueName = Value->AsString();
// Try to convert numeric string to number first
if (EnumValueName.IsNumeric())
{
int64 EnumValue = FCString::Atoi64(*EnumValueName);
UnderlyingNumericProp->SetIntPropertyValue(PropertyAddr, EnumValue);
UE_LOG(LogTemp, Display, TEXT("Setting enum property %s to numeric string value: %s -> %lld"),
*PropertyName, *EnumValueName, EnumValue);
return true;
}
// Handle qualified enum names
if (EnumValueName.Contains(TEXT("::")))
{
EnumValueName.Split(TEXT("::"), nullptr, &EnumValueName);
}
int64 EnumValue = EnumDef->GetValueByNameString(EnumValueName);
if (EnumValue == INDEX_NONE)
{
// Try with full name as fallback
EnumValue = EnumDef->GetValueByNameString(Value->AsString());
}
if (EnumValue != INDEX_NONE)
{
UnderlyingNumericProp->SetIntPropertyValue(PropertyAddr, EnumValue);
UE_LOG(LogTemp, Display, TEXT("Setting enum property %s to name value: %s -> %lld"),
*PropertyName, *EnumValueName, EnumValue);
return true;
}
else
{
// Log all possible enum values for debugging
UE_LOG(LogTemp, Warning, TEXT("Could not find enum value for '%s'. Available options:"), *EnumValueName);
for (int32 i = 0; i < EnumDef->NumEnums(); i++)
{
UE_LOG(LogTemp, Warning, TEXT(" - %s (value: %d)"),
*EnumDef->GetNameStringByIndex(i), EnumDef->GetValueByIndex(i));
}
OutErrorMessage = FString::Printf(TEXT("Could not find enum value for '%s'"), *EnumValueName);
return false;
}
}
}
}
OutErrorMessage = FString::Printf(TEXT("Unsupported property type: %s for property %s"),
*Property->GetClass()->GetName(), *PropertyName);
return false;
}

View file

@ -0,0 +1,600 @@
#include "Commands/UnrealMCPEditorCommands.h"
#include "Commands/UnrealMCPCommonUtils.h"
#include "Editor.h"
#include "EditorViewportClient.h"
#include "LevelEditorViewport.h"
#include "ImageUtils.h"
#include "HighResScreenshot.h"
#include "Engine/GameViewportClient.h"
#include "Misc/FileHelper.h"
#include "GameFramework/Actor.h"
#include "Engine/Selection.h"
#include "Kismet/GameplayStatics.h"
#include "Engine/StaticMeshActor.h"
#include "Engine/DirectionalLight.h"
#include "Engine/PointLight.h"
#include "Engine/SpotLight.h"
#include "Camera/CameraActor.h"
#include "Components/StaticMeshComponent.h"
#include "EditorSubsystem.h"
#include "Subsystems/EditorActorSubsystem.h"
#include "Engine/Blueprint.h"
#include "Engine/BlueprintGeneratedClass.h"
FUnrealMCPEditorCommands::FUnrealMCPEditorCommands()
{
}
TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleCommand(const FString& CommandType, const TSharedPtr<FJsonObject>& Params)
{
// Actor manipulation commands
if (CommandType == TEXT("get_actors_in_level"))
{
return HandleGetActorsInLevel(Params);
}
else if (CommandType == TEXT("find_actors_by_name"))
{
return HandleFindActorsByName(Params);
}
else if (CommandType == TEXT("spawn_actor") || CommandType == TEXT("create_actor"))
{
if (CommandType == TEXT("create_actor"))
{
UE_LOG(LogTemp, Warning, TEXT("'create_actor' command is deprecated and will be removed in a future version. Please use 'spawn_actor' instead."));
}
return HandleSpawnActor(Params);
}
else if (CommandType == TEXT("delete_actor"))
{
return HandleDeleteActor(Params);
}
else if (CommandType == TEXT("set_actor_transform"))
{
return HandleSetActorTransform(Params);
}
else if (CommandType == TEXT("get_actor_properties"))
{
return HandleGetActorProperties(Params);
}
else if (CommandType == TEXT("set_actor_property"))
{
return HandleSetActorProperty(Params);
}
// Blueprint actor spawning
else if (CommandType == TEXT("spawn_blueprint_actor"))
{
return HandleSpawnBlueprintActor(Params);
}
// Editor viewport commands
else if (CommandType == TEXT("focus_viewport"))
{
return HandleFocusViewport(Params);
}
else if (CommandType == TEXT("take_screenshot"))
{
return HandleTakeScreenshot(Params);
}
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Unknown editor command: %s"), *CommandType));
}
TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetActorsInLevel(const TSharedPtr<FJsonObject>& Params)
{
TArray<AActor*> AllActors;
UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
TArray<TSharedPtr<FJsonValue>> ActorArray;
for (AActor* Actor : AllActors)
{
if (Actor)
{
ActorArray.Add(FUnrealMCPCommonUtils::ActorToJson(Actor));
}
}
TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
ResultObj->SetArrayField(TEXT("actors"), ActorArray);
return ResultObj;
}
TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleFindActorsByName(const TSharedPtr<FJsonObject>& Params)
{
FString Pattern;
if (!Params->TryGetStringField(TEXT("pattern"), Pattern))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'pattern' parameter"));
}
TArray<AActor*> AllActors;
UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
TArray<TSharedPtr<FJsonValue>> MatchingActors;
for (AActor* Actor : AllActors)
{
if (Actor && Actor->GetName().Contains(Pattern))
{
MatchingActors.Add(FUnrealMCPCommonUtils::ActorToJson(Actor));
}
}
TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
ResultObj->SetArrayField(TEXT("actors"), MatchingActors);
return ResultObj;
}
TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSpawnActor(const TSharedPtr<FJsonObject>& Params)
{
// Get required parameters
FString ActorType;
if (!Params->TryGetStringField(TEXT("type"), ActorType))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'type' parameter"));
}
// Get actor name (required parameter)
FString ActorName;
if (!Params->TryGetStringField(TEXT("name"), ActorName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
}
// Get optional transform parameters
FVector Location(0.0f, 0.0f, 0.0f);
FRotator Rotation(0.0f, 0.0f, 0.0f);
FVector Scale(1.0f, 1.0f, 1.0f);
if (Params->HasField(TEXT("location")))
{
Location = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("location"));
}
if (Params->HasField(TEXT("rotation")))
{
Rotation = FUnrealMCPCommonUtils::GetRotatorFromJson(Params, TEXT("rotation"));
}
if (Params->HasField(TEXT("scale")))
{
Scale = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("scale"));
}
// Create the actor based on type
AActor* NewActor = nullptr;
UWorld* World = GEditor->GetEditorWorldContext().World();
if (!World)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world"));
}
// Check if an actor with this name already exists
TArray<AActor*> AllActors;
UGameplayStatics::GetAllActorsOfClass(World, AActor::StaticClass(), AllActors);
for (AActor* Actor : AllActors)
{
if (Actor && Actor->GetName() == ActorName)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor with name '%s' already exists"), *ActorName));
}
}
FActorSpawnParameters SpawnParams;
SpawnParams.Name = *ActorName;
if (ActorType == TEXT("StaticMeshActor"))
{
NewActor = World->SpawnActor<AStaticMeshActor>(AStaticMeshActor::StaticClass(), Location, Rotation, SpawnParams);
}
else if (ActorType == TEXT("PointLight"))
{
NewActor = World->SpawnActor<APointLight>(APointLight::StaticClass(), Location, Rotation, SpawnParams);
}
else if (ActorType == TEXT("SpotLight"))
{
NewActor = World->SpawnActor<ASpotLight>(ASpotLight::StaticClass(), Location, Rotation, SpawnParams);
}
else if (ActorType == TEXT("DirectionalLight"))
{
NewActor = World->SpawnActor<ADirectionalLight>(ADirectionalLight::StaticClass(), Location, Rotation, SpawnParams);
}
else if (ActorType == TEXT("CameraActor"))
{
NewActor = World->SpawnActor<ACameraActor>(ACameraActor::StaticClass(), Location, Rotation, SpawnParams);
}
else
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Unknown actor type: %s"), *ActorType));
}
if (NewActor)
{
// Set scale (since SpawnActor only takes location and rotation)
FTransform Transform = NewActor->GetTransform();
Transform.SetScale3D(Scale);
NewActor->SetActorTransform(Transform);
// Return the created actor's details
return FUnrealMCPCommonUtils::ActorToJsonObject(NewActor, true);
}
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to create actor"));
}
TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleDeleteActor(const TSharedPtr<FJsonObject>& Params)
{
FString ActorName;
if (!Params->TryGetStringField(TEXT("name"), ActorName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
}
TArray<AActor*> AllActors;
UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
for (AActor* Actor : AllActors)
{
if (Actor && Actor->GetName() == ActorName)
{
// Store actor info before deletion for the response
TSharedPtr<FJsonObject> ActorInfo = FUnrealMCPCommonUtils::ActorToJsonObject(Actor);
// Delete the actor
Actor->Destroy();
TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
ResultObj->SetObjectField(TEXT("deleted_actor"), ActorInfo);
return ResultObj;
}
}
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *ActorName));
}
TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSetActorTransform(const TSharedPtr<FJsonObject>& Params)
{
// Get actor name
FString ActorName;
if (!Params->TryGetStringField(TEXT("name"), ActorName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
}
// Find the actor
AActor* TargetActor = nullptr;
TArray<AActor*> AllActors;
UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
for (AActor* Actor : AllActors)
{
if (Actor && Actor->GetName() == ActorName)
{
TargetActor = Actor;
break;
}
}
if (!TargetActor)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *ActorName));
}
// Get transform parameters
FTransform NewTransform = TargetActor->GetTransform();
if (Params->HasField(TEXT("location")))
{
NewTransform.SetLocation(FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("location")));
}
if (Params->HasField(TEXT("rotation")))
{
NewTransform.SetRotation(FQuat(FUnrealMCPCommonUtils::GetRotatorFromJson(Params, TEXT("rotation"))));
}
if (Params->HasField(TEXT("scale")))
{
NewTransform.SetScale3D(FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("scale")));
}
// Set the new transform
TargetActor->SetActorTransform(NewTransform);
// Return updated actor info
return FUnrealMCPCommonUtils::ActorToJsonObject(TargetActor, true);
}
TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetActorProperties(const TSharedPtr<FJsonObject>& Params)
{
// Get actor name
FString ActorName;
if (!Params->TryGetStringField(TEXT("name"), ActorName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
}
// Find the actor
AActor* TargetActor = nullptr;
TArray<AActor*> AllActors;
UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
for (AActor* Actor : AllActors)
{
if (Actor && Actor->GetName() == ActorName)
{
TargetActor = Actor;
break;
}
}
if (!TargetActor)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *ActorName));
}
// Always return detailed properties for this command
return FUnrealMCPCommonUtils::ActorToJsonObject(TargetActor, true);
}
TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSetActorProperty(const TSharedPtr<FJsonObject>& Params)
{
// Get actor name
FString ActorName;
if (!Params->TryGetStringField(TEXT("name"), ActorName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
}
// Find the actor
AActor* TargetActor = nullptr;
TArray<AActor*> AllActors;
UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
for (AActor* Actor : AllActors)
{
if (Actor && Actor->GetName() == ActorName)
{
TargetActor = Actor;
break;
}
}
if (!TargetActor)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *ActorName));
}
// Get property name
FString PropertyName;
if (!Params->TryGetStringField(TEXT("property_name"), PropertyName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'property_name' parameter"));
}
// Get property value
if (!Params->HasField(TEXT("property_value")))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'property_value' parameter"));
}
TSharedPtr<FJsonValue> PropertyValue = Params->Values.FindRef(TEXT("property_value"));
// Set the property using our utility function
FString ErrorMessage;
if (FUnrealMCPCommonUtils::SetObjectProperty(TargetActor, PropertyName, PropertyValue, ErrorMessage))
{
// Property set successfully
TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
ResultObj->SetStringField(TEXT("actor"), ActorName);
ResultObj->SetStringField(TEXT("property"), PropertyName);
ResultObj->SetBoolField(TEXT("success"), true);
// Also include the full actor details
ResultObj->SetObjectField(TEXT("actor_details"), FUnrealMCPCommonUtils::ActorToJsonObject(TargetActor, true));
return ResultObj;
}
else
{
return FUnrealMCPCommonUtils::CreateErrorResponse(ErrorMessage);
}
}
TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSpawnBlueprintActor(const TSharedPtr<FJsonObject>& Params)
{
// Get required parameters
FString BlueprintName;
if (!Params->TryGetStringField(TEXT("blueprint_name"), BlueprintName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'blueprint_name' parameter"));
}
FString ActorName;
if (!Params->TryGetStringField(TEXT("actor_name"), ActorName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'actor_name' parameter"));
}
// Find the blueprint
if (BlueprintName.IsEmpty())
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Blueprint name is empty"));
}
FString Root = TEXT("/Game/Blueprints/");
FString AssetPath = Root + BlueprintName;
if (!FPackageName::DoesPackageExist(AssetPath))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Blueprint '%s' not found it must reside under /Game/Blueprints"), *BlueprintName));
}
UBlueprint* Blueprint = LoadObject<UBlueprint>(nullptr, *AssetPath);
if (!Blueprint)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Blueprint not found: %s"), *BlueprintName));
}
// Get transform parameters
FVector Location(0.0f, 0.0f, 0.0f);
FRotator Rotation(0.0f, 0.0f, 0.0f);
FVector Scale(1.0f, 1.0f, 1.0f);
if (Params->HasField(TEXT("location")))
{
Location = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("location"));
}
if (Params->HasField(TEXT("rotation")))
{
Rotation = FUnrealMCPCommonUtils::GetRotatorFromJson(Params, TEXT("rotation"));
}
if (Params->HasField(TEXT("scale")))
{
Scale = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("scale"));
}
// Spawn the actor
UWorld* World = GEditor->GetEditorWorldContext().World();
if (!World)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world"));
}
FTransform SpawnTransform;
SpawnTransform.SetLocation(Location);
SpawnTransform.SetRotation(FQuat(Rotation));
SpawnTransform.SetScale3D(Scale);
FActorSpawnParameters SpawnParams;
SpawnParams.Name = *ActorName;
AActor* NewActor = World->SpawnActor<AActor>(Blueprint->GeneratedClass, SpawnTransform, SpawnParams);
if (NewActor)
{
return FUnrealMCPCommonUtils::ActorToJsonObject(NewActor, true);
}
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to spawn blueprint actor"));
}
TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleFocusViewport(const TSharedPtr<FJsonObject>& Params)
{
// Get target actor name if provided
FString TargetActorName;
bool HasTargetActor = Params->TryGetStringField(TEXT("target"), TargetActorName);
// Get location if provided
FVector Location(0.0f, 0.0f, 0.0f);
bool HasLocation = false;
if (Params->HasField(TEXT("location")))
{
Location = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("location"));
HasLocation = true;
}
// Get distance
float Distance = 1000.0f;
if (Params->HasField(TEXT("distance")))
{
Distance = Params->GetNumberField(TEXT("distance"));
}
// Get orientation if provided
FRotator Orientation(0.0f, 0.0f, 0.0f);
bool HasOrientation = false;
if (Params->HasField(TEXT("orientation")))
{
Orientation = FUnrealMCPCommonUtils::GetRotatorFromJson(Params, TEXT("orientation"));
HasOrientation = true;
}
// Get the active viewport
FLevelEditorViewportClient* ViewportClient = (FLevelEditorViewportClient*)GEditor->GetActiveViewport()->GetClient();
if (!ViewportClient)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get active viewport"));
}
// If we have a target actor, focus on it
if (HasTargetActor)
{
// Find the actor
AActor* TargetActor = nullptr;
TArray<AActor*> AllActors;
UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
for (AActor* Actor : AllActors)
{
if (Actor && Actor->GetName() == TargetActorName)
{
TargetActor = Actor;
break;
}
}
if (!TargetActor)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *TargetActorName));
}
// Focus on the actor
ViewportClient->SetViewLocation(TargetActor->GetActorLocation() - FVector(Distance, 0.0f, 0.0f));
}
// Otherwise use the provided location
else if (HasLocation)
{
ViewportClient->SetViewLocation(Location - FVector(Distance, 0.0f, 0.0f));
}
else
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Either 'target' or 'location' must be provided"));
}
// Set orientation if provided
if (HasOrientation)
{
ViewportClient->SetViewRotation(Orientation);
}
// Force viewport to redraw
ViewportClient->Invalidate();
TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
ResultObj->SetBoolField(TEXT("success"), true);
return ResultObj;
}
TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleTakeScreenshot(const TSharedPtr<FJsonObject>& Params)
{
// Get file path parameter
FString FilePath;
if (!Params->TryGetStringField(TEXT("filepath"), FilePath))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'filepath' parameter"));
}
// Ensure the file path has a proper extension
if (!FilePath.EndsWith(TEXT(".png")))
{
FilePath += TEXT(".png");
}
// Get the active viewport
if (GEditor && GEditor->GetActiveViewport())
{
FViewport* Viewport = GEditor->GetActiveViewport();
TArray<FColor> Bitmap;
FIntRect ViewportRect(0, 0, Viewport->GetSizeXY().X, Viewport->GetSizeXY().Y);
if (Viewport->ReadPixels(Bitmap, FReadSurfaceDataFlags(), ViewportRect))
{
TArray<uint8> CompressedBitmap;
FImageUtils::CompressImageArray(Viewport->GetSizeXY().X, Viewport->GetSizeXY().Y, Bitmap, CompressedBitmap);
if (FFileHelper::SaveArrayToFile(CompressedBitmap, *FilePath))
{
TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
ResultObj->SetStringField(TEXT("filepath"), FilePath);
return ResultObj;
}
}
}
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to take screenshot"));
}

View file

@ -0,0 +1,72 @@
#include "Commands/UnrealMCPProjectCommands.h"
#include "Commands/UnrealMCPCommonUtils.h"
#include "GameFramework/InputSettings.h"
FUnrealMCPProjectCommands::FUnrealMCPProjectCommands()
{
}
TSharedPtr<FJsonObject> FUnrealMCPProjectCommands::HandleCommand(const FString& CommandType, const TSharedPtr<FJsonObject>& Params)
{
if (CommandType == TEXT("create_input_mapping"))
{
return HandleCreateInputMapping(Params);
}
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Unknown project command: %s"), *CommandType));
}
TSharedPtr<FJsonObject> FUnrealMCPProjectCommands::HandleCreateInputMapping(const TSharedPtr<FJsonObject>& Params)
{
// Get required parameters
FString ActionName;
if (!Params->TryGetStringField(TEXT("action_name"), ActionName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'action_name' parameter"));
}
FString Key;
if (!Params->TryGetStringField(TEXT("key"), Key))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'key' parameter"));
}
// Get the input settings
UInputSettings* InputSettings = GetMutableDefault<UInputSettings>();
if (!InputSettings)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get input settings"));
}
// Create the input action mapping
FInputActionKeyMapping ActionMapping;
ActionMapping.ActionName = FName(*ActionName);
ActionMapping.Key = FKey(*Key);
// Add modifiers if provided
if (Params->HasField(TEXT("shift")))
{
ActionMapping.bShift = Params->GetBoolField(TEXT("shift"));
}
if (Params->HasField(TEXT("ctrl")))
{
ActionMapping.bCtrl = Params->GetBoolField(TEXT("ctrl"));
}
if (Params->HasField(TEXT("alt")))
{
ActionMapping.bAlt = Params->GetBoolField(TEXT("alt"));
}
if (Params->HasField(TEXT("cmd")))
{
ActionMapping.bCmd = Params->GetBoolField(TEXT("cmd"));
}
// Add the mapping
InputSettings->AddActionMapping(ActionMapping);
InputSettings->SaveConfig();
TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
ResultObj->SetStringField(TEXT("action_name"), ActionName);
ResultObj->SetStringField(TEXT("key"), Key);
return ResultObj;
}

View file

@ -0,0 +1,544 @@
#include "Commands/UnrealMCPUMGCommands.h"
#include "Commands/UnrealMCPCommonUtils.h"
#include "Editor.h"
#include "EditorAssetLibrary.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Blueprint/UserWidget.h"
#include "Components/TextBlock.h"
#include "WidgetBlueprint.h"
// We'll create widgets using regular Factory classes
#include "Factories/Factory.h"
// Remove problematic includes that don't exist in UE 5.5
// #include "UMGEditorSubsystem.h"
// #include "WidgetBlueprintFactory.h"
#include "WidgetBlueprintEditor.h"
#include "Blueprint/WidgetTree.h"
#include "Components/CanvasPanel.h"
#include "Components/CanvasPanelSlot.h"
#include "JsonObjectConverter.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Components/Button.h"
#include "K2Node_FunctionEntry.h"
#include "K2Node_CallFunction.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "K2Node_Event.h"
FUnrealMCPUMGCommands::FUnrealMCPUMGCommands()
{
}
TSharedPtr<FJsonObject> FUnrealMCPUMGCommands::HandleCommand(const FString& CommandName, const TSharedPtr<FJsonObject>& Params)
{
if (CommandName == TEXT("create_umg_widget_blueprint"))
{
return HandleCreateUMGWidgetBlueprint(Params);
}
else if (CommandName == TEXT("add_text_block_to_widget"))
{
return HandleAddTextBlockToWidget(Params);
}
else if (CommandName == TEXT("add_widget_to_viewport"))
{
return HandleAddWidgetToViewport(Params);
}
else if (CommandName == TEXT("add_button_to_widget"))
{
return HandleAddButtonToWidget(Params);
}
else if (CommandName == TEXT("bind_widget_event"))
{
return HandleBindWidgetEvent(Params);
}
else if (CommandName == TEXT("set_text_block_binding"))
{
return HandleSetTextBlockBinding(Params);
}
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Unknown UMG command: %s"), *CommandName));
}
TSharedPtr<FJsonObject> FUnrealMCPUMGCommands::HandleCreateUMGWidgetBlueprint(const TSharedPtr<FJsonObject>& Params)
{
// Get required parameters
FString BlueprintName;
if (!Params->TryGetStringField(TEXT("name"), BlueprintName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
}
// Create the full asset path
FString PackagePath = TEXT("/Game/Widgets/");
FString AssetName = BlueprintName;
FString FullPath = PackagePath + AssetName;
// Check if asset already exists
if (UEditorAssetLibrary::DoesAssetExist(FullPath))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Widget Blueprint '%s' already exists"), *BlueprintName));
}
// Create package
UPackage* Package = CreatePackage(*FullPath);
if (!Package)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to create package"));
}
// Create Widget Blueprint using KismetEditorUtilities
UBlueprint* NewBlueprint = FKismetEditorUtilities::CreateBlueprint(
UUserWidget::StaticClass(), // Parent class
Package, // Outer package
FName(*AssetName), // Blueprint name
BPTYPE_Normal, // Blueprint type
UBlueprint::StaticClass(), // Blueprint class
UBlueprintGeneratedClass::StaticClass(), // Generated class
FName("CreateUMGWidget") // Creation method name
);
// Make sure the Blueprint was created successfully
UWidgetBlueprint* WidgetBlueprint = Cast<UWidgetBlueprint>(NewBlueprint);
if (!WidgetBlueprint)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to create Widget Blueprint"));
}
// Add a default Canvas Panel if one doesn't exist
if (!WidgetBlueprint->WidgetTree->RootWidget)
{
UCanvasPanel* RootCanvas = WidgetBlueprint->WidgetTree->ConstructWidget<UCanvasPanel>(UCanvasPanel::StaticClass());
WidgetBlueprint->WidgetTree->RootWidget = RootCanvas;
}
// Mark the package dirty and notify asset registry
Package->MarkPackageDirty();
FAssetRegistryModule::AssetCreated(WidgetBlueprint);
// Compile the blueprint
FKismetEditorUtilities::CompileBlueprint(WidgetBlueprint);
// Create success response
TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
ResultObj->SetStringField(TEXT("name"), BlueprintName);
ResultObj->SetStringField(TEXT("path"), FullPath);
return ResultObj;
}
TSharedPtr<FJsonObject> FUnrealMCPUMGCommands::HandleAddTextBlockToWidget(const TSharedPtr<FJsonObject>& Params)
{
// Get required parameters
FString BlueprintName;
if (!Params->TryGetStringField(TEXT("blueprint_name"), BlueprintName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'blueprint_name' parameter"));
}
FString WidgetName;
if (!Params->TryGetStringField(TEXT("widget_name"), WidgetName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'widget_name' parameter"));
}
// Find the Widget Blueprint
FString FullPath = TEXT("/Game/Widgets/") + BlueprintName;
UWidgetBlueprint* WidgetBlueprint = Cast<UWidgetBlueprint>(UEditorAssetLibrary::LoadAsset(FullPath));
if (!WidgetBlueprint)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Widget Blueprint '%s' not found"), *BlueprintName));
}
// Get optional parameters
FString InitialText = TEXT("New Text Block");
Params->TryGetStringField(TEXT("text"), InitialText);
FVector2D Position(0.0f, 0.0f);
if (Params->HasField(TEXT("position")))
{
const TArray<TSharedPtr<FJsonValue>>* PosArray;
if (Params->TryGetArrayField(TEXT("position"), PosArray) && PosArray->Num() >= 2)
{
Position.X = (*PosArray)[0]->AsNumber();
Position.Y = (*PosArray)[1]->AsNumber();
}
}
// Create Text Block widget
UTextBlock* TextBlock = WidgetBlueprint->WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), *WidgetName);
if (!TextBlock)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to create Text Block widget"));
}
// Set initial text
TextBlock->SetText(FText::FromString(InitialText));
// Add to canvas panel
UCanvasPanel* RootCanvas = Cast<UCanvasPanel>(WidgetBlueprint->WidgetTree->RootWidget);
if (!RootCanvas)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Root Canvas Panel not found"));
}
UCanvasPanelSlot* PanelSlot = RootCanvas->AddChildToCanvas(TextBlock);
PanelSlot->SetPosition(Position);
// Mark the package dirty and compile
WidgetBlueprint->MarkPackageDirty();
FKismetEditorUtilities::CompileBlueprint(WidgetBlueprint);
// Create success response
TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
ResultObj->SetStringField(TEXT("widget_name"), WidgetName);
ResultObj->SetStringField(TEXT("text"), InitialText);
return ResultObj;
}
TSharedPtr<FJsonObject> FUnrealMCPUMGCommands::HandleAddWidgetToViewport(const TSharedPtr<FJsonObject>& Params)
{
// Get required parameters
FString BlueprintName;
if (!Params->TryGetStringField(TEXT("blueprint_name"), BlueprintName))
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'blueprint_name' parameter"));
}
// Find the Widget Blueprint
FString FullPath = TEXT("/Game/Widgets/") + BlueprintName;
UWidgetBlueprint* WidgetBlueprint = Cast<UWidgetBlueprint>(UEditorAssetLibrary::LoadAsset(FullPath));
if (!WidgetBlueprint)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Widget Blueprint '%s' not found"), *BlueprintName));
}
// Get optional Z-order parameter
int32 ZOrder = 0;
Params->TryGetNumberField(TEXT("z_order"), ZOrder);
// Create widget instance
UClass* WidgetClass = WidgetBlueprint->GeneratedClass;
if (!WidgetClass)
{
return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get widget class"));
}
// Note: This creates the widget but doesn't add it to viewport
// The actual addition to viewport should be done through Blueprint nodes
// as it requires a game context
// Create success response with instructions
TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
ResultObj->SetStringField(TEXT("blueprint_name"), BlueprintName);
ResultObj->SetStringField(TEXT("class_path"), WidgetClass->GetPathName());
ResultObj->SetNumberField(TEXT("z_order"), ZOrder);
ResultObj->SetStringField(TEXT("note"), TEXT("Widget class ready. Use CreateWidget and AddToViewport nodes in Blueprint to display in game."));
return ResultObj;
}
TSharedPtr<FJsonObject> FUnrealMCPUMGCommands::HandleAddButtonToWidget(const TSharedPtr<FJsonObject>& Params)
{
TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
// Get required parameters
FString BlueprintName;
if (!Params->TryGetStringField(TEXT("blueprint_name"), BlueprintName))
{
Response->SetStringField(TEXT("error"), TEXT("Missing blueprint_name parameter"));
return Response;
}
FString WidgetName;
if (!Params->TryGetStringField(TEXT("widget_name"), WidgetName))
{
Response->SetStringField(TEXT("error"), TEXT("Missing widget_name parameter"));
return Response;
}
FString ButtonText;
if (!Params->TryGetStringField(TEXT("text"), ButtonText))
{
Response->SetStringField(TEXT("error"), TEXT("Missing text parameter"));
return Response;
}
// Load the Widget Blueprint
const FString BlueprintPath = FString::Printf(TEXT("/Game/Widgets/%s.%s"), *BlueprintName, *BlueprintName);
UWidgetBlueprint* WidgetBlueprint = Cast<UWidgetBlueprint>(UEditorAssetLibrary::LoadAsset(BlueprintPath));
if (!WidgetBlueprint)
{
Response->SetStringField(TEXT("error"), FString::Printf(TEXT("Failed to load Widget Blueprint: %s"), *BlueprintPath));
return Response;
}
// Create Button widget
UButton* Button = NewObject<UButton>(WidgetBlueprint->GeneratedClass->GetDefaultObject(), UButton::StaticClass(), *WidgetName);
if (!Button)
{
Response->SetStringField(TEXT("error"), TEXT("Failed to create Button widget"));
return Response;
}
// Set button text
UTextBlock* ButtonTextBlock = NewObject<UTextBlock>(Button, UTextBlock::StaticClass(), *(WidgetName + TEXT("_Text")));
if (ButtonTextBlock)
{
ButtonTextBlock->SetText(FText::FromString(ButtonText));
Button->AddChild(ButtonTextBlock);
}
// Get canvas panel and add button
UCanvasPanel* RootCanvas = Cast<UCanvasPanel>(WidgetBlueprint->WidgetTree->RootWidget);
if (!RootCanvas)
{
Response->SetStringField(TEXT("error"), TEXT("Root widget is not a Canvas Panel"));
return Response;
}
// Add to canvas and set position
UCanvasPanelSlot* ButtonSlot = RootCanvas->AddChildToCanvas(Button);
if (ButtonSlot)
{
const TArray<TSharedPtr<FJsonValue>>* Position;
if (Params->TryGetArrayField(TEXT("position"), Position) && Position->Num() >= 2)
{
FVector2D Pos(
(*Position)[0]->AsNumber(),
(*Position)[1]->AsNumber()
);
ButtonSlot->SetPosition(Pos);
}
}
// Save the Widget Blueprint
FKismetEditorUtilities::CompileBlueprint(WidgetBlueprint);
UEditorAssetLibrary::SaveAsset(BlueprintPath, false);
Response->SetBoolField(TEXT("success"), true);
Response->SetStringField(TEXT("widget_name"), WidgetName);
return Response;
}
TSharedPtr<FJsonObject> FUnrealMCPUMGCommands::HandleBindWidgetEvent(const TSharedPtr<FJsonObject>& Params)
{
TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
// Get required parameters
FString BlueprintName;
if (!Params->TryGetStringField(TEXT("blueprint_name"), BlueprintName))
{
Response->SetStringField(TEXT("error"), TEXT("Missing blueprint_name parameter"));
return Response;
}
FString WidgetName;
if (!Params->TryGetStringField(TEXT("widget_name"), WidgetName))
{
Response->SetStringField(TEXT("error"), TEXT("Missing widget_name parameter"));
return Response;
}
FString EventName;
if (!Params->TryGetStringField(TEXT("event_name"), EventName))
{
Response->SetStringField(TEXT("error"), TEXT("Missing event_name parameter"));
return Response;
}
// Load the Widget Blueprint
const FString BlueprintPath = FString::Printf(TEXT("/Game/Widgets/%s.%s"), *BlueprintName, *BlueprintName);
UWidgetBlueprint* WidgetBlueprint = Cast<UWidgetBlueprint>(UEditorAssetLibrary::LoadAsset(BlueprintPath));
if (!WidgetBlueprint)
{
Response->SetStringField(TEXT("error"), FString::Printf(TEXT("Failed to load Widget Blueprint: %s"), *BlueprintPath));
return Response;
}
// Create the event graph if it doesn't exist
UEdGraph* EventGraph = FBlueprintEditorUtils::FindEventGraph(WidgetBlueprint);
if (!EventGraph)
{
Response->SetStringField(TEXT("error"), TEXT("Failed to find or create event graph"));
return Response;
}
// Find the widget in the blueprint
UWidget* Widget = WidgetBlueprint->WidgetTree->FindWidget(*WidgetName);
if (!Widget)
{
Response->SetStringField(TEXT("error"), FString::Printf(TEXT("Failed to find widget: %s"), *WidgetName));
return Response;
}
// Create the event node (e.g., OnClicked for buttons)
UK2Node_Event* EventNode = nullptr;
// Find existing nodes first
TArray<UK2Node_Event*> AllEventNodes;
FBlueprintEditorUtils::GetAllNodesOfClass<UK2Node_Event>(WidgetBlueprint, AllEventNodes);
for (UK2Node_Event* Node : AllEventNodes)
{
if (Node->CustomFunctionName == FName(*EventName) && Node->EventReference.GetMemberParentClass() == Widget->GetClass())
{
EventNode = Node;
break;
}
}
// If no existing node, create a new one
if (!EventNode)
{
// Calculate position - place it below existing nodes
float MaxHeight = 0.0f;
for (UEdGraphNode* Node : EventGraph->Nodes)
{
MaxHeight = FMath::Max(MaxHeight, Node->NodePosY);
}
const FVector2D NodePos(200, MaxHeight + 200);
// Call CreateNewBoundEventForClass, which returns void, so we can't capture the return value directly
// We'll need to find the node after creating it
FKismetEditorUtilities::CreateNewBoundEventForClass(
Widget->GetClass(),
FName(*EventName),
WidgetBlueprint,
nullptr // We don't need a specific property binding
);
// Now find the newly created node
TArray<UK2Node_Event*> UpdatedEventNodes;
FBlueprintEditorUtils::GetAllNodesOfClass<UK2Node_Event>(WidgetBlueprint, UpdatedEventNodes);
for (UK2Node_Event* Node : UpdatedEventNodes)
{
if (Node->CustomFunctionName == FName(*EventName) && Node->EventReference.GetMemberParentClass() == Widget->GetClass())
{
EventNode = Node;
// Set position of the node
EventNode->NodePosX = NodePos.X;
EventNode->NodePosY = NodePos.Y;
break;
}
}
}
if (!EventNode)
{
Response->SetStringField(TEXT("error"), TEXT("Failed to create event node"));
return Response;
}
// Save the Widget Blueprint
FKismetEditorUtilities::CompileBlueprint(WidgetBlueprint);
UEditorAssetLibrary::SaveAsset(BlueprintPath, false);
Response->SetBoolField(TEXT("success"), true);
Response->SetStringField(TEXT("event_name"), EventName);
return Response;
}
TSharedPtr<FJsonObject> FUnrealMCPUMGCommands::HandleSetTextBlockBinding(const TSharedPtr<FJsonObject>& Params)
{
TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
// Get required parameters
FString BlueprintName;
if (!Params->TryGetStringField(TEXT("blueprint_name"), BlueprintName))
{
Response->SetStringField(TEXT("error"), TEXT("Missing blueprint_name parameter"));
return Response;
}
FString WidgetName;
if (!Params->TryGetStringField(TEXT("widget_name"), WidgetName))
{
Response->SetStringField(TEXT("error"), TEXT("Missing widget_name parameter"));
return Response;
}
FString BindingName;
if (!Params->TryGetStringField(TEXT("binding_name"), BindingName))
{
Response->SetStringField(TEXT("error"), TEXT("Missing binding_name parameter"));
return Response;
}
// Load the Widget Blueprint
const FString BlueprintPath = FString::Printf(TEXT("/Game/Widgets/%s.%s"), *BlueprintName, *BlueprintName);
UWidgetBlueprint* WidgetBlueprint = Cast<UWidgetBlueprint>(UEditorAssetLibrary::LoadAsset(BlueprintPath));
if (!WidgetBlueprint)
{
Response->SetStringField(TEXT("error"), FString::Printf(TEXT("Failed to load Widget Blueprint: %s"), *BlueprintPath));
return Response;
}
// Create a variable for binding if it doesn't exist
FBlueprintEditorUtils::AddMemberVariable(
WidgetBlueprint,
FName(*BindingName),
FEdGraphPinType(UEdGraphSchema_K2::PC_Text, NAME_None, nullptr, EPinContainerType::None, false, FEdGraphTerminalType())
);
// Find the TextBlock widget
UTextBlock* TextBlock = Cast<UTextBlock>(WidgetBlueprint->WidgetTree->FindWidget(FName(*WidgetName)));
if (!TextBlock)
{
Response->SetStringField(TEXT("error"), FString::Printf(TEXT("Failed to find TextBlock widget: %s"), *WidgetName));
return Response;
}
// Create binding function
const FString FunctionName = FString::Printf(TEXT("Get%s"), *BindingName);
UEdGraph* FuncGraph = FBlueprintEditorUtils::CreateNewGraph(
WidgetBlueprint,
FName(*FunctionName),
UEdGraph::StaticClass(),
UEdGraphSchema_K2::StaticClass()
);
if (FuncGraph)
{
// Add the function to the blueprint with proper template parameter
// Template requires null for last parameter when not using a signature-source
FBlueprintEditorUtils::AddFunctionGraph<UClass>(WidgetBlueprint, FuncGraph, false, nullptr);
// Create entry node
UK2Node_FunctionEntry* EntryNode = nullptr;
// Create entry node - use the API that exists in UE 5.5
EntryNode = NewObject<UK2Node_FunctionEntry>(FuncGraph);
FuncGraph->AddNode(EntryNode, false, false);
EntryNode->NodePosX = 0;
EntryNode->NodePosY = 0;
EntryNode->FunctionReference.SetExternalMember(FName(*FunctionName), WidgetBlueprint->GeneratedClass);
EntryNode->AllocateDefaultPins();
// Create get variable node
UK2Node_VariableGet* GetVarNode = NewObject<UK2Node_VariableGet>(FuncGraph);
GetVarNode->VariableReference.SetSelfMember(FName(*BindingName));
FuncGraph->AddNode(GetVarNode, false, false);
GetVarNode->NodePosX = 200;
GetVarNode->NodePosY = 0;
GetVarNode->AllocateDefaultPins();
// Connect nodes
UEdGraphPin* EntryThenPin = EntryNode->FindPin(UEdGraphSchema_K2::PN_Then);
UEdGraphPin* GetVarOutPin = GetVarNode->FindPin(UEdGraphSchema_K2::PN_ReturnValue);
if (EntryThenPin && GetVarOutPin)
{
EntryThenPin->MakeLinkTo(GetVarOutPin);
}
}
// Save the Widget Blueprint
FKismetEditorUtilities::CompileBlueprint(WidgetBlueprint);
UEditorAssetLibrary::SaveAsset(BlueprintPath, false);
Response->SetBoolField(TEXT("success"), true);
Response->SetStringField(TEXT("binding_name"), BindingName);
return Response;
}

View file

@ -0,0 +1,318 @@
#include "MCPServerRunnable.h"
#include "UnrealMCPBridge.h"
#include "Sockets.h"
#include "SocketSubsystem.h"
#include "Interfaces/IPv4/IPv4Address.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonReader.h"
#include "JsonObjectConverter.h"
#include "Misc/ScopeLock.h"
#include "HAL/PlatformTime.h"
FMCPServerRunnable::FMCPServerRunnable(UUnrealMCPBridge* InBridge, TSharedPtr<FSocket> InListenerSocket)
: Bridge(InBridge)
, ListenerSocket(InListenerSocket)
, bRunning(true)
{
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Created server runnable"));
}
FMCPServerRunnable::~FMCPServerRunnable()
{
// Note: We don't delete the sockets here as they're owned by the bridge
}
bool FMCPServerRunnable::Init()
{
return true;
}
uint32 FMCPServerRunnable::Run()
{
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Server thread starting..."));
while (bRunning)
{
// UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Waiting for client connection..."));
bool bPending = false;
if (ListenerSocket->HasPendingConnection(bPending) && bPending)
{
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Client connection pending, accepting..."));
ClientSocket = MakeShareable(ListenerSocket->Accept(TEXT("MCPClient")));
if (ClientSocket.IsValid())
{
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Client connection accepted"));
// Set socket options to improve connection stability
ClientSocket->SetNoDelay(true);
int32 SocketBufferSize = 65536; // 64KB buffer
ClientSocket->SetSendBufferSize(SocketBufferSize, SocketBufferSize);
ClientSocket->SetReceiveBufferSize(SocketBufferSize, SocketBufferSize);
uint8 Buffer[8192];
while (bRunning)
{
int32 BytesRead = 0;
if (ClientSocket->Recv(Buffer, sizeof(Buffer), BytesRead))
{
if (BytesRead == 0)
{
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Client disconnected (zero bytes)"));
break;
}
// Convert received data to string
Buffer[BytesRead] = '\0';
FString ReceivedText = UTF8_TO_TCHAR(Buffer);
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Received: %s"), *ReceivedText);
// Parse JSON
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ReceivedText);
if (FJsonSerializer::Deserialize(Reader, JsonObject))
{
// Get command type
FString CommandType;
if (JsonObject->TryGetStringField(TEXT("type"), CommandType))
{
// Execute command
FString Response = Bridge->ExecuteCommand(CommandType, JsonObject->GetObjectField(TEXT("params")));
// Log response for debugging
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Sending response: %s"), *Response);
// Send response
int32 BytesSent = 0;
if (!ClientSocket->Send((uint8*)TCHAR_TO_UTF8(*Response), Response.Len(), BytesSent))
{
UE_LOG(LogTemp, Warning, TEXT("MCPServerRunnable: Failed to send response"));
}
else {
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Response sent successfully, bytes: %d"), BytesSent);
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("MCPServerRunnable: Missing 'type' field in command"));
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("MCPServerRunnable: Failed to parse JSON from: %s"), *ReceivedText);
}
}
else
{
int32 LastError = (int32)ISocketSubsystem::Get()->GetLastErrorCode();
// Don't break the connection for WouldBlock error, which is normal for non-blocking sockets
bool bShouldBreak = true;
// Check for "would block" error which isn't a real error for non-blocking sockets
if (LastError == SE_EWOULDBLOCK)
{
UE_LOG(LogTemp, Verbose, TEXT("MCPServerRunnable: Socket would block, continuing..."));
bShouldBreak = false;
// Small sleep to prevent tight loop when no data
FPlatformProcess::Sleep(0.01f);
}
// Check for other transient errors we might want to tolerate
else if (LastError == SE_EINTR) // Interrupted system call
{
UE_LOG(LogTemp, Warning, TEXT("MCPServerRunnable: Socket read interrupted, continuing..."));
bShouldBreak = false;
}
else
{
UE_LOG(LogTemp, Warning, TEXT("MCPServerRunnable: Client disconnected or error. Last error code: %d"), LastError);
}
if (bShouldBreak)
{
break;
}
}
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("MCPServerRunnable: Failed to accept client connection"));
}
}
// Small sleep to prevent tight loop
FPlatformProcess::Sleep(0.1f);
}
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Server thread stopping"));
return 0;
}
void FMCPServerRunnable::Stop()
{
bRunning = false;
}
void FMCPServerRunnable::Exit()
{
}
void FMCPServerRunnable::HandleClientConnection(TSharedPtr<FSocket> InClientSocket)
{
if (!InClientSocket.IsValid())
{
UE_LOG(LogTemp, Error, TEXT("MCPServerRunnable: Invalid client socket passed to HandleClientConnection"));
return;
}
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Starting to handle client connection"));
// Set socket options for better connection stability
InClientSocket->SetNonBlocking(false);
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Set socket to blocking mode"));
// Properly read full message with timeout
const int32 MaxBufferSize = 4096;
uint8 Buffer[MaxBufferSize];
FString MessageBuffer;
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Starting message receive loop"));
while (bRunning && InClientSocket.IsValid())
{
// Log socket state
bool bIsConnected = InClientSocket->GetConnectionState() == SCS_Connected;
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Socket state - Connected: %s"),
bIsConnected ? TEXT("true") : TEXT("false"));
// Log pending data status before receive
uint32 PendingDataSize = 0;
bool HasPendingData = InClientSocket->HasPendingData(PendingDataSize);
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Before Recv - HasPendingData=%s, Size=%d"),
HasPendingData ? TEXT("true") : TEXT("false"), PendingDataSize);
// Try to receive data with timeout
int32 BytesRead = 0;
bool bReadSuccess = false;
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Attempting to receive data..."));
bReadSuccess = InClientSocket->Recv(Buffer, MaxBufferSize, BytesRead, ESocketReceiveFlags::None);
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Recv attempt complete - Success=%s, BytesRead=%d"),
bReadSuccess ? TEXT("true") : TEXT("false"), BytesRead);
if (BytesRead > 0)
{
// Log raw data for debugging
FString HexData;
for (int32 i = 0; i < FMath::Min(BytesRead, 50); ++i)
{
HexData += FString::Printf(TEXT("%02X "), Buffer[i]);
}
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Raw data (first 50 bytes hex): %s%s"),
*HexData, BytesRead > 50 ? TEXT("...") : TEXT(""));
// Convert and log received data
Buffer[BytesRead] = 0; // Null terminate
FString ReceivedData = UTF8_TO_TCHAR(Buffer);
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Received data as string: '%s'"), *ReceivedData);
// Append to message buffer
MessageBuffer.Append(ReceivedData);
// Process complete messages (messages are terminated with newline)
if (MessageBuffer.Contains(TEXT("\n")))
{
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Newline detected in buffer, processing messages"));
TArray<FString> Messages;
MessageBuffer.ParseIntoArray(Messages, TEXT("\n"), true);
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Found %d message(s) in buffer"), Messages.Num());
// Process all complete messages
for (int32 i = 0; i < Messages.Num() - 1; ++i)
{
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Processing message %d: '%s'"),
i + 1, *Messages[i]);
ProcessMessage(InClientSocket, Messages[i]);
}
// Keep any incomplete message in the buffer
MessageBuffer = Messages.Last();
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Remaining buffer after processing: %s"),
*MessageBuffer);
}
else
{
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: No complete message yet (no newline detected)"));
}
}
else if (!bReadSuccess)
{
UE_LOG(LogTemp, Warning, TEXT("MCPServerRunnable: Connection closed or error occurred - Last error: %d"),
(int32)ISocketSubsystem::Get()->GetLastErrorCode());
break;
}
// Small sleep to prevent tight loop
FPlatformProcess::Sleep(0.01f);
}
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Exited message receive loop"));
}
void FMCPServerRunnable::ProcessMessage(TSharedPtr<FSocket> Client, const FString& Message)
{
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Processing message: %s"), *Message);
// Parse message as JSON
TSharedPtr<FJsonObject> JsonMessage;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Message);
if (!FJsonSerializer::Deserialize(Reader, JsonMessage) || !JsonMessage.IsValid())
{
UE_LOG(LogTemp, Warning, TEXT("MCPServerRunnable: Failed to parse message as JSON"));
return;
}
// Extract command type and parameters using MCP protocol format
FString CommandType;
TSharedPtr<FJsonObject> Params = MakeShareable(new FJsonObject());
if (!JsonMessage->TryGetStringField(TEXT("command"), CommandType))
{
UE_LOG(LogTemp, Warning, TEXT("MCPServerRunnable: Message missing 'command' field"));
return;
}
// Parameters are optional in MCP protocol
if (JsonMessage->HasField(TEXT("params")))
{
TSharedPtr<FJsonValue> ParamsValue = JsonMessage->TryGetField(TEXT("params"));
if (ParamsValue.IsValid() && ParamsValue->Type == EJson::Object)
{
Params = ParamsValue->AsObject();
}
}
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Executing command: %s"), *CommandType);
// Execute command
FString Response = Bridge->ExecuteCommand(CommandType, Params);
// Send response with newline terminator
Response += TEXT("\n");
int32 BytesSent = 0;
UE_LOG(LogTemp, Display, TEXT("MCPServerRunnable: Sending response: %s"), *Response);
if (!Client->Send((uint8*)TCHAR_TO_UTF8(*Response), Response.Len(), BytesSent))
{
UE_LOG(LogTemp, Error, TEXT("MCPServerRunnable: Failed to send response"));
}
}

View file

@ -0,0 +1,332 @@
#include "UnrealMCPBridge.h"
#include "MCPServerRunnable.h"
#include "Sockets.h"
#include "SocketSubsystem.h"
#include "HAL/RunnableThread.h"
#include "Interfaces/IPv4/IPv4Address.h"
#include "Interfaces/IPv4/IPv4Endpoint.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonWriter.h"
#include "Engine/StaticMeshActor.h"
#include "Engine/DirectionalLight.h"
#include "Engine/PointLight.h"
#include "Engine/SpotLight.h"
#include "Camera/CameraActor.h"
#include "EditorAssetLibrary.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "JsonObjectConverter.h"
#include "GameFramework/Actor.h"
#include "Engine/Selection.h"
#include "Kismet/GameplayStatics.h"
#include "Async/Async.h"
// Add Blueprint related includes
#include "Engine/Blueprint.h"
#include "Engine/BlueprintGeneratedClass.h"
#include "Factories/BlueprintFactory.h"
#include "EdGraphSchema_K2.h"
#include "K2Node_Event.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "Components/StaticMeshComponent.h"
#include "Components/BoxComponent.h"
#include "Components/SphereComponent.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
// UE5.5 correct includes
#include "Engine/SimpleConstructionScript.h"
#include "Engine/SCS_Node.h"
#include "UObject/Field.h"
#include "UObject/FieldPath.h"
// Blueprint Graph specific includes
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "K2Node_CallFunction.h"
#include "K2Node_InputAction.h"
#include "K2Node_Self.h"
#include "GameFramework/InputSettings.h"
#include "EditorSubsystem.h"
#include "Subsystems/EditorActorSubsystem.h"
// Include our new command handler classes
#include "Commands/UnrealMCPEditorCommands.h"
#include "Commands/UnrealMCPBlueprintCommands.h"
#include "Commands/UnrealMCPBlueprintNodeCommands.h"
#include "Commands/UnrealMCPProjectCommands.h"
#include "Commands/UnrealMCPCommonUtils.h"
#include "Commands/UnrealMCPUMGCommands.h"
// Default settings
#define MCP_SERVER_HOST "127.0.0.1"
#define MCP_SERVER_PORT 55557
UUnrealMCPBridge::UUnrealMCPBridge()
{
EditorCommands = MakeShared<FUnrealMCPEditorCommands>();
BlueprintCommands = MakeShared<FUnrealMCPBlueprintCommands>();
BlueprintNodeCommands = MakeShared<FUnrealMCPBlueprintNodeCommands>();
ProjectCommands = MakeShared<FUnrealMCPProjectCommands>();
UMGCommands = MakeShared<FUnrealMCPUMGCommands>();
}
UUnrealMCPBridge::~UUnrealMCPBridge()
{
EditorCommands.Reset();
BlueprintCommands.Reset();
BlueprintNodeCommands.Reset();
ProjectCommands.Reset();
UMGCommands.Reset();
}
// Initialize subsystem
void UUnrealMCPBridge::Initialize(FSubsystemCollectionBase& Collection)
{
UE_LOG(LogTemp, Display, TEXT("UnrealMCPBridge: Initializing"));
bIsRunning = false;
ListenerSocket = nullptr;
ConnectionSocket = nullptr;
ServerThread = nullptr;
Port = MCP_SERVER_PORT;
FIPv4Address::Parse(MCP_SERVER_HOST, ServerAddress);
// Start the server automatically
StartServer();
}
// Clean up resources when subsystem is destroyed
void UUnrealMCPBridge::Deinitialize()
{
UE_LOG(LogTemp, Display, TEXT("UnrealMCPBridge: Shutting down"));
StopServer();
}
// Start the MCP server
void UUnrealMCPBridge::StartServer()
{
if (bIsRunning)
{
UE_LOG(LogTemp, Warning, TEXT("UnrealMCPBridge: Server is already running"));
return;
}
// Create socket subsystem
ISocketSubsystem* SocketSubsystem = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
if (!SocketSubsystem)
{
UE_LOG(LogTemp, Error, TEXT("UnrealMCPBridge: Failed to get socket subsystem"));
return;
}
// Create listener socket
TSharedPtr<FSocket> NewListenerSocket = MakeShareable(SocketSubsystem->CreateSocket(NAME_Stream, TEXT("UnrealMCPListener"), false));
if (!NewListenerSocket.IsValid())
{
UE_LOG(LogTemp, Error, TEXT("UnrealMCPBridge: Failed to create listener socket"));
return;
}
// Allow address reuse for quick restarts
NewListenerSocket->SetReuseAddr(true);
NewListenerSocket->SetNonBlocking(true);
// Bind to address
FIPv4Endpoint Endpoint(ServerAddress, Port);
if (!NewListenerSocket->Bind(*Endpoint.ToInternetAddr()))
{
UE_LOG(LogTemp, Error, TEXT("UnrealMCPBridge: Failed to bind listener socket to %s:%d"), *ServerAddress.ToString(), Port);
return;
}
// Start listening
if (!NewListenerSocket->Listen(5))
{
UE_LOG(LogTemp, Error, TEXT("UnrealMCPBridge: Failed to start listening"));
return;
}
ListenerSocket = NewListenerSocket;
bIsRunning = true;
UE_LOG(LogTemp, Display, TEXT("UnrealMCPBridge: Server started on %s:%d"), *ServerAddress.ToString(), Port);
// Start server thread
ServerThread = FRunnableThread::Create(
new FMCPServerRunnable(this, ListenerSocket),
TEXT("UnrealMCPServerThread"),
0, TPri_Normal
);
if (!ServerThread)
{
UE_LOG(LogTemp, Error, TEXT("UnrealMCPBridge: Failed to create server thread"));
StopServer();
return;
}
}
// Stop the MCP server
void UUnrealMCPBridge::StopServer()
{
if (!bIsRunning)
{
return;
}
bIsRunning = false;
// Clean up thread
if (ServerThread)
{
ServerThread->Kill(true);
delete ServerThread;
ServerThread = nullptr;
}
// Close sockets
if (ConnectionSocket.IsValid())
{
ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(ConnectionSocket.Get());
ConnectionSocket.Reset();
}
if (ListenerSocket.IsValid())
{
ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(ListenerSocket.Get());
ListenerSocket.Reset();
}
UE_LOG(LogTemp, Display, TEXT("UnrealMCPBridge: Server stopped"));
}
// Execute a command received from a client
FString UUnrealMCPBridge::ExecuteCommand(const FString& CommandType, const TSharedPtr<FJsonObject>& Params)
{
UE_LOG(LogTemp, Display, TEXT("UnrealMCPBridge: Executing command: %s"), *CommandType);
// Create a promise to wait for the result
TPromise<FString> Promise;
TFuture<FString> Future = Promise.GetFuture();
// Queue execution on Game Thread
AsyncTask(ENamedThreads::GameThread, [this, CommandType, Params, Promise = MoveTemp(Promise)]() mutable
{
TSharedPtr<FJsonObject> ResponseJson = MakeShareable(new FJsonObject);
try
{
TSharedPtr<FJsonObject> ResultJson;
if (CommandType == TEXT("ping"))
{
ResultJson = MakeShareable(new FJsonObject);
ResultJson->SetStringField(TEXT("message"), TEXT("pong"));
}
// Editor Commands (including actor manipulation)
else if (CommandType == TEXT("get_actors_in_level") ||
CommandType == TEXT("find_actors_by_name") ||
CommandType == TEXT("spawn_actor") ||
CommandType == TEXT("create_actor") ||
CommandType == TEXT("delete_actor") ||
CommandType == TEXT("set_actor_transform") ||
CommandType == TEXT("get_actor_properties") ||
CommandType == TEXT("set_actor_property") ||
CommandType == TEXT("spawn_blueprint_actor") ||
CommandType == TEXT("focus_viewport") ||
CommandType == TEXT("take_screenshot"))
{
ResultJson = EditorCommands->HandleCommand(CommandType, Params);
}
// Blueprint Commands
else if (CommandType == TEXT("create_blueprint") ||
CommandType == TEXT("add_component_to_blueprint") ||
CommandType == TEXT("set_component_property") ||
CommandType == TEXT("set_physics_properties") ||
CommandType == TEXT("compile_blueprint") ||
CommandType == TEXT("set_blueprint_property") ||
CommandType == TEXT("set_static_mesh_properties") ||
CommandType == TEXT("set_pawn_properties"))
{
ResultJson = BlueprintCommands->HandleCommand(CommandType, Params);
}
// Blueprint Node Commands
else if (CommandType == TEXT("connect_blueprint_nodes") ||
CommandType == TEXT("add_blueprint_get_self_component_reference") ||
CommandType == TEXT("add_blueprint_self_reference") ||
CommandType == TEXT("find_blueprint_nodes") ||
CommandType == TEXT("add_blueprint_event_node") ||
CommandType == TEXT("add_blueprint_input_action_node") ||
CommandType == TEXT("add_blueprint_function_node") ||
CommandType == TEXT("add_blueprint_get_component_node") ||
CommandType == TEXT("add_blueprint_variable"))
{
ResultJson = BlueprintNodeCommands->HandleCommand(CommandType, Params);
}
// Project Commands
else if (CommandType == TEXT("create_input_mapping"))
{
ResultJson = ProjectCommands->HandleCommand(CommandType, Params);
}
// UMG Commands
else if (CommandType == TEXT("create_umg_widget_blueprint") ||
CommandType == TEXT("add_text_block_to_widget") ||
CommandType == TEXT("add_button_to_widget") ||
CommandType == TEXT("bind_widget_event") ||
CommandType == TEXT("set_text_block_binding") ||
CommandType == TEXT("add_widget_to_viewport"))
{
ResultJson = UMGCommands->HandleCommand(CommandType, Params);
}
else
{
ResponseJson->SetStringField(TEXT("status"), TEXT("error"));
ResponseJson->SetStringField(TEXT("error"), FString::Printf(TEXT("Unknown command: %s"), *CommandType));
FString ResultString;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&ResultString);
FJsonSerializer::Serialize(ResponseJson.ToSharedRef(), Writer);
Promise.SetValue(ResultString);
return;
}
// Check if the result contains an error
bool bSuccess = true;
FString ErrorMessage;
if (ResultJson->HasField(TEXT("success")))
{
bSuccess = ResultJson->GetBoolField(TEXT("success"));
if (!bSuccess && ResultJson->HasField(TEXT("error")))
{
ErrorMessage = ResultJson->GetStringField(TEXT("error"));
}
}
if (bSuccess)
{
// Set success status and include the result
ResponseJson->SetStringField(TEXT("status"), TEXT("success"));
ResponseJson->SetObjectField(TEXT("result"), ResultJson);
}
else
{
// Set error status and include the error message
ResponseJson->SetStringField(TEXT("status"), TEXT("error"));
ResponseJson->SetStringField(TEXT("error"), ErrorMessage);
}
}
catch (const std::exception& e)
{
ResponseJson->SetStringField(TEXT("status"), TEXT("error"));
ResponseJson->SetStringField(TEXT("error"), UTF8_TO_TCHAR(e.what()));
}
FString ResultString;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&ResultString);
FJsonSerializer::Serialize(ResponseJson.ToSharedRef(), Writer);
Promise.SetValue(ResultString);
});
return Future.Get();
}

View file

@ -0,0 +1,21 @@
#include "UnrealMCPModule.h"
#include "UnrealMCPBridge.h"
#include "Modules/ModuleManager.h"
#include "EditorSubsystem.h"
#include "Editor.h"
#define LOCTEXT_NAMESPACE "FUnrealMCPModule"
void FUnrealMCPModule::StartupModule()
{
UE_LOG(LogTemp, Display, TEXT("Unreal MCP Module has started"));
}
void FUnrealMCPModule::ShutdownModule()
{
UE_LOG(LogTemp, Display, TEXT("Unreal MCP Module has shut down"));
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FUnrealMCPModule, UnrealMCPChong)

View file

@ -0,0 +1,34 @@
#pragma once
#include "CoreMinimal.h"
#include "Json.h"
/**
* Handler class for Blueprint-related MCP commands
*/
class UNREALMCPCHONG_API FUnrealMCPBlueprintCommands
{
public:
FUnrealMCPBlueprintCommands();
// Handle blueprint commands
TSharedPtr<FJsonObject> HandleCommand(const FString& CommandType, const TSharedPtr<FJsonObject>& Params);
private:
// Specific blueprint command handlers
TSharedPtr<FJsonObject> HandleCreateBlueprint(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleAddComponentToBlueprint(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleSetComponentProperty(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleSetPhysicsProperties(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleCompileBlueprint(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleSpawnBlueprintActor(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleSetBlueprintProperty(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleSetStaticMeshProperties(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleSetPawnProperties(const TSharedPtr<FJsonObject>& Params);
// Helper functions
TSharedPtr<FJsonObject> AddComponentToBlueprint(const FString& BlueprintName, const FString& ComponentType,
const FString& ComponentName, const FString& MeshType,
const TArray<float>& Location, const TArray<float>& Rotation,
const TArray<float>& Scale, const TSharedPtr<FJsonObject>& ComponentProperties);
};

View file

@ -0,0 +1,27 @@
#pragma once
#include "CoreMinimal.h"
#include "Json.h"
/**
* Handler class for Blueprint Node-related MCP commands
*/
class UNREALMCPCHONG_API FUnrealMCPBlueprintNodeCommands
{
public:
FUnrealMCPBlueprintNodeCommands();
// Handle blueprint node commands
TSharedPtr<FJsonObject> HandleCommand(const FString& CommandType, const TSharedPtr<FJsonObject>& Params);
private:
// Specific blueprint node command handlers
TSharedPtr<FJsonObject> HandleConnectBlueprintNodes(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleAddBlueprintGetSelfComponentReference(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleAddBlueprintEvent(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleAddBlueprintFunctionCall(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleAddBlueprintVariable(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleAddBlueprintInputActionNode(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleAddBlueprintSelfReference(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleFindBlueprintNodes(const TSharedPtr<FJsonObject>& Params);
};

View file

@ -0,0 +1,60 @@
#pragma once
#include "CoreMinimal.h"
#include "Json.h"
// Forward declarations
class AActor;
class UBlueprint;
class UEdGraph;
class UEdGraphNode;
class UEdGraphPin;
class UK2Node_Event;
class UK2Node_CallFunction;
class UK2Node_VariableGet;
class UK2Node_VariableSet;
class UK2Node_InputAction;
class UK2Node_Self;
class UFunction;
/**
* Common utilities for UnrealMCP commands
*/
class UNREALMCPCHONG_API FUnrealMCPCommonUtils
{
public:
// JSON utilities
static TSharedPtr<FJsonObject> CreateErrorResponse(const FString& Message);
static TSharedPtr<FJsonObject> CreateSuccessResponse(const TSharedPtr<FJsonObject>& Data = nullptr);
static void GetIntArrayFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName, TArray<int32>& OutArray);
static void GetFloatArrayFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName, TArray<float>& OutArray);
static FVector2D GetVector2DFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName);
static FVector GetVectorFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName);
static FRotator GetRotatorFromJson(const TSharedPtr<FJsonObject>& JsonObject, const FString& FieldName);
// Actor utilities
static TSharedPtr<FJsonValue> ActorToJson(AActor* Actor);
static TSharedPtr<FJsonObject> ActorToJsonObject(AActor* Actor, bool bDetailed = false);
// Blueprint utilities
static UBlueprint* FindBlueprint(const FString& BlueprintName);
static UBlueprint* FindBlueprintByName(const FString& BlueprintName);
static UClass* FindClassByName(const FString& ClassName, const TCHAR* CurrentOperation = nullptr);
static UEdGraph* FindOrCreateEventGraph(UBlueprint* Blueprint);
// Blueprint node utilities
static UK2Node_Event* CreateEventNode(UEdGraph* Graph, const FString& EventName, const FVector2D& Position);
static UK2Node_CallFunction* CreateFunctionCallNode(UEdGraph* Graph, UFunction* Function, const FVector2D& Position);
static UK2Node_VariableGet* CreateVariableGetNode(UEdGraph* Graph, UBlueprint* Blueprint, const FString& VariableName, const FVector2D& Position);
static UK2Node_VariableSet* CreateVariableSetNode(UEdGraph* Graph, UBlueprint* Blueprint, const FString& VariableName, const FVector2D& Position);
static UK2Node_InputAction* CreateInputActionNode(UEdGraph* Graph, const FString& ActionName, const FVector2D& Position);
static UK2Node_Self* CreateSelfReferenceNode(UEdGraph* Graph, const FVector2D& Position);
static bool ConnectGraphNodes(UEdGraph* Graph, UEdGraphNode* SourceNode, const FString& SourcePinName,
UEdGraphNode* TargetNode, const FString& TargetPinName);
static UEdGraphPin* FindPin(UEdGraphNode* Node, const FString& PinName, EEdGraphPinDirection Direction = EGPD_MAX);
static UK2Node_Event* FindExistingEventNode(UEdGraph* Graph, const FString& EventName);
// Property utilities
static bool SetObjectProperty(UObject* Object, const FString& PropertyName,
const TSharedPtr<FJsonValue>& Value, FString& OutErrorMessage);
};

View file

@ -0,0 +1,34 @@
#pragma once
#include "CoreMinimal.h"
#include "Json.h"
/**
* Handler class for Editor-related MCP commands
* Handles viewport control, actor manipulation, and level management
*/
class UNREALMCPCHONG_API FUnrealMCPEditorCommands
{
public:
FUnrealMCPEditorCommands();
// Handle editor commands
TSharedPtr<FJsonObject> HandleCommand(const FString& CommandType, const TSharedPtr<FJsonObject>& Params);
private:
// Actor manipulation commands
TSharedPtr<FJsonObject> HandleGetActorsInLevel(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleFindActorsByName(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleSpawnActor(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleDeleteActor(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleSetActorTransform(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleGetActorProperties(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleSetActorProperty(const TSharedPtr<FJsonObject>& Params);
// Blueprint actor spawning
TSharedPtr<FJsonObject> HandleSpawnBlueprintActor(const TSharedPtr<FJsonObject>& Params);
// Editor viewport commands
TSharedPtr<FJsonObject> HandleFocusViewport(const TSharedPtr<FJsonObject>& Params);
TSharedPtr<FJsonObject> HandleTakeScreenshot(const TSharedPtr<FJsonObject>& Params);
};

View file

@ -0,0 +1,20 @@
#pragma once
#include "CoreMinimal.h"
#include "Json.h"
/**
* Handler class for Project-wide MCP commands
*/
class UNREALMCPCHONG_API FUnrealMCPProjectCommands
{
public:
FUnrealMCPProjectCommands();
// Handle project commands
TSharedPtr<FJsonObject> HandleCommand(const FString& CommandType, const TSharedPtr<FJsonObject>& Params);
private:
// Specific project command handlers
TSharedPtr<FJsonObject> HandleCreateInputMapping(const TSharedPtr<FJsonObject>& Params);
};

View file

@ -0,0 +1,82 @@
#pragma once
#include "CoreMinimal.h"
#include "Json.h"
/**
* Handles UMG (Widget Blueprint) related MCP commands
* Responsible for creating and modifying UMG Widget Blueprints,
* adding widget components, and managing widget instances in the viewport.
*/
class UNREALMCPCHONG_API FUnrealMCPUMGCommands
{
public:
FUnrealMCPUMGCommands();
/**
* Handle UMG-related commands
* @param CommandType - The type of command to handle
* @param Params - JSON parameters for the command
* @return JSON response with results or error
*/
TSharedPtr<FJsonObject> HandleCommand(const FString& CommandType, const TSharedPtr<FJsonObject>& Params);
private:
/**
* Create a new UMG Widget Blueprint
* @param Params - Must include "name" for the blueprint name
* @return JSON response with the created blueprint details
*/
TSharedPtr<FJsonObject> HandleCreateUMGWidgetBlueprint(const TSharedPtr<FJsonObject>& Params);
/**
* Add a Text Block widget to a UMG Widget Blueprint
* @param Params - Must include:
* "blueprint_name" - Name of the target Widget Blueprint
* "widget_name" - Name for the new Text Block
* "text" - Initial text content (optional)
* "position" - [X, Y] position in the canvas (optional)
* @return JSON response with the added widget details
*/
TSharedPtr<FJsonObject> HandleAddTextBlockToWidget(const TSharedPtr<FJsonObject>& Params);
/**
* Add a widget instance to the game viewport
* @param Params - Must include:
* "blueprint_name" - Name of the Widget Blueprint to instantiate
* "z_order" - Z-order for widget display (optional)
* @return JSON response with the widget instance details
*/
TSharedPtr<FJsonObject> HandleAddWidgetToViewport(const TSharedPtr<FJsonObject>& Params);
/**
* Add a Button widget to a UMG Widget Blueprint
* @param Params - Must include:
* "blueprint_name" - Name of the target Widget Blueprint
* "widget_name" - Name for the new Button
* "text" - Button text
* "position" - [X, Y] position in the canvas
* @return JSON response with the added widget details
*/
TSharedPtr<FJsonObject> HandleAddButtonToWidget(const TSharedPtr<FJsonObject>& Params);
/**
* Bind an event to a widget (e.g. button click)
* @param Params - Must include:
* "blueprint_name" - Name of the target Widget Blueprint
* "widget_name" - Name of the widget to bind
* "event_name" - Name of the event to bind
* @return JSON response with the binding details
*/
TSharedPtr<FJsonObject> HandleBindWidgetEvent(const TSharedPtr<FJsonObject>& Params);
/**
* Set up text block binding for dynamic updates
* @param Params - Must include:
* "blueprint_name" - Name of the target Widget Blueprint
* "widget_name" - Name of the widget to bind
* "binding_name" - Name of the binding to set up
* @return JSON response with the binding details
*/
TSharedPtr<FJsonObject> HandleSetTextBlockBinding(const TSharedPtr<FJsonObject>& Params);
};

View file

@ -0,0 +1,34 @@
#pragma once
#include "CoreMinimal.h"
#include "HAL/Runnable.h"
#include "Sockets.h"
#include "Interfaces/IPv4/IPv4Address.h"
class UUnrealMCPBridge;
/**
* Runnable class for the MCP server thread
*/
class FMCPServerRunnable : public FRunnable
{
public:
FMCPServerRunnable(UUnrealMCPBridge* InBridge, TSharedPtr<FSocket> InListenerSocket);
virtual ~FMCPServerRunnable();
// FRunnable interface
virtual bool Init() override;
virtual uint32 Run() override;
virtual void Stop() override;
virtual void Exit() override;
protected:
void HandleClientConnection(TSharedPtr<FSocket> ClientSocket);
void ProcessMessage(TSharedPtr<FSocket> Client, const FString& Message);
private:
UUnrealMCPBridge* Bridge;
TSharedPtr<FSocket> ListenerSocket;
TSharedPtr<FSocket> ClientSocket;
bool bRunning;
};

View file

@ -0,0 +1,64 @@
#pragma once
#include "CoreMinimal.h"
#include "EditorSubsystem.h"
#include "Sockets.h"
#include "SocketSubsystem.h"
#include "Http.h"
#include "Json.h"
#include "Interfaces/IPv4/IPv4Address.h"
#include "Interfaces/IPv4/IPv4Endpoint.h"
#include "Commands/UnrealMCPEditorCommands.h"
#include "Commands/UnrealMCPBlueprintCommands.h"
#include "Commands/UnrealMCPBlueprintNodeCommands.h"
#include "Commands/UnrealMCPProjectCommands.h"
#include "Commands/UnrealMCPUMGCommands.h"
#include "UnrealMCPBridge.generated.h"
class FMCPServerRunnable;
/**
* Editor subsystem for MCP Bridge
* Handles communication between external tools and the Unreal Editor
* through a TCP socket connection. Commands are received as JSON and
* routed to appropriate command handlers.
*/
UCLASS()
class UNREALMCPCHONG_API UUnrealMCPBridge : public UEditorSubsystem
{
GENERATED_BODY()
public:
UUnrealMCPBridge();
virtual ~UUnrealMCPBridge();
// UEditorSubsystem implementation
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
// Server functions
void StartServer();
void StopServer();
bool IsRunning() const { return bIsRunning; }
// Command execution
FString ExecuteCommand(const FString& CommandType, const TSharedPtr<FJsonObject>& Params);
private:
// Server state
bool bIsRunning;
TSharedPtr<FSocket> ListenerSocket;
TSharedPtr<FSocket> ConnectionSocket;
FRunnableThread* ServerThread;
// Server configuration
FIPv4Address ServerAddress;
uint16 Port;
// Command handler instances
TSharedPtr<FUnrealMCPEditorCommands> EditorCommands;
TSharedPtr<FUnrealMCPBlueprintCommands> BlueprintCommands;
TSharedPtr<FUnrealMCPBlueprintNodeCommands> BlueprintNodeCommands;
TSharedPtr<FUnrealMCPProjectCommands> ProjectCommands;
TSharedPtr<FUnrealMCPUMGCommands> UMGCommands;
};

View file

@ -0,0 +1,22 @@
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
class FUnrealMCPModule : public IModuleInterface
{
public:
/** IModuleInterface implementation */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
static inline FUnrealMCPModule& Get()
{
return FModuleManager::LoadModuleChecked<FUnrealMCPModule>("UnrealMCPChong");
}
static inline bool IsAvailable()
{
return FModuleManager::Get().IsModuleLoaded("UnrealMCPChong");
}
};

View file

@ -0,0 +1,78 @@
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class UnrealMCPChong : ModuleRules
{
public UnrealMCPChong(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
// Use IWYUSupport instead of the deprecated bEnforceIWYU in UE5.5
IWYUSupport = IWYUSupport.Full;
PublicIncludePaths.AddRange(
new string[] {
// ... add public include paths required here ...
}
);
PrivateIncludePaths.AddRange(
new string[] {
// ... add other private include paths required here ...
}
);
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"CoreUObject",
"Engine",
"InputCore",
"Networking",
"Sockets",
"HTTP",
"Json",
"JsonUtilities",
"DeveloperSettings"
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"UnrealEd",
"EditorScriptingUtilities",
"EditorSubsystem",
"Slate",
"SlateCore",
"UMG",
"Kismet",
"KismetCompiler",
"BlueprintGraph",
"Projects",
"AssetRegistry"
}
);
if (Target.bBuildEditor == true)
{
PrivateDependencyModuleNames.AddRange(
new string[]
{
"PropertyEditor", // For widget property editing
"ToolMenus", // For editor UI
"BlueprintEditorLibrary", // For Blueprint utilities
"UMGEditor" // For WidgetBlueprint.h and other UMG editor functionality
}
);
}
DynamicallyLoadedModuleNames.AddRange(
new string[]
{
// ... add any modules that your module loads dynamically here ...
}
);
}
}

View file

@ -0,0 +1,35 @@
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "UnrealMCP Chong",
"Description": "Model Context Protocol implementation for Unreal Engine",
"Category": "Editor",
"CreatedBy": "Your Name",
"CreatedByURL": "",
"DocsURL": "",
"MarketplaceURL": "",
"SupportURL": "",
"CanContainContent": true,
"IsBetaVersion": false,
"IsExperimentalVersion": false,
"Installed": false,
"Modules": [
{
"Name": "UnrealMCPChong",
"Type": "Editor",
"LoadingPhase": "Default",
"WhitelistPlatforms": [
"Win64",
"Mac",
"Linux"
]
}
],
"Plugins": [
{
"Name": "EditorScriptingUtilities",
"Enabled": true
}
]
}

View file

@ -1,7 +1,73 @@
#include "HyperTwistTraining/HyperTwistTrainingLibrary.h"
#include "HyperTwistReplay/HyperTwistReplayLibrary.h"
#include "JsonObjectConverter.h"
namespace HyperTwistTrainingLibraryInternal
{
bool IsModeAllowed(
const FHyperTwistTrainingCase& TrainingCase,
EHyperTwistTrainingDeliveryMode Mode
)
{
return TrainingCase.AllowedDeliveryModes.Num() == 0 || TrainingCase.AllowedDeliveryModes.Contains(Mode);
}
const FHyperTwistProgressionMemory* FindMemoryForCase(
const TArray<FHyperTwistProgressionMemory>& Memories,
const FString& CaseId
)
{
for (const FHyperTwistProgressionMemory& Memory : Memories)
{
if (Memory.CaseId == CaseId)
{
return &Memory;
}
}
return nullptr;
}
float GetBaseDifficultyWeight(const FHyperTwistTrainingCase& TrainingCase)
{
return TrainingCase.Difficulty > 0.0f ? TrainingCase.Difficulty : 1.0f;
}
float GetBaseResurfacePriority(const FHyperTwistTrainingCase& TrainingCase)
{
return FMath::Max(1.0f, GetBaseDifficultyWeight(TrainingCase) * 10.0f);
}
float ComputeSelectionScore(
const FHyperTwistTrainingCase& TrainingCase,
const FHyperTwistProgressionMemory* Memory,
const FString& CurrentCaseId
)
{
float Score = Memory ? Memory->ResurfacePriority : GetBaseResurfacePriority(TrainingCase);
if (Memory)
{
const int32 FailPressure = FMath::Max(0, Memory->AttemptCount - Memory->SuccessCount);
Score += static_cast<float>(FailPressure) * 0.35f;
Score -= static_cast<float>(Memory->SuccessCount) * 0.10f;
Score -= FMath::Max(0.0f, Memory->StrengthScore) * 0.20f;
if (Memory->LastOutcome != EHyperTwistTrainingAttemptResult::Success)
{
Score += 0.5f;
}
}
if (!CurrentCaseId.IsEmpty() && TrainingCase.CaseId == CurrentCaseId)
{
Score -= 1000.0f;
}
return Score;
}
bool MatchesPuzzleFamily(const FString& PuzzleId, EHyperTwistPuzzleFamily PuzzleFamily)
{
switch (PuzzleFamily)
@ -14,6 +80,14 @@ namespace HyperTwistTrainingLibraryInternal
return !PuzzleId.IsEmpty();
}
}
template <typename TStruct>
FString SerializePayload(const TStruct& Payload)
{
FString PayloadJson;
FJsonObjectConverter::UStructToJsonObjectString(Payload, PayloadJson);
return PayloadJson;
}
}
FHyperTwistContentPack UHyperTwistTrainingLibrary::MakeSampleClassicContentPack()
@ -176,6 +250,31 @@ FHyperTwistTrainingSession UHyperTwistTrainingLibrary::AdvanceTrainingSession(
return UpdatedSession;
}
FHyperTwistTrainingAdvanceResult UHyperTwistTrainingLibrary::AdvanceTrainingSessionWithSelection(
const FHyperTwistTrainingSession& Session,
const FHyperTwistTrainingDeck& Deck,
const TArray<FHyperTwistProgressionMemory>& Memories,
EHyperTwistTrainingSessionState TargetState
)
{
FHyperTwistTrainingAdvanceResult AdvanceResult;
AdvanceResult.UpdatedSession = Session;
FString NextCaseId;
if ((TargetState == EHyperTwistTrainingSessionState::Active || TargetState == EHyperTwistTrainingSessionState::Reviewing)
&& Deck.IsStructurallyValid())
{
AdvanceResult.NextCaseSelection = SelectNextTrainingCase(Deck, Memories, Session.Mode, Session.CurrentCaseId);
if (AdvanceResult.NextCaseSelection.IsStructurallyValid())
{
NextCaseId = AdvanceResult.NextCaseSelection.TrainingCase.CaseId;
}
}
AdvanceResult.UpdatedSession = AdvanceTrainingSession(Session, TargetState, NextCaseId);
return AdvanceResult;
}
FHyperTwistTrainingSession UHyperTwistTrainingLibrary::RecordTrainingAttempt(
const FHyperTwistTrainingSession& Session,
const FHyperTwistTrainingAttempt& Attempt
@ -235,7 +334,323 @@ FHyperTwistProgressionMemory UHyperTwistTrainingLibrary::MakeInitialProgressionM
Memory.LastOutcome = EHyperTwistTrainingAttemptResult::Failure;
Memory.AttemptCount = 0;
Memory.SuccessCount = 0;
Memory.DifficultyWeight = TrainingCase.Difficulty <= 0.0f ? 1.0f : TrainingCase.Difficulty;
Memory.ResurfacePriority = FMath::Max(1.0f, TrainingCase.Difficulty * 10.0f);
Memory.DifficultyWeight = HyperTwistTrainingLibraryInternal::GetBaseDifficultyWeight(TrainingCase);
Memory.ResurfacePriority = HyperTwistTrainingLibraryInternal::GetBaseResurfacePriority(TrainingCase);
return Memory;
}
FHyperTwistProgressionMemory UHyperTwistTrainingLibrary::UpdateProgressionMemory(
const FHyperTwistTrainingSession& Session,
const FHyperTwistTrainingCase& TrainingCase,
const FHyperTwistProgressionMemory& CurrentMemory,
const FHyperTwistTrainingAttempt& Attempt
)
{
FHyperTwistProgressionMemory UpdatedMemory = CurrentMemory;
if (!TrainingCase.IsStructurallyValid() || !Attempt.IsStructurallyValid())
{
return UpdatedMemory;
}
if (!UpdatedMemory.UserId.Len())
{
UpdatedMemory.UserId = Session.UserId;
}
if (!UpdatedMemory.DeckId.Len())
{
UpdatedMemory.DeckId = Session.DeckId;
}
UpdatedMemory.CaseId = TrainingCase.CaseId;
UpdatedMemory.DifficultyWeight = UpdatedMemory.DifficultyWeight > 0.0f
? UpdatedMemory.DifficultyWeight
: HyperTwistTrainingLibraryInternal::GetBaseDifficultyWeight(TrainingCase);
if (UpdatedMemory.ResurfacePriority <= 0.0f)
{
UpdatedMemory.ResurfacePriority = HyperTwistTrainingLibraryInternal::GetBaseResurfacePriority(TrainingCase);
}
++UpdatedMemory.AttemptCount;
UpdatedMemory.LastSeenAtUtc = !Attempt.CompletedAtUtc.IsEmpty() ? Attempt.CompletedAtUtc : Session.StartedAtUtc;
UpdatedMemory.LastOutcome = Attempt.Result;
switch (Attempt.Result)
{
case EHyperTwistTrainingAttemptResult::Success:
++UpdatedMemory.SuccessCount;
UpdatedMemory.StrengthScore += 1.0f;
UpdatedMemory.ResurfacePriority -= 0.75f;
break;
case EHyperTwistTrainingAttemptResult::Failure:
UpdatedMemory.StrengthScore -= 0.25f;
UpdatedMemory.ResurfacePriority += 1.25f;
break;
case EHyperTwistTrainingAttemptResult::Timeout:
UpdatedMemory.StrengthScore -= 0.50f;
UpdatedMemory.ResurfacePriority += 1.50f;
break;
case EHyperTwistTrainingAttemptResult::Aborted:
UpdatedMemory.StrengthScore -= 0.10f;
UpdatedMemory.ResurfacePriority += 0.50f;
break;
default:
break;
}
if (Attempt.Mistakes > 0)
{
UpdatedMemory.StrengthScore -= static_cast<float>(Attempt.Mistakes) * 0.05f;
UpdatedMemory.ResurfacePriority += static_cast<float>(Attempt.Mistakes) * 0.10f;
}
if (TrainingCase.TimeTargetMs > 0 && Attempt.TotalTimeMs > 0)
{
if (Attempt.TotalTimeMs <= TrainingCase.TimeTargetMs)
{
UpdatedMemory.StrengthScore += 0.25f;
}
else
{
const float OvertimeRatio = static_cast<float>(Attempt.TotalTimeMs - TrainingCase.TimeTargetMs) / static_cast<float>(TrainingCase.TimeTargetMs);
UpdatedMemory.ResurfacePriority += FMath::Clamp(OvertimeRatio, 0.0f, 1.5f);
}
}
UpdatedMemory.StrengthScore = FMath::Clamp(UpdatedMemory.StrengthScore, 0.0f, 10.0f);
UpdatedMemory.ResurfacePriority = FMath::Max(0.25f, UpdatedMemory.ResurfacePriority);
return UpdatedMemory;
}
FHyperTwistTrainingCaseSelection UHyperTwistTrainingLibrary::SelectNextTrainingCase(
const FHyperTwistTrainingDeck& Deck,
const TArray<FHyperTwistProgressionMemory>& Memories,
EHyperTwistTrainingDeliveryMode Mode,
const FString& CurrentCaseId
)
{
FHyperTwistTrainingCaseSelection BestSelection;
BestSelection.SelectionScore = -1000.0f;
if (!Deck.IsStructurallyValid())
{
return BestSelection;
}
for (const FHyperTwistTrainingCase& TrainingCase : Deck.Cases)
{
if (!TrainingCase.IsStructurallyValid())
{
continue;
}
const bool bModeCompatible = HyperTwistTrainingLibraryInternal::IsModeAllowed(TrainingCase, Mode);
if (!bModeCompatible)
{
continue;
}
const FHyperTwistProgressionMemory* Memory = HyperTwistTrainingLibraryInternal::FindMemoryForCase(Memories, TrainingCase.CaseId);
const float Score = HyperTwistTrainingLibraryInternal::ComputeSelectionScore(TrainingCase, Memory, CurrentCaseId);
if (Score > BestSelection.SelectionScore)
{
BestSelection.TrainingCase = TrainingCase;
BestSelection.SelectionScore = Score;
BestSelection.DifficultyWeight = Memory ? Memory->DifficultyWeight : HyperTwistTrainingLibraryInternal::GetBaseDifficultyWeight(TrainingCase);
BestSelection.ResurfacePriority = Memory ? Memory->ResurfacePriority : HyperTwistTrainingLibraryInternal::GetBaseResurfacePriority(TrainingCase);
BestSelection.AttemptCount = Memory ? Memory->AttemptCount : 0;
BestSelection.SuccessCount = Memory ? Memory->SuccessCount : 0;
BestSelection.bModeCompatible = true;
BestSelection.bUsedStoredMemory = Memory != nullptr;
}
}
return BestSelection;
}
FHyperTwistTrainingSessionSummary UHyperTwistTrainingLibrary::DeriveTrainingSessionSummary(
const FHyperTwistTrainingSession& Session,
const TArray<FHyperTwistTrainingAttempt>& Attempts
)
{
FHyperTwistTrainingSessionSummary Summary;
Summary.TrainingSessionId = Session.TrainingSessionId;
Summary.DeckId = Session.DeckId;
Summary.UserId = Session.UserId;
Summary.SessionState = Session.SessionState;
for (const FHyperTwistTrainingAttempt& Attempt : Attempts)
{
if (!Attempt.IsStructurallyValid() || Attempt.TrainingSessionId != Session.TrainingSessionId)
{
continue;
}
++Summary.CompletedAttemptCount;
Summary.TotalTimeMs += FMath::Max(0, Attempt.TotalTimeMs);
Summary.TotalExecutionTimeMs += FMath::Max(0, Attempt.ExecutionTimeMs);
Summary.TotalRecognitionTimeMs += FMath::Max(0, Attempt.RecognitionTimeMs);
Summary.TotalMistakes += FMath::Max(0, Attempt.Mistakes);
if (Attempt.TotalTimeMs > 0)
{
Summary.BestTotalTimeMs = Summary.BestTotalTimeMs == 0
? Attempt.TotalTimeMs
: FMath::Min(Summary.BestTotalTimeMs, Attempt.TotalTimeMs);
}
switch (Attempt.Result)
{
case EHyperTwistTrainingAttemptResult::Success:
++Summary.SuccessCount;
break;
case EHyperTwistTrainingAttemptResult::Failure:
++Summary.FailureCount;
break;
case EHyperTwistTrainingAttemptResult::Timeout:
++Summary.TimeoutCount;
break;
case EHyperTwistTrainingAttemptResult::Aborted:
++Summary.AbortedCount;
break;
default:
break;
}
if (Summary.LastCompletedAtUtc.IsEmpty() || Attempt.CompletedAtUtc >= Summary.LastCompletedAtUtc)
{
Summary.LastCompletedAtUtc = Attempt.CompletedAtUtc;
Summary.LastCaseId = Attempt.CaseId;
Summary.LastReplayId = Attempt.ReplayId;
}
}
Summary.AttemptCount = FMath::Max(Session.AttemptCount, Summary.CompletedAttemptCount);
if (Summary.CompletedAttemptCount > 0)
{
Summary.AverageTotalTimeMs = Summary.TotalTimeMs / Summary.CompletedAttemptCount;
Summary.SuccessRate = static_cast<float>(Summary.SuccessCount) / static_cast<float>(Summary.CompletedAttemptCount);
}
return Summary;
}
FHyperTwistReplayEvent UHyperTwistTrainingLibrary::MakeTrainingAttemptReplayEvent(
const FHyperTwistTrainingSession& Session,
const FHyperTwistTrainingAttempt& Attempt,
int32 TimeMs
)
{
FHyperTwistReplayEvent ReplayEvent;
if (!Session.IsStructurallyValid() || !Attempt.IsStructurallyValid())
{
return ReplayEvent;
}
FHyperTwistTrainingAttemptReplayPayload Payload;
Payload.TrainingSessionId = Session.TrainingSessionId;
Payload.AttemptId = Attempt.AttemptId;
Payload.DeckId = Session.DeckId;
Payload.UserId = Session.UserId;
Payload.CaseId = Attempt.CaseId;
Payload.DeliveryMode = Session.Mode;
Payload.Result = Attempt.Result;
Payload.TotalTimeMs = Attempt.TotalTimeMs;
Payload.ExecutionTimeMs = Attempt.ExecutionTimeMs;
Payload.RecognitionTimeMs = Attempt.RecognitionTimeMs;
Payload.Mistakes = Attempt.Mistakes;
Payload.ReplayId = Attempt.ReplayId;
Payload.CompletedAtUtc = Attempt.CompletedAtUtc;
ReplayEvent.EventId = Attempt.AttemptId.IsEmpty()
? TEXT("training_attempt")
: FString::Printf(TEXT("training_attempt_%s"), *Attempt.AttemptId);
ReplayEvent.Sequence = 0;
ReplayEvent.TimeMs = FMath::Max(0, TimeMs > 0 ? TimeMs : Attempt.TotalTimeMs);
ReplayEvent.EventType = EHyperTwistReplayEventType::Annotation;
ReplayEvent.PayloadJson = HyperTwistTrainingLibraryInternal::SerializePayload(Payload);
return ReplayEvent;
}
FHyperTwistReplayPacket UHyperTwistTrainingLibrary::AppendTrainingAttemptToReplay(
const FHyperTwistReplayPacket& ReplayPacket,
const FHyperTwistTrainingSession& Session,
const FHyperTwistTrainingAttempt& Attempt,
int32 TimeMs
)
{
const FHyperTwistReplayEvent ReplayEvent = MakeTrainingAttemptReplayEvent(Session, Attempt, TimeMs);
if (!ReplayEvent.IsStructurallyValid())
{
return ReplayPacket;
}
return UHyperTwistReplayLibrary::AppendReplayEvent(ReplayPacket, ReplayEvent);
}
FHyperTwistTrainingAttemptResolution UHyperTwistTrainingLibrary::ResolveTrainingAttempt(
const FHyperTwistTrainingSession& Session,
const FHyperTwistTrainingDeck& Deck,
const FHyperTwistTrainingCase& TrainingCase,
const TArray<FHyperTwistProgressionMemory>& Memories,
const FHyperTwistTrainingAttempt& Attempt,
int32 TimeMs
)
{
FHyperTwistTrainingAttemptResolution Resolution;
if (!Session.IsStructurallyValid() || !Deck.IsStructurallyValid() || !TrainingCase.IsStructurallyValid() || !Attempt.IsStructurallyValid())
{
return Resolution;
}
FHyperTwistProgressionMemory SeedMemory;
bool bFoundExistingMemory = false;
for (const FHyperTwistProgressionMemory& Memory : Memories)
{
if (Memory.CaseId == TrainingCase.CaseId)
{
SeedMemory = Memory;
bFoundExistingMemory = true;
break;
}
}
if (!bFoundExistingMemory)
{
SeedMemory = MakeInitialProgressionMemory(Session, TrainingCase);
}
Resolution.UpdatedProgressionMemory = UpdateProgressionMemory(Session, TrainingCase, SeedMemory, Attempt);
Resolution.ReplayEvent = MakeTrainingAttemptReplayEvent(Session, Attempt, TimeMs);
TArray<FHyperTwistProgressionMemory> UpdatedMemories = Memories;
bool bReplacedExistingMemory = false;
for (FHyperTwistProgressionMemory& Memory : UpdatedMemories)
{
if (Memory.CaseId == Resolution.UpdatedProgressionMemory.CaseId)
{
Memory = Resolution.UpdatedProgressionMemory;
bReplacedExistingMemory = true;
break;
}
}
if (!bReplacedExistingMemory)
{
UpdatedMemories.Add(Resolution.UpdatedProgressionMemory);
}
const FHyperTwistTrainingSession ReviewSession = RecordTrainingAttempt(Session, Attempt);
const FHyperTwistTrainingAdvanceResult AdvanceResult = AdvanceTrainingSessionWithSelection(
ReviewSession,
Deck,
UpdatedMemories,
EHyperTwistTrainingSessionState::Active
);
Resolution.UpdatedSession = AdvanceResult.UpdatedSession;
Resolution.NextCaseSelection = AdvanceResult.NextCaseSelection;
return Resolution;
}

View file

@ -2,6 +2,7 @@
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "HyperTwistReplay/HyperTwistReplayTypes.h"
#include "HyperTwistTraining/HyperTwistTrainingTypes.h"
#include "HyperTwistTrainingLibrary.generated.h"
@ -47,6 +48,14 @@ public:
const FString& NextCaseId
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
static FHyperTwistTrainingAdvanceResult AdvanceTrainingSessionWithSelection(
const FHyperTwistTrainingSession& Session,
const FHyperTwistTrainingDeck& Deck,
const TArray<FHyperTwistProgressionMemory>& Memories,
EHyperTwistTrainingSessionState TargetState
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
static FHyperTwistTrainingSession RecordTrainingAttempt(
const FHyperTwistTrainingSession& Session,
@ -64,4 +73,51 @@ public:
const FHyperTwistTrainingSession& Session,
const FHyperTwistTrainingCase& TrainingCase
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
static FHyperTwistProgressionMemory UpdateProgressionMemory(
const FHyperTwistTrainingSession& Session,
const FHyperTwistTrainingCase& TrainingCase,
const FHyperTwistProgressionMemory& CurrentMemory,
const FHyperTwistTrainingAttempt& Attempt
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
static FHyperTwistTrainingCaseSelection SelectNextTrainingCase(
const FHyperTwistTrainingDeck& Deck,
const TArray<FHyperTwistProgressionMemory>& Memories,
EHyperTwistTrainingDeliveryMode Mode,
const FString& CurrentCaseId
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
static FHyperTwistTrainingSessionSummary DeriveTrainingSessionSummary(
const FHyperTwistTrainingSession& Session,
const TArray<FHyperTwistTrainingAttempt>& Attempts
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
static FHyperTwistReplayEvent MakeTrainingAttemptReplayEvent(
const FHyperTwistTrainingSession& Session,
const FHyperTwistTrainingAttempt& Attempt,
int32 TimeMs
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
static FHyperTwistReplayPacket AppendTrainingAttemptToReplay(
const FHyperTwistReplayPacket& ReplayPacket,
const FHyperTwistTrainingSession& Session,
const FHyperTwistTrainingAttempt& Attempt,
int32 TimeMs
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training")
static FHyperTwistTrainingAttemptResolution ResolveTrainingAttempt(
const FHyperTwistTrainingSession& Session,
const FHyperTwistTrainingDeck& Deck,
const FHyperTwistTrainingCase& TrainingCase,
const TArray<FHyperTwistProgressionMemory>& Memories,
const FHyperTwistTrainingAttempt& Attempt,
int32 TimeMs
);
};

View file

@ -258,6 +258,120 @@ struct FHyperTwistTrainingAttempt
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingSessionSummary
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString TrainingSessionId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DeckId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString UserId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
EHyperTwistTrainingSessionState SessionState = EHyperTwistTrainingSessionState::Created;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 AttemptCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 CompletedAttemptCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 SuccessCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 FailureCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TimeoutCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 AbortedCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalTimeMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalExecutionTimeMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalRecognitionTimeMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalMistakes = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 BestTotalTimeMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 AverageTotalTimeMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
float SuccessRate = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString LastCaseId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString LastReplayId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString LastCompletedAtUtc;
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingAttemptReplayPayload
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString EventKind = TEXT("training-attempt");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString TrainingSessionId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString AttemptId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString DeckId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString UserId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString CaseId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
EHyperTwistTrainingDeliveryMode DeliveryMode = EHyperTwistTrainingDeliveryMode::Timer;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
EHyperTwistTrainingAttemptResult Result = EHyperTwistTrainingAttemptResult::Failure;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 TotalTimeMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 ExecutionTimeMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 RecognitionTimeMs = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 Mistakes = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString ReplayId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FString CompletedAtUtc;
};
USTRUCT(BlueprintType)
struct FHyperTwistProgressionMemory
{
@ -293,3 +407,79 @@ struct FHyperTwistProgressionMemory
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
float ResurfacePriority = 0.0f;
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingCaseSelection
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistTrainingCase TrainingCase;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
float SelectionScore = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
float DifficultyWeight = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
float ResurfacePriority = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 AttemptCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
int32 SuccessCount = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bModeCompatible = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
bool bUsedStoredMemory = false;
bool IsStructurallyValid() const
{
return TrainingCase.IsStructurallyValid() && bModeCompatible && SelectionScore > -999.0f;
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingAdvanceResult
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistTrainingSession UpdatedSession;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistTrainingCaseSelection NextCaseSelection;
bool IsStructurallyValid() const
{
return UpdatedSession.IsStructurallyValid();
}
};
USTRUCT(BlueprintType)
struct FHyperTwistTrainingAttemptResolution
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistTrainingSession UpdatedSession;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistProgressionMemory UpdatedProgressionMemory;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistReplayEvent ReplayEvent;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
FHyperTwistTrainingCaseSelection NextCaseSelection;
bool IsStructurallyValid() const
{
return UpdatedSession.IsStructurallyValid()
&& UpdatedProgressionMemory.CaseId.Len() > 0;
}
};