ultimate-unreal-engine-mcp 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// MCPActorCommands.cpp
|
|
2
|
-
// Implements
|
|
3
|
-
// actor.list
|
|
4
|
-
// actor.spawn
|
|
5
|
-
// actor.transform
|
|
6
|
-
// actor.delete
|
|
2
|
+
// Implements actor command handlers for the MCP bridge:
|
|
3
|
+
// actor.list -- enumerate all actors in the open level
|
|
4
|
+
// actor.spawn -- spawn an actor by class name at a given location
|
|
5
|
+
// actor.transform -- move/rotate/scale an actor by label
|
|
6
|
+
// actor.delete -- remove an actor from the level
|
|
7
|
+
// actor.setProperty -- set a UPROPERTY on an actor or component
|
|
8
|
+
// actor.componentBounds -- get bounds of individual components on an actor
|
|
7
9
|
//
|
|
8
10
|
// All handlers run on the game thread (guaranteed by FMCPCommandRouter::Dispatch).
|
|
9
11
|
// actor.transform and actor.delete call Actor->Modify() before any state change
|
|
@@ -634,4 +636,102 @@ void RegisterActorCommands(FMCPCommandRouter& Router)
|
|
|
634
636
|
|
|
635
637
|
SendResponse(BuildActorSuccessResponse(CorrId, Data) + TEXT("\n"));
|
|
636
638
|
});
|
|
639
|
+
|
|
640
|
+
// -----------------------------------------------------------------------
|
|
641
|
+
// actor.componentBounds
|
|
642
|
+
// Returns bounds of each scene component on an actor, or a specific one.
|
|
643
|
+
// Payload: { actor_label, component_name? }
|
|
644
|
+
// If component_name is omitted, returns all scene components with bounds.
|
|
645
|
+
// -----------------------------------------------------------------------
|
|
646
|
+
Router.RegisterHandler(TEXT("actor.componentBounds"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
|
|
647
|
+
{
|
|
648
|
+
const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
|
|
649
|
+
|
|
650
|
+
TSharedPtr<FJsonObject> Payload;
|
|
651
|
+
const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
|
|
652
|
+
if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
|
|
653
|
+
{
|
|
654
|
+
Payload = (*PayloadVal)->AsObject();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
FString ActorLabel;
|
|
658
|
+
if (!Payload.IsValid()
|
|
659
|
+
|| !Payload->TryGetStringField(TEXT("actor_label"), ActorLabel) || ActorLabel.IsEmpty())
|
|
660
|
+
{
|
|
661
|
+
SendResponse(BuildActorErrorResponse(CorrId, TEXT("missing_actor_label")) + TEXT("\n"));
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
FString FilterComponent;
|
|
666
|
+
Payload->TryGetStringField(TEXT("component_name"), FilterComponent);
|
|
667
|
+
|
|
668
|
+
UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
|
|
669
|
+
if (!World)
|
|
670
|
+
{
|
|
671
|
+
SendResponse(BuildActorErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
AActor* TargetActor = nullptr;
|
|
676
|
+
for (TActorIterator<AActor> It(World); It; ++It)
|
|
677
|
+
{
|
|
678
|
+
if ((*It)->GetActorLabel() == ActorLabel)
|
|
679
|
+
{
|
|
680
|
+
TargetActor = *It;
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (!TargetActor)
|
|
685
|
+
{
|
|
686
|
+
SendResponse(BuildActorErrorResponse(CorrId, TEXT("actor_not_found")) + TEXT("\n"));
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
TArray<TSharedPtr<FJsonValue>> ComponentsArray;
|
|
691
|
+
|
|
692
|
+
TInlineComponentArray<USceneComponent*> Components;
|
|
693
|
+
TargetActor->GetComponents(Components);
|
|
694
|
+
|
|
695
|
+
for (USceneComponent* Comp : Components)
|
|
696
|
+
{
|
|
697
|
+
if (!Comp) continue;
|
|
698
|
+
if (!FilterComponent.IsEmpty() && Comp->GetName() != FilterComponent) continue;
|
|
699
|
+
|
|
700
|
+
FBoxSphereBounds Bounds = Comp->CalcBounds(Comp->GetComponentTransform());
|
|
701
|
+
|
|
702
|
+
TSharedPtr<FJsonObject> CompObj = MakeShared<FJsonObject>();
|
|
703
|
+
CompObj->SetStringField(TEXT("name"), Comp->GetName());
|
|
704
|
+
CompObj->SetStringField(TEXT("class"), Comp->GetClass()->GetName());
|
|
705
|
+
|
|
706
|
+
// World-space bounds
|
|
707
|
+
TSharedPtr<FJsonObject> OriginObj = MakeShared<FJsonObject>();
|
|
708
|
+
OriginObj->SetNumberField(TEXT("x"), Bounds.Origin.X);
|
|
709
|
+
OriginObj->SetNumberField(TEXT("y"), Bounds.Origin.Y);
|
|
710
|
+
OriginObj->SetNumberField(TEXT("z"), Bounds.Origin.Z);
|
|
711
|
+
CompObj->SetObjectField(TEXT("origin"), OriginObj);
|
|
712
|
+
|
|
713
|
+
TSharedPtr<FJsonObject> ExtentObj = MakeShared<FJsonObject>();
|
|
714
|
+
ExtentObj->SetNumberField(TEXT("x"), Bounds.BoxExtent.X);
|
|
715
|
+
ExtentObj->SetNumberField(TEXT("y"), Bounds.BoxExtent.Y);
|
|
716
|
+
ExtentObj->SetNumberField(TEXT("z"), Bounds.BoxExtent.Z);
|
|
717
|
+
CompObj->SetObjectField(TEXT("extent"), ExtentObj);
|
|
718
|
+
|
|
719
|
+
// Relative transform
|
|
720
|
+
FVector RelLoc = Comp->GetRelativeLocation();
|
|
721
|
+
TSharedPtr<FJsonObject> RelLocObj = MakeShared<FJsonObject>();
|
|
722
|
+
RelLocObj->SetNumberField(TEXT("x"), RelLoc.X);
|
|
723
|
+
RelLocObj->SetNumberField(TEXT("y"), RelLoc.Y);
|
|
724
|
+
RelLocObj->SetNumberField(TEXT("z"), RelLoc.Z);
|
|
725
|
+
CompObj->SetObjectField(TEXT("relative_location"), RelLocObj);
|
|
726
|
+
|
|
727
|
+
ComponentsArray.Add(MakeShared<FJsonValueObject>(CompObj));
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
731
|
+
Data->SetStringField(TEXT("label"), ActorLabel);
|
|
732
|
+
Data->SetArrayField(TEXT("components"), ComponentsArray);
|
|
733
|
+
Data->SetNumberField(TEXT("count"), static_cast<double>(ComponentsArray.Num()));
|
|
734
|
+
|
|
735
|
+
SendResponse(BuildActorSuccessResponse(CorrId, Data) + TEXT("\n"));
|
|
736
|
+
});
|
|
637
737
|
}
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
#include "Misc/DateTime.h"
|
|
33
33
|
#include "HAL/FileManager.h"
|
|
34
34
|
#include "Misc/FileHelper.h"
|
|
35
|
+
#include "ImageUtils.h"
|
|
35
36
|
#include "Engine/World.h"
|
|
36
37
|
#include "GameFramework/Actor.h"
|
|
37
38
|
#include "EngineUtils.h"
|
|
@@ -186,10 +187,11 @@ static FString GetMCPScreenshotDir()
|
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
/**
|
|
189
|
-
*
|
|
190
|
+
* Captures an MCP screenshot synchronously using FViewport::ReadPixels().
|
|
190
191
|
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
192
|
+
* Unlike FScreenshotRequest (async, one-shot per render, unreliable for repeated calls),
|
|
193
|
+
* this forces a viewport redraw and reads the framebuffer directly.
|
|
194
|
+
* Returns the file path on success, or empty string on failure.
|
|
193
195
|
*
|
|
194
196
|
* Threat T-31-05: width clamped 64..7680, height clamped 64..4320 (same as T-12-06).
|
|
195
197
|
*/
|
|
@@ -199,13 +201,41 @@ static FString TakeScreenshotToFile(int32 Width, int32 Height)
|
|
|
199
201
|
Width = FMath::Clamp(Width, 64, 7680);
|
|
200
202
|
Height = FMath::Clamp(Height, 64, 4320);
|
|
201
203
|
|
|
204
|
+
FLevelEditorViewportClient* ViewportClient = GetActiveViewportClient();
|
|
205
|
+
if (!ViewportClient || !ViewportClient->Viewport)
|
|
206
|
+
{
|
|
207
|
+
return FString();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Force the viewport to redraw so we capture the current frame.
|
|
211
|
+
ViewportClient->Viewport->Invalidate();
|
|
212
|
+
ViewportClient->Viewport->Draw();
|
|
213
|
+
|
|
214
|
+
// Read pixels from the viewport framebuffer.
|
|
215
|
+
TArray<FColor> Pixels;
|
|
216
|
+
if (!ViewportClient->Viewport->ReadPixels(Pixels))
|
|
217
|
+
{
|
|
218
|
+
return FString();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const int32 VPWidth = ViewportClient->Viewport->GetSizeXY().X;
|
|
222
|
+
const int32 VPHeight = ViewportClient->Viewport->GetSizeXY().Y;
|
|
223
|
+
|
|
224
|
+
if (Pixels.Num() != VPWidth * VPHeight || VPWidth == 0 || VPHeight == 0)
|
|
225
|
+
{
|
|
226
|
+
return FString();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Compress to PNG.
|
|
230
|
+
TArray64<uint8> PngData;
|
|
231
|
+
FImageUtils::PNGCompressImageArray(VPWidth, VPHeight, Pixels, PngData);
|
|
232
|
+
|
|
233
|
+
// Write to file.
|
|
202
234
|
const FString Timestamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S_%s"));
|
|
203
235
|
const FString FileName = FString::Printf(TEXT("mcp_screenshot_%s.png"), *Timestamp);
|
|
204
236
|
const FString FilePath = FPaths::Combine(GetMCPScreenshotDir(), FileName);
|
|
205
237
|
|
|
206
|
-
|
|
207
|
-
GScreenshotResolutionY = Height;
|
|
208
|
-
FScreenshotRequest::RequestScreenshot(FilePath, false /* bShowUI */, false /* bAddFilenameSuffix */);
|
|
238
|
+
FFileHelper::SaveArrayToFile(PngData, *FilePath);
|
|
209
239
|
|
|
210
240
|
return FilePath;
|
|
211
241
|
}
|
|
@@ -301,19 +331,20 @@ void RegisterViewportCommands(FMCPCommandRouter& Router)
|
|
|
301
331
|
}
|
|
302
332
|
}
|
|
303
333
|
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
GScreenshotResolutionY = Height;
|
|
307
|
-
FScreenshotRequest::RequestScreenshot(false /* bShowUI */);
|
|
334
|
+
// Synchronous capture via ReadPixels — works reliably for repeated calls.
|
|
335
|
+
const FString FilePath = TakeScreenshotToFile(Width, Height);
|
|
308
336
|
|
|
309
|
-
|
|
310
|
-
|
|
337
|
+
if (FilePath.IsEmpty())
|
|
338
|
+
{
|
|
339
|
+
SendResponse(BuildViewportErrorResponse(CorrId, TEXT("screenshot_capture_failed")) + TEXT("\n"));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
311
342
|
|
|
312
343
|
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
313
|
-
Data->SetStringField(TEXT("
|
|
344
|
+
Data->SetStringField(TEXT("file_path"), FilePath);
|
|
314
345
|
Data->SetNumberField(TEXT("width"), static_cast<double>(Width));
|
|
315
346
|
Data->SetNumberField(TEXT("height"), static_cast<double>(Height));
|
|
316
|
-
Data->SetBoolField(TEXT("
|
|
347
|
+
Data->SetBoolField(TEXT("saved"), true);
|
|
317
348
|
|
|
318
349
|
SendResponse(BuildViewportSuccessResponse(CorrId, Data) + TEXT("\n"));
|
|
319
350
|
});
|