Merge of the two project copies into one self-contained plugin (the superset: variable_op + variables.py, full pcg_op runtime/declarative/preset ops, the CreateWidgetFloatAnimation widget tool, and full Voxel graph authoring). Made fully project- and machine-agnostic — no hardcoded paths: - New src/projectPaths.js auto-detects the host .uproject (walk-up), project name, Editor build target, log file, and engine install (EngineAssociation via launcher manifest/registry, else installed-engine scan). All overridable via UE_* env vars. - Rewired buildOrchestrator/insights/launcher/insightsExporter/server.js and the Python workers (cpp_scaffold, live_coding, apply_graph, console) off the old C:/Github/ihy, IHY*, E:/UE_Versions and UE_5.7 literals onto the resolver. - Voxel made optional: Build.cs auto-detects the Voxel plugin (env UE_MCP_WITH_VOXEL override) and the C++ compiles to stubs under WITH_MCP_VOXEL, so the module builds in projects without Voxel; .uplugin marks Voxel optional. - De-branded the agent-gateway and docs; scrubbed a leaked API key; excluded node_modules/Binaries/Intermediate/__pycache__/secrets from the repo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
348 lines
12 KiB
C++
348 lines
12 KiB
C++
#include "UEBlueprintMCPEditor.h"
|
|
|
|
#include "HttpModule.h"
|
|
#include "Interfaces/IHttpResponse.h"
|
|
#include "Misc/CoreDelegates.h"
|
|
#include "Framework/Notifications/NotificationManager.h"
|
|
#include "Widgets/Notifications/SNotificationList.h"
|
|
#include "BlueprintEditor.h"
|
|
#include "BlueprintEditorContext.h"
|
|
#include "EdGraph/EdGraph.h"
|
|
#include "EdGraph/EdGraphNode.h"
|
|
#include "EdGraphNode_Comment.h"
|
|
#include "Framework/Application/IInputProcessor.h"
|
|
#include "Framework/Application/SlateApplication.h"
|
|
#include "Framework/Commands/UIAction.h"
|
|
#include "Kismet2/BlueprintEditorUtils.h"
|
|
#include "ScopedTransaction.h"
|
|
#include "SGraphPanel.h"
|
|
#include "ToolMenu.h"
|
|
#include "ToolMenuEntry.h"
|
|
#include "ToolMenuSection.h"
|
|
#include "ToolMenus.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "UEBlueprintMCPEditor"
|
|
|
|
namespace
|
|
{
|
|
const TCHAR* MCPZoneCommentText = TEXT("MCP Zone");
|
|
const TCHAR* LegacyMCPZoneCommentText = TEXT("MCP Draft Zone");
|
|
|
|
bool IsMCPZoneComment(const UEdGraphNode* Node)
|
|
{
|
|
const UEdGraphNode_Comment* CommentNode = Cast<UEdGraphNode_Comment>(Node);
|
|
return CommentNode
|
|
&& (CommentNode->NodeComment.Equals(MCPZoneCommentText, ESearchCase::IgnoreCase)
|
|
|| CommentNode->NodeComment.Equals(LegacyMCPZoneCommentText, ESearchCase::IgnoreCase));
|
|
}
|
|
}
|
|
|
|
class FMCPZoneInputProcessor : public IInputProcessor
|
|
{
|
|
public:
|
|
explicit FMCPZoneInputProcessor(FUEBlueprintMCPEditorModule& InOwner)
|
|
: Owner(InOwner)
|
|
{
|
|
}
|
|
|
|
virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) override
|
|
{
|
|
}
|
|
|
|
virtual bool HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
|
|
{
|
|
if (!Owner.bMCPZoneModeActive || MouseEvent.GetEffectingButton() != EKeys::LeftMouseButton)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
TSharedPtr<FBlueprintEditor> Editor = Owner.ActiveMCPZoneEditor.Pin();
|
|
if (!Editor.IsValid() || !Editor->GetFocusedGraph())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
TSharedPtr<SGraphEditor> GraphEditor = Editor->OpenGraphAndBringToFront(Editor->GetFocusedGraph(), false);
|
|
SGraphPanel* GraphPanel = GraphEditor.IsValid() ? GraphEditor->GetGraphPanel() : nullptr;
|
|
if (!GraphPanel)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const FGeometry& Geometry = GraphPanel->GetTickSpaceGeometry();
|
|
const FVector2f ScreenPosition = UE::Slate::CastToVector2f(MouseEvent.GetScreenSpacePosition());
|
|
if (!Geometry.IsUnderLocation(ScreenPosition))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bDragging = true;
|
|
StartScreenPosition = ScreenPosition;
|
|
return true;
|
|
}
|
|
|
|
virtual bool HandleMouseMoveEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
|
|
{
|
|
return bDragging && Owner.bMCPZoneModeActive;
|
|
}
|
|
|
|
virtual bool HandleMouseButtonUpEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
|
|
{
|
|
if (!bDragging || MouseEvent.GetEffectingButton() != EKeys::LeftMouseButton)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bDragging = false;
|
|
const FVector2f EndScreenPosition = UE::Slate::CastToVector2f(MouseEvent.GetScreenSpacePosition());
|
|
Owner.TryCreateMCPZoneFromScreenDrag(StartScreenPosition, EndScreenPosition);
|
|
return true;
|
|
}
|
|
|
|
virtual const TCHAR* GetDebugName() const override
|
|
{
|
|
return TEXT("MCPZoneInputProcessor");
|
|
}
|
|
|
|
private:
|
|
FUEBlueprintMCPEditorModule& Owner;
|
|
bool bDragging = false;
|
|
FVector2f StartScreenPosition = FVector2f::ZeroVector;
|
|
};
|
|
|
|
IMPLEMENT_MODULE(FUEBlueprintMCPEditorModule, UEBlueprintMCPEditor)
|
|
|
|
void FUEBlueprintMCPEditorModule::StartupModule()
|
|
{
|
|
UToolMenus::RegisterStartupCallback(
|
|
FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FUEBlueprintMCPEditorModule::RegisterMenus));
|
|
|
|
FCoreDelegates::OnPostEngineInit.AddRaw(this, &FUEBlueprintMCPEditorModule::NotifyBridgeStatus);
|
|
}
|
|
|
|
void FUEBlueprintMCPEditorModule::NotifyBridgeStatus()
|
|
{
|
|
const FString Url = TEXT("http://127.0.0.1:30010/remote/info");
|
|
TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
|
|
Request->SetVerb(TEXT("GET"));
|
|
Request->SetURL(Url);
|
|
Request->SetTimeout(3.0f);
|
|
Request->OnProcessRequestComplete().BindLambda(
|
|
[](FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bSucceeded)
|
|
{
|
|
const bool bOk = bSucceeded && Resp.IsValid() && Resp->GetResponseCode() == 200;
|
|
FNotificationInfo Info(bOk
|
|
? LOCTEXT("MCPBridgeUp", "UE Blueprint MCP bridge ready (Remote Control :30010)")
|
|
: LOCTEXT("MCPBridgeDown", "UE Blueprint MCP bridge NOT reachable on :30010 — check Remote Control plugin"));
|
|
Info.ExpireDuration = bOk ? 4.0f : 8.0f;
|
|
Info.bUseSuccessFailIcons = true;
|
|
Info.bFireAndForget = true;
|
|
TSharedPtr<SNotificationItem> Item = FSlateNotificationManager::Get().AddNotification(Info);
|
|
if (Item.IsValid())
|
|
{
|
|
Item->SetCompletionState(bOk ? SNotificationItem::CS_Success : SNotificationItem::CS_Fail);
|
|
}
|
|
});
|
|
Request->ProcessRequest();
|
|
}
|
|
|
|
void FUEBlueprintMCPEditorModule::ShutdownModule()
|
|
{
|
|
FCoreDelegates::OnPostEngineInit.RemoveAll(this);
|
|
|
|
if (MCPZoneInputProcessor.IsValid() && FSlateApplication::IsInitialized())
|
|
{
|
|
FSlateApplication::Get().UnregisterInputPreProcessor(MCPZoneInputProcessor);
|
|
MCPZoneInputProcessor.Reset();
|
|
}
|
|
|
|
if (UToolMenus::IsToolMenuUIEnabled())
|
|
{
|
|
UToolMenus::UnRegisterStartupCallback(this);
|
|
UToolMenus::UnregisterOwner(this);
|
|
}
|
|
}
|
|
|
|
void FUEBlueprintMCPEditorModule::RegisterMenus()
|
|
{
|
|
FToolMenuOwnerScoped OwnerScoped(this);
|
|
UToolMenu* Toolbar = UToolMenus::Get()->ExtendMenu("AssetEditor.BlueprintEditor.ToolBar");
|
|
FToolMenuSection& Section = Toolbar->FindOrAddSection("Settings");
|
|
|
|
Section.AddDynamicEntry("MCPDraftZoneCommands", FNewToolMenuSectionDelegate::CreateLambda([this](FToolMenuSection& InSection)
|
|
{
|
|
UBlueprintEditorToolMenuContext* Context = InSection.FindContext<UBlueprintEditorToolMenuContext>();
|
|
if (!Context || !Context->BlueprintEditor.IsValid() || !Context->GetBlueprintObj())
|
|
{
|
|
return;
|
|
}
|
|
|
|
TWeakPtr<FBlueprintEditor> WeakEditor = Context->BlueprintEditor;
|
|
FUIAction Action(
|
|
FExecuteAction::CreateLambda([this, WeakEditor]()
|
|
{
|
|
ToggleMCPZoneMode(WeakEditor);
|
|
}),
|
|
FCanExecuteAction::CreateLambda([WeakEditor]()
|
|
{
|
|
return WeakEditor.IsValid() && WeakEditor.Pin()->GetFocusedGraph() != nullptr;
|
|
}),
|
|
FIsActionChecked::CreateLambda([this, WeakEditor]()
|
|
{
|
|
return IsMCPZoneModeActive(WeakEditor);
|
|
}));
|
|
|
|
FToolMenuEntry Entry = FToolMenuEntry::InitToolBarButton(
|
|
FName(TEXT("MCPDraftZone")),
|
|
FToolUIActionChoice(Action),
|
|
LOCTEXT("MCPDraftZone_Label", "MCP Zone"),
|
|
LOCTEXT("MCPDraftZone_Tooltip", "Toggle MCP Zone mode. While active, left-mouse drag in the graph to create a generation zone. Toggle off to remove MCP Zone comments while keeping generated nodes."),
|
|
FSlateIcon(),
|
|
EUserInterfaceActionType::ToggleButton);
|
|
InSection.AddEntry(Entry);
|
|
}));
|
|
}
|
|
|
|
void FUEBlueprintMCPEditorModule::ToggleMCPZoneMode(TWeakPtr<FBlueprintEditor> BlueprintEditor)
|
|
{
|
|
TSharedPtr<FBlueprintEditor> Editor = BlueprintEditor.Pin();
|
|
if (!Editor.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (bMCPZoneModeActive && ActiveMCPZoneEditor.Pin() == Editor)
|
|
{
|
|
RemoveMCPZones(Editor->GetBlueprintObj());
|
|
bMCPZoneModeActive = false;
|
|
ActiveMCPZoneEditor.Reset();
|
|
if (MCPZoneInputProcessor.IsValid() && FSlateApplication::IsInitialized())
|
|
{
|
|
FSlateApplication::Get().UnregisterInputPreProcessor(MCPZoneInputProcessor);
|
|
MCPZoneInputProcessor.Reset();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (bMCPZoneModeActive)
|
|
{
|
|
if (TSharedPtr<FBlueprintEditor> PreviousEditor = ActiveMCPZoneEditor.Pin())
|
|
{
|
|
RemoveMCPZones(PreviousEditor->GetBlueprintObj());
|
|
}
|
|
}
|
|
|
|
bMCPZoneModeActive = true;
|
|
ActiveMCPZoneEditor = Editor;
|
|
if (!MCPZoneInputProcessor.IsValid() && FSlateApplication::IsInitialized())
|
|
{
|
|
MCPZoneInputProcessor = MakeShared<FMCPZoneInputProcessor>(*this);
|
|
FSlateApplication::Get().RegisterInputPreProcessor(MCPZoneInputProcessor);
|
|
}
|
|
}
|
|
|
|
bool FUEBlueprintMCPEditorModule::IsMCPZoneModeActive(TWeakPtr<FBlueprintEditor> BlueprintEditor) const
|
|
{
|
|
return bMCPZoneModeActive && ActiveMCPZoneEditor.Pin() == BlueprintEditor.Pin();
|
|
}
|
|
|
|
bool FUEBlueprintMCPEditorModule::TryCreateMCPZoneFromScreenDrag(const FVector2f& StartScreenPosition, const FVector2f& EndScreenPosition)
|
|
{
|
|
TSharedPtr<FBlueprintEditor> Editor = ActiveMCPZoneEditor.Pin();
|
|
if (!Editor.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
UBlueprint* Blueprint = Editor->GetBlueprintObj();
|
|
UEdGraph* Graph = Editor->GetFocusedGraph();
|
|
if (!Blueprint || !Graph)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
TSharedPtr<SGraphEditor> GraphEditor = Editor->OpenGraphAndBringToFront(Graph, false);
|
|
SGraphPanel* GraphPanel = GraphEditor.IsValid() ? GraphEditor->GetGraphPanel() : nullptr;
|
|
if (!GraphPanel)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const FGeometry& Geometry = GraphPanel->GetTickSpaceGeometry();
|
|
const FVector2f LocalStart = UE::Slate::CastToVector2f(Geometry.AbsoluteToLocal(StartScreenPosition));
|
|
const FVector2f LocalEnd = UE::Slate::CastToVector2f(Geometry.AbsoluteToLocal(EndScreenPosition));
|
|
const FVector2f GraphStart = UE::Slate::CastToVector2f(GraphPanel->PanelCoordToGraphCoord(LocalStart));
|
|
const FVector2f GraphEnd = UE::Slate::CastToVector2f(GraphPanel->PanelCoordToGraphCoord(LocalEnd));
|
|
|
|
CreateMCPZone(Graph, Blueprint, GraphStart, GraphEnd);
|
|
return true;
|
|
}
|
|
|
|
void FUEBlueprintMCPEditorModule::CreateMCPZone(UEdGraph* Graph, UBlueprint* Blueprint, const FVector2f& GraphStart, const FVector2f& GraphEnd) const
|
|
{
|
|
if (!Blueprint || !Graph)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const FScopedTransaction Transaction(LOCTEXT("CreateMCPDraftZone", "Create MCP Draft Zone"));
|
|
Graph->Modify();
|
|
Blueprint->Modify();
|
|
|
|
constexpr int32 MinWidth = 240;
|
|
constexpr int32 MinHeight = 160;
|
|
const int32 MinX = FMath::FloorToInt(FMath::Min(GraphStart.X, GraphEnd.X));
|
|
const int32 MinY = FMath::FloorToInt(FMath::Min(GraphStart.Y, GraphEnd.Y));
|
|
const int32 MaxX = FMath::CeilToInt(FMath::Max(GraphStart.X, GraphEnd.X));
|
|
const int32 MaxY = FMath::CeilToInt(FMath::Max(GraphStart.Y, GraphEnd.Y));
|
|
|
|
UEdGraphNode_Comment* ZoneNode = NewObject<UEdGraphNode_Comment>(Graph);
|
|
ZoneNode->CreateNewGuid();
|
|
ZoneNode->NodeComment = MCPZoneCommentText;
|
|
ZoneNode->bCommentBubbleVisible = false;
|
|
ZoneNode->CommentColor = FLinearColor(0.05f, 0.32f, 0.85f, 0.35f);
|
|
ZoneNode->NodePosX = MinX;
|
|
ZoneNode->NodePosY = MinY;
|
|
ZoneNode->NodeWidth = FMath::Max(MinWidth, MaxX - MinX);
|
|
ZoneNode->NodeHeight = FMath::Max(MinHeight, MaxY - MinY);
|
|
|
|
Graph->AddNode(ZoneNode, true, false);
|
|
ZoneNode->PostPlacedNewNode();
|
|
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
|
|
}
|
|
|
|
void FUEBlueprintMCPEditorModule::RemoveMCPZones(UBlueprint* Blueprint) const
|
|
{
|
|
if (!Blueprint)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const FScopedTransaction Transaction(LOCTEXT("RemoveMCPDraftZones", "Remove MCP Draft Zones"));
|
|
Blueprint->Modify();
|
|
|
|
TArray<UEdGraph*> Graphs;
|
|
Blueprint->GetAllGraphs(Graphs);
|
|
for (UEdGraph* Graph : Graphs)
|
|
{
|
|
if (!Graph)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Graph->Modify();
|
|
TArray<UEdGraphNode*> Nodes = Graph->Nodes;
|
|
for (UEdGraphNode* Node : Nodes)
|
|
{
|
|
if (IsMCPZoneComment(Node))
|
|
{
|
|
Graph->RemoveNode(Node);
|
|
}
|
|
}
|
|
}
|
|
|
|
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|