Add empty test level and Unreal MCP tooling

- L_TestFlight: empty, zero-gravity level for Phase 0 flight testing,
  set as editor/game default map.
- Enable UnrealMCPython + Python Editor Script Plugin, third-party
  MCP plugin (GenOrca/unreal-mcp v2.2.0) giving direct editor control
  (actors, blueprints, levels, materials, etc.) alongside Epic's
  built-in Unreal MCP plugin.
- Vendor the mcp-server Python source used to bridge to the plugin.
- .mcp.json intentionally gitignored (machine-specific absolute paths).
This commit is contained in:
Joshua Deville
2026-07-08 14:56:57 -04:00
parent 43494a614b
commit 496320cc8c
82 changed files with 19577 additions and 1 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,85 @@
// Copyright (c) 2025 GenOrca (by zenoengine). All Rights Reserved.
// Shared internal helpers for the split MCPythonHelper_*.cpp translation units.
#pragma once
#include "CoreMinimal.h"
#include "Dom/JsonObject.h"
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "Engine/Blueprint.h"
inline FString MakeJsonError(const FString& Message)
{
TSharedPtr<FJsonObject> Obj = MakeShareable(new FJsonObject());
Obj->SetBoolField(TEXT("success"), false);
Obj->SetStringField(TEXT("message"), Message);
FString Out;
TSharedRef<TJsonWriter<>> W = TJsonWriterFactory<>::Create(&Out);
FJsonSerializer::Serialize(Obj.ToSharedRef(), W);
return Out;
}
inline FString MakeJsonSuccess(const FString& Message)
{
TSharedPtr<FJsonObject> Obj = MakeShareable(new FJsonObject());
Obj->SetBoolField(TEXT("success"), true);
Obj->SetStringField(TEXT("message"), Message);
FString Out;
TSharedRef<TJsonWriter<>> W = TJsonWriterFactory<>::Create(&Out);
FJsonSerializer::Serialize(Obj.ToSharedRef(), W);
return Out;
}
inline FString SerializeJsonObj(TSharedPtr<FJsonObject> Obj)
{
FString Out;
TSharedRef<TJsonWriter<>> W = TJsonWriterFactory<>::Create(&Out);
FJsonSerializer::Serialize(Obj.ToSharedRef(), W);
return Out;
}
inline UEdGraph* FindGraphByName(UBlueprint* Blueprint, const FString& GraphName)
{
for (UEdGraph* Graph : Blueprint->UbergraphPages)
{
if (Graph && Graph->GetName() == GraphName)
return Graph;
}
for (UEdGraph* Graph : Blueprint->FunctionGraphs)
{
if (Graph && Graph->GetName() == GraphName)
return Graph;
}
return nullptr;
}
inline UEdGraphNode* FindBPNodeByName(UEdGraph* Graph, const FString& NodeName)
{
for (UEdGraphNode* Node : Graph->Nodes)
{
if (Node && Node->GetName() == NodeName)
return Node;
}
return nullptr;
}
inline UEdGraphPin* FindPinByName(UEdGraphNode* Node, const FString& PinName, EEdGraphPinDirection Direction = EGPD_MAX)
{
for (UEdGraphPin* Pin : Node->Pins)
{
if (!Pin || Pin->bHidden) continue;
if (Direction != EGPD_MAX && Pin->Direction != Direction) continue;
// Match by internal name
if (Pin->GetName() == PinName)
return Pin;
// Match by friendly name
FString Friendly = Pin->PinFriendlyName.ToString();
if (!Friendly.IsEmpty() && Friendly == PinName)
return Pin;
}
return nullptr;
}

View File

@@ -0,0 +1,352 @@
// Copyright (c) 2025 GenOrca (by zenoengine). All Rights Reserved.
// AnimGraph authoring — split out of MCPythonHelper.cpp.
#include "MCPythonHelper.h"
#include "MCPythonHelperInternal.h"
#include "Engine/SCS_Node.h"
#include "Engine/SimpleConstructionScript.h"
#include "WidgetBlueprint.h"
#include "Blueprint/WidgetTree.h"
#include "Components/Widget.h"
#include "Components/PanelWidget.h"
#include "Editor.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "Toolkits/AssetEditorToolkit.h"
#include "BlueprintEditor.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"
#include "BehaviorTree/BTCompositeNode.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BehaviorTree/BTDecorator.h"
#include "BehaviorTree/BTService.h"
#include "BehaviorTreeEditor.h"
#include "BehaviorTreeGraphNode.h"
#include "BehaviorTreeGraph.h"
#include "BehaviorTreeGraphNode_Root.h"
#include "BehaviorTreeGraphNode_Composite.h"
#include "BehaviorTreeGraphNode_Task.h"
#include "BehaviorTreeGraphNode_Decorator.h"
#include "BehaviorTreeGraphNode_Service.h"
#include "BehaviorTreeGraphNode_SimpleParallel.h"
#include "BehaviorTreeGraphNode_SubtreeTask.h"
#include "EdGraphSchema_BehaviorTree.h"
#include "EdGraph/EdGraph.h"
#include "UObject/UObjectIterator.h"
#include "Engine/SkeletalMesh.h"
#include "Engine/SkeletalMeshSocket.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonReader.h"
// Blueprint graph includes
#include "K2Node_Event.h"
#include "K2Node_ComponentBoundEvent.h"
#include "K2Node_CustomEvent.h"
#include "K2Node_CallFunction.h"
#include "K2Node_IfThenElse.h"
#include "K2Node_ExecutionSequence.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "K2Node_MacroInstance.h"
#include "K2Node_DynamicCast.h"
#include "K2Node_InputKey.h"
#include "K2Node_SpawnActorFromClass.h"
#include "EdGraphSchema_K2.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Engine/Blueprint.h"
#include "UObject/UnrealType.h"
#include "UObject/TextProperty.h"
#include "Components/CanvasPanelSlot.h"
#include "Components/TextBlock.h"
// AnimGraph authoring (editor-only AnimGraph module)
#include "Animation/AnimBlueprint.h"
#include "Animation/AnimSequence.h"
#include "AnimGraphNode_StateMachine.h"
#include "AnimGraphNode_SequencePlayer.h"
#include "AnimGraphNode_Root.h"
#include "AnimGraphNode_StateResult.h"
#include "AnimGraphNode_TransitionResult.h"
#include "AnimStateNode.h"
#include "AnimStateTransitionNode.h"
#include "AnimStateEntryNode.h"
#include "AnimationStateMachineGraph.h"
#include "Kismet/KismetMathLibrary.h"
// Editor viewport projection
#include "LevelEditorViewport.h"
#include "EditorViewportClient.h"
#include "SceneView.h"
// ─── AnimGraph authoring ─────────────────────────────────────────────────────
static UEdGraphPin* FirstVisiblePin(UEdGraphNode* Node, EEdGraphPinDirection Dir)
{
if (!Node) return nullptr;
for (UEdGraphPin* P : Node->Pins)
if (P && !P->bHidden && P->Direction == Dir)
return P;
return nullptr;
}
template <typename T>
static T* FindNodeOfType(UEdGraph* Graph)
{
if (!Graph) return nullptr;
for (UEdGraphNode* N : Graph->Nodes)
if (T* Hit = Cast<T>(N))
return Hit;
return nullptr;
}
// Add a Sequence Player playing Seq into Graph and link its pose output to PoseSinkInputPin (if given).
static UAnimGraphNode_SequencePlayer* SpawnSequencePlayer(UEdGraph* Graph, UAnimSequence* Seq,
int32 X, int32 Y, UEdGraphPin* PoseSinkInputPin)
{
FGraphNodeCreator<UAnimGraphNode_SequencePlayer> Creator(*Graph);
UAnimGraphNode_SequencePlayer* Node = Creator.CreateNode(false);
Node->Node.SetSequence(Seq);
Node->Node.SetLoopAnimation(true);
Node->NodePosX = X;
Node->NodePosY = Y;
Creator.Finalize();
if (PoseSinkInputPin)
{
if (UEdGraphPin* PoseOut = FirstVisiblePin(Node, EGPD_Output))
PoseOut->MakeLinkTo(PoseSinkInputPin);
}
return Node;
}
FString UMCPythonHelper::AddAnimGraphSequencePlayer(UAnimBlueprint* AnimBP,
const FString& AnimSequencePath, bool bLinkToOutputPose)
{
if (!AnimBP)
return MakeJsonError(TEXT("Invalid AnimBlueprint."));
UEdGraph* AnimGraph = FindGraphByName(AnimBP, TEXT("AnimGraph"));
if (!AnimGraph)
return MakeJsonError(TEXT("AnimGraph not found on this AnimBlueprint."));
UAnimSequence* Seq = Cast<UAnimSequence>(StaticLoadObject(UAnimSequence::StaticClass(), nullptr, *AnimSequencePath));
if (!Seq)
return MakeJsonError(FString::Printf(TEXT("AnimSequence not found: %s"), *AnimSequencePath));
UAnimGraphNode_Root* Root = FindNodeOfType<UAnimGraphNode_Root>(AnimGraph);
UEdGraphPin* RootIn = Root ? FindPinByName(Root, TEXT("Result"), EGPD_Input) : nullptr;
UAnimGraphNode_SequencePlayer* Player =
SpawnSequencePlayer(AnimGraph, Seq, -400, 0, (bLinkToOutputPose && RootIn) ? RootIn : nullptr);
FKismetEditorUtilities::CompileBlueprint(AnimBP);
TSharedPtr<FJsonObject> R = MakeShareable(new FJsonObject());
R->SetBoolField(TEXT("success"), true);
R->SetStringField(TEXT("node_name"), Player->GetName());
R->SetStringField(TEXT("sequence"), Seq->GetPathName());
R->SetBoolField(TEXT("linked_to_output"), bLinkToOutputPose && RootIn != nullptr);
return SerializeJsonObj(R);
}
// Populate a transition's rule graph with: Get(SpeedVar) (Greater|Less) Threshold -> bCanEnterTransition.
// Map a comparison operator string to a UKismetMathLibrary double-comparison UFunction.
static UFunction* FindFloatCompareFunc(const FString& Op)
{
FName FnName;
if (Op == TEXT(">")) FnName = FName(TEXT("Greater_DoubleDouble"));
else if (Op == TEXT("<")) FnName = FName(TEXT("Less_DoubleDouble"));
else if (Op == TEXT(">=")) FnName = FName(TEXT("GreaterEqual_DoubleDouble"));
else if (Op == TEXT("<=")) FnName = FName(TEXT("LessEqual_DoubleDouble"));
else if (Op == TEXT("==")) FnName = FName(TEXT("EqualEqual_DoubleDouble"));
else return nullptr;
return UKismetMathLibrary::StaticClass()->FindFunctionByName(FnName);
}
// Populate a transition rule graph with: Get(Var) <Op> Value -> bCanEnterTransition.
static bool BuildFloatTransitionRule(UEdGraph* TransitionGraph, const FString& Var, const FString& Op, float Value)
{
UAnimGraphNode_TransitionResult* Result = FindNodeOfType<UAnimGraphNode_TransitionResult>(TransitionGraph);
if (!Result) return false;
UEdGraphPin* CanEnter = FindPinByName(Result, TEXT("bCanEnterTransition"), EGPD_Input);
if (!CanEnter) return false;
UFunction* CmpFunc = FindFloatCompareFunc(Op);
if (!CmpFunc) return false;
FGraphNodeCreator<UK2Node_VariableGet> GetCreator(*TransitionGraph);
UK2Node_VariableGet* GetNode = GetCreator.CreateNode(false);
GetNode->VariableReference.SetSelfMember(FName(*Var));
GetNode->NodePosX = -500;
GetCreator.Finalize();
FGraphNodeCreator<UK2Node_CallFunction> CmpCreator(*TransitionGraph);
UK2Node_CallFunction* CmpNode = CmpCreator.CreateNode(false);
CmpNode->SetFromFunction(CmpFunc);
CmpNode->NodePosX = -250;
CmpCreator.Finalize();
UEdGraphPin* VarOut = FirstVisiblePin(GetNode, EGPD_Output);
UEdGraphPin* PinA = FindPinByName(CmpNode, TEXT("A"), EGPD_Input);
UEdGraphPin* PinB = FindPinByName(CmpNode, TEXT("B"), EGPD_Input);
UEdGraphPin* CmpRet = FindPinByName(CmpNode, TEXT("ReturnValue"), EGPD_Output);
if (VarOut && PinA) VarOut->MakeLinkTo(PinA);
if (PinB) PinB->DefaultValue = FString::SanitizeFloat(Value);
if (CmpRet) CmpRet->MakeLinkTo(CanEnter);
return VarOut && PinA && CmpRet;
}
// Core builder shared by the generic and the locomotion-convenience UFUNCTIONs.
// Spec: { machine_name?, entry?, states:[{name, anim?}], transitions:[{from,to,var?,op?,value?}] }
static FString BuildStateMachineFromSpec(UAnimBlueprint* AnimBP, const TSharedPtr<FJsonObject>& Spec)
{
UEdGraph* AnimGraph = FindGraphByName(AnimBP, TEXT("AnimGraph"));
if (!AnimGraph)
return MakeJsonError(TEXT("AnimGraph not found on this AnimBlueprint."));
const TArray<TSharedPtr<FJsonValue>>* StatesJson = nullptr;
if (!Spec->TryGetArrayField(TEXT("states"), StatesJson) || StatesJson->Num() == 0)
return MakeJsonError(TEXT("Spec must contain a non-empty 'states' array."));
// Resolve + validate every state's anim up front (fail before mutating the graph).
struct FStateDef { FString Name; UAnimSequence* Seq; };
TArray<FStateDef> StateDefs;
for (const TSharedPtr<FJsonValue>& SV : *StatesJson)
{
const TSharedPtr<FJsonObject> SO = SV->AsObject();
if (!SO.IsValid())
return MakeJsonError(TEXT("Each entry in 'states' must be an object."));
FString Name, AnimPath;
if (!SO->TryGetStringField(TEXT("name"), Name) || Name.IsEmpty())
return MakeJsonError(TEXT("Each state needs a non-empty 'name'."));
SO->TryGetStringField(TEXT("anim"), AnimPath);
UAnimSequence* Seq = nullptr;
if (!AnimPath.IsEmpty())
{
Seq = Cast<UAnimSequence>(StaticLoadObject(UAnimSequence::StaticClass(), nullptr, *AnimPath));
if (!Seq)
return MakeJsonError(FString::Printf(TEXT("State '%s': AnimSequence not found: %s"), *Name, *AnimPath));
}
StateDefs.Add({ Name, Seq });
}
TArray<TSharedPtr<FJsonValue>> Warnings;
// State machine node, linked to the Output Pose.
UAnimGraphNode_Root* Root = FindNodeOfType<UAnimGraphNode_Root>(AnimGraph);
UEdGraphPin* RootIn = Root ? FindPinByName(Root, TEXT("Result"), EGPD_Input) : nullptr;
FGraphNodeCreator<UAnimGraphNode_StateMachine> SMCreator(*AnimGraph);
UAnimGraphNode_StateMachine* SMNode = SMCreator.CreateNode(false);
SMNode->NodePosX = -350;
SMCreator.Finalize();
if (RootIn)
{
if (UEdGraphPin* SMOut = FirstVisiblePin(SMNode, EGPD_Output))
{
RootIn->BreakAllPinLinks();
SMOut->MakeLinkTo(RootIn);
}
}
TArray<UEdGraph*> Subs = SMNode->GetSubGraphs();
UEdGraph* SMGraph = Subs.Num() ? Subs[0] : nullptr;
if (!SMGraph)
return MakeJsonError(TEXT("State machine graph was not created."));
// States — each with a looping sequence player wired to the state result.
TMap<FString, UAnimStateNode*> StateByName;
int32 Col = 0;
for (const FStateDef& SD : StateDefs)
{
FGraphNodeCreator<UAnimStateNode> Creator(*SMGraph);
UAnimStateNode* State = Creator.CreateNode(false);
Creator.Finalize();
if (UEdGraph* Bound = State->GetBoundGraph())
FBlueprintEditorUtils::RenameGraph(Bound, SD.Name);
if (SD.Seq)
{
if (UAnimGraphNode_StateResult* SR = State->GetResultNodeInsideState())
{
UEdGraphPin* SRIn = FindPinByName(SR, TEXT("Result"), EGPD_Input);
SpawnSequencePlayer(State->GetBoundGraph(), SD.Seq, -400, 0, SRIn);
}
}
State->NodePosX = Col * 350;
State->NodePosY = 0;
++Col;
StateByName.Add(SD.Name, State);
}
// Entry -> entry state (defaults to the first state).
FString EntryName = StateDefs[0].Name;
Spec->TryGetStringField(TEXT("entry"), EntryName);
UAnimStateNode** EntryState = StateByName.Find(EntryName);
if (!EntryState)
return MakeJsonError(FString::Printf(TEXT("Entry state '%s' is not one of the states."), *EntryName));
if (UAnimStateEntryNode* Entry = FindNodeOfType<UAnimStateEntryNode>(SMGraph))
{
UEdGraphPin* EntryOut = FirstVisiblePin(Entry, EGPD_Output);
UEdGraphPin* StateIn = FirstVisiblePin(*EntryState, EGPD_Input);
if (EntryOut && StateIn) EntryOut->MakeLinkTo(StateIn);
else Warnings.Add(MakeShareable(new FJsonValueString(TEXT("Could not connect the entry node."))));
}
// Transitions, each with an optional float-comparison rule.
int32 TransCount = 0;
const TArray<TSharedPtr<FJsonValue>>* TransJson = nullptr;
if (Spec->TryGetArrayField(TEXT("transitions"), TransJson))
{
for (const TSharedPtr<FJsonValue>& TV : *TransJson)
{
const TSharedPtr<FJsonObject> TO = TV->AsObject();
if (!TO.IsValid()) continue;
FString From, To;
TO->TryGetStringField(TEXT("from"), From);
TO->TryGetStringField(TEXT("to"), To);
UAnimStateNode** FromState = StateByName.Find(From);
UAnimStateNode** ToState = StateByName.Find(To);
if (!FromState || !ToState)
return MakeJsonError(FString::Printf(TEXT("Transition references unknown state(s): '%s' -> '%s'."), *From, *To));
FGraphNodeCreator<UAnimStateTransitionNode> Creator(*SMGraph);
UAnimStateTransitionNode* Trans = Creator.CreateNode(false);
Creator.Finalize();
Trans->CreateConnections(*FromState, *ToState);
++TransCount;
FString Var, Op;
if (TO->TryGetStringField(TEXT("var"), Var) && TO->TryGetStringField(TEXT("op"), Op))
{
double Value = 0.0;
TO->TryGetNumberField(TEXT("value"), Value);
if (!BuildFloatTransitionRule(Trans->GetBoundGraph(), Var, Op, (float)Value))
Warnings.Add(MakeShareable(new FJsonValueString(
FString::Printf(TEXT("Rule for %s->%s left at default (bad var/op?)."), *From, *To))));
}
}
}
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(AnimBP);
FKismetEditorUtilities::CompileBlueprint(AnimBP);
TSharedPtr<FJsonObject> R = MakeShareable(new FJsonObject());
R->SetBoolField(TEXT("success"), true);
R->SetStringField(TEXT("state_machine"), SMNode->GetName());
TArray<TSharedPtr<FJsonValue>> StateNames;
for (const FStateDef& SD : StateDefs)
StateNames.Add(MakeShareable(new FJsonValueString(SD.Name)));
R->SetArrayField(TEXT("states"), StateNames);
R->SetNumberField(TEXT("transition_count"), TransCount);
R->SetArrayField(TEXT("warnings"), Warnings);
return SerializeJsonObj(R);
}
FString UMCPythonHelper::BuildAnimStateMachine(UAnimBlueprint* AnimBP, const FString& SpecJson)
{
if (!AnimBP)
return MakeJsonError(TEXT("Invalid AnimBlueprint."));
TSharedPtr<FJsonObject> Spec;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(SpecJson);
if (!FJsonSerializer::Deserialize(Reader, Spec) || !Spec.IsValid())
return MakeJsonError(TEXT("Failed to parse spec JSON."));
return BuildStateMachineFromSpec(AnimBP, Spec);
}

View File

@@ -0,0 +1,863 @@
// Copyright (c) 2025 GenOrca (by zenoengine). All Rights Reserved.
// Behavior Tree authoring helpers — split out of MCPythonHelper.cpp.
#include "MCPythonHelper.h"
#include "Engine/SCS_Node.h"
#include "Engine/SimpleConstructionScript.h"
#include "WidgetBlueprint.h"
#include "Blueprint/WidgetTree.h"
#include "Components/Widget.h"
#include "Components/PanelWidget.h"
#include "Editor.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "Toolkits/AssetEditorToolkit.h"
#include "BlueprintEditor.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"
#include "BehaviorTree/BTCompositeNode.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BehaviorTree/BTDecorator.h"
#include "BehaviorTree/BTService.h"
#include "BehaviorTreeEditor.h"
#include "BehaviorTreeGraphNode.h"
#include "BehaviorTreeGraph.h"
#include "BehaviorTreeGraphNode_Root.h"
#include "BehaviorTreeGraphNode_Composite.h"
#include "BehaviorTreeGraphNode_Task.h"
#include "BehaviorTreeGraphNode_Decorator.h"
#include "BehaviorTreeGraphNode_Service.h"
#include "BehaviorTreeGraphNode_SimpleParallel.h"
#include "BehaviorTreeGraphNode_SubtreeTask.h"
#include "EdGraphSchema_BehaviorTree.h"
#include "EdGraph/EdGraph.h"
#include "UObject/UObjectIterator.h"
#include "Engine/SkeletalMesh.h"
#include "Engine/SkeletalMeshSocket.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonReader.h"
// Blueprint graph includes
#include "K2Node_Event.h"
#include "K2Node_ComponentBoundEvent.h"
#include "K2Node_CustomEvent.h"
#include "K2Node_CallFunction.h"
#include "K2Node_IfThenElse.h"
#include "K2Node_ExecutionSequence.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "K2Node_MacroInstance.h"
#include "K2Node_DynamicCast.h"
#include "K2Node_InputKey.h"
#include "K2Node_SpawnActorFromClass.h"
#include "EdGraphSchema_K2.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Engine/Blueprint.h"
#include "UObject/UnrealType.h"
#include "UObject/TextProperty.h"
#include "Components/CanvasPanelSlot.h"
#include "Components/TextBlock.h"
// AnimGraph authoring (editor-only AnimGraph module)
#include "Animation/AnimBlueprint.h"
#include "Animation/AnimSequence.h"
#include "AnimGraphNode_StateMachine.h"
#include "AnimGraphNode_SequencePlayer.h"
#include "AnimGraphNode_Root.h"
#include "AnimGraphNode_StateResult.h"
#include "AnimGraphNode_TransitionResult.h"
#include "AnimStateNode.h"
#include "AnimStateTransitionNode.h"
#include "AnimStateEntryNode.h"
#include "AnimationStateMachineGraph.h"
#include "Kismet/KismetMathLibrary.h"
// Editor viewport projection
#include "LevelEditorViewport.h"
#include "EditorViewportClient.h"
#include "SceneView.h"
#include "MCPythonHelperInternal.h"
// ─── Behavior Tree Helpers (internal) ────────────────────────────────────────
static FMCPythonBTNodeInfo SerializeBTNode(UBTCompositeNode* Node)
{
FMCPythonBTNodeInfo Info;
if (!Node) return Info;
Info.NodeName = Node->GetNodeName();
Info.NodeClass = Node->GetClass()->GetName();
// Services on this composite node
for (UBTService* Svc : Node->Services)
{
if (Svc)
{
Info.ServiceClasses.Add(Svc->GetClass()->GetName());
Info.ServiceNames.Add(Svc->GetNodeName());
}
}
// Children
for (const FBTCompositeChild& Child : Node->Children)
{
if (Child.ChildComposite)
{
FMCPythonBTNodeInfo ChildInfo = SerializeBTNode(Child.ChildComposite);
// Decorators are stored per-child-connection
for (UBTDecorator* Dec : Child.Decorators)
{
if (Dec)
{
ChildInfo.DecoratorClasses.Add(Dec->GetClass()->GetName());
ChildInfo.DecoratorNames.Add(Dec->GetNodeName());
}
}
Info.Children.Add(ChildInfo);
}
else if (Child.ChildTask)
{
FMCPythonBTNodeInfo TaskInfo;
TaskInfo.NodeName = Child.ChildTask->GetNodeName();
TaskInfo.NodeClass = Child.ChildTask->GetClass()->GetName();
// Decorators on this child connection
for (UBTDecorator* Dec : Child.Decorators)
{
if (Dec)
{
TaskInfo.DecoratorClasses.Add(Dec->GetClass()->GetName());
TaskInfo.DecoratorNames.Add(Dec->GetNodeName());
}
}
// Services on task node
for (UBTService* Svc : Child.ChildTask->Services)
{
if (Svc)
{
TaskInfo.ServiceClasses.Add(Svc->GetClass()->GetName());
TaskInfo.ServiceNames.Add(Svc->GetNodeName());
}
}
Info.Children.Add(TaskInfo);
}
}
return Info;
}
static UBTNode* FindNodeByName(UBTCompositeNode* Root, const FString& Name)
{
if (!Root) return nullptr;
// Check root itself
if (Root->GetNodeName() == Name || Root->GetName() == Name)
return Root;
// Check root's services
for (UBTService* Svc : Root->Services)
{
if (Svc && (Svc->GetNodeName() == Name || Svc->GetName() == Name))
return Svc;
}
// Check children
for (const FBTCompositeChild& Child : Root->Children)
{
// Check decorators on this child
for (UBTDecorator* Dec : Child.Decorators)
{
if (Dec && (Dec->GetNodeName() == Name || Dec->GetName() == Name))
return Dec;
}
if (Child.ChildComposite)
{
UBTNode* Found = FindNodeByName(Child.ChildComposite, Name);
if (Found) return Found;
}
else if (Child.ChildTask)
{
if (Child.ChildTask->GetNodeName() == Name || Child.ChildTask->GetName() == Name)
return Child.ChildTask;
// Check task's services
for (UBTService* Svc : Child.ChildTask->Services)
{
if (Svc && (Svc->GetNodeName() == Name || Svc->GetName() == Name))
return Svc;
}
}
}
return nullptr;
}
// ─── JSON serialization for BT tree ─────────────────────────────────────────
static TSharedPtr<FJsonObject> BTNodeInfoToJson(const FMCPythonBTNodeInfo& Info)
{
TSharedPtr<FJsonObject> Obj = MakeShareable(new FJsonObject());
Obj->SetStringField(TEXT("node_name"), Info.NodeName);
Obj->SetStringField(TEXT("node_class"), Info.NodeClass);
if (Info.DecoratorClasses.Num() > 0)
{
TArray<TSharedPtr<FJsonValue>> DecArr;
for (int32 i = 0; i < Info.DecoratorClasses.Num(); ++i)
{
TSharedPtr<FJsonObject> DecObj = MakeShareable(new FJsonObject());
DecObj->SetStringField(TEXT("class"), Info.DecoratorClasses[i]);
if (Info.DecoratorNames.IsValidIndex(i))
DecObj->SetStringField(TEXT("name"), Info.DecoratorNames[i]);
DecArr.Add(MakeShareable(new FJsonValueObject(DecObj)));
}
Obj->SetArrayField(TEXT("decorators"), DecArr);
}
if (Info.ServiceClasses.Num() > 0)
{
TArray<TSharedPtr<FJsonValue>> SvcArr;
for (int32 i = 0; i < Info.ServiceClasses.Num(); ++i)
{
TSharedPtr<FJsonObject> SvcObj = MakeShareable(new FJsonObject());
SvcObj->SetStringField(TEXT("class"), Info.ServiceClasses[i]);
if (Info.ServiceNames.IsValidIndex(i))
SvcObj->SetStringField(TEXT("name"), Info.ServiceNames[i]);
SvcArr.Add(MakeShareable(new FJsonValueObject(SvcObj)));
}
Obj->SetArrayField(TEXT("services"), SvcArr);
}
if (Info.Children.Num() > 0)
{
TArray<TSharedPtr<FJsonValue>> ChildArr;
for (const FMCPythonBTNodeInfo& Child : Info.Children)
{
ChildArr.Add(MakeShareable(new FJsonValueObject(BTNodeInfoToJson(Child))));
}
Obj->SetArrayField(TEXT("children"), ChildArr);
}
return Obj;
}
// ─── Behavior Tree UFUNCTION Implementations ────────────────────────────────
FString UMCPythonHelper::GetBehaviorTreeStructure(UBehaviorTree* BehaviorTree)
{
if (!BehaviorTree || !BehaviorTree->RootNode)
{
return TEXT("{\"success\":false,\"message\":\"Invalid BehaviorTree or empty tree.\"}");
}
FMCPythonBTNodeInfo RootInfo = SerializeBTNode(BehaviorTree->RootNode);
TSharedPtr<FJsonObject> ResultObj = MakeShareable(new FJsonObject());
ResultObj->SetBoolField(TEXT("success"), true);
ResultObj->SetObjectField(TEXT("root"), BTNodeInfoToJson(RootInfo));
FString OutputString;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString);
FJsonSerializer::Serialize(ResultObj.ToSharedRef(), Writer);
return OutputString;
}
bool UMCPythonHelper::SetBehaviorTreeBlackboard(UBehaviorTree* BehaviorTree, UBlackboardData* BlackboardData)
{
if (!BehaviorTree) return false;
BehaviorTree->BlackboardAsset = BlackboardData;
BehaviorTree->MarkPackageDirty();
return true;
}
FString UMCPythonHelper::GetBehaviorTreeNodeDetails(UBehaviorTree* BehaviorTree, const FString& NodeName)
{
if (!BehaviorTree || !BehaviorTree->RootNode)
{
return TEXT("{\"success\":false,\"message\":\"Invalid BehaviorTree or empty tree.\"}");
}
UBTNode* FoundNode = FindNodeByName(BehaviorTree->RootNode, NodeName);
if (!FoundNode)
{
TSharedPtr<FJsonObject> ErrObj = MakeShareable(new FJsonObject());
ErrObj->SetBoolField(TEXT("success"), false);
ErrObj->SetStringField(TEXT("message"), FString::Printf(TEXT("Node '%s' not found in behavior tree."), *NodeName));
FString ErrStr;
auto ErrWriter = TJsonWriterFactory<>::Create(&ErrStr);
FJsonSerializer::Serialize(ErrObj.ToSharedRef(), ErrWriter);
return ErrStr;
}
TSharedPtr<FJsonObject> JsonObj = MakeShareable(new FJsonObject());
JsonObj->SetBoolField(TEXT("success"), true);
JsonObj->SetStringField(TEXT("node_name"), FoundNode->GetNodeName());
JsonObj->SetStringField(TEXT("node_class"), FoundNode->GetClass()->GetName());
// Serialize EditAnywhere properties
TSharedPtr<FJsonObject> PropsObj = MakeShareable(new FJsonObject());
for (TFieldIterator<FProperty> PropIt(FoundNode->GetClass()); PropIt; ++PropIt)
{
FProperty* Prop = *PropIt;
if (!Prop->HasAnyPropertyFlags(CPF_Edit)) continue;
FString ValueStr;
const void* ValueAddr = Prop->ContainerPtrToValuePtr<void>(FoundNode);
Prop->ExportText_Direct(ValueStr, ValueAddr, nullptr, FoundNode, PPF_None);
PropsObj->SetStringField(Prop->GetName(), ValueStr);
}
JsonObj->SetObjectField(TEXT("properties"), PropsObj);
// If composite node, include services and child count
UBTCompositeNode* CompNode = Cast<UBTCompositeNode>(FoundNode);
if (CompNode)
{
JsonObj->SetNumberField(TEXT("child_count"), CompNode->Children.Num());
TArray<TSharedPtr<FJsonValue>> ServicesArr;
for (UBTService* Svc : CompNode->Services)
{
if (Svc)
{
TSharedPtr<FJsonObject> SvcObj = MakeShareable(new FJsonObject());
SvcObj->SetStringField(TEXT("name"), Svc->GetNodeName());
SvcObj->SetStringField(TEXT("class"), Svc->GetClass()->GetName());
ServicesArr.Add(MakeShareable(new FJsonValueObject(SvcObj)));
}
}
if (ServicesArr.Num() > 0)
{
JsonObj->SetArrayField(TEXT("services"), ServicesArr);
}
}
FString OutputString;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString);
FJsonSerializer::Serialize(JsonObj.ToSharedRef(), Writer);
return OutputString;
}
FString UMCPythonHelper::GetSelectedBTNodes()
{
if (!GEditor)
{
return TEXT("{\"success\":false,\"message\":\"GEditor is null.\"}");
}
auto* Subsystem = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
if (!Subsystem)
{
return TEXT("{\"success\":false,\"message\":\"AssetEditorSubsystem not available.\"}");
}
for (UObject* Asset : Subsystem->GetAllEditedAssets())
{
UBehaviorTree* BT = Cast<UBehaviorTree>(Asset);
if (!BT) continue;
IAssetEditorInstance* EditorInstance = Subsystem->FindEditorForAsset(Asset, false);
FAssetEditorToolkit* EditorToolkit = static_cast<FAssetEditorToolkit*>(EditorInstance);
if (!EditorToolkit) continue;
TSharedPtr<SDockTab> Tab = EditorToolkit->GetTabManager()->GetOwnerTab();
if (!Tab.IsValid() || !Tab->IsForeground()) continue;
FBehaviorTreeEditor* BTEditor = static_cast<FBehaviorTreeEditor*>(EditorToolkit);
if (!BTEditor) continue;
FGraphPanelSelectionSet SelectedNodes = BTEditor->GetSelectedNodes();
TArray<TSharedPtr<FJsonValue>> NodesArr;
for (UObject* NodeObj : SelectedNodes)
{
UBehaviorTreeGraphNode* GraphNode = Cast<UBehaviorTreeGraphNode>(NodeObj);
if (!GraphNode) continue;
UBTNode* BTNode = Cast<UBTNode>(GraphNode->NodeInstance);
if (!BTNode) continue;
TSharedPtr<FJsonObject> NodeJson = MakeShareable(new FJsonObject());
NodeJson->SetStringField(TEXT("node_name"), BTNode->GetNodeName());
NodeJson->SetStringField(TEXT("node_class"), BTNode->GetClass()->GetName());
// Classify node type
FString NodeType;
if (Cast<UBTCompositeNode>(BTNode))
NodeType = TEXT("composite");
else if (Cast<UBTTaskNode>(BTNode))
NodeType = TEXT("task");
else if (Cast<UBTDecorator>(BTNode))
NodeType = TEXT("decorator");
else if (Cast<UBTService>(BTNode))
NodeType = TEXT("service");
else
NodeType = TEXT("unknown");
NodeJson->SetStringField(TEXT("node_type"), NodeType);
// Serialize EditAnywhere properties
TSharedPtr<FJsonObject> PropsObj = MakeShareable(new FJsonObject());
for (TFieldIterator<FProperty> PropIt(BTNode->GetClass()); PropIt; ++PropIt)
{
FProperty* Prop = *PropIt;
if (!Prop->HasAnyPropertyFlags(CPF_Edit)) continue;
FString ValueStr;
const void* ValueAddr = Prop->ContainerPtrToValuePtr<void>(BTNode);
Prop->ExportText_Direct(ValueStr, ValueAddr, nullptr, BTNode, PPF_None);
PropsObj->SetStringField(Prop->GetName(), ValueStr);
}
NodeJson->SetObjectField(TEXT("properties"), PropsObj);
NodesArr.Add(MakeShareable(new FJsonValueObject(NodeJson)));
}
// Build result
TSharedPtr<FJsonObject> ResultObj = MakeShareable(new FJsonObject());
ResultObj->SetBoolField(TEXT("success"), true);
ResultObj->SetStringField(TEXT("behavior_tree_path"), BT->GetPathName());
ResultObj->SetArrayField(TEXT("selected_nodes"), NodesArr);
ResultObj->SetNumberField(TEXT("count"), NodesArr.Num());
FString ResultStr;
TSharedRef<TJsonWriter<>> ResultWriter = TJsonWriterFactory<>::Create(&ResultStr);
FJsonSerializer::Serialize(ResultObj.ToSharedRef(), ResultWriter);
return ResultStr;
}
return TEXT("{\"success\":false,\"message\":\"No Behavior Tree editor is open in the foreground.\"}");
}
// ─── Build BT Helpers ────────────────────────────────────────────────────────
static UClass* FindBTNodeClass(const FString& ClassName)
{
for (TObjectIterator<UClass> It; It; ++It)
{
UClass* Cls = *It;
if (Cls->GetName() == ClassName &&
Cls->IsChildOf(UBTNode::StaticClass()) &&
!Cls->HasAnyClassFlags(CLASS_Abstract))
{
return Cls;
}
}
return nullptr;
}
static void SetBTNodeProperties(UBTNode* Node, const TSharedPtr<FJsonObject>& PropertiesObj)
{
if (!Node || !PropertiesObj.IsValid()) return;
for (auto& Pair : PropertiesObj->Values)
{
FProperty* Prop = Node->GetClass()->FindPropertyByName(FName(*Pair.Key));
if (!Prop) continue;
FString ValueStr;
if (Pair.Value->TryGetString(ValueStr))
{
// Already a string — use as-is
}
else if (Pair.Value->Type == EJson::Number)
{
ValueStr = FString::SanitizeFloat(Pair.Value->AsNumber());
}
else if (Pair.Value->Type == EJson::Boolean)
{
ValueStr = Pair.Value->AsBool() ? TEXT("true") : TEXT("false");
}
else
{
continue;
}
void* ValueAddr = Prop->ContainerPtrToValuePtr<void>(Node);
FStructProperty* StructProp = CastField<FStructProperty>(Prop);
if (StructProp && StructProp->Struct->FindPropertyByName(TEXT("DefaultValue")))
{
FString WrappedValue = FString::Printf(TEXT("(DefaultValue=%s)"), *ValueStr);
Prop->ImportText_Direct(*WrappedValue, ValueAddr, Node, PPF_None);
}
else
{
Prop->ImportText_Direct(*ValueStr, ValueAddr, Node, PPF_None);
}
}
}
static UEdGraphPin* FindGraphPin(UEdGraphNode* Node, EEdGraphPinDirection Direction)
{
for (UEdGraphPin* Pin : Node->Pins)
{
if (Pin->Direction == Direction)
return Pin;
}
return nullptr;
}
static int32 CountSubtreeLeaves(UEdGraphNode* Node)
{
int32 Total = 0;
for (UEdGraphPin* Pin : Node->Pins)
{
if (Pin->Direction == EGPD_Output)
{
for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
{
Total += CountSubtreeLeaves(LinkedPin->GetOwningNode());
}
}
}
return FMath::Max(1, Total);
}
static void LayoutBTGraphNodes(UEdGraphNode* Node, float LeftX, float Width, float Y)
{
const float NodeWidth = 280.0f;
const float YStep = 200.0f;
Node->NodePosX = (int32)(LeftX + Width / 2.0f - NodeWidth / 2.0f);
Node->NodePosY = (int32)Y;
float SubNodeHeight = 0.0f;
UBehaviorTreeGraphNode* BTNode = Cast<UBehaviorTreeGraphNode>(Node);
if (BTNode)
{
SubNodeHeight = (BTNode->Decorators.Num() + BTNode->Services.Num()) * 60.0f;
}
float ChildY = Y + YStep + SubNodeHeight;
float ChildX = LeftX;
for (UEdGraphPin* Pin : Node->Pins)
{
if (Pin->Direction == EGPD_Output)
{
for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
{
UEdGraphNode* Child = LinkedPin->GetOwningNode();
int32 ChildLeaves = CountSubtreeLeaves(Child);
float ChildWidth = ChildLeaves * (NodeWidth + 40.0f);
LayoutBTGraphNodes(Child, ChildX, ChildWidth, ChildY);
ChildX += ChildWidth;
}
}
}
}
static bool CheckClassAncestor(UClass* NodeClass, const TCHAR* AncestorName)
{
for (UClass* C = NodeClass; C; C = C->GetSuperClass())
{
if (C->GetName() == AncestorName)
return true;
}
return false;
}
static UBehaviorTreeGraphNode* CreateBTGraphNodeRecursive(
UBehaviorTreeGraph* Graph,
UBehaviorTree* BT,
const TSharedPtr<FJsonObject>& JsonNode)
{
if (!JsonNode.IsValid() || !JsonNode->HasField(TEXT("node_class")))
return nullptr;
FString NodeClassName = JsonNode->GetStringField(TEXT("node_class"));
UClass* NodeClass = FindBTNodeClass(NodeClassName);
if (!NodeClass)
{
UE_LOG(LogTemp, Warning, TEXT("BuildBT: Class '%s' not found"), *NodeClassName);
return nullptr;
}
// Create runtime node
UBTNode* RuntimeNode = NewObject<UBTNode>(BT, NodeClass);
// Classify node type
bool bIsComposite = NodeClass->IsChildOf(UBTCompositeNode::StaticClass());
bool bIsTask = NodeClass->IsChildOf(UBTTaskNode::StaticClass());
bool bIsSimpleParallel = CheckClassAncestor(NodeClass, TEXT("BTComposite_SimpleParallel"));
bool bIsSubtreeTask = CheckClassAncestor(NodeClass, TEXT("BTTask_RunBehavior"))
|| CheckClassAncestor(NodeClass, TEXT("BTTask_RunBehaviorDynamic"));
// Create appropriate graph node
UBehaviorTreeGraphNode* GraphNode = nullptr;
if (bIsSimpleParallel)
{
FGraphNodeCreator<UBehaviorTreeGraphNode_SimpleParallel> Creator(*Graph);
GraphNode = Creator.CreateNode(false);
Creator.Finalize();
}
else if (bIsComposite)
{
FGraphNodeCreator<UBehaviorTreeGraphNode_Composite> Creator(*Graph);
GraphNode = Creator.CreateNode(false);
Creator.Finalize();
}
else if (bIsSubtreeTask)
{
FGraphNodeCreator<UBehaviorTreeGraphNode_SubtreeTask> Creator(*Graph);
GraphNode = Creator.CreateNode(false);
Creator.Finalize();
}
else if (bIsTask)
{
FGraphNodeCreator<UBehaviorTreeGraphNode_Task> Creator(*Graph);
GraphNode = Creator.CreateNode(false);
Creator.Finalize();
}
else
{
UE_LOG(LogTemp, Warning, TEXT("BuildBT: Unsupported node type for '%s'"), *NodeClassName);
return nullptr;
}
// Set NodeInstance
GraphNode->NodeInstance = RuntimeNode;
// Set properties on the runtime node
if (JsonNode->HasField(TEXT("properties")))
{
const TSharedPtr<FJsonObject>& PropsObj = JsonNode->GetObjectField(TEXT("properties"));
SetBTNodeProperties(RuntimeNode, PropsObj);
}
// Add decorators as sub-nodes
if (JsonNode->HasField(TEXT("decorators")))
{
const TArray<TSharedPtr<FJsonValue>>& DecoratorsArr = JsonNode->GetArrayField(TEXT("decorators"));
for (const auto& DecVal : DecoratorsArr)
{
const TSharedPtr<FJsonObject>& DecObj = DecVal->AsObject();
if (!DecObj.IsValid() || !DecObj->HasField(TEXT("class"))) continue;
FString DecClassName = DecObj->GetStringField(TEXT("class"));
UClass* DecClass = FindBTNodeClass(DecClassName);
if (!DecClass || !DecClass->IsChildOf(UBTDecorator::StaticClass()))
{
UE_LOG(LogTemp, Warning, TEXT("BuildBT: Decorator class '%s' not found or invalid"), *DecClassName);
continue;
}
UBTDecorator* DecRuntime = NewObject<UBTDecorator>(BT, DecClass);
if (DecObj->HasField(TEXT("properties")))
{
SetBTNodeProperties(DecRuntime, DecObj->GetObjectField(TEXT("properties")));
}
UBehaviorTreeGraphNode_Decorator* DecGraphNode =
NewObject<UBehaviorTreeGraphNode_Decorator>(Graph);
DecGraphNode->NodeInstance = DecRuntime;
GraphNode->AddSubNode(DecGraphNode, Graph);
}
}
// Add services as sub-nodes
if (JsonNode->HasField(TEXT("services")))
{
const TArray<TSharedPtr<FJsonValue>>& ServicesArr = JsonNode->GetArrayField(TEXT("services"));
for (const auto& SvcVal : ServicesArr)
{
const TSharedPtr<FJsonObject>& SvcObj = SvcVal->AsObject();
if (!SvcObj.IsValid() || !SvcObj->HasField(TEXT("class"))) continue;
FString SvcClassName = SvcObj->GetStringField(TEXT("class"));
UClass* SvcClass = FindBTNodeClass(SvcClassName);
if (!SvcClass || !SvcClass->IsChildOf(UBTService::StaticClass()))
{
UE_LOG(LogTemp, Warning, TEXT("BuildBT: Service class '%s' not found or invalid"), *SvcClassName);
continue;
}
UBTService* SvcRuntime = NewObject<UBTService>(BT, SvcClass);
if (SvcObj->HasField(TEXT("properties")))
{
SetBTNodeProperties(SvcRuntime, SvcObj->GetObjectField(TEXT("properties")));
}
UBehaviorTreeGraphNode_Service* SvcGraphNode =
NewObject<UBehaviorTreeGraphNode_Service>(Graph);
SvcGraphNode->NodeInstance = SvcRuntime;
GraphNode->AddSubNode(SvcGraphNode, Graph);
}
}
// Recurse for children (only composites have children)
if (bIsComposite && JsonNode->HasField(TEXT("children")))
{
const TArray<TSharedPtr<FJsonValue>>& ChildrenArr = JsonNode->GetArrayField(TEXT("children"));
UEdGraphPin* OutputPin = FindGraphPin(GraphNode, EGPD_Output);
if (OutputPin)
{
for (const auto& ChildVal : ChildrenArr)
{
const TSharedPtr<FJsonObject>& ChildObj = ChildVal->AsObject();
if (!ChildObj.IsValid()) continue;
UBehaviorTreeGraphNode* ChildGraphNode =
CreateBTGraphNodeRecursive(Graph, BT, ChildObj);
if (ChildGraphNode)
{
UEdGraphPin* ChildInputPin = FindGraphPin(ChildGraphNode, EGPD_Input);
if (ChildInputPin)
{
OutputPin->MakeLinkTo(ChildInputPin);
}
}
}
}
}
return GraphNode;
}
// ─── BuildBehaviorTree UFUNCTION ─────────────────────────────────────────────
FString UMCPythonHelper::BuildBehaviorTree(UBehaviorTree* BehaviorTree, const FString& TreeStructureJson)
{
if (!BehaviorTree)
{
return TEXT("{\"success\":false,\"message\":\"Invalid BehaviorTree asset.\"}");
}
// Parse JSON
TSharedPtr<FJsonObject> JsonObj;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(TreeStructureJson);
if (!FJsonSerializer::Deserialize(Reader, JsonObj) || !JsonObj.IsValid())
{
return TEXT("{\"success\":false,\"message\":\"Failed to parse JSON input.\"}");
}
// Get BT graph — create if missing (e.g. asset created without factory)
UBehaviorTreeGraph* BTGraph = Cast<UBehaviorTreeGraph>(BehaviorTree->BTGraph);
if (!BTGraph)
{
UBehaviorTreeGraph* NewGraph = NewObject<UBehaviorTreeGraph>(BehaviorTree, NAME_None, RF_Transactional);
NewGraph->Schema = UEdGraphSchema_BehaviorTree::StaticClass();
BehaviorTree->BTGraph = NewGraph;
const UEdGraphSchema* Schema = NewGraph->GetSchema();
if (Schema)
{
Schema->CreateDefaultNodesForGraph(*NewGraph);
}
BTGraph = NewGraph;
}
// Find root graph node
UBehaviorTreeGraphNode_Root* RootGraphNode = nullptr;
for (UEdGraphNode* Node : BTGraph->Nodes)
{
RootGraphNode = Cast<UBehaviorTreeGraphNode_Root>(Node);
if (RootGraphNode) break;
}
if (!RootGraphNode)
{
return TEXT("{\"success\":false,\"message\":\"No root node found in BT graph.\"}");
}
// Remove all existing non-root graph nodes
TArray<UEdGraphNode*> NodesToRemove;
for (UEdGraphNode* Node : BTGraph->Nodes)
{
if (Node != RootGraphNode)
{
NodesToRemove.Add(Node);
}
}
for (UEdGraphNode* Node : NodesToRemove)
{
BTGraph->RemoveNode(Node);
}
// Clear root pin links and sub-nodes
for (UEdGraphPin* Pin : RootGraphNode->Pins)
{
Pin->BreakAllPinLinks();
}
RootGraphNode->Decorators.Empty();
RootGraphNode->Services.Empty();
// Create graph nodes from JSON
UBehaviorTreeGraphNode* FirstChild = CreateBTGraphNodeRecursive(BTGraph, BehaviorTree, JsonObj);
if (!FirstChild)
{
return TEXT("{\"success\":false,\"message\":\"Failed to create root node from JSON. Check node_class names.\"}");
}
// Connect root to first child
UEdGraphPin* RootOutput = FindGraphPin(RootGraphNode, EGPD_Output);
UEdGraphPin* ChildInput = FindGraphPin(FirstChild, EGPD_Input);
if (RootOutput && ChildInput)
{
RootOutput->MakeLinkTo(ChildInput);
}
// Layout nodes BEFORE UpdateAsset — RebuildChildOrder sorts children by NodePosX
float TotalWidth = CountSubtreeLeaves(RootGraphNode) * 320.0f;
LayoutBTGraphNodes(RootGraphNode, 0.0f, TotalWidth, 0.0f);
// Compile graph → runtime tree (uses node positions for child ordering)
BTGraph->UpdateAsset();
BehaviorTree->MarkPackageDirty();
// Return success
TSharedPtr<FJsonObject> ResultObj = MakeShareable(new FJsonObject());
ResultObj->SetBoolField(TEXT("success"), true);
ResultObj->SetStringField(TEXT("message"), TEXT("Behavior tree built successfully from JSON."));
FString ResultStr;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&ResultStr);
FJsonSerializer::Serialize(ResultObj.ToSharedRef(), Writer);
return ResultStr;
}
// ─── ListBTNodeClasses UFUNCTION ─────────────────────────────────────────────
FString UMCPythonHelper::ListBTNodeClasses()
{
TArray<TSharedPtr<FJsonValue>> Composites, Tasks, Decorators, Services;
for (TObjectIterator<UClass> It; It; ++It)
{
UClass* Cls = *It;
if (Cls->HasAnyClassFlags(CLASS_Abstract | CLASS_Deprecated | CLASS_NewerVersionExists))
continue;
FString ClassName = Cls->GetName();
TSharedPtr<FJsonValue> NameVal = MakeShareable(new FJsonValueString(ClassName));
if (Cls->IsChildOf(UBTCompositeNode::StaticClass()))
Composites.Add(NameVal);
else if (Cls->IsChildOf(UBTTaskNode::StaticClass()))
Tasks.Add(NameVal);
else if (Cls->IsChildOf(UBTDecorator::StaticClass()))
Decorators.Add(NameVal);
else if (Cls->IsChildOf(UBTService::StaticClass()))
Services.Add(NameVal);
}
TSharedPtr<FJsonObject> Result = MakeShareable(new FJsonObject());
Result->SetBoolField(TEXT("success"), true);
Result->SetArrayField(TEXT("composites"), Composites);
Result->SetArrayField(TEXT("tasks"), Tasks);
Result->SetArrayField(TEXT("decorators"), Decorators);
Result->SetArrayField(TEXT("services"), Services);
FString OutputString;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString);
FJsonSerializer::Serialize(Result.ToSharedRef(), Writer);
return OutputString;
}

View File

@@ -0,0 +1,622 @@
// Copyright (c) 2025 GenOrca (by zenoengine). All Rights Reserved.
// UMG Widget Blueprint authoring — split out of MCPythonHelper.cpp.
#include "MCPythonHelper.h"
#include "MCPythonHelperInternal.h"
#include "Engine/SCS_Node.h"
#include "Engine/SimpleConstructionScript.h"
#include "WidgetBlueprint.h"
#include "Blueprint/WidgetTree.h"
#include "Components/Widget.h"
#include "Components/PanelWidget.h"
#include "Editor.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "Toolkits/AssetEditorToolkit.h"
#include "BlueprintEditor.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"
#include "BehaviorTree/BTCompositeNode.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BehaviorTree/BTDecorator.h"
#include "BehaviorTree/BTService.h"
#include "BehaviorTreeEditor.h"
#include "BehaviorTreeGraphNode.h"
#include "BehaviorTreeGraph.h"
#include "BehaviorTreeGraphNode_Root.h"
#include "BehaviorTreeGraphNode_Composite.h"
#include "BehaviorTreeGraphNode_Task.h"
#include "BehaviorTreeGraphNode_Decorator.h"
#include "BehaviorTreeGraphNode_Service.h"
#include "BehaviorTreeGraphNode_SimpleParallel.h"
#include "BehaviorTreeGraphNode_SubtreeTask.h"
#include "EdGraphSchema_BehaviorTree.h"
#include "EdGraph/EdGraph.h"
#include "UObject/UObjectIterator.h"
#include "Engine/SkeletalMesh.h"
#include "Engine/SkeletalMeshSocket.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonReader.h"
// Blueprint graph includes
#include "K2Node_Event.h"
#include "K2Node_ComponentBoundEvent.h"
#include "K2Node_CustomEvent.h"
#include "K2Node_CallFunction.h"
#include "K2Node_IfThenElse.h"
#include "K2Node_ExecutionSequence.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "K2Node_MacroInstance.h"
#include "K2Node_DynamicCast.h"
#include "K2Node_InputKey.h"
#include "K2Node_SpawnActorFromClass.h"
#include "EdGraphSchema_K2.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Engine/Blueprint.h"
#include "UObject/UnrealType.h"
#include "UObject/TextProperty.h"
#include "Components/CanvasPanelSlot.h"
#include "Components/TextBlock.h"
// AnimGraph authoring (editor-only AnimGraph module)
#include "Animation/AnimBlueprint.h"
#include "Animation/AnimSequence.h"
#include "AnimGraphNode_StateMachine.h"
#include "AnimGraphNode_SequencePlayer.h"
#include "AnimGraphNode_Root.h"
#include "AnimGraphNode_StateResult.h"
#include "AnimGraphNode_TransitionResult.h"
#include "AnimStateNode.h"
#include "AnimStateTransitionNode.h"
#include "AnimStateEntryNode.h"
#include "AnimationStateMachineGraph.h"
#include "Kismet/KismetMathLibrary.h"
// Editor viewport projection
#include "LevelEditorViewport.h"
#include "EditorViewportClient.h"
#include "SceneView.h"
// ─── UMG Widget Blueprint Helpers ─────────────────────────────────────────────
namespace
{
static UClass* FindUMGWidgetClass(const FString& TypeName)
{
static const TMap<FString, FString> TypeMap = {
{TEXT("CanvasPanel"), TEXT("/Script/UMG.CanvasPanel")},
{TEXT("TextBlock"), TEXT("/Script/UMG.TextBlock")},
{TEXT("Button"), TEXT("/Script/UMG.Button")},
{TEXT("Image"), TEXT("/Script/UMG.Image")},
{TEXT("HorizontalBox"), TEXT("/Script/UMG.HorizontalBox")},
{TEXT("VerticalBox"), TEXT("/Script/UMG.VerticalBox")},
{TEXT("Border"), TEXT("/Script/UMG.Border")},
{TEXT("Overlay"), TEXT("/Script/UMG.Overlay")},
{TEXT("ScrollBox"), TEXT("/Script/UMG.ScrollBox")},
{TEXT("SizeBox"), TEXT("/Script/UMG.SizeBox")},
{TEXT("CheckBox"), TEXT("/Script/UMG.CheckBox")},
{TEXT("EditableText"), TEXT("/Script/UMG.EditableText")},
{TEXT("EditableTextBox"), TEXT("/Script/UMG.EditableTextBox")},
{TEXT("ProgressBar"), TEXT("/Script/UMG.ProgressBar")},
{TEXT("Slider"), TEXT("/Script/UMG.Slider")},
};
const FString* Path = TypeMap.Find(TypeName);
if (!Path) return nullptr;
return LoadObject<UClass>(nullptr, **Path);
}
static FString UmgErrorJson(const FString& Msg)
{
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
Obj->SetBoolField(TEXT("success"), false);
Obj->SetStringField(TEXT("message"), Msg);
return SerializeJsonObj(Obj);
}
}
FString UMCPythonHelper::UmgGetWidgetInfo(UBlueprint* WidgetBP)
{
UWidgetBlueprint* WB = Cast<UWidgetBlueprint>(WidgetBP);
if (!WB) return UmgErrorJson(TEXT("Asset is not a WidgetBlueprint."));
UWidgetTree* WT = WB->WidgetTree;
if (!WT) return UmgErrorJson(TEXT("Widget tree is null."));
TSharedPtr<FJsonObject> Root = MakeShared<FJsonObject>();
Root->SetBoolField(TEXT("success"), true);
if (WT->RootWidget)
Root->SetStringField(TEXT("root_widget"), WT->RootWidget->GetName());
else
Root->SetField(TEXT("root_widget"), MakeShared<FJsonValueNull>());
TArray<TSharedPtr<FJsonValue>> WidgetArr;
WT->ForEachWidget([&](UWidget* W) {
TSharedPtr<FJsonObject> WObj = MakeShared<FJsonObject>();
WObj->SetStringField(TEXT("name"), W->GetName());
WObj->SetStringField(TEXT("type"), W->GetClass()->GetName());
if (UWidget* Parent = W->GetParent())
WObj->SetStringField(TEXT("parent"), Parent->GetName());
WidgetArr.Add(MakeShared<FJsonValueObject>(WObj));
});
Root->SetArrayField(TEXT("widgets"), WidgetArr);
Root->SetNumberField(TEXT("widget_count"), WidgetArr.Num());
return SerializeJsonObj(Root);
}
FString UMCPythonHelper::UmgAddWidget(UBlueprint* WidgetBP, const FString& WidgetType, const FString& WidgetName, const FString& ParentName)
{
UWidgetBlueprint* WB = Cast<UWidgetBlueprint>(WidgetBP);
if (!WB) return UmgErrorJson(TEXT("Asset is not a WidgetBlueprint."));
UWidgetTree* WT = WB->WidgetTree;
if (!WT) return UmgErrorJson(TEXT("Widget tree is null."));
UClass* WidgetClass = FindUMGWidgetClass(WidgetType);
if (!WidgetClass)
return UmgErrorJson(FString::Printf(TEXT("Unknown widget type '%s'."), *WidgetType));
WT->Modify();
UWidget* NewWidget = WT->ConstructWidget<UWidget>(WidgetClass, FName(*WidgetName));
if (!NewWidget)
return UmgErrorJson(FString::Printf(TEXT("Failed to construct widget '%s'."), *WidgetName));
// Mark as variable so Blueprint graph can reference it directly
NewWidget->bIsVariable = true;
FString ActualParent;
bool bIsRoot = false;
if (!ParentName.IsEmpty())
{
UWidget* ParentWidget = WT->FindWidget(FName(*ParentName));
if (!ParentWidget)
return UmgErrorJson(FString::Printf(TEXT("Parent widget '%s' not found."), *ParentName));
UPanelWidget* Panel = Cast<UPanelWidget>(ParentWidget);
if (!Panel)
return UmgErrorJson(FString::Printf(TEXT("Parent '%s' is not a panel widget."), *ParentName));
Panel->AddChild(NewWidget);
ActualParent = ParentName;
}
else if (!WT->RootWidget)
{
WT->RootWidget = NewWidget;
bIsRoot = true;
}
else
{
UPanelWidget* RootPanel = Cast<UPanelWidget>(WT->RootWidget);
if (!RootPanel)
return UmgErrorJson(TEXT("Root widget is not a panel. Specify 'parent_name' explicitly."));
RootPanel->AddChild(NewWidget);
ActualParent = RootPanel->GetName();
}
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(WB);
TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("widget_name"), NewWidget->GetName());
Result->SetStringField(TEXT("widget_type"), WidgetType);
Result->SetBoolField(TEXT("is_root"), bIsRoot);
if (!ActualParent.IsEmpty())
Result->SetStringField(TEXT("parent"), ActualParent);
return SerializeJsonObj(Result);
}
UWidget* UMCPythonHelper::UmgFindWidget(UBlueprint* WidgetBP, const FString& WidgetName)
{
UWidgetBlueprint* WB = Cast<UWidgetBlueprint>(WidgetBP);
if (!WB || !WB->WidgetTree) return nullptr;
return WB->WidgetTree->FindWidget(FName(*WidgetName));
}
FString UMCPythonHelper::UmgRemoveWidget(UBlueprint* WidgetBP, const FString& WidgetName)
{
UWidgetBlueprint* WB = Cast<UWidgetBlueprint>(WidgetBP);
if (!WB) return UmgErrorJson(TEXT("Asset is not a WidgetBlueprint."));
UWidgetTree* WT = WB->WidgetTree;
if (!WT) return UmgErrorJson(TEXT("Widget tree is null."));
UWidget* Widget = WT->FindWidget(FName(*WidgetName));
if (!Widget)
return UmgErrorJson(FString::Printf(TEXT("Widget '%s' not found."), *WidgetName));
WT->Modify();
UPanelWidget* Parent = Cast<UPanelWidget>(Widget->GetParent());
if (Parent)
{
Parent->RemoveChild(Widget);
}
else if (WT->RootWidget && WT->RootWidget->GetName() == WidgetName)
{
WT->RootWidget = nullptr;
}
else
{
return UmgErrorJson(FString::Printf(TEXT("Cannot remove '%s': not attached to a panel or root."), *WidgetName));
}
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(WB);
TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("message"), FString::Printf(TEXT("Widget '%s' removed successfully."), *WidgetName));
return SerializeJsonObj(Result);
}
// ─── UmgSetWidgetIsVariable UFUNCTION ────────────────────────────────────────
FString UMCPythonHelper::UmgSetWidgetIsVariable(UBlueprint* WidgetBP, const FString& WidgetName, bool bIsVariable)
{
UWidgetBlueprint* WB = Cast<UWidgetBlueprint>(WidgetBP);
if (!WB) return UmgErrorJson(TEXT("Asset is not a WidgetBlueprint."));
UWidgetTree* WT = WB->WidgetTree;
if (!WT) return UmgErrorJson(TEXT("Widget tree is null."));
UWidget* Widget = WT->FindWidget(FName(*WidgetName));
if (!Widget)
return UmgErrorJson(FString::Printf(TEXT("Widget '%s' not found."), *WidgetName));
Widget->Modify();
Widget->bIsVariable = bIsVariable;
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(WB);
TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("widget_name"), WidgetName);
Result->SetBoolField(TEXT("is_variable"), bIsVariable);
return SerializeJsonObj(Result);
}
// ─── UmgSetSlotLayout UFUNCTION ──────────────────────────────────────────────
FString UMCPythonHelper::UmgSetSlotLayout(UBlueprint* WidgetBP, const FString& WidgetName,
float AnchorMinX, float AnchorMinY, float AnchorMaxX, float AnchorMaxY,
float OffsetX, float OffsetY, float SizeX, float SizeY)
{
UWidgetBlueprint* WB = Cast<UWidgetBlueprint>(WidgetBP);
if (!WB || !WB->WidgetTree)
return UmgErrorJson(TEXT("Not a WidgetBlueprint or no WidgetTree."));
UWidget* Widget = WB->WidgetTree->FindWidget(FName(*WidgetName));
if (!Widget)
return UmgErrorJson(FString::Printf(TEXT("Widget '%s' not found."), *WidgetName));
UCanvasPanelSlot* CPS = Cast<UCanvasPanelSlot>(Widget->Slot);
if (!CPS)
return UmgErrorJson(FString::Printf(TEXT("Widget '%s' is not in a CanvasPanel."), *WidgetName));
CPS->Modify();
FAnchorData Data;
Data.Anchors.Minimum = FVector2D(AnchorMinX, AnchorMinY);
Data.Anchors.Maximum = FVector2D(AnchorMaxX, AnchorMaxY);
Data.Offsets = FMargin(OffsetX, OffsetY, SizeX, SizeY);
Data.Alignment = FVector2D(0.5f, 0.5f);
CPS->SetLayout(Data);
FBlueprintEditorUtils::MarkBlueprintAsModified(WB);
TSharedPtr<FJsonObject> R = MakeShared<FJsonObject>();
R->SetBoolField(TEXT("success"), true);
R->SetStringField(TEXT("widget"), WidgetName);
R->SetStringField(TEXT("message"), FString::Printf(TEXT("Layout set on '%s'."), *WidgetName));
return SerializeJsonObj(R);
}
// ─── UmgSetTextStyle UFUNCTION ───────────────────────────────────────────────
FString UMCPythonHelper::UmgSetTextStyle(UBlueprint* WidgetBP, const FString& WidgetName,
int32 FontSize, float ColorR, float ColorG, float ColorB, float ColorA,
int32 OutlineSize)
{
UWidgetBlueprint* WB = Cast<UWidgetBlueprint>(WidgetBP);
if (!WB || !WB->WidgetTree)
return UmgErrorJson(TEXT("Not a WidgetBlueprint or no WidgetTree."));
UWidget* Widget = WB->WidgetTree->FindWidget(FName(*WidgetName));
if (!Widget)
return UmgErrorJson(FString::Printf(TEXT("Widget '%s' not found."), *WidgetName));
UTextBlock* TB = Cast<UTextBlock>(Widget);
if (!TB)
return UmgErrorJson(FString::Printf(TEXT("Widget '%s' is not a TextBlock."), *WidgetName));
TB->Modify();
FSlateFontInfo Font = TB->GetFont();
Font.Size = FontSize;
if (OutlineSize >= 0)
Font.OutlineSettings.OutlineSize = OutlineSize;
TB->SetFont(Font);
FLinearColor Color(ColorR, ColorG, ColorB, ColorA);
TB->SetColorAndOpacity(FSlateColor(Color));
FBlueprintEditorUtils::MarkBlueprintAsModified(WB);
TSharedPtr<FJsonObject> R = MakeShared<FJsonObject>();
R->SetBoolField(TEXT("success"), true);
R->SetStringField(TEXT("widget"), WidgetName);
R->SetStringField(TEXT("message"), FString::Printf(TEXT("Text style set on '%s': size=%d outline=%d."), *WidgetName, FontSize, OutlineSize));
return SerializeJsonObj(R);
}
// ─── UmgGetWidgetProperty UFUNCTION ──────────────────────────────────────────
FString UMCPythonHelper::UmgGetWidgetProperty(UBlueprint* WidgetBP, const FString& WidgetName, const FString& PropertyName)
{
UWidgetBlueprint* WB = Cast<UWidgetBlueprint>(WidgetBP);
if (!WB || !WB->WidgetTree)
return UmgErrorJson(TEXT("Not a WidgetBlueprint or no WidgetTree."));
UWidget* Widget = WB->WidgetTree->FindWidget(FName(*WidgetName));
if (!Widget)
return UmgErrorJson(FString::Printf(TEXT("Widget '%s' not found."), *WidgetName));
FProperty* Prop = Widget->GetClass()->FindPropertyByName(FName(*PropertyName));
if (!Prop)
return UmgErrorJson(FString::Printf(TEXT("Property '%s' not found on widget '%s'."), *PropertyName, *WidgetName));
FString ValueStr;
const void* ValueAddr = Prop->ContainerPtrToValuePtr<void>(Widget);
Prop->ExportTextItem_Direct(ValueStr, ValueAddr, nullptr, Widget, PPF_None);
TSharedPtr<FJsonObject> R = MakeShared<FJsonObject>();
R->SetBoolField(TEXT("success"), true);
R->SetStringField(TEXT("widget"), WidgetName);
R->SetStringField(TEXT("property"), PropertyName);
R->SetStringField(TEXT("value"), ValueStr);
R->SetStringField(TEXT("type"), Prop->GetCPPType());
return SerializeJsonObj(R);
}
// ─── UmgSetWidgetProperty UFUNCTION ──────────────────────────────────────────
FString UMCPythonHelper::UmgSetWidgetProperty(UBlueprint* WidgetBP, const FString& WidgetName, const FString& PropertyName, const FString& Value)
{
UWidgetBlueprint* WB = Cast<UWidgetBlueprint>(WidgetBP);
if (!WB || !WB->WidgetTree)
return UmgErrorJson(TEXT("Not a WidgetBlueprint or no WidgetTree."));
UWidget* Widget = WB->WidgetTree->FindWidget(FName(*WidgetName));
if (!Widget)
return UmgErrorJson(FString::Printf(TEXT("Widget '%s' not found."), *WidgetName));
FProperty* Prop = Widget->GetClass()->FindPropertyByName(FName(*PropertyName));
if (!Prop)
return UmgErrorJson(FString::Printf(TEXT("Property '%s' not found on widget '%s'."), *PropertyName, *WidgetName));
Widget->Modify();
void* ValueAddr = Prop->ContainerPtrToValuePtr<void>(Widget);
const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, ValueAddr, Widget, PPF_None);
if (!ImportResult)
return UmgErrorJson(FString::Printf(TEXT("Failed to set property '%s' to '%s'."), *PropertyName, *Value));
FBlueprintEditorUtils::MarkBlueprintAsModified(WB);
TSharedPtr<FJsonObject> R = MakeShared<FJsonObject>();
R->SetBoolField(TEXT("success"), true);
R->SetStringField(TEXT("widget"), WidgetName);
R->SetStringField(TEXT("property"), PropertyName);
R->SetStringField(TEXT("value"), Value);
return SerializeJsonObj(R);
}
// ─── UMG hierarchy ops (reparent / wrap / replace) ───────────────────────────
// True if Ancestor is Widget itself or one of its ancestors (for reparent cycle guard).
static bool UmgIsAncestorOf(UWidget* Ancestor, UWidget* Widget)
{
for (UWidget* W = Widget; W; W = W->GetParent())
if (W == Ancestor) return true;
return false;
}
FString UMCPythonHelper::UmgReparentWidget(UBlueprint* WidgetBP, const FString& WidgetName, const FString& NewParentName)
{
UWidgetBlueprint* WB = Cast<UWidgetBlueprint>(WidgetBP);
if (!WB || !WB->WidgetTree) return UmgErrorJson(TEXT("Not a WidgetBlueprint or no WidgetTree."));
UWidgetTree* WT = WB->WidgetTree;
UWidget* Widget = WT->FindWidget(FName(*WidgetName));
if (!Widget) return UmgErrorJson(FString::Printf(TEXT("Widget '%s' not found."), *WidgetName));
UWidget* NewParentWidget = WT->FindWidget(FName(*NewParentName));
if (!NewParentWidget) return UmgErrorJson(FString::Printf(TEXT("New parent '%s' not found."), *NewParentName));
UPanelWidget* NewParent = Cast<UPanelWidget>(NewParentWidget);
if (!NewParent) return UmgErrorJson(FString::Printf(TEXT("New parent '%s' is not a panel widget."), *NewParentName));
if (Widget == NewParent) return UmgErrorJson(TEXT("Cannot reparent a widget into itself."));
if (UmgIsAncestorOf(Widget, NewParent))
return UmgErrorJson(TEXT("Cannot reparent: target is an ancestor of the new parent (cycle)."));
if (!NewParent->CanHaveMultipleChildren() && NewParent->GetChildrenCount() > 0)
return UmgErrorJson(FString::Printf(TEXT("Panel '%s' already holds its single allowed child."), *NewParentName));
WT->Modify();
if (UPanelWidget* OldParent = Cast<UPanelWidget>(Widget->GetParent()))
OldParent->RemoveChild(Widget);
else if (WT->RootWidget == Widget)
WT->RootWidget = nullptr;
NewParent->AddChild(Widget);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(WB);
TSharedPtr<FJsonObject> R = MakeShared<FJsonObject>();
R->SetBoolField(TEXT("success"), true);
R->SetStringField(TEXT("widget"), WidgetName);
R->SetStringField(TEXT("new_parent"), NewParentName);
return SerializeJsonObj(R);
}
FString UMCPythonHelper::UmgWrapWidget(UBlueprint* WidgetBP, const FString& WidgetName, const FString& WrapperType, const FString& WrapperName)
{
UWidgetBlueprint* WB = Cast<UWidgetBlueprint>(WidgetBP);
if (!WB || !WB->WidgetTree) return UmgErrorJson(TEXT("Not a WidgetBlueprint or no WidgetTree."));
UWidgetTree* WT = WB->WidgetTree;
UWidget* Widget = WT->FindWidget(FName(*WidgetName));
if (!Widget) return UmgErrorJson(FString::Printf(TEXT("Widget '%s' not found."), *WidgetName));
UClass* WrapperClass = FindUMGWidgetClass(WrapperType);
if (!WrapperClass) return UmgErrorJson(FString::Printf(TEXT("Unknown widget type '%s'."), *WrapperType));
if (!WrapperClass->IsChildOf(UPanelWidget::StaticClass()))
return UmgErrorJson(FString::Printf(TEXT("Wrapper type '%s' is not a panel widget."), *WrapperType));
WT->Modify();
UPanelWidget* Wrapper = WT->ConstructWidget<UPanelWidget>(WrapperClass, FName(*WrapperName));
if (!Wrapper) return UmgErrorJson(FString::Printf(TEXT("Failed to construct wrapper '%s'."), *WrapperName));
Wrapper->bIsVariable = true;
if (UPanelWidget* OldParent = Cast<UPanelWidget>(Widget->GetParent()))
{
const int32 Index = OldParent->GetChildIndex(Widget);
OldParent->ReplaceChildAt(Index, Wrapper); // wrapper takes the widget's slot
Wrapper->AddChild(Widget); // widget moves inside the wrapper
}
else if (WT->RootWidget == Widget)
{
WT->RootWidget = Wrapper;
Wrapper->AddChild(Widget);
}
else
{
return UmgErrorJson(FString::Printf(TEXT("Widget '%s' is not attached to a panel or root."), *WidgetName));
}
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(WB);
TSharedPtr<FJsonObject> R = MakeShared<FJsonObject>();
R->SetBoolField(TEXT("success"), true);
R->SetStringField(TEXT("wrapped"), WidgetName);
R->SetStringField(TEXT("wrapper"), Wrapper->GetName());
R->SetStringField(TEXT("wrapper_type"), WrapperType);
return SerializeJsonObj(R);
}
FString UMCPythonHelper::UmgReplaceWidget(UBlueprint* WidgetBP, const FString& WidgetName, const FString& NewType, const FString& NewName)
{
UWidgetBlueprint* WB = Cast<UWidgetBlueprint>(WidgetBP);
if (!WB || !WB->WidgetTree) return UmgErrorJson(TEXT("Not a WidgetBlueprint or no WidgetTree."));
UWidgetTree* WT = WB->WidgetTree;
UWidget* Widget = WT->FindWidget(FName(*WidgetName));
if (!Widget) return UmgErrorJson(FString::Printf(TEXT("Widget '%s' not found."), *WidgetName));
UClass* NewClass = FindUMGWidgetClass(NewType);
if (!NewClass) return UmgErrorJson(FString::Printf(TEXT("Unknown widget type '%s'."), *NewType));
WT->Modify();
UWidget* NewWidget = WT->ConstructWidget<UWidget>(NewClass, FName(*NewName));
if (!NewWidget) return UmgErrorJson(FString::Printf(TEXT("Failed to construct widget '%s'."), *NewName));
NewWidget->bIsVariable = true;
if (UPanelWidget* OldParent = Cast<UPanelWidget>(Widget->GetParent()))
{
const int32 Index = OldParent->GetChildIndex(Widget);
OldParent->ReplaceChildAt(Index, NewWidget); // old widget (and its subtree) is discarded
}
else if (WT->RootWidget == Widget)
{
WT->RootWidget = NewWidget;
}
else
{
return UmgErrorJson(FString::Printf(TEXT("Widget '%s' is not attached to a panel or root."), *WidgetName));
}
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(WB);
TSharedPtr<FJsonObject> R = MakeShared<FJsonObject>();
R->SetBoolField(TEXT("success"), true);
R->SetStringField(TEXT("replaced"), WidgetName);
R->SetStringField(TEXT("new_widget"), NewWidget->GetName());
R->SetStringField(TEXT("new_type"), NewType);
return SerializeJsonObj(R);
}
// ─── UMG event binding (widget delegate -> bound event node) ─────────────────
FString UMCPythonHelper::UmgListWidgetEvents(UBlueprint* WidgetBP, const FString& WidgetName)
{
UWidgetBlueprint* WB = Cast<UWidgetBlueprint>(WidgetBP);
if (!WB || !WB->WidgetTree) return UmgErrorJson(TEXT("Not a WidgetBlueprint or no WidgetTree."));
UWidget* W = WB->WidgetTree->FindWidget(FName(*WidgetName));
if (!W) return UmgErrorJson(FString::Printf(TEXT("Widget '%s' not found."), *WidgetName));
TArray<TSharedPtr<FJsonValue>> Events;
for (TFieldIterator<FMulticastDelegateProperty> It(W->GetClass()); It; ++It)
Events.Add(MakeShareable(new FJsonValueString(It->GetName())));
TSharedPtr<FJsonObject> R = MakeShared<FJsonObject>();
R->SetBoolField(TEXT("success"), true);
R->SetStringField(TEXT("widget"), WidgetName);
R->SetStringField(TEXT("widget_class"), W->GetClass()->GetName());
R->SetArrayField(TEXT("events"), Events);
return SerializeJsonObj(R);
}
FString UMCPythonHelper::UmgBindWidgetEvent(UBlueprint* WidgetBP, const FString& WidgetName, const FString& EventName)
{
UWidgetBlueprint* WB = Cast<UWidgetBlueprint>(WidgetBP);
if (!WB || !WB->WidgetTree) return UmgErrorJson(TEXT("Not a WidgetBlueprint or no WidgetTree."));
UWidget* W = WB->WidgetTree->FindWidget(FName(*WidgetName));
if (!W) return UmgErrorJson(FString::Printf(TEXT("Widget '%s' not found."), *WidgetName));
// The delegate must exist on the widget class.
if (!FindFProperty<FMulticastDelegateProperty>(W->GetClass(), FName(*EventName)))
{
TArray<FString> Avail;
for (TFieldIterator<FMulticastDelegateProperty> It(W->GetClass()); It; ++It) Avail.Add(It->GetName());
return UmgErrorJson(FString::Printf(TEXT("Event '%s' not found on %s. Available: %s"),
*EventName, *W->GetClass()->GetName(), *FString::Join(Avail, TEXT(", "))));
}
// A bindable widget must be a variable.
if (!W->bIsVariable)
{
W->bIsVariable = true;
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(WB);
}
// Resolve the generated variable property; compile ONCE only if it isn't present yet.
FObjectProperty* VarProp = FindFProperty<FObjectProperty>(WB->SkeletonGeneratedClass, FName(*WidgetName));
if (!VarProp)
{
FKismetEditorUtilities::CompileBlueprint(WB);
VarProp = FindFProperty<FObjectProperty>(WB->SkeletonGeneratedClass, FName(*WidgetName));
}
if (!VarProp)
return UmgErrorJson(FString::Printf(TEXT("Could not resolve the widget variable property for '%s'."), *WidgetName));
const bool bAlready = FKismetEditorUtilities::FindBoundEventForComponent(WB, FName(*EventName), VarProp->GetFName()) != nullptr;
if (!bAlready)
{
// Mirror UMG's own detail panel: create the node and let the editor recompile on its
// deferred tick / on save. A manual CompileBlueprint here triggers mid-task reinstancing
// that crashes a subsequent save/delete of the same asset within one game-thread task.
FKismetEditorUtilities::CreateNewBoundEventForClass(W->GetClass(), FName(*EventName), WB, VarProp);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(WB);
}
const UK2Node_ComponentBoundEvent* Node =
FKismetEditorUtilities::FindBoundEventForComponent(WB, FName(*EventName), VarProp->GetFName());
TSharedPtr<FJsonObject> R = MakeShared<FJsonObject>();
R->SetBoolField(TEXT("success"), Node != nullptr);
if (!Node)
{
R->SetStringField(TEXT("message"), TEXT("Bound event node was not created."));
return SerializeJsonObj(R);
}
R->SetStringField(TEXT("widget"), WidgetName);
R->SetStringField(TEXT("event"), EventName);
R->SetStringField(TEXT("node"), Node->GetName());
R->SetBoolField(TEXT("already_existed"), bAlready);
return SerializeJsonObj(R);
}

View File

@@ -0,0 +1,633 @@
// Copyright (c) 2025 GenOrca (by zenoengine). All Rights Reserved.
#include "MCPythonTcpServer.h"
#include "Sockets.h"
#include "SocketSubsystem.h"
#include "IPAddress.h"
#include "Common/TcpListener.h"
#include "IPythonScriptPlugin.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonReader.h"
#include "MCPythonHelper.h"
#include "Dom/JsonObject.h"
#include "ILiveCodingModule.h"
#include "HAL/FileManager.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Misc/ScopeLock.h"
DEFINE_LOG_CATEGORY_STATIC(LogMCPython, Log, All);
namespace
{
// Accumulates lines emitted under the LogLiveCoding log category while a live
// coding compile runs. Live Coding dispatches compiler output from worker threads,
// so Serialize must be callable from any thread.
class FMCPCompileLogCapture : public FOutputDevice
{
public:
virtual void Serialize(const TCHAR* Message, ELogVerbosity::Type Verbosity, const FName& Category) override
{
static const FName TargetCategory(TEXT("LogLiveCoding"));
if (Category != TargetCategory)
return;
FScopeLock Guard(&LineLock);
CapturedLines.Add(FString(Message));
}
virtual bool CanBeUsedOnAnyThread() const override { return true; }
// Returns all captured lines joined by newlines and clears the buffer.
FString GetAndClear()
{
FScopeLock Guard(&LineLock);
FString Result = FString::Join(CapturedLines, TEXT("\n"));
CapturedLines.Reset();
return Result;
}
private:
FCriticalSection LineLock;
TArray<FString> CapturedLines;
};
FString LCCompileResultToString(ELiveCodingCompileResult Result)
{
switch (Result)
{
case ELiveCodingCompileResult::Success: return TEXT("Success");
case ELiveCodingCompileResult::NoChanges: return TEXT("NoChanges");
case ELiveCodingCompileResult::Failure: return TEXT("Failure");
case ELiveCodingCompileResult::CompileStillActive: return TEXT("CompileStillActive");
case ELiveCodingCompileResult::NotStarted: return TEXT("NotStarted");
case ELiveCodingCompileResult::Cancelled: return TEXT("Cancelled");
case ELiveCodingCompileResult::InProgress: return TEXT("InProgress");
default: return TEXT("Unknown");
}
}
// Reads UBT's Log.txt if it was written during this compile (detected by comparing
// the file's modification time before and after the compile call) and returns lines
// that look like MSVC compiler diagnostics. MSVC writes errors and warnings in the
// form "file(line): error/warning/fatal error CNNNN: ..." which is a well-known
// public format, independent of any specific engine implementation.
FString CollectUBTDiagnostics(const FString& UBTLogFilePath, const FDateTime& TimestampBefore)
{
const FDateTime TimestampAfter = IFileManager::Get().GetTimeStamp(*UBTLogFilePath);
if (TimestampAfter == FDateTime::MinValue() || TimestampAfter == TimestampBefore)
return FString();
FString LogContent;
if (!FFileHelper::LoadFileToString(LogContent, *UBTLogFilePath))
return FString();
TArray<FString> AllLines;
LogContent.ParseIntoArrayLines(AllLines, /*bCullEmpty=*/true);
TArray<FString> DiagnosticLines;
for (const FString& Line : AllLines)
{
// Match the standard MSVC diagnostic format: path(row,col): severity CXXXX:
const bool bError = Line.Contains(TEXT("): error "));
const bool bFatal = Line.Contains(TEXT("): fatal error "));
const bool bWarning = Line.Contains(TEXT("): warning "));
if (bError || bFatal || bWarning)
DiagnosticLines.Add(Line);
}
return FString::Join(DiagnosticLines, TEXT("\n"));
}
}
// Helper function to convert FJsonValue to Python literal string
FString ConvertJsonValueToPythonLiteral(const TSharedPtr<FJsonValue>& JsonVal)
{
if (!JsonVal.IsValid() || JsonVal->Type == EJson::Null) return TEXT("None");
switch (JsonVal->Type)
{
case EJson::String:
{
FString EscapedString = JsonVal->AsString();
// Order of replacement is important.
// Escape backslashes: "\" -> "\\"
EscapedString = EscapedString.Replace(TEXT("\\"), TEXT("\\\\"));
// Escape single quotes: ' -> \'
EscapedString = EscapedString.Replace(TEXT("\'"), TEXT("\\\'"));
// Escape double quotes: \" -> \\\"
EscapedString = EscapedString.Replace(TEXT("\""), TEXT("\\\""));
// Escape newlines: \n -> \\n
EscapedString = EscapedString.Replace(TEXT("\n"), TEXT("\\n"));
// Escape carriage returns: \r -> \\r
EscapedString = EscapedString.Replace(TEXT("\r"), TEXT("\\r"));
// Escape tabs: \t -> \\t
EscapedString = EscapedString.Replace(TEXT("\t"), TEXT("\\t"));
return FString::Printf(TEXT("\'%s\'"), *EscapedString);
}
case EJson::Number:
return JsonVal->AsString();
case EJson::Boolean:
return JsonVal->AsBool() ? TEXT("True") : TEXT("False");
case EJson::Array:
{
FString ArrayLiteral = TEXT("[");
const auto& Array = JsonVal->AsArray();
for (int32 i = 0; i < Array.Num(); ++i) {
ArrayLiteral += ConvertJsonValueToPythonLiteral(Array[i]);
if (i < Array.Num() - 1) ArrayLiteral += TEXT(", ");
}
ArrayLiteral += TEXT("]");
return ArrayLiteral;
}
case EJson::Object:
{
FString DictLiteral = TEXT("{");
const auto& Object = JsonVal->AsObject();
bool bFirst = true;
for (const auto& Pair : Object->Values) {
if (!bFirst) DictLiteral += TEXT(", ");
// UE 5.7: FJsonObject::Values key is FString; UE 5.8: UE::FSharedString.
// operator* yields const TCHAR* on both, so this builds on either engine.
FString KeyString = *Pair.Key;
// Escape key string as well (similar to EJson::String case)
KeyString = KeyString.Replace(TEXT("\\"), TEXT("\\\\"));
KeyString = KeyString.Replace(TEXT("\'"), TEXT("\\\'"));
KeyString = KeyString.Replace(TEXT("\""), TEXT("\\\""));
KeyString = KeyString.Replace(TEXT("\n"), TEXT("\\n"));
KeyString = KeyString.Replace(TEXT("\r"), TEXT("\\r"));
KeyString = KeyString.Replace(TEXT("\t"), TEXT("\\t"));
DictLiteral += FString::Printf(TEXT("\'%s\': %s"), *KeyString, *ConvertJsonValueToPythonLiteral(Pair.Value));
bFirst = false;
}
DictLiteral += TEXT("}");
return DictLiteral;
}
default:
return TEXT("None");
}
}
FMCPythonTcpServer::FMCPythonTcpServer()
{
RegisterNativeHandlers();
}
FMCPythonTcpServer::~FMCPythonTcpServer() { Stop(); }
void FMCPythonTcpServer::RegisterNativeHandlers()
{
NativeHandlers.Add(TEXT("livecoding_compile"), [this](TSharedPtr<FJsonObject> JsonObj, FSocket* ClientSocket)
{
HandleLiveCodingCompile(JsonObj, ClientSocket);
});
}
void FMCPythonTcpServer::SendJsonResponse(TSharedPtr<FJsonObject> ResponseJson, FSocket* ClientSocket, bool bCloseSocket)
{
FString ResultJson;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&ResultJson);
FJsonSerializer::Serialize(ResponseJson.ToSharedRef(), Writer);
Writer->Close();
FTCHARToUTF8 ResultUtf8(*ResultJson);
const uint8* DataPtr = (const uint8*)ResultUtf8.Get();
int32 TotalSize = ResultUtf8.Length();
int32 TotalSent = 0;
while (TotalSent < TotalSize)
{
int32 SentNow = 0;
if (!ClientSocket->Send(DataPtr + TotalSent, TotalSize - TotalSent, SentNow))
{
break;
}
if (SentNow == 0)
{
break;
}
TotalSent += SentNow;
}
if (bCloseSocket)
{
ClientSocket->Close();
ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(ClientSocket);
}
}
bool FMCPythonTcpServer::Start(const FString& InIP, uint16 InPort)
{
FIPv4Address IPAddr;
FIPv4Address::Parse(InIP, IPAddr);
FIPv4Endpoint Endpoint(IPAddr, InPort);
TcpListener = MakeShared<FTcpListener>(Endpoint, FTimespan::FromMilliseconds(100), false);
TcpListener->OnConnectionAccepted().BindRaw(this, &FMCPythonTcpServer::HandleIncomingConnection);
bShouldRun = true;
UE_LOG(LogMCPython, Log, TEXT("TCP server started at %s:%d."), *InIP, InPort);
return true;
}
void FMCPythonTcpServer::Stop()
{
bShouldRun = false;
TcpListener.Reset();
UE_LOG(LogMCPython, Log, TEXT("TCP server stopped."));
}
bool FMCPythonTcpServer::HandleIncomingConnection(FSocket* ClientSocket, const FIPv4Endpoint& ClientEndpoint)
{
UE_LOG(LogMCPython, Verbose, TEXT("Incoming connection from %s"), *ClientEndpoint.ToString());
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, ClientSocket, ClientEndpoint]() {
TArray<uint8> ReceivedData;
// Wait (bounded) for the client to actually send something. Liveness
// probes connect and close without sending a byte — the old
// `while (HasPendingData || ReceivedData.IsEmpty())` loop hot-spun a
// background worker forever per such connection, eventually starving
// the AnyBackgroundThread pool and freezing ALL request processing
// (connections still got accepted/logged by the listener thread, but
// nothing was ever handled).
if (!ClientSocket->Wait(ESocketWaitConditions::WaitForRead, FTimespan::FromSeconds(5)))
{
ClientSocket->Close();
ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(ClientSocket);
return;
}
uint32 DataSize = 0;
while (ClientSocket->HasPendingData(DataSize))
{
TArray<uint8> Buffer;
Buffer.SetNumZeroed(DataSize);
int32 BytesRead = 0;
if (!ClientSocket->Recv(Buffer.GetData(), Buffer.Num(), BytesRead) || BytesRead <= 0)
{
break;
}
Buffer.SetNum(BytesRead);
ReceivedData.Append(Buffer);
}
// Connect-and-close probe (or peer reset): nothing to process.
if (ReceivedData.IsEmpty())
{
ClientSocket->Close();
ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(ClientSocket);
return;
}
ReceivedData.Add(NULL);
FString ReceivedString = FString(UTF8_TO_TCHAR(reinterpret_cast<const char*>(ReceivedData.GetData())));
AsyncTask(ENamedThreads::GameThread, [this, ReceivedString, ClientSocket, ClientEndpoint]() {
ProcessDataOnGameThread(ReceivedString, ClientSocket, ClientEndpoint);
});
});
return true;
}
void FMCPythonTcpServer::ProcessDataOnGameThread(const FString& Data, FSocket* ClientSocket, const FIPv4Endpoint& ClientEndpoint)
{
UE_LOG(LogMCPython, Verbose, TEXT("Processing Data on Game Thread: %s"), *Data);
TSharedPtr<FJsonObject> JsonObj;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Data);
FString TypeField;
FString CodeField;
FString ResultMsg;
bool bExecSuccess = false;
if (FJsonSerializer::Deserialize(Reader, JsonObj) && JsonObj.IsValid())
{
if (JsonObj->TryGetStringField(TEXT("type"), TypeField))
{
if (TypeField == TEXT("python"))
{
if (!JsonObj->TryGetStringField(TEXT("code"), CodeField))
{
ResultMsg = TEXT("Failed: 'code' field missing for type 'python'");
CodeField = TEXT("import json; print(json.dumps({'success': False, 'message': 'Error: code field missing'}))");
}
}
else if (TypeField == TEXT("python_call"))
{
FString ModuleName, FunctionName;
if (JsonObj->TryGetStringField(TEXT("module"), ModuleName) &&
JsonObj->TryGetStringField(TEXT("function"), FunctionName))
{
const TSharedPtr<FJsonObject>* ArgsJsonObjectPtr = nullptr; // Changed from TArray<TSharedPtr<FJsonValue>>*
JsonObj->TryGetObjectField(TEXT("args"), ArgsJsonObjectPtr); // Changed from TryGetArrayField
FString PyArgsStringForCall;
if (ArgsJsonObjectPtr && ArgsJsonObjectPtr->IsValid()) // Check if the pointer and the object it points to are valid
{
// Wrap the FJsonObject in an FJsonValueObject to pass to ConvertJsonValueToPythonLiteral
TSharedPtr<FJsonValueObject> ArgsJsonValue = MakeShareable(new FJsonValueObject(*ArgsJsonObjectPtr));
PyArgsStringForCall = ConvertJsonValueToPythonLiteral(ArgsJsonValue);
}
else
{
PyArgsStringForCall = TEXT("{}"); // Default to an empty Python dictionary string if "args" is not a valid object or is missing
}
// Generate a short script to call the execute_action function from the mcp_unreal_actions module
// The first argument is the target module name, the second is the target function name, and the third is the argument dictionary.
CodeField = FString::Printf(TEXT("import unreal;from UnrealMCPython import mcp_unreal_actions;unreal.MCPythonHelper.submit_result(mcp_unreal_actions.execute_action(\'%s\', \'%s\', %s));"), // result handed back via SubmitResult, NOT print (print echoed every response into the Output Log)
*ModuleName,
*FunctionName,
*PyArgsStringForCall);
UE_LOG(LogMCPython, Verbose, TEXT("Generated Python Call (via execute_action):\\n%s"), *CodeField);
}
else
{
ResultMsg = TEXT("Failed: Missing 'module' or 'function' field for type 'python_call'");
CodeField = TEXT("import json; print(json.dumps({'success': False, 'message': 'Error: module/function field missing'}))");
}
}
else if (FNativeCommandHandler* Handler = NativeHandlers.Find(TypeField))
{
(*Handler)(JsonObj, ClientSocket);
return;
}
else
{
ResultMsg = FString::Printf(TEXT("Failed: Unsupported type: %s"), *TypeField);
FString EscapedTypeField = TypeField.Replace(TEXT("\'"), TEXT("\\\'"));
CodeField = FString::Printf(TEXT("import json; print(json.dumps({'success': False, 'message': 'Unsupported type: %s'}))"), *EscapedTypeField);
}
if (IPythonScriptPlugin::Get())
{
UMCPythonHelper::ClearSubmittedResult();
LogCapture.Clear();
GLog->AddOutputDevice(&LogCapture);
FPythonCommandEx PythonCommand;
PythonCommand.Command = CodeField;
PythonCommand.ExecutionMode = EPythonCommandExecutionMode::ExecuteFile;
bExecSuccess = IPythonScriptPlugin::Get()->ExecPythonCommandEx(PythonCommand);
GLog->RemoveOutputDevice(&LogCapture);
// Prefer the directly-submitted result (clean transport, nothing
// echoed to the log). Fall back to the print/log capture for code
// paths that still print (error stubs, legacy).
FString CapturedLogs;
if (!UMCPythonHelper::ConsumeSubmittedResult(CapturedLogs))
{
CapturedLogs = LogCapture.GetLogs().TrimStartAndEnd();
}
bool bIsJson = false;
if (CapturedLogs.StartsWith(TEXT("{")) || CapturedLogs.StartsWith(TEXT("["))) {
bIsJson = true;
}
if (!bIsJson) {
TSharedPtr<FJsonObject> ErrorJson = MakeShareable(new FJsonObject);
ErrorJson->SetBoolField(TEXT("success"), false);
ErrorJson->SetStringField(TEXT("message"), TEXT("Python did not return JSON"));
ErrorJson->SetStringField(TEXT("raw_result"), CapturedLogs);
FString WrappedJson;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&WrappedJson);
FJsonSerializer::Serialize(ErrorJson.ToSharedRef(), Writer);
Writer->Close();
CapturedLogs = WrappedJson;
}
UE_LOG(LogMCPython, Verbose, TEXT("Python Command Executed. Success: %s. Output Log: %s"),
bExecSuccess ? TEXT("True") : TEXT("False"), *CapturedLogs);
TSharedPtr<FJsonObject> ResponseToClient = MakeShareable(new FJsonObject);
ResponseToClient->SetBoolField(TEXT("success"), bExecSuccess); // Overall success of ExecPythonCommandEx
if (!ResultMsg.IsEmpty()) // If there was a pre-execution error message (e.g. bad JSON input from client)
{
ResponseToClient->SetStringField(TEXT("message"), ResultMsg);
}
else if (!bExecSuccess) // Python execution itself failed
{
if (!CapturedLogs.IsEmpty())
{
// If execution failed and logs are available, they likely contain the Python error
ResponseToClient->SetStringField(TEXT("message"), TEXT("Python execution failed. See result for details."));
}
else
{
// If execution failed and no logs, it's a more generic failure
ResponseToClient->SetStringField(TEXT("message"), TEXT("Python execution failed with no specific error log."));
}
}
else // bExecSuccess is true
{
ResponseToClient->SetStringField(TEXT("message"), TEXT("Python command executed successfully."));
}
// The "result" field will contain whatever the Python script printed.
ResponseToClient->SetStringField(TEXT("result"), CapturedLogs);
FString ResultJson;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&ResultJson);
FJsonSerializer::Serialize(ResponseToClient.ToSharedRef(), Writer);
Writer->Close();
FTCHARToUTF8 ResultUtf8(*ResultJson);
const uint8* DataPtr = (const uint8*)ResultUtf8.Get();
int32 TotalSize = ResultUtf8.Length();
int32 TotalSent = 0;
while (TotalSent < TotalSize)
{
int32 SentNow = 0;
if (!ClientSocket->Send(DataPtr + TotalSent, TotalSize - TotalSent, SentNow))
{
break; // Error occurred
}
if (SentNow == 0)
{
break; // Connection closed or can't send more
}
TotalSent += SentNow;
}
}
else
{
ResultMsg = TEXT("Failed: PythonScriptPlugin not found");
TSharedPtr<FJsonObject> ErrorResponse = MakeShareable(new FJsonObject);
ErrorResponse->SetBoolField(TEXT("success"), false);
ErrorResponse->SetStringField(TEXT("message"), ResultMsg);
FString ErrorJson;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&ErrorJson);
FJsonSerializer::Serialize(ErrorResponse.ToSharedRef(), Writer);
Writer->Close();
FTCHARToUTF8 ResultUtf8(*ErrorJson);
const uint8* DataPtr = (const uint8*)ResultUtf8.Get();
int32 TotalSize = ResultUtf8.Length();
int32 TotalSent = 0;
while (TotalSent < TotalSize)
{
int32 SentNow = 0;
if (!ClientSocket->Send(DataPtr + TotalSent, TotalSize - TotalSent, SentNow))
{
break;
}
if (SentNow == 0)
{
break;
}
TotalSent += SentNow;
}
}
}
else
{
ResultMsg = TEXT("Failed: Missing 'type' field in JSON request");
TSharedPtr<FJsonObject> ErrorResponse = MakeShareable(new FJsonObject);
ErrorResponse->SetBoolField(TEXT("success"), false);
ErrorResponse->SetStringField(TEXT("message"), ResultMsg);
FString ErrorJson;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&ErrorJson);
FJsonSerializer::Serialize(ErrorResponse.ToSharedRef(), Writer);
Writer->Close();
FTCHARToUTF8 ResultUtf8(*ErrorJson);
const uint8* DataPtr = (const uint8*)ResultUtf8.Get();
int32 TotalSize = ResultUtf8.Length();
int32 TotalSent = 0;
while (TotalSent < TotalSize)
{
int32 SentNow = 0;
if (!ClientSocket->Send(DataPtr + TotalSent, TotalSize - TotalSent, SentNow))
{
break;
}
if (SentNow == 0)
{
break;
}
TotalSent += SentNow;
}
}
}
else
{
ResultMsg = TEXT("Failed: JSON parse error on received data");
TSharedPtr<FJsonObject> ErrorResponse = MakeShareable(new FJsonObject);
ErrorResponse->SetBoolField(TEXT("success"), false);
ErrorResponse->SetStringField(TEXT("message"), ResultMsg);
ErrorResponse->SetStringField(TEXT("raw_data"), Data);
FString ErrorJson;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&ErrorJson);
FJsonSerializer::Serialize(ErrorResponse.ToSharedRef(), Writer);
Writer->Close();
FTCHARToUTF8 ResultUtf8(*ErrorJson);
const uint8* DataPtr = (const uint8*)ResultUtf8.Get();
int32 TotalSize = ResultUtf8.Length();
int32 TotalSent = 0;
while (TotalSent < TotalSize)
{
int32 SentNow = 0;
if (!ClientSocket->Send(DataPtr + TotalSent, TotalSize - TotalSent, SentNow))
{
break;
}
if (SentNow == 0)
{
break;
}
TotalSent += SentNow;
}
}
ClientSocket->Close();
ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(ClientSocket);
}
void FMCPythonTcpServer::HandleLiveCodingCompile(TSharedPtr<FJsonObject> JsonObj, FSocket* ClientSocket)
{
ILiveCodingModule* LiveCoding = FModuleManager::GetModulePtr<ILiveCodingModule>(TEXT("LiveCoding"));
if (!LiveCoding)
{
TSharedPtr<FJsonObject> Response = MakeShareable(new FJsonObject);
Response->SetBoolField(TEXT("success"), false);
Response->SetStringField(TEXT("message"), TEXT("LiveCoding module is not available."));
SendJsonResponse(Response, ClientSocket);
return;
}
if (!LiveCoding->IsEnabledForSession())
{
TSharedPtr<FJsonObject> Response = MakeShareable(new FJsonObject);
Response->SetBoolField(TEXT("success"), false);
Response->SetStringField(TEXT("message"), TEXT("LiveCoding is not enabled for this session. Enable it in Editor Preferences > Live Coding."));
SendJsonResponse(Response, ClientSocket);
return;
}
const FString UBTLogPath = FPaths::Combine(FPaths::EngineDir(), TEXT("Programs"), TEXT("UnrealBuildTool"), TEXT("Log.txt"));
const FDateTime UBTLogTimestampBefore = IFileManager::Get().GetTimeStamp(*UBTLogPath);
FMCPCompileLogCapture CompileCapture;
GLog->AddOutputDevice(&CompileCapture);
UE_LOG(LogMCPython, Log, TEXT("LiveCoding compile started (WaitForCompletion)..."));
const double StartTime = FPlatformTime::Seconds();
ELiveCodingCompileResult CompileResult = ELiveCodingCompileResult::NotStarted;
const bool bStarted = LiveCoding->Compile(ELiveCodingCompileFlags::WaitForCompletion, &CompileResult);
GLog->RemoveOutputDevice(&CompileCapture);
const double ElapsedTime = FPlatformTime::Seconds() - StartTime;
const bool bSuccess = bStarted &&
(CompileResult == ELiveCodingCompileResult::Success ||
CompileResult == ELiveCodingCompileResult::NoChanges);
UE_LOG(LogMCPython, Log, TEXT("LiveCoding compile finished in %.1fs: %s"),
ElapsedTime, *LCCompileResultToString(CompileResult));
FString Message;
switch (CompileResult)
{
case ELiveCodingCompileResult::Success:
Message = FString::Printf(TEXT("Compilation succeeded in %.1f seconds."), ElapsedTime);
break;
case ELiveCodingCompileResult::NoChanges:
Message = FString::Printf(TEXT("Compilation finished in %.1f seconds (no changes detected)."), ElapsedTime);
break;
case ELiveCodingCompileResult::Failure:
Message = FString::Printf(TEXT("Compilation failed in %.1f seconds. See compile_output for details."), ElapsedTime);
break;
case ELiveCodingCompileResult::Cancelled:
Message = TEXT("Compilation was cancelled.");
break;
case ELiveCodingCompileResult::CompileStillActive:
Message = TEXT("A prior compilation is still in progress.");
break;
case ELiveCodingCompileResult::NotStarted:
Message = TEXT("Compilation could not be started (Live Coding monitor failed to launch).");
break;
default:
Message = FString::Printf(TEXT("Compilation ended with result: %s"), *LCCompileResultToString(CompileResult));
break;
}
TSharedPtr<FJsonObject> Response = MakeShareable(new FJsonObject);
Response->SetBoolField(TEXT("success"), bSuccess);
Response->SetStringField(TEXT("compile_result"), LCCompileResultToString(CompileResult));
Response->SetStringField(TEXT("message"), Message);
Response->SetNumberField(TEXT("elapsed_seconds"), ElapsedTime);
const FString CapturedLog = CompileCapture.GetAndClear();
if (!CapturedLog.IsEmpty())
Response->SetStringField(TEXT("compile_output"), CapturedLog);
const FString Diagnostics = CollectUBTDiagnostics(UBTLogPath, UBTLogTimestampBefore);
if (!Diagnostics.IsEmpty())
Response->SetStringField(TEXT("compiler_diagnostics"), Diagnostics);
SendJsonResponse(Response, ClientSocket);
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2025 GenOrca (by zenoengine). All Rights Reserved.
#include "UnrealMCPython.h"
#include "Sockets.h"
#include "SocketSubsystem.h"
#include "Common/TcpListener.h"
#include "IPythonScriptPlugin.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonReader.h"
#include "Dom/JsonObject.h"
#include "MCPythonTcpServer.h"
#define LOCTEXT_NAMESPACE "FUnrealMCPythonModule"
void FUnrealMCPythonModule::StartupModule()
{
static const uint16 SERVER_PORT = 12029;
static const FString SERVER_IP = TEXT("127.0.0.1");
TcpServer = MakeUnique<FMCPythonTcpServer>();
TcpServer->Start(SERVER_IP, SERVER_PORT);
}
void FUnrealMCPythonModule::ShutdownModule()
{
if (TcpServer)
{
TcpServer->Stop();
TcpServer.Reset();
}
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FUnrealMCPythonModule, UnrealMCPython)

View File

@@ -0,0 +1,354 @@
// Copyright (c) 2025 GenOrca (by zenoengine). All Rights Reserved.
#pragma once
#include "Kismet/BlueprintFunctionLibrary.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"
#include "Components/Widget.h"
#include "MCPythonHelper.generated.h"
USTRUCT(BlueprintType)
struct FMCPythonPinLinkInfo
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category="MCPython")
FString NodeName;
UPROPERTY(BlueprintReadOnly, Category="MCPython")
FString NodeTitle;
UPROPERTY(BlueprintReadOnly, Category="MCPython")
FString PinName;
};
USTRUCT(BlueprintType)
struct FMCPythonBlueprintPinInfo
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category="MCPython")
FString PinName;
UPROPERTY(BlueprintReadOnly, Category="MCPython")
FString FriendlyName;
UPROPERTY(BlueprintReadOnly, Category="MCPython")
FString Direction;
UPROPERTY(BlueprintReadOnly, Category="MCPython")
FString PinType;
UPROPERTY(BlueprintReadOnly, Category="MCPython")
FString PinSubType;
UPROPERTY(BlueprintReadOnly, Category="MCPython")
FString DefaultValue;
UPROPERTY(BlueprintReadOnly, Category="MCPython")
TArray<FMCPythonPinLinkInfo> LinkedTo;
};
USTRUCT(BlueprintType)
struct FMCPythonBlueprintNodeInfo
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category="MCPython")
FString NodeName;
UPROPERTY(BlueprintReadOnly, Category="MCPython")
FString NodeTitle;
UPROPERTY(BlueprintReadOnly, Category="MCPython")
FString NodeComment;
UPROPERTY(BlueprintReadOnly, Category="MCPython")
TArray<FMCPythonBlueprintPinInfo> Pins;
};
USTRUCT(BlueprintType)
struct FMCPythonBTNodeInfo
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category="MCPython")
FString NodeName;
UPROPERTY(BlueprintReadOnly, Category="MCPython")
FString NodeClass;
UPROPERTY(BlueprintReadOnly, Category="MCPython")
TArray<FString> DecoratorClasses;
UPROPERTY(BlueprintReadOnly, Category="MCPython")
TArray<FString> DecoratorNames;
UPROPERTY(BlueprintReadOnly, Category="MCPython")
TArray<FString> ServiceClasses;
UPROPERTY(BlueprintReadOnly, Category="MCPython")
TArray<FString> ServiceNames;
TArray<FMCPythonBTNodeInfo> Children;
};
UCLASS()
class UNREALMCPYTHON_API UMCPythonHelper : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
// 모든 에디터에서 열려있는 에셋 반환
UFUNCTION(BlueprintCallable, Category="Editor|MCPython", CallInEditor)
static TArray<UObject*> GetAllEditedAssets();
// (예시) 선택된 블루프린트 노드 반환
UFUNCTION(BlueprintCallable, Category="Editor|MCPython", CallInEditor)
static TArray<UObject*> GetSelectedBlueprintNodes();
// 선택된 블루프린트 노드의 연결 정보 반환
UFUNCTION(BlueprintCallable, Category="Editor|MCPython", CallInEditor)
static TArray<FMCPythonBlueprintNodeInfo> GetSelectedBlueprintNodeInfos();
// ─── Behavior Tree Helpers ──────────────────────────────────────────
/** Get the full tree structure of a Behavior Tree as JSON string (accesses RootNode via C++) */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString GetBehaviorTreeStructure(UBehaviorTree* BehaviorTree);
/** Set the Blackboard asset on a Behavior Tree (setter not exposed to Python) */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static bool SetBehaviorTreeBlackboard(UBehaviorTree* BehaviorTree, UBlackboardData* BlackboardData);
/** Get detailed properties of a specific node by name, returned as JSON string */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString GetBehaviorTreeNodeDetails(UBehaviorTree* BehaviorTree, const FString& NodeName);
/** Get details of selected nodes in the BT editor as JSON */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString GetSelectedBTNodes();
/** Build a complete Behavior Tree from a JSON structure */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString BuildBehaviorTree(UBehaviorTree* BehaviorTree, const FString& TreeStructureJson);
/** List all available BT node classes (composites, tasks, decorators, services) */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString ListBTNodeClasses();
// ─── Blueprint Graph Helpers ──────────────────────────────────────────
/** Get the full graph info (all nodes, pins, connections) for a Blueprint graph */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString GetBlueprintGraphInfo(UBlueprint* Blueprint, const FString& GraphName);
/** List callable functions available in a Blueprint context */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString ListCallableFunctions(UBlueprint* Blueprint, const FString& Filter);
/** List all variables defined in a Blueprint */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString ListBlueprintVariables(UBlueprint* Blueprint);
/** Add a single node to a Blueprint graph from JSON description */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString AddBlueprintNode(UBlueprint* Blueprint, const FString& GraphName, const FString& NodeJson);
/** Connect two pins in a Blueprint graph */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString ConnectBlueprintPins(UBlueprint* Blueprint, const FString& GraphName,
const FString& SourceNodeName, const FString& SourcePinName,
const FString& TargetNodeName, const FString& TargetPinName);
/** Remove a node from a Blueprint graph */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString RemoveBlueprintNode(UBlueprint* Blueprint, const FString& GraphName,
const FString& NodeName);
/** Build a Blueprint graph from JSON adjacency list (nodes + connections) */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString BuildBlueprintGraph(UBlueprint* Blueprint, const FString& GraphName,
const FString& GraphJson);
/** Compile a Blueprint and return the result */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString CompileBlueprint(UBlueprint* Blueprint);
/** Set any CDO property including inherited C++ UPROPERTYs (e.g. DefaultPawnClass on GameModeBase BPs).
* Uses TFieldIterator with IncludeSuper to bypass the Python set_editor_property limitation. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString SetBlueprintCDOProperty(UBlueprint* Blueprint, const FString& PropertyName, const FString& ValueStr);
// ─── UMG Widget Blueprint Helpers ─────────────────────────────────────────
// UE 5.7 Python bindings mark UWidgetTree::RootWidget, AllWidgets, and
// ConstructWidget as protected, so direct Python access is blocked.
// These UFUNCTIONs proxy the calls through C++ where the members are accessible.
/** Get widget tree info (root widget, all widgets) as JSON */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString UmgGetWidgetInfo(UBlueprint* WidgetBP);
/** Add a widget to the widget tree. ParentName="" means auto-root or root panel. Returns JSON. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString UmgAddWidget(UBlueprint* WidgetBP, const FString& WidgetType, const FString& WidgetName, const FString& ParentName);
/** Find a widget by name in the widget tree. Returns nullptr if not found. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static UWidget* UmgFindWidget(UBlueprint* WidgetBP, const FString& WidgetName);
/** Remove a widget from the widget tree. Returns JSON. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString UmgRemoveWidget(UBlueprint* WidgetBP, const FString& WidgetName);
/** Set bIsVariable on a named widget so Blueprint can access it as a variable. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString UmgSetWidgetIsVariable(UBlueprint* WidgetBP, const FString& WidgetName, bool bIsVariable);
/** Set CanvasPanelSlot layout (anchors + position/size) in one call.
* AnchorMin/Max: 0..1 fractions. OffsetX/Y: pixel offset from anchor. SizeX/Y: pixel size. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString UmgSetSlotLayout(UBlueprint* WidgetBP, const FString& WidgetName,
float AnchorMinX, float AnchorMinY, float AnchorMaxX, float AnchorMaxY,
float OffsetX, float OffsetY, float SizeX, float SizeY);
/** Set font size, text color, and outline size on a TextBlock widget in one call. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString UmgSetTextStyle(UBlueprint* WidgetBP, const FString& WidgetName,
int32 FontSize, float ColorR, float ColorG, float ColorB, float ColorA,
int32 OutlineSize);
/** Get an editor property value from a widget as a JSON string. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString UmgGetWidgetProperty(UBlueprint* WidgetBP, const FString& WidgetName, const FString& PropertyName);
/** Set an editor property value on a widget from a string. Supports bool, int, float, and string properties. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString UmgSetWidgetProperty(UBlueprint* WidgetBP, const FString& WidgetName, const FString& PropertyName, const FString& Value);
/** Move a widget under a different panel parent (preserves the widget; cycle-guarded). Returns JSON. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString UmgReparentWidget(UBlueprint* WidgetBP, const FString& WidgetName, const FString& NewParentName);
/** Wrap a widget in a newly-created panel of WrapperType, taking the widget's place in the tree. Returns JSON. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString UmgWrapWidget(UBlueprint* WidgetBP, const FString& WidgetName, const FString& WrapperType, const FString& WrapperName);
/** Replace a widget with a new widget of NewType at the same slot (old subtree is discarded). Returns JSON. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString UmgReplaceWidget(UBlueprint* WidgetBP, const FString& WidgetName, const FString& NewType, const FString& NewName);
/** List the bindable multicast-delegate events on a widget (e.g. OnClicked). Returns JSON. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString UmgListWidgetEvents(UBlueprint* WidgetBP, const FString& WidgetName);
/** Create a bound event node in the widget BP's event graph for a widget's delegate. Returns JSON. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString UmgBindWidgetEvent(UBlueprint* WidgetBP, const FString& WidgetName, const FString& EventName);
/** Add a component to a Blueprint's SCS.
* ComponentClassPath e.g. "/Script/Engine.CameraComponent"
* ParentComponentName: name of the parent SCS node, or "" to attach to root */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString AddComponentToBlueprint(UBlueprint* Blueprint,
const FString& ComponentClassPath,
const FString& ComponentName,
float LocationX, float LocationY, float LocationZ,
float RotationPitch, float RotationYaw, float RotationRoll,
const FString& ParentComponentName);
/** List all SCS components on a Blueprint. Returns JSON array of {name, class, variable_name}. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString ListBlueprintComponents(UBlueprint* Blueprint);
/** Remove a component by variable name from a Blueprint's SCS.
* Promotes children to the removed node's parent (safe remove). */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString RemoveComponentFromBlueprint(UBlueprint* Blueprint, const FString& ComponentName);
/** Set a property on a component template in a Blueprint's SCS.
* ComponentName: the variable name of the component.
* PropertyName: the property to set (e.g. "relative_location", "sphere_radius").
* Value: string representation (e.g. "(X=0,Y=0,Z=100)" or "50.0"). */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString SetComponentProperty(UBlueprint* Blueprint,
const FString& ComponentName,
const FString& PropertyName,
const FString& Value);
/** Set the canvas position of a node in a Blueprint graph by node name. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString SetBlueprintNodePosition(UBlueprint* Blueprint,
const FString& GraphName,
const FString& NodeName,
float PosX, float PosY);
/** Set a pin default on a blueprint node.
* For object pins, Value should be an asset path like "/Engine/BasicShapes/Sphere.Sphere".
* For numeric/bool pins, Value is the literal string like "3.14" or "true". */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString SetBlueprintNodePinDefault(UBlueprint* Blueprint,
const FString& GraphName,
const FString& NodeName,
const FString& PinName,
const FString& Value);
// ─── SkeletalMesh / Skeleton Helpers ──────────────────────────────────────
// Python does not expose reference-skeleton bone enumeration, and
// USkeletalMeshSocket::SocketName is read-only via Python reflection.
// These proxy the calls through C++.
/** List reference-skeleton bones of a SkeletalMesh as JSON [{name, index, parent}]. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString GetSkeletonBones(USkeletalMesh* Mesh);
/** Add a socket to a SkeletalMesh on a bone with a relative transform. Returns JSON. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString AddSkeletalMeshSocket(USkeletalMesh* Mesh, const FString& SocketName,
const FString& BoneName,
float LocationX, float LocationY, float LocationZ,
float RotationPitch, float RotationYaw, float RotationRoll);
/** Remove a named socket from a SkeletalMesh. Returns JSON. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString RemoveSkeletalMeshSocket(USkeletalMesh* Mesh, const FString& SocketName);
// ─── Response transport (python_call) ─────────────────────────────────────
// The python_call path used to transport results via print() + a GLog capture
// device. Because the capture device only ADDS to GLog routing, every response
// was also echoed into the Output Log / log file — including megabyte base64
// payloads from vision captures, and get_output_log responses re-echoing
// themselves into ever-deeper escaping. submit_result hands the JSON straight
// to the server instead, so responses never touch the log.
//
// Thread-safety: a single static slot is sufficient because each request is one
// game-thread task — the write (last python statement) and the read (right
// after ExecPythonCommandEx returns) are adjacent within that task, and any
// re-entrant nested request completes atomically in between, never partially.
/** Called by generated python_call code to hand the action's JSON result back. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static void SubmitResult(const FString& ResultJson);
/** C++ side: move the submitted result out (returns false if nothing was submitted). */
static bool ConsumeSubmittedResult(FString& OutResult);
/** C++ side: drop any stale submitted result before executing a call. */
static void ClearSubmittedResult();
// ─── AnimGraph authoring (editor-only AnimGraph module) ───────────────────────
// AnimGraph node classes (UAnimGraphNode_*) live in the editor-only AnimGraph
// module and are not exposed to Python, and UAnimationGraph::Nodes is protected,
// so these operations need C++. Read-only AnimGraph introspection is already
// served by GetBlueprintGraphInfo (graph_name="AnimGraph").
/** Add a Sequence Player node to the AnimGraph, optionally linked to the Output Pose. Returns JSON. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString AddAnimGraphSequencePlayer(UAnimBlueprint* AnimBP, const FString& AnimSequencePath, bool bLinkToOutputPose);
/** Build an arbitrary state machine in the AnimGraph from a JSON spec
({states:[{name,anim?}], entry?, transitions:[{from,to,var?,op?,value?}]}). Returns JSON. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString BuildAnimStateMachine(UAnimBlueprint* AnimBP, const FString& SpecJson);
// ─── Editor viewport projection ───────────────────────────────────────────────
// FEditorViewportClient / FSceneView are not exposed to Python, so world<->screen
// projection against the active level editor viewport needs C++.
/** Project a world location to active-level-viewport pixel coords. Returns JSON {x,y,visible,viewport_*}. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString WorldToScreen(FVector WorldLocation);
/** Deproject a viewport pixel to a world location at the given distance along the view ray. Returns JSON. */
UFUNCTION(BlueprintCallable, Category="Editor|MCPython")
static FString ScreenToWorld(float ScreenX, float ScreenY, float Distance);
};

View File

@@ -0,0 +1,59 @@
// Copyright (c) 2025 GenOrca (by zenoengine). All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Interfaces/IPv4/IPv4Endpoint.h"
#include <memory>
#include "Misc/OutputDeviceRedirector.h"
class FTcpListener;
class FSocket;
class FPythonLogCapture : public FOutputDevice
{
public:
FPythonLogCapture() : FOutputDevice() {}
virtual void Serialize(const TCHAR* InData, ELogVerbosity::Type Verbosity, const FName& Category) override
{
if (Category == FName("LogPython"))
{
CapturedLogs.Append(InData);
CapturedLogs.Append(TEXT("\n"));
}
}
void Clear() { CapturedLogs.Empty(); }
FString GetLogs() const { return CapturedLogs; }
private:
FString CapturedLogs;
};
using FNativeCommandHandler = TFunction<void(TSharedPtr<FJsonObject> JsonObj, FSocket* ClientSocket)>;
class FMCPythonTcpServer
{
public:
FMCPythonTcpServer();
~FMCPythonTcpServer();
bool Start(const FString& InIP, uint16 InPort);
void Stop();
private:
TSharedPtr<FTcpListener> TcpListener;
FSocket* ListenSocket = nullptr;
bool bShouldRun = false;
FPythonLogCapture LogCapture;
TMap<FString, FNativeCommandHandler> NativeHandlers;
void RegisterNativeHandlers();
bool HandleIncomingConnection(FSocket* ClientSocket, const FIPv4Endpoint& ClientEndpoint);
void ProcessDataOnGameThread(const FString& Data, FSocket* ClientSocket, const FIPv4Endpoint& ClientEndpoint);
void SendJsonResponse(TSharedPtr<FJsonObject> ResponseJson, FSocket* ClientSocket, bool bCloseSocket = true);
// Native command handlers
void HandleLiveCodingCompile(TSharedPtr<FJsonObject> JsonObj, FSocket* ClientSocket);
};

View File

@@ -0,0 +1,19 @@
// Copyright (c) 2025 GenOrca (by zenoengine). All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
#include "MCPythonTcpServer.h"
class FUnrealMCPythonModule : public IModuleInterface
{
public:
/** IModuleInterface implementation */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
private:
TUniquePtr<FMCPythonTcpServer> TcpServer;
};

View File

@@ -0,0 +1,73 @@
// Copyright (c) 2025 GenOrca (by zenoengine). All Rights Reserved.
using UnrealBuildTool;
public class UnrealMCPython : ModuleRules
{
public UnrealMCPython(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
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",
"Sockets",
"Networking",
"Json",
"JsonUtilities",
"PythonScriptPlugin"
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"UnrealEd",
"EditorSubsystem",
"AssetTools",
"BlueprintGraph",
"Kismet",
"AIModule",
"GameplayTasks",
"AIGraph",
"BehaviorTreeEditor",
"LiveCoding",
"UMG",
"UMGEditor",
"AnimGraph",
"AnimGraphRuntime",
}
);
DynamicallyLoadedModuleNames.AddRange(
new string[]
{
// ... add any modules that your module loads dynamically here ...
}
);
}
}