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:
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 ...
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user