flockbay 0.10.15 → 0.10.16

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.
Files changed (31) hide show
  1. package/dist/codex/flockbayMcpStdioBridge.cjs +339 -0
  2. package/dist/codex/flockbayMcpStdioBridge.mjs +339 -0
  3. package/dist/{index--o4BPz5o.cjs → index-Cau-_Qvn.cjs} +2683 -609
  4. package/dist/{index-CUp3juDS.mjs → index-DtmFQzXY.mjs} +2684 -611
  5. package/dist/index.cjs +3 -5
  6. package/dist/index.mjs +3 -5
  7. package/dist/lib.cjs +7 -9
  8. package/dist/lib.d.cts +219 -531
  9. package/dist/lib.d.mts +219 -531
  10. package/dist/lib.mjs +7 -9
  11. package/dist/{runCodex-o6PCbHQ7.mjs → runCodex-Di9eHddq.mjs} +263 -42
  12. package/dist/{runCodex-D3eT-TvB.cjs → runCodex-DzP3VUa-.cjs} +264 -43
  13. package/dist/{runGemini-Bt0oEj_g.mjs → runGemini-BS6sBU_V.mjs} +63 -28
  14. package/dist/{runGemini-CBxZp6I7.cjs → runGemini-CpmehDQ2.cjs} +64 -29
  15. package/dist/{types-DGd6ea2Z.mjs → types-CwzNqYEx.mjs} +465 -1142
  16. package/dist/{types-C-jnUdn_.cjs → types-SUAKq-K0.cjs} +466 -1146
  17. package/package.json +1 -1
  18. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPBlueprintCommands.cpp +195 -6
  19. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPBlueprintNodeCommands.cpp +376 -5
  20. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPCommandSchema.cpp +731 -0
  21. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPCommonUtils.cpp +476 -8
  22. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPEditorCommands.cpp +1518 -94
  23. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/MCPServerRunnable.cpp +7 -4
  24. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/UnrealMCPBridge.cpp +150 -112
  25. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPBlueprintCommands.h +2 -1
  26. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPBlueprintNodeCommands.h +4 -1
  27. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPCommandSchema.h +42 -0
  28. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPEditorCommands.h +21 -0
  29. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/UnrealMCP.Build.cs +4 -1
  30. package/dist/flockbayScreenshotGate-DJX3Is5d.mjs +0 -136
  31. package/dist/flockbayScreenshotGate-DkxU24cR.cjs +0 -138
@@ -1,6 +1,7 @@
1
1
  #include "Commands/UnrealMCPEditorCommands.h"
2
2
  #include "Commands/UnrealMCPCommonUtils.h"
3
3
  #include "Editor.h"
4
+ #include "Editor/EditorEngine.h"
4
5
  #include "EditorViewportClient.h"
5
6
  #include "LevelEditorViewport.h"
6
7
  #include "IAssetViewport.h"
@@ -15,11 +16,17 @@
15
16
  #include "Kismet/GameplayStatics.h"
16
17
  #include "Engine/StaticMeshActor.h"
17
18
  #include "Engine/StaticMesh.h"
19
+ #include "Engine/Texture2D.h"
20
+ #include "Engine/SkeletalMesh.h"
18
21
  #include "Engine/DirectionalLight.h"
19
22
  #include "Engine/PointLight.h"
20
23
  #include "Engine/SpotLight.h"
21
24
  #include "Camera/CameraActor.h"
25
+ #include "Landscape.h"
26
+ #include "LandscapeImportHelper.h"
22
27
  #include "Components/StaticMeshComponent.h"
28
+ #include "Materials/MaterialInterface.h"
29
+ #include "Sound/SoundWave.h"
23
30
  #include "EditorSubsystem.h"
24
31
  #include "Subsystems/EditorActorSubsystem.h"
25
32
  #include "Engine/Blueprint.h"
@@ -32,6 +39,16 @@
32
39
  #include "PlayInEditorDataTypes.h"
33
40
  #include "Settings/LevelEditorPlaySettings.h"
34
41
  #include "Framework/Application/SlateApplication.h"
42
+ #include "AssetRegistry/AssetRegistryModule.h"
43
+ #include "UObject/SoftObjectPath.h"
44
+ #include "UObject/Package.h"
45
+ #include "FileHelpers.h"
46
+ #include "MessageLogModule.h"
47
+ #include "IMessageLogListing.h"
48
+ #include "Engine/World.h"
49
+ #include "Engine/EngineTypes.h"
50
+ #include "GameFramework/PlayerController.h"
51
+ #include "Camera/PlayerCameraManager.h"
35
52
 
36
53
  FUnrealMCPEditorCommands::FUnrealMCPEditorCommands()
37
54
  {
@@ -56,6 +73,10 @@ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleCommand(const FString& C
56
73
  }
57
74
  return HandleSpawnActor(Params);
58
75
  }
76
+ else if (CommandType == TEXT("create_landscape"))
77
+ {
78
+ return HandleCreateLandscape(Params);
79
+ }
59
80
  else if (CommandType == TEXT("delete_actor"))
60
81
  {
61
82
  return HandleDeleteActor(Params);
@@ -86,6 +107,10 @@ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleCommand(const FString& C
86
107
  {
87
108
  return HandleTakeScreenshot(Params);
88
109
  }
110
+ else if (CommandType == TEXT("save_all"))
111
+ {
112
+ return HandleSaveAll(Params);
113
+ }
89
114
  else if (CommandType == TEXT("get_play_in_editor_status"))
90
115
  {
91
116
  return HandleGetPlayInEditorStatus(Params);
@@ -102,6 +127,50 @@ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleCommand(const FString& C
102
127
  {
103
128
  return HandleStopPlayInEditor(Params);
104
129
  }
130
+ else if (CommandType == TEXT("search_assets"))
131
+ {
132
+ return HandleSearchAssets(Params);
133
+ }
134
+ else if (CommandType == TEXT("get_asset_info"))
135
+ {
136
+ return HandleGetAssetInfo(Params);
137
+ }
138
+ else if (CommandType == TEXT("list_asset_packs"))
139
+ {
140
+ return HandleListAssetPacks(Params);
141
+ }
142
+ else if (CommandType == TEXT("place_asset"))
143
+ {
144
+ return HandlePlaceAsset(Params);
145
+ }
146
+ else if (CommandType == TEXT("map_check"))
147
+ {
148
+ return HandleMapCheck(Params);
149
+ }
150
+ else if (CommandType == TEXT("get_editor_context"))
151
+ {
152
+ return HandleGetEditorContext(Params);
153
+ }
154
+ else if (CommandType == TEXT("get_player_context"))
155
+ {
156
+ return HandleGetPlayerContext(Params);
157
+ }
158
+ else if (CommandType == TEXT("raycast_from_camera"))
159
+ {
160
+ return HandleRaycastFromCamera(Params);
161
+ }
162
+ else if (CommandType == TEXT("raycast_down"))
163
+ {
164
+ return HandleRaycastDown(Params);
165
+ }
166
+ else if (CommandType == TEXT("get_actor_transform"))
167
+ {
168
+ return HandleGetActorTransform(Params);
169
+ }
170
+ else if (CommandType == TEXT("get_actor_bounds"))
171
+ {
172
+ return HandleGetActorBounds(Params);
173
+ }
105
174
 
106
175
  return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Unknown editor command: %s"), *CommandType));
107
176
  }
@@ -134,142 +203,1224 @@ static bool IsSafeToModifyEditorWorld(FString& OutError)
134
203
  return false;
135
204
  }
136
205
 
137
- return true;
138
- }
206
+ return true;
207
+ }
208
+
209
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSaveAll(const TSharedPtr<FJsonObject>& Params)
210
+ {
211
+ FString Err;
212
+ if (!IsSafeToModifyEditorWorld(Err))
213
+ {
214
+ return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
215
+ }
216
+
217
+ TArray<UPackage*> DirtyBefore;
218
+ FEditorFileUtils::GetDirtyPackages(DirtyBefore, /*bIncludeMapPackages=*/true, /*bIncludeContentPackages=*/true);
219
+
220
+ TSet<FString> DirtyBeforeNames;
221
+ for (UPackage* Pkg : DirtyBefore)
222
+ {
223
+ if (!Pkg) continue;
224
+ DirtyBeforeNames.Add(Pkg->GetName());
225
+ }
226
+
227
+ const int32 DirtyBeforeCount = DirtyBeforeNames.Num();
228
+
229
+ // Save without prompting; fail-hard if anything remains dirty.
230
+ const bool bSaveOk = FEditorFileUtils::SaveDirtyPackages(
231
+ /*bPromptUserToSave=*/false,
232
+ /*bSaveMapPackages=*/true,
233
+ /*bSaveContentPackages=*/true,
234
+ /*bFastSave=*/false
235
+ );
236
+
237
+ TArray<UPackage*> DirtyAfter;
238
+ FEditorFileUtils::GetDirtyPackages(DirtyAfter, /*bIncludeMapPackages=*/true, /*bIncludeContentPackages=*/true);
239
+
240
+ TSet<FString> DirtyAfterNames;
241
+ for (UPackage* Pkg : DirtyAfter)
242
+ {
243
+ if (!Pkg) continue;
244
+ DirtyAfterNames.Add(Pkg->GetName());
245
+ }
246
+
247
+ TArray<TSharedPtr<FJsonValue>> Attempted;
248
+ for (const FString& Name : DirtyBeforeNames)
249
+ {
250
+ Attempted.Add(MakeShareable(new FJsonValueString(Name)));
251
+ }
252
+
253
+ TArray<TSharedPtr<FJsonValue>> StillDirty;
254
+ for (const FString& Name : DirtyAfterNames)
255
+ {
256
+ StillDirty.Add(MakeShareable(new FJsonValueString(Name)));
257
+ }
258
+
259
+ TArray<TSharedPtr<FJsonValue>> Saved;
260
+ for (const FString& Name : DirtyBeforeNames)
261
+ {
262
+ if (!DirtyAfterNames.Contains(Name))
263
+ {
264
+ Saved.Add(MakeShareable(new FJsonValueString(Name)));
265
+ }
266
+ }
267
+
268
+ if (DirtyBeforeCount == 0)
269
+ {
270
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
271
+ ResultObj->SetBoolField(TEXT("success"), true);
272
+ ResultObj->SetNumberField(TEXT("dirtyBefore"), DirtyBeforeCount);
273
+ ResultObj->SetNumberField(TEXT("dirtyAfter"), DirtyAfterNames.Num());
274
+ ResultObj->SetArrayField(TEXT("attemptedPackages"), Attempted);
275
+ ResultObj->SetArrayField(TEXT("savedPackages"), Saved);
276
+ ResultObj->SetArrayField(TEXT("stillDirtyPackages"), StillDirty);
277
+ ResultObj->SetStringField(TEXT("message"), TEXT("No unsaved changes were detected."));
278
+ return ResultObj;
279
+ }
280
+
281
+ if (DirtyAfterNames.Num() > 0)
282
+ {
283
+ TSharedPtr<FJsonObject> Details = MakeShared<FJsonObject>();
284
+ Details->SetBoolField(TEXT("saveOk"), bSaveOk);
285
+ Details->SetNumberField(TEXT("dirtyBefore"), DirtyBeforeCount);
286
+ Details->SetNumberField(TEXT("dirtyAfter"), DirtyAfterNames.Num());
287
+ Details->SetArrayField(TEXT("attemptedPackages"), Attempted);
288
+ Details->SetArrayField(TEXT("savedPackages"), Saved);
289
+ Details->SetArrayField(TEXT("stillDirtyPackages"), StillDirty);
290
+
291
+ TSharedPtr<FJsonObject> ErrObj = FUnrealMCPCommonUtils::CreateErrorResponse(
292
+ TEXT("Some packages are still dirty after save_all. They may require manual Save As, source control checkout, or have save errors. Resolve in-editor and retry save_all."),
293
+ );
294
+ ErrObj->SetObjectField(TEXT("details"), Details);
295
+ return ErrObj;
296
+ }
297
+
298
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
299
+ ResultObj->SetBoolField(TEXT("success"), true);
300
+ ResultObj->SetNumberField(TEXT("dirtyBefore"), DirtyBeforeCount);
301
+ ResultObj->SetNumberField(TEXT("dirtyAfter"), DirtyAfterNames.Num());
302
+ ResultObj->SetArrayField(TEXT("attemptedPackages"), Attempted);
303
+ ResultObj->SetArrayField(TEXT("savedPackages"), Saved);
304
+ ResultObj->SetArrayField(TEXT("stillDirtyPackages"), StillDirty);
305
+ ResultObj->SetStringField(TEXT("message"), TEXT("Saved all dirty packages."));
306
+ return ResultObj;
307
+ }
308
+
309
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetPlayInEditorStatus(const TSharedPtr<FJsonObject>& Params)
310
+ {
311
+ return CreatePlayStatusResponse();
312
+ }
313
+
314
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandlePlayInEditor(const TSharedPtr<FJsonObject>& Params)
315
+ {
316
+ if (!GEditor)
317
+ {
318
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
319
+ }
320
+
321
+ if (GEditor->IsPlaySessionInProgress())
322
+ {
323
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Play session already running or queued."));
324
+ }
325
+
326
+ // Use the active editor viewport, matching how the LevelEditor subsystem triggers PIE.
327
+ FLevelEditorModule* LevelEditorModule = FModuleManager::LoadModulePtr<FLevelEditorModule>(TEXT("LevelEditor"));
328
+ if (!LevelEditorModule)
329
+ {
330
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("LevelEditor module not available."));
331
+ }
332
+
333
+ TSharedPtr<IAssetViewport> ActiveViewport = LevelEditorModule->GetFirstActiveViewport();
334
+ if (!ActiveViewport.IsValid())
335
+ {
336
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("No active level viewport. Click the viewport and retry."));
337
+ }
338
+
339
+ FRequestPlaySessionParams SessionParams;
340
+ SessionParams.WorldType = EPlaySessionWorldType::PlayInEditor;
341
+ SessionParams.DestinationSlateViewport = ActiveViewport;
342
+
343
+ GEditor->RequestPlaySession(SessionParams);
344
+ const bool bQueued = GEditor->IsPlaySessionRequestQueued();
345
+
346
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
347
+ ResultObj->SetBoolField(TEXT("requested"), true);
348
+ ResultObj->SetBoolField(TEXT("queued"), bQueued);
349
+ ResultObj->SetBoolField(TEXT("started"), bQueued);
350
+ ResultObj->SetStringField(TEXT("mode"), TEXT("pie"));
351
+ return ResultObj;
352
+ }
353
+
354
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandlePlayInEditorWindowed(const TSharedPtr<FJsonObject>& Params)
355
+ {
356
+ if (!GEditor)
357
+ {
358
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
359
+ }
360
+
361
+ if (GEditor->IsPlaySessionInProgress())
362
+ {
363
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Play session already running or queued."));
364
+ }
365
+
366
+ // Match the editor's built-in "New Editor Window (PIE)" behavior by setting the play mode type and
367
+ // not specifying a DestinationSlateViewport.
368
+ ULevelEditorPlaySettings* PlaySettings = GetMutableDefault<ULevelEditorPlaySettings>();
369
+ if (PlaySettings)
370
+ {
371
+ PlaySettings->LastExecutedPlayModeType = EPlayModeType::PlayMode_InEditorFloating;
372
+
373
+ if (FProperty* Prop = ULevelEditorPlaySettings::StaticClass()->FindPropertyByName(
374
+ GET_MEMBER_NAME_CHECKED(ULevelEditorPlaySettings, LastExecutedPlayModeType)))
375
+ {
376
+ FPropertyChangedEvent PropChangeEvent(Prop);
377
+ PlaySettings->PostEditChangeProperty(PropChangeEvent);
378
+ }
379
+
380
+ PlaySettings->SaveConfig();
381
+ }
382
+
383
+ const bool bAtPlayerStart =
384
+ PlaySettings && PlaySettings->LastExecutedPlayModeLocation == EPlayModeLocations::PlayLocation_DefaultPlayerStart;
385
+
386
+ FRequestPlaySessionParams SessionParams;
387
+ SessionParams.WorldType = EPlaySessionWorldType::PlayInEditor;
388
+
389
+ // If the user is playing from current camera location, use the active viewport camera as the start transform.
390
+ if (!bAtPlayerStart)
391
+ {
392
+ FLevelEditorModule* LevelEditorModule = FModuleManager::LoadModulePtr<FLevelEditorModule>(TEXT("LevelEditor"));
393
+ if (LevelEditorModule)
394
+ {
395
+ TSharedPtr<IAssetViewport> ActiveViewport = LevelEditorModule->GetFirstActiveViewport();
396
+ if (ActiveViewport.IsValid() && FSlateApplication::IsInitialized() &&
397
+ FSlateApplication::Get().FindWidgetWindow(ActiveViewport->AsWidget()).IsValid())
398
+ {
399
+ SessionParams.StartLocation = ActiveViewport->GetAssetViewportClient().GetViewLocation();
400
+ SessionParams.StartRotation = ActiveViewport->GetAssetViewportClient().GetViewRotation();
401
+ }
402
+ }
403
+ }
404
+
405
+ GEditor->RequestPlaySession(SessionParams);
406
+ const bool bQueued = GEditor->IsPlaySessionRequestQueued();
407
+
408
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
409
+ ResultObj->SetBoolField(TEXT("requested"), true);
410
+ ResultObj->SetBoolField(TEXT("queued"), bQueued);
411
+ ResultObj->SetBoolField(TEXT("started"), bQueued);
412
+ ResultObj->SetStringField(TEXT("mode"), TEXT("pie_new_window"));
413
+ return ResultObj;
414
+ }
415
+
416
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleStopPlayInEditor(const TSharedPtr<FJsonObject>& Params)
417
+ {
418
+ if (!GEditor)
419
+ {
420
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
421
+ }
422
+
423
+ if (GEditor->IsPlaySessionRequestQueued() && !GEditor->IsPlayingSessionInEditor())
424
+ {
425
+ GEditor->CancelRequestPlaySession();
426
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
427
+ ResultObj->SetBoolField(TEXT("stopped"), true);
428
+ ResultObj->SetBoolField(TEXT("canceled"), true);
429
+ return ResultObj;
430
+ }
431
+
432
+ if (!GEditor->IsPlayingSessionInEditor())
433
+ {
434
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("No play session is running."));
435
+ }
436
+
437
+ GEditor->RequestEndPlayMap();
438
+
439
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
440
+ ResultObj->SetBoolField(TEXT("stopped"), true);
441
+ return ResultObj;
442
+ }
443
+
444
+ static FString NormalizeContentRoot(const FString& Root)
445
+ {
446
+ FString Out = Root.TrimStartAndEnd();
447
+ if (Out.IsEmpty())
448
+ {
449
+ return TEXT("/Game/");
450
+ }
451
+ if (!Out.StartsWith(TEXT("/")))
452
+ {
453
+ Out = TEXT("/") + Out;
454
+ }
455
+ if (!Out.EndsWith(TEXT("/")))
456
+ {
457
+ Out += TEXT("/");
458
+ }
459
+ return Out;
460
+ }
461
+
462
+ static bool TryResolveAssetClassFilter(const TSharedPtr<FJsonObject>& Params, FARFilter& Filter, FString& OutError)
463
+ {
464
+ if (!Params.IsValid() || !Params->HasField(TEXT("class")))
465
+ {
466
+ return true;
467
+ }
468
+
469
+ TArray<FString> ClassNames;
470
+
471
+ FString Single;
472
+ if (Params->TryGetStringField(TEXT("class"), Single))
473
+ {
474
+ Single = Single.TrimStartAndEnd();
475
+ if (!Single.IsEmpty())
476
+ {
477
+ ClassNames.Add(Single);
478
+ }
479
+ }
480
+ else
481
+ {
482
+ const TArray<TSharedPtr<FJsonValue>>* Arr = nullptr;
483
+ if (Params->TryGetArrayField(TEXT("class"), Arr) && Arr)
484
+ {
485
+ for (const TSharedPtr<FJsonValue>& V : *Arr)
486
+ {
487
+ if (!V.IsValid() || V->Type != EJson::String) continue;
488
+ FString S = V->AsString().TrimStartAndEnd();
489
+ if (!S.IsEmpty()) ClassNames.Add(S);
490
+ }
491
+ }
492
+ }
493
+
494
+ for (const FString& ClassNameRaw : ClassNames)
495
+ {
496
+ const FString ClassName = ClassNameRaw.TrimStartAndEnd();
497
+ if (ClassName.IsEmpty()) continue;
498
+
499
+ UClass* Class = nullptr;
500
+
501
+ if (ClassName.Equals(TEXT("StaticMesh"), ESearchCase::IgnoreCase) || ClassName.Equals(TEXT("UStaticMesh"), ESearchCase::IgnoreCase))
502
+ {
503
+ Class = UStaticMesh::StaticClass();
504
+ }
505
+ else if (ClassName.Equals(TEXT("Material"), ESearchCase::IgnoreCase) || ClassName.Equals(TEXT("MaterialInterface"), ESearchCase::IgnoreCase))
506
+ {
507
+ Class = UMaterialInterface::StaticClass();
508
+ }
509
+ else if (ClassName.Equals(TEXT("Texture2D"), ESearchCase::IgnoreCase))
510
+ {
511
+ Class = UTexture2D::StaticClass();
512
+ }
513
+ else if (ClassName.Equals(TEXT("Blueprint"), ESearchCase::IgnoreCase) || ClassName.Equals(TEXT("UBlueprint"), ESearchCase::IgnoreCase))
514
+ {
515
+ Class = UBlueprint::StaticClass();
516
+ }
517
+ else if (ClassName.Equals(TEXT("SkeletalMesh"), ESearchCase::IgnoreCase))
518
+ {
519
+ Class = USkeletalMesh::StaticClass();
520
+ }
521
+ else if (ClassName.Equals(TEXT("SoundWave"), ESearchCase::IgnoreCase))
522
+ {
523
+ Class = USoundWave::StaticClass();
524
+ }
525
+ else
526
+ {
527
+ Class = FindObject<UClass>(ANY_PACKAGE, *ClassName);
528
+ if (!Class && !ClassName.StartsWith(TEXT("U")))
529
+ {
530
+ Class = FindObject<UClass>(ANY_PACKAGE, *(TEXT("U") + ClassName));
531
+ }
532
+ }
533
+
534
+ if (!Class)
535
+ {
536
+ OutError = FString::Printf(TEXT("Unknown asset class filter: %s"), *ClassNameRaw);
537
+ return false;
538
+ }
539
+
540
+ Filter.ClassPaths.Add(Class->GetClassPathName());
541
+ }
542
+
543
+ return true;
544
+ }
545
+
546
+ static int32 ScoreAssetMatch(const FString& Query, const FAssetData& Asset)
547
+ {
548
+ if (Query.IsEmpty()) return 0;
549
+
550
+ const FString Name = Asset.AssetName.ToString();
551
+ const FString Path = Asset.GetObjectPathString();
552
+
553
+ if (Name.Equals(Query, ESearchCase::IgnoreCase)) return 0;
554
+ if (Name.StartsWith(Query, ESearchCase::IgnoreCase)) return 1;
555
+ if (Path.EndsWith(Query, ESearchCase::IgnoreCase)) return 2;
556
+ if (Name.Contains(Query, ESearchCase::IgnoreCase)) return 3;
557
+ if (Path.Contains(Query, ESearchCase::IgnoreCase)) return 4;
558
+ return 100;
559
+ }
560
+
561
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSearchAssets(const TSharedPtr<FJsonObject>& Params)
562
+ {
563
+ FString Root = TEXT("/Game/");
564
+ Params->TryGetStringField(TEXT("root"), Root);
565
+ Root = NormalizeContentRoot(Root);
566
+
567
+ FString Query;
568
+ Params->TryGetStringField(TEXT("query"), Query);
569
+ Query = Query.TrimStartAndEnd();
570
+
571
+ int32 Limit = 25;
572
+ if (Params->HasField(TEXT("limit")))
573
+ {
574
+ Limit = (int32)Params->GetNumberField(TEXT("limit"));
575
+ }
576
+ Limit = FMath::Clamp(Limit, 1, 200);
577
+
578
+ FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
579
+ IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
580
+
581
+ FARFilter Filter;
582
+ Filter.PackagePaths.Add(*Root);
583
+ Filter.bRecursivePaths = true;
584
+
585
+ FString ClassErr;
586
+ if (!TryResolveAssetClassFilter(Params, Filter, ClassErr))
587
+ {
588
+ return FUnrealMCPCommonUtils::CreateErrorResponse(ClassErr);
589
+ }
590
+
591
+ TArray<FAssetData> Assets;
592
+ AssetRegistry.GetAssets(Filter, Assets);
593
+
594
+ if (!Query.IsEmpty())
595
+ {
596
+ Assets.RemoveAll([&Query](const FAssetData& Asset) {
597
+ const FString Name = Asset.AssetName.ToString();
598
+ const FString Path = Asset.GetObjectPathString();
599
+ return !(Name.Contains(Query, ESearchCase::IgnoreCase) || Path.Contains(Query, ESearchCase::IgnoreCase));
600
+ });
601
+
602
+ Assets.Sort([&Query](const FAssetData& A, const FAssetData& B) {
603
+ const int32 SA = ScoreAssetMatch(Query, A);
604
+ const int32 SB = ScoreAssetMatch(Query, B);
605
+ if (SA != SB) return SA < SB;
606
+ return A.AssetName.LexicalLess(B.AssetName);
607
+ });
608
+ }
609
+ else
610
+ {
611
+ Assets.Sort([](const FAssetData& A, const FAssetData& B) {
612
+ return A.AssetName.LexicalLess(B.AssetName);
613
+ });
614
+ }
615
+
616
+ const int32 Total = Assets.Num();
617
+ if (Assets.Num() > Limit)
618
+ {
619
+ Assets.SetNum(Limit);
620
+ }
621
+
622
+ TArray<TSharedPtr<FJsonValue>> OutAssets;
623
+ OutAssets.Reserve(Assets.Num());
624
+ for (const FAssetData& Asset : Assets)
625
+ {
626
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
627
+ Obj->SetStringField(TEXT("name"), Asset.AssetName.ToString());
628
+ Obj->SetStringField(TEXT("objectPath"), Asset.GetObjectPathString());
629
+ Obj->SetStringField(TEXT("packagePath"), Asset.PackagePath.ToString());
630
+ Obj->SetStringField(TEXT("class"), Asset.AssetClassPath.ToString());
631
+ OutAssets.Add(MakeShared<FJsonValueObject>(Obj));
632
+ }
633
+
634
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
635
+ Result->SetStringField(TEXT("root"), Root);
636
+ Result->SetStringField(TEXT("query"), Query);
637
+ Result->SetNumberField(TEXT("count"), OutAssets.Num());
638
+ Result->SetNumberField(TEXT("totalMatches"), Total);
639
+ Result->SetArrayField(TEXT("assets"), OutAssets);
640
+ return Result;
641
+ }
642
+
643
+ static FString InferPlacementKindFromAssetClassPath(const FString& ClassPath)
644
+ {
645
+ if (ClassPath.Contains(TEXT("StaticMesh"), ESearchCase::IgnoreCase)) return TEXT("static_mesh");
646
+ if (ClassPath.Contains(TEXT("Blueprint"), ESearchCase::IgnoreCase)) return TEXT("blueprint");
647
+ if (ClassPath.Contains(TEXT("Material"), ESearchCase::IgnoreCase)) return TEXT("material");
648
+ if (ClassPath.Contains(TEXT("Texture"), ESearchCase::IgnoreCase)) return TEXT("texture");
649
+ return TEXT("unknown");
650
+ }
651
+
652
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetAssetInfo(const TSharedPtr<FJsonObject>& Params)
653
+ {
654
+ FString ObjectPath;
655
+ if (!Params->TryGetStringField(TEXT("objectPath"), ObjectPath) && !Params->TryGetStringField(TEXT("object_path"), ObjectPath))
656
+ {
657
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'objectPath' parameter"));
658
+ }
659
+ ObjectPath = ObjectPath.TrimStartAndEnd();
660
+ if (ObjectPath.IsEmpty())
661
+ {
662
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("objectPath is empty"));
663
+ }
664
+
665
+ FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
666
+ IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
667
+
668
+ const FAssetData Asset = AssetRegistry.GetAssetByObjectPath(FSoftObjectPath(ObjectPath));
669
+ if (!Asset.IsValid())
670
+ {
671
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Asset not found: %s"), *ObjectPath));
672
+ }
673
+
674
+ const FString ClassPath = Asset.AssetClassPath.ToString();
675
+
676
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
677
+ Result->SetStringField(TEXT("name"), Asset.AssetName.ToString());
678
+ Result->SetStringField(TEXT("objectPath"), Asset.GetObjectPathString());
679
+ Result->SetStringField(TEXT("packageName"), Asset.PackageName.ToString());
680
+ Result->SetStringField(TEXT("packagePath"), Asset.PackagePath.ToString());
681
+ Result->SetStringField(TEXT("class"), ClassPath);
682
+ Result->SetStringField(TEXT("placementKind"), InferPlacementKindFromAssetClassPath(ClassPath));
683
+
684
+ if (Params->HasField(TEXT("includeDependencies")) && Params->GetBoolField(TEXT("includeDependencies")))
685
+ {
686
+ TArray<FName> Deps;
687
+ AssetRegistry.GetDependencies(Asset.PackageName, Deps);
688
+ TArray<TSharedPtr<FJsonValue>> DepArr;
689
+ DepArr.Reserve(Deps.Num());
690
+ for (const FName& Dep : Deps)
691
+ {
692
+ DepArr.Add(MakeShared<FJsonValueString>(Dep.ToString()));
693
+ }
694
+ Result->SetArrayField(TEXT("dependencies"), DepArr);
695
+ }
696
+
697
+ return Result;
698
+ }
699
+
700
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleListAssetPacks(const TSharedPtr<FJsonObject>& Params)
701
+ {
702
+ int32 Limit = 200;
703
+ if (Params->HasField(TEXT("limit")))
704
+ {
705
+ Limit = (int32)Params->GetNumberField(TEXT("limit"));
706
+ }
707
+ Limit = FMath::Clamp(Limit, 1, 500);
708
+
709
+ FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
710
+ IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
711
+
712
+ TArray<FString> SubPaths;
713
+ AssetRegistry.GetSubPaths(TEXT("/Game"), SubPaths, false);
714
+ SubPaths.Sort();
715
+
716
+ TArray<TSharedPtr<FJsonValue>> Packs;
717
+ for (const FString& Path : SubPaths)
718
+ {
719
+ if (Packs.Num() >= Limit) break;
720
+ if (Path.StartsWith(TEXT("/Game/__External"), ESearchCase::IgnoreCase)) continue;
721
+ if (Path.StartsWith(TEXT("/Game/Developers"), ESearchCase::IgnoreCase)) continue;
722
+
723
+ FString Name = Path;
724
+ int32 SlashIndex;
725
+ if (Name.FindLastChar(TEXT('/'), SlashIndex))
726
+ {
727
+ Name = Name.Mid(SlashIndex + 1);
728
+ }
729
+
730
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
731
+ Obj->SetStringField(TEXT("name"), Name);
732
+ Obj->SetStringField(TEXT("path"), Path + TEXT("/"));
733
+ Packs.Add(MakeShared<FJsonValueObject>(Obj));
734
+ }
735
+
736
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
737
+ Result->SetNumberField(TEXT("count"), Packs.Num());
738
+ Result->SetArrayField(TEXT("packs"), Packs);
739
+ return Result;
740
+ }
741
+
742
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandlePlaceAsset(const TSharedPtr<FJsonObject>& Params)
743
+ {
744
+ {
745
+ FString Err;
746
+ if (!IsSafeToModifyEditorWorld(Err))
747
+ {
748
+ return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
749
+ }
750
+ }
751
+
752
+ FString ObjectPath;
753
+ if (!Params->TryGetStringField(TEXT("objectPath"), ObjectPath) && !Params->TryGetStringField(TEXT("object_path"), ObjectPath))
754
+ {
755
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'objectPath' parameter"));
756
+ }
757
+ ObjectPath = ObjectPath.TrimStartAndEnd();
758
+ if (ObjectPath.IsEmpty())
759
+ {
760
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("objectPath is empty"));
761
+ }
762
+
763
+ FVector Location(0.0f, 0.0f, 0.0f);
764
+ FRotator Rotation(0.0f, 0.0f, 0.0f);
765
+ FVector Scale(1.0f, 1.0f, 1.0f);
766
+
767
+ if (Params->HasField(TEXT("location")))
768
+ {
769
+ Location = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("location"));
770
+ }
771
+ if (Params->HasField(TEXT("rotation")))
772
+ {
773
+ Rotation = FUnrealMCPCommonUtils::GetRotatorFromJson(Params, TEXT("rotation"));
774
+ }
775
+ if (Params->HasField(TEXT("scale")))
776
+ {
777
+ Scale = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("scale"));
778
+ }
779
+
780
+ UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
781
+ if (!World)
782
+ {
783
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world"));
784
+ }
785
+
786
+ FString AssetClassPath;
787
+ FString AssetName;
788
+ {
789
+ FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
790
+ IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
791
+ const FAssetData Asset = AssetRegistry.GetAssetByObjectPath(FSoftObjectPath(ObjectPath));
792
+ if (Asset.IsValid())
793
+ {
794
+ AssetClassPath = Asset.AssetClassPath.ToString();
795
+ AssetName = Asset.AssetName.ToString();
796
+ }
797
+ }
798
+
799
+ FString RequestedName;
800
+ Params->TryGetStringField(TEXT("name"), RequestedName);
801
+ RequestedName = RequestedName.TrimStartAndEnd();
802
+ if (RequestedName.IsEmpty())
803
+ {
804
+ RequestedName = !AssetName.IsEmpty() ? AssetName : TEXT("PlacedAsset");
805
+ }
806
+
807
+ AActor* NewActor = nullptr;
808
+ FString PlacementKind = !AssetClassPath.IsEmpty() ? InferPlacementKindFromAssetClassPath(AssetClassPath) : TEXT("unknown");
809
+
810
+ UObject* AssetObj = LoadObject<UObject>(nullptr, *ObjectPath);
811
+ if (!AssetObj && !ObjectPath.Contains(TEXT("'")))
812
+ {
813
+ AssetObj = LoadObject<UObject>(nullptr, *FString::Printf(TEXT("StaticMesh'%s'"), *ObjectPath));
814
+ if (!AssetObj)
815
+ {
816
+ AssetObj = LoadObject<UObject>(nullptr, *FString::Printf(TEXT("Blueprint'%s'"), *ObjectPath));
817
+ }
818
+ }
819
+
820
+ if (!AssetObj)
821
+ {
822
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Failed to load asset: %s"), *ObjectPath));
823
+ }
824
+
825
+ FActorSpawnParameters SpawnParams;
826
+ SpawnParams.Name = MakeUniqueObjectName(World, AActor::StaticClass(), FName(*RequestedName));
827
+
828
+ if (UStaticMesh* StaticMesh = Cast<UStaticMesh>(AssetObj))
829
+ {
830
+ PlacementKind = TEXT("static_mesh");
831
+ AStaticMeshActor* StaticMeshActor = World->SpawnActor<AStaticMeshActor>(AStaticMeshActor::StaticClass(), Location, Rotation, SpawnParams);
832
+ if (!StaticMeshActor || !StaticMeshActor->GetStaticMeshComponent())
833
+ {
834
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to spawn StaticMeshActor"));
835
+ }
836
+
837
+ StaticMeshActor->GetStaticMeshComponent()->SetStaticMesh(StaticMesh);
838
+ StaticMeshActor->GetStaticMeshComponent()->MarkRenderStateDirty();
839
+ NewActor = StaticMeshActor;
840
+ }
841
+ else if (UBlueprint* Blueprint = Cast<UBlueprint>(AssetObj))
842
+ {
843
+ PlacementKind = TEXT("blueprint");
844
+ if (!Blueprint->GeneratedClass)
845
+ {
846
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Blueprint has no GeneratedClass (compile it and retry)."));
847
+ }
848
+ const FTransform SpawnTransform(FQuat(Rotation), Location, Scale);
849
+ NewActor = World->SpawnActor<AActor>(Blueprint->GeneratedClass, SpawnTransform, SpawnParams);
850
+ }
851
+ else if (UBlueprintGeneratedClass* GeneratedClass = Cast<UBlueprintGeneratedClass>(AssetObj))
852
+ {
853
+ PlacementKind = TEXT("blueprint");
854
+ const FTransform SpawnTransform(FQuat(Rotation), Location, Scale);
855
+ NewActor = World->SpawnActor<AActor>(GeneratedClass, SpawnTransform, SpawnParams);
856
+ }
857
+ else if (UClass* Class = Cast<UClass>(AssetObj))
858
+ {
859
+ if (!Class->IsChildOf(AActor::StaticClass()))
860
+ {
861
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Loaded class is not an Actor class: %s"), *Class->GetName()));
862
+ }
863
+ PlacementKind = TEXT("blueprint");
864
+ const FTransform SpawnTransform(FQuat(Rotation), Location, Scale);
865
+ NewActor = World->SpawnActor<AActor>(Class, SpawnTransform, SpawnParams);
866
+ }
867
+ else
868
+ {
869
+ const FString ClassName = AssetObj->GetClass() ? AssetObj->GetClass()->GetName() : TEXT("UnknownClass");
870
+ return FUnrealMCPCommonUtils::CreateErrorResponse(
871
+ FString::Printf(TEXT("Asset is not placeable (class: %s). Placeable: StaticMesh, Blueprint. Asset: %s"), *ClassName, *ObjectPath));
872
+ }
873
+
874
+ if (!NewActor)
875
+ {
876
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to spawn actor for asset."));
877
+ }
878
+
879
+ NewActor->SetActorScale3D(Scale);
880
+
881
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
882
+ Result->SetStringField(TEXT("placementKind"), PlacementKind);
883
+ Result->SetStringField(TEXT("requestedObjectPath"), ObjectPath);
884
+ Result->SetStringField(TEXT("spawnedName"), NewActor->GetName());
885
+ Result->SetObjectField(TEXT("actor"), FUnrealMCPCommonUtils::ActorToJsonObject(NewActor, true));
886
+ return Result;
887
+ }
888
+
889
+ static FString MessageSeverityToString(EMessageSeverity::Type Severity)
890
+ {
891
+ switch (Severity)
892
+ {
893
+ case EMessageSeverity::CriticalError:
894
+ case EMessageSeverity::Error:
895
+ return TEXT("error");
896
+ case EMessageSeverity::Warning:
897
+ case EMessageSeverity::PerformanceWarning:
898
+ return TEXT("warning");
899
+ case EMessageSeverity::Info:
900
+ return TEXT("info");
901
+ default:
902
+ return TEXT("info");
903
+ }
904
+ }
905
+
906
+ static TArray<TSharedPtr<FJsonValue>> VectorToJsonArray(const FVector& V)
907
+ {
908
+ TArray<TSharedPtr<FJsonValue>> Arr;
909
+ Arr.Add(MakeShared<FJsonValueNumber>(V.X));
910
+ Arr.Add(MakeShared<FJsonValueNumber>(V.Y));
911
+ Arr.Add(MakeShared<FJsonValueNumber>(V.Z));
912
+ return Arr;
913
+ }
914
+
915
+ static TArray<TSharedPtr<FJsonValue>> RotatorToJsonArray(const FRotator& R)
916
+ {
917
+ TArray<TSharedPtr<FJsonValue>> Arr;
918
+ Arr.Add(MakeShared<FJsonValueNumber>(R.Pitch));
919
+ Arr.Add(MakeShared<FJsonValueNumber>(R.Yaw));
920
+ Arr.Add(MakeShared<FJsonValueNumber>(R.Roll));
921
+ return Arr;
922
+ }
923
+
924
+ static AActor* FindActorByNameOrLabel(UWorld* World, const FString& NameOrLabel)
925
+ {
926
+ if (!World) return nullptr;
927
+ if (NameOrLabel.IsEmpty()) return nullptr;
928
+
929
+ TArray<AActor*> AllActors;
930
+ UGameplayStatics::GetAllActorsOfClass(World, AActor::StaticClass(), AllActors);
931
+ for (AActor* Actor : AllActors)
932
+ {
933
+ if (!Actor) continue;
934
+ if (Actor->GetName().Equals(NameOrLabel, ESearchCase::CaseSensitive) ||
935
+ Actor->GetName().Equals(NameOrLabel, ESearchCase::IgnoreCase))
936
+ {
937
+ return Actor;
938
+ }
939
+ #if WITH_EDITOR
940
+ if (Actor->GetActorLabel().Equals(NameOrLabel, ESearchCase::IgnoreCase))
941
+ {
942
+ return Actor;
943
+ }
944
+ #endif
945
+ }
946
+
947
+ return nullptr;
948
+ }
949
+
950
+ static TSharedPtr<FJsonObject> RaycastInWorld(UWorld* World, const FVector& Start, const FVector& Dir, float MaxDistance, const TArray<FString>& IgnoreActorNames)
951
+ {
952
+ if (!World)
953
+ {
954
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("No world available for raycast."));
955
+ }
956
+
957
+ const FVector Direction = Dir.GetSafeNormal();
958
+ if (Direction.IsNearlyZero())
959
+ {
960
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Raycast direction is zero."));
961
+ }
962
+
963
+ MaxDistance = FMath::Clamp(MaxDistance, 1.0f, 1.0e7f);
964
+ const FVector End = Start + Direction * MaxDistance;
965
+
966
+ FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(UnrealMCP_Raycast), true);
967
+ QueryParams.bReturnPhysicalMaterial = false;
968
+
969
+ for (const FString& Name : IgnoreActorNames)
970
+ {
971
+ if (AActor* Actor = FindActorByNameOrLabel(World, Name))
972
+ {
973
+ QueryParams.AddIgnoredActor(Actor);
974
+ }
975
+ }
976
+
977
+ FHitResult Hit;
978
+ const bool bHit = World->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, QueryParams);
979
+
980
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
981
+ Result->SetBoolField(TEXT("hit"), bHit);
982
+ Result->SetArrayField(TEXT("start"), VectorToJsonArray(Start));
983
+ Result->SetArrayField(TEXT("end"), VectorToJsonArray(End));
984
+ Result->SetNumberField(TEXT("maxDistance"), MaxDistance);
985
+
986
+ if (!bHit)
987
+ {
988
+ return Result;
989
+ }
990
+
991
+ Result->SetArrayField(TEXT("location"), VectorToJsonArray(Hit.ImpactPoint));
992
+ Result->SetArrayField(TEXT("normal"), VectorToJsonArray(Hit.ImpactNormal));
993
+ Result->SetNumberField(TEXT("distance"), Hit.Distance);
994
+ if (AActor* HitActor = Hit.GetActor())
995
+ {
996
+ Result->SetStringField(TEXT("actor"), HitActor->GetName());
997
+ #if WITH_EDITOR
998
+ Result->SetStringField(TEXT("actorLabel"), HitActor->GetActorLabel());
999
+ #endif
1000
+ }
1001
+ if (UPrimitiveComponent* HitComp = Hit.GetComponent())
1002
+ {
1003
+ Result->SetStringField(TEXT("component"), HitComp->GetName());
1004
+ }
1005
+
1006
+ return Result;
1007
+ }
1008
+
1009
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleMapCheck(const TSharedPtr<FJsonObject>& Params)
1010
+ {
1011
+ {
1012
+ FString Err;
1013
+ if (!IsSafeToModifyEditorWorld(Err))
1014
+ {
1015
+ return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
1016
+ }
1017
+ }
1018
+
1019
+ const bool bIncludeWarnings = !Params->HasField(TEXT("includeWarnings")) || Params->GetBoolField(TEXT("includeWarnings"));
1020
+ int32 Limit = 200;
1021
+ if (Params->HasField(TEXT("limit")))
1022
+ {
1023
+ Limit = (int32)Params->GetNumberField(TEXT("limit"));
1024
+ }
1025
+ Limit = FMath::Clamp(Limit, 1, 1000);
1026
+
1027
+ if (!GEditor)
1028
+ {
1029
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
1030
+ }
1031
+
1032
+ UWorld* World = GEditor->GetEditorWorldContext().World();
1033
+ if (!World)
1034
+ {
1035
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world"));
1036
+ }
1037
+
1038
+ FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>(TEXT("MessageLog"));
1039
+ const TSharedRef<IMessageLogListing> Listing = MessageLogModule.GetLogListing(TEXT("MapCheck"));
1040
+ Listing->ClearMessages();
1041
+
1042
+ FOutputDeviceNull Ar;
1043
+ // Use the editor's exec pathway so we can request "don't display dialog" without relying on private enums.
1044
+ const bool bOk = GEditor->Exec(World, TEXT("MAP CHECK DONTDISPLAYDIALOG"), Ar);
1045
+ if (!bOk)
1046
+ {
1047
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to run Map Check."));
1048
+ }
1049
+
1050
+ const TArray<TSharedRef<FTokenizedMessage>>& Messages = Listing->GetFilteredMessages();
1051
+ int32 ErrorCount = 0;
1052
+ int32 WarningCount = 0;
1053
+ int32 IncludedMessageCount = 0;
1054
+ bool bTruncated = false;
1055
+
1056
+ TArray<TSharedPtr<FJsonValue>> Issues;
1057
+ for (const TSharedRef<FTokenizedMessage>& Message : Messages)
1058
+ {
1059
+ const EMessageSeverity::Type Severity = Message->GetSeverity();
1060
+ const bool bIsWarning =
1061
+ Severity == EMessageSeverity::Warning || Severity == EMessageSeverity::PerformanceWarning;
1062
+ const bool bIsError = Severity == EMessageSeverity::Error || Severity == EMessageSeverity::CriticalError;
1063
+
1064
+ if (bIsError) ErrorCount++;
1065
+ if (bIsWarning) WarningCount++;
1066
+
1067
+ if (!bIncludeWarnings && bIsWarning) continue;
1068
+
1069
+ IncludedMessageCount++;
1070
+ if (Issues.Num() >= Limit)
1071
+ {
1072
+ bTruncated = true;
1073
+ continue;
1074
+ }
1075
+
1076
+ TSharedPtr<FJsonObject> Issue = MakeShared<FJsonObject>();
1077
+ Issue->SetStringField(TEXT("severity"), MessageSeverityToString(Severity));
1078
+ Issue->SetStringField(TEXT("message"), Message->ToText().ToString());
1079
+ Issues.Add(MakeShared<FJsonValueObject>(Issue));
1080
+ }
1081
+
1082
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1083
+ Result->SetBoolField(TEXT("ok"), ErrorCount == 0);
1084
+ Result->SetNumberField(TEXT("errors"), ErrorCount);
1085
+ Result->SetNumberField(TEXT("warnings"), WarningCount);
1086
+ Result->SetBoolField(TEXT("includeWarnings"), bIncludeWarnings);
1087
+ Result->SetArrayField(TEXT("issues"), Issues);
1088
+ Result->SetBoolField(TEXT("truncated"), bTruncated || IncludedMessageCount > Issues.Num());
1089
+ return Result;
1090
+ }
1091
+
1092
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetEditorContext(const TSharedPtr<FJsonObject>& Params)
1093
+ {
1094
+ if (!GEditor)
1095
+ {
1096
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
1097
+ }
1098
+
1099
+ UWorld* World = GEditor->GetEditorWorldContext().World();
1100
+ if (!World)
1101
+ {
1102
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world"));
1103
+ }
1104
+
1105
+ const FDateTime Now = FDateTime::UtcNow();
1106
+ const int64 TimestampMs = Now.ToUnixTimestamp() * 1000 + Now.GetMillisecond();
1107
+
1108
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1109
+ Result->SetNumberField(TEXT("timestampMs"), (double)TimestampMs);
1110
+
1111
+ const FString MapPackage = World->GetOutermost() ? World->GetOutermost()->GetName() : World->GetMapName();
1112
+ Result->SetStringField(TEXT("map"), MapPackage);
1113
+
1114
+ // Selection (editor)
1115
+ TArray<TSharedPtr<FJsonValue>> Selection;
1116
+ if (USelection* SelectedActors = GEditor->GetSelectedActors())
1117
+ {
1118
+ for (FSelectionIterator It(*SelectedActors); It; ++It)
1119
+ {
1120
+ if (AActor* Actor = Cast<AActor>(*It))
1121
+ {
1122
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
1123
+ Obj->SetStringField(TEXT("name"), Actor->GetName());
1124
+ #if WITH_EDITOR
1125
+ Obj->SetStringField(TEXT("label"), Actor->GetActorLabel());
1126
+ #endif
1127
+ Obj->SetArrayField(TEXT("location"), VectorToJsonArray(Actor->GetActorLocation()));
1128
+ Obj->SetArrayField(TEXT("rotation"), RotatorToJsonArray(Actor->GetActorRotation()));
1129
+ Selection.Add(MakeShared<FJsonValueObject>(Obj));
1130
+ }
1131
+ }
1132
+ }
1133
+ Result->SetArrayField(TEXT("selection"), Selection);
1134
+
1135
+ // Active viewport camera (best-effort)
1136
+ TSharedPtr<FJsonObject> ViewportCamera;
1137
+ FLevelEditorModule* LevelEditorModule = FModuleManager::LoadModulePtr<FLevelEditorModule>(TEXT("LevelEditor"));
1138
+ if (LevelEditorModule)
1139
+ {
1140
+ TSharedPtr<IAssetViewport> ActiveViewport = LevelEditorModule->GetFirstActiveViewport();
1141
+ if (ActiveViewport.IsValid())
1142
+ {
1143
+ const FEditorViewportClient& VC = ActiveViewport->GetAssetViewportClient();
1144
+ ViewportCamera = MakeShared<FJsonObject>();
1145
+ ViewportCamera->SetArrayField(TEXT("location"), VectorToJsonArray(VC.GetViewLocation()));
1146
+ ViewportCamera->SetArrayField(TEXT("rotation"), RotatorToJsonArray(VC.GetViewRotation()));
1147
+ ViewportCamera->SetNumberField(TEXT("fov"), VC.ViewFOV);
1148
+ ViewportCamera->SetArrayField(TEXT("forward"), VectorToJsonArray(VC.GetViewRotation().Vector()));
1149
+ }
1150
+ }
1151
+ if (ViewportCamera.IsValid())
1152
+ {
1153
+ Result->SetObjectField(TEXT("viewportCamera"), ViewportCamera);
1154
+ }
1155
+ else
1156
+ {
1157
+ Result->SetField(TEXT("viewportCamera"), MakeShared<FJsonValueNull>());
1158
+ }
1159
+
1160
+ return Result;
1161
+ }
1162
+
1163
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetPlayerContext(const TSharedPtr<FJsonObject>& Params)
1164
+ {
1165
+ if (!GEditor)
1166
+ {
1167
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
1168
+ }
1169
+
1170
+ UWorld* PlayWorld = GEditor->PlayWorld;
1171
+ const bool bIsPlaying = PlayWorld != nullptr;
1172
+
1173
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1174
+ Result->SetBoolField(TEXT("isPlaying"), bIsPlaying);
1175
+
1176
+ if (!bIsPlaying)
1177
+ {
1178
+ Result->SetField(TEXT("pawn"), MakeShared<FJsonValueNull>());
1179
+ Result->SetField(TEXT("camera"), MakeShared<FJsonValueNull>());
1180
+ return Result;
1181
+ }
1182
+
1183
+ APlayerController* PC = PlayWorld->GetFirstPlayerController();
1184
+ if (!PC)
1185
+ {
1186
+ Result->SetField(TEXT("pawn"), MakeShared<FJsonValueNull>());
1187
+ Result->SetField(TEXT("camera"), MakeShared<FJsonValueNull>());
1188
+ return Result;
1189
+ }
1190
+
1191
+ Result->SetStringField(TEXT("controller"), PC->GetName());
1192
+
1193
+ if (APawn* Pawn = PC->GetPawn())
1194
+ {
1195
+ TSharedPtr<FJsonObject> PawnObj = MakeShared<FJsonObject>();
1196
+ PawnObj->SetStringField(TEXT("name"), Pawn->GetName());
1197
+ PawnObj->SetArrayField(TEXT("location"), VectorToJsonArray(Pawn->GetActorLocation()));
1198
+ PawnObj->SetArrayField(TEXT("rotation"), RotatorToJsonArray(Pawn->GetActorRotation()));
1199
+ PawnObj->SetArrayField(TEXT("scale"), VectorToJsonArray(Pawn->GetActorScale3D()));
1200
+ Result->SetObjectField(TEXT("pawn"), PawnObj);
1201
+ }
1202
+ else
1203
+ {
1204
+ Result->SetField(TEXT("pawn"), MakeShared<FJsonValueNull>());
1205
+ }
1206
+
1207
+ FVector CamLoc;
1208
+ FRotator CamRot;
1209
+ PC->GetPlayerViewPoint(CamLoc, CamRot);
1210
+
1211
+ TSharedPtr<FJsonObject> CamObj = MakeShared<FJsonObject>();
1212
+ CamObj->SetArrayField(TEXT("location"), VectorToJsonArray(CamLoc));
1213
+ CamObj->SetArrayField(TEXT("rotation"), RotatorToJsonArray(CamRot));
1214
+ CamObj->SetArrayField(TEXT("forward"), VectorToJsonArray(CamRot.Vector()));
1215
+ Result->SetObjectField(TEXT("camera"), CamObj);
139
1216
 
140
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetPlayInEditorStatus(const TSharedPtr<FJsonObject>& Params)
141
- {
142
- return CreatePlayStatusResponse();
1217
+ return Result;
143
1218
  }
144
1219
 
145
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandlePlayInEditor(const TSharedPtr<FJsonObject>& Params)
1220
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleRaycastFromCamera(const TSharedPtr<FJsonObject>& Params)
146
1221
  {
1222
+ const FString Source = Params->HasField(TEXT("source")) ? Params->GetStringField(TEXT("source")).ToLower() : TEXT("editor");
1223
+ const float MaxDistance = Params->HasField(TEXT("maxDistance")) ? (float)Params->GetNumberField(TEXT("maxDistance")) : 10000.0f;
1224
+
1225
+ TArray<FString> Ignore;
1226
+ if (Params->HasField(TEXT("ignoreActors")))
1227
+ {
1228
+ const TArray<TSharedPtr<FJsonValue>> Arr = Params->GetArrayField(TEXT("ignoreActors"));
1229
+ for (const TSharedPtr<FJsonValue>& V : Arr)
1230
+ {
1231
+ if (V.IsValid() && V->Type == EJson::String)
1232
+ {
1233
+ const FString S = V->AsString().TrimStartAndEnd();
1234
+ if (!S.IsEmpty()) Ignore.Add(S);
1235
+ }
1236
+ }
1237
+ }
1238
+
147
1239
  if (!GEditor)
148
1240
  {
149
1241
  return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
150
1242
  }
151
1243
 
152
- if (GEditor->IsPlaySessionInProgress())
1244
+ if (Source == TEXT("pie") || Source == TEXT("player") || Source == TEXT("runtime"))
153
1245
  {
154
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Play session already running or queued."));
1246
+ UWorld* PlayWorld = GEditor->PlayWorld;
1247
+ if (!PlayWorld)
1248
+ {
1249
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("PIE is not running (no PlayWorld)."));
1250
+ }
1251
+ APlayerController* PC = PlayWorld->GetFirstPlayerController();
1252
+ if (!PC)
1253
+ {
1254
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("PIE is running but no player controller was found."));
1255
+ }
1256
+ FVector CamLoc;
1257
+ FRotator CamRot;
1258
+ PC->GetPlayerViewPoint(CamLoc, CamRot);
1259
+ return RaycastInWorld(PlayWorld, CamLoc, CamRot.Vector(), MaxDistance, Ignore);
155
1260
  }
156
1261
 
157
- // Use the active editor viewport, matching how the LevelEditor subsystem triggers PIE.
1262
+ // Default: editor viewport camera.
158
1263
  FLevelEditorModule* LevelEditorModule = FModuleManager::LoadModulePtr<FLevelEditorModule>(TEXT("LevelEditor"));
159
1264
  if (!LevelEditorModule)
160
1265
  {
161
1266
  return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("LevelEditor module not available."));
162
1267
  }
163
-
164
1268
  TSharedPtr<IAssetViewport> ActiveViewport = LevelEditorModule->GetFirstActiveViewport();
165
1269
  if (!ActiveViewport.IsValid())
166
1270
  {
167
1271
  return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("No active level viewport. Click the viewport and retry."));
168
1272
  }
169
1273
 
170
- FRequestPlaySessionParams SessionParams;
171
- SessionParams.WorldType = EPlaySessionWorldType::PlayInEditor;
172
- SessionParams.DestinationSlateViewport = ActiveViewport;
173
-
174
- GEditor->RequestPlaySession(SessionParams);
175
- const bool bQueued = GEditor->IsPlaySessionRequestQueued();
176
-
177
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
178
- ResultObj->SetBoolField(TEXT("requested"), true);
179
- ResultObj->SetBoolField(TEXT("queued"), bQueued);
180
- ResultObj->SetBoolField(TEXT("started"), bQueued);
181
- ResultObj->SetStringField(TEXT("mode"), TEXT("pie"));
182
- return ResultObj;
1274
+ const FEditorViewportClient& VC = ActiveViewport->GetAssetViewportClient();
1275
+ UWorld* World = GEditor->GetEditorWorldContext().World();
1276
+ return RaycastInWorld(World, VC.GetViewLocation(), VC.GetViewRotation().Vector(), MaxDistance, Ignore);
183
1277
  }
184
1278
 
185
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandlePlayInEditorWindowed(const TSharedPtr<FJsonObject>& Params)
1279
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleRaycastDown(const TSharedPtr<FJsonObject>& Params)
186
1280
  {
187
1281
  if (!GEditor)
188
1282
  {
189
1283
  return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
190
1284
  }
191
1285
 
192
- if (GEditor->IsPlaySessionInProgress())
1286
+ FString WorldSource = TEXT("editor");
1287
+ if (Params->HasField(TEXT("source")))
193
1288
  {
194
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Play session already running or queued."));
1289
+ WorldSource = Params->GetStringField(TEXT("source")).ToLower();
195
1290
  }
196
1291
 
197
- // Match the editor's built-in "New Editor Window (PIE)" behavior by setting the play mode type and
198
- // not specifying a DestinationSlateViewport.
199
- ULevelEditorPlaySettings* PlaySettings = GetMutableDefault<ULevelEditorPlaySettings>();
200
- if (PlaySettings)
1292
+ FVector Start;
1293
+ if (Params->HasField(TEXT("startLocation")))
201
1294
  {
202
- PlaySettings->LastExecutedPlayModeType = EPlayModeType::PlayMode_InEditorFloating;
1295
+ Start = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("startLocation"));
1296
+ }
1297
+ else
1298
+ {
1299
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'startLocation' parameter"));
1300
+ }
203
1301
 
204
- if (FProperty* Prop = ULevelEditorPlaySettings::StaticClass()->FindPropertyByName(
205
- GET_MEMBER_NAME_CHECKED(ULevelEditorPlaySettings, LastExecutedPlayModeType)))
1302
+ const float MaxDistance = Params->HasField(TEXT("maxDistance")) ? (float)Params->GetNumberField(TEXT("maxDistance")) : 100000.0f;
1303
+
1304
+ UWorld* World = nullptr;
1305
+ if (WorldSource == TEXT("pie"))
1306
+ {
1307
+ World = GEditor->PlayWorld;
1308
+ if (!World)
206
1309
  {
207
- FPropertyChangedEvent PropChangeEvent(Prop);
208
- PlaySettings->PostEditChangeProperty(PropChangeEvent);
1310
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("PIE is not running (no PlayWorld)."));
209
1311
  }
210
-
211
- PlaySettings->SaveConfig();
1312
+ }
1313
+ else
1314
+ {
1315
+ World = GEditor->GetEditorWorldContext().World();
212
1316
  }
213
1317
 
214
- const bool bAtPlayerStart =
215
- PlaySettings && PlaySettings->LastExecutedPlayModeLocation == EPlayModeLocations::PlayLocation_DefaultPlayerStart;
1318
+ return RaycastInWorld(World, Start, FVector(0.0f, 0.0f, -1.0f), MaxDistance, {});
1319
+ }
216
1320
 
217
- FRequestPlaySessionParams SessionParams;
218
- SessionParams.WorldType = EPlaySessionWorldType::PlayInEditor;
1321
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetActorTransform(const TSharedPtr<FJsonObject>& Params)
1322
+ {
1323
+ if (!GEditor)
1324
+ {
1325
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
1326
+ }
219
1327
 
220
- // If the user is playing from current camera location, use the active viewport camera as the start transform.
221
- if (!bAtPlayerStart)
1328
+ FString Name;
1329
+ if (!Params->TryGetStringField(TEXT("name"), Name) && !Params->TryGetStringField(TEXT("actor"), Name))
222
1330
  {
223
- FLevelEditorModule* LevelEditorModule = FModuleManager::LoadModulePtr<FLevelEditorModule>(TEXT("LevelEditor"));
224
- if (LevelEditorModule)
225
- {
226
- TSharedPtr<IAssetViewport> ActiveViewport = LevelEditorModule->GetFirstActiveViewport();
227
- if (ActiveViewport.IsValid() && FSlateApplication::IsInitialized() &&
228
- FSlateApplication::Get().FindWidgetWindow(ActiveViewport->AsWidget()).IsValid())
229
- {
230
- SessionParams.StartLocation = ActiveViewport->GetAssetViewportClient().GetViewLocation();
231
- SessionParams.StartRotation = ActiveViewport->GetAssetViewportClient().GetViewRotation();
232
- }
233
- }
1331
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
1332
+ }
1333
+ Name = Name.TrimStartAndEnd();
1334
+ if (Name.IsEmpty())
1335
+ {
1336
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Actor name is empty"));
234
1337
  }
235
1338
 
236
- GEditor->RequestPlaySession(SessionParams);
237
- const bool bQueued = GEditor->IsPlaySessionRequestQueued();
1339
+ FString Source = Params->HasField(TEXT("source")) ? Params->GetStringField(TEXT("source")).ToLower() : TEXT("editor");
1340
+ UWorld* World = nullptr;
1341
+ if (Source == TEXT("pie"))
1342
+ {
1343
+ World = GEditor->PlayWorld;
1344
+ }
1345
+ else
1346
+ {
1347
+ World = GEditor->GetEditorWorldContext().World();
1348
+ }
1349
+ if (!World)
1350
+ {
1351
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("World is not available."));
1352
+ }
238
1353
 
239
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
240
- ResultObj->SetBoolField(TEXT("requested"), true);
241
- ResultObj->SetBoolField(TEXT("queued"), bQueued);
242
- ResultObj->SetBoolField(TEXT("started"), bQueued);
243
- ResultObj->SetStringField(TEXT("mode"), TEXT("pie_new_window"));
244
- return ResultObj;
1354
+ AActor* Actor = FindActorByNameOrLabel(World, Name);
1355
+ if (!Actor)
1356
+ {
1357
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *Name));
1358
+ }
1359
+
1360
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1361
+ Result->SetStringField(TEXT("name"), Actor->GetName());
1362
+ #if WITH_EDITOR
1363
+ Result->SetStringField(TEXT("label"), Actor->GetActorLabel());
1364
+ #endif
1365
+ Result->SetArrayField(TEXT("location"), VectorToJsonArray(Actor->GetActorLocation()));
1366
+ Result->SetArrayField(TEXT("rotation"), RotatorToJsonArray(Actor->GetActorRotation()));
1367
+ Result->SetArrayField(TEXT("scale"), VectorToJsonArray(Actor->GetActorScale3D()));
1368
+ return Result;
245
1369
  }
246
1370
 
247
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleStopPlayInEditor(const TSharedPtr<FJsonObject>& Params)
1371
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetActorBounds(const TSharedPtr<FJsonObject>& Params)
248
1372
  {
249
1373
  if (!GEditor)
250
1374
  {
251
1375
  return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
252
1376
  }
253
1377
 
254
- if (GEditor->IsPlaySessionRequestQueued() && !GEditor->IsPlayingSessionInEditor())
1378
+ FString Name;
1379
+ if (!Params->TryGetStringField(TEXT("name"), Name) && !Params->TryGetStringField(TEXT("actor"), Name))
255
1380
  {
256
- GEditor->CancelRequestPlaySession();
257
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
258
- ResultObj->SetBoolField(TEXT("stopped"), true);
259
- ResultObj->SetBoolField(TEXT("canceled"), true);
260
- return ResultObj;
1381
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
1382
+ }
1383
+ Name = Name.TrimStartAndEnd();
1384
+ if (Name.IsEmpty())
1385
+ {
1386
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Actor name is empty"));
261
1387
  }
262
1388
 
263
- if (!GEditor->IsPlayingSessionInEditor())
1389
+ FString Source = Params->HasField(TEXT("source")) ? Params->GetStringField(TEXT("source")).ToLower() : TEXT("editor");
1390
+ UWorld* World = nullptr;
1391
+ if (Source == TEXT("pie"))
264
1392
  {
265
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("No play session is running."));
1393
+ World = GEditor->PlayWorld;
1394
+ }
1395
+ else
1396
+ {
1397
+ World = GEditor->GetEditorWorldContext().World();
1398
+ }
1399
+ if (!World)
1400
+ {
1401
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("World is not available."));
266
1402
  }
267
1403
 
268
- GEditor->RequestEndPlayMap();
1404
+ AActor* Actor = FindActorByNameOrLabel(World, Name);
1405
+ if (!Actor)
1406
+ {
1407
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *Name));
1408
+ }
269
1409
 
270
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
271
- ResultObj->SetBoolField(TEXT("stopped"), true);
272
- return ResultObj;
1410
+ FVector Origin;
1411
+ FVector Extent;
1412
+ Actor->GetActorBounds(true, Origin, Extent, false);
1413
+
1414
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1415
+ Result->SetStringField(TEXT("name"), Actor->GetName());
1416
+ #if WITH_EDITOR
1417
+ Result->SetStringField(TEXT("label"), Actor->GetActorLabel());
1418
+ #endif
1419
+ Result->SetArrayField(TEXT("origin"), VectorToJsonArray(Origin));
1420
+ Result->SetArrayField(TEXT("extent"), VectorToJsonArray(Extent));
1421
+ Result->SetArrayField(TEXT("min"), VectorToJsonArray(Origin - Extent));
1422
+ Result->SetArrayField(TEXT("max"), VectorToJsonArray(Origin + Extent));
1423
+ return Result;
273
1424
  }
274
1425
 
275
1426
  TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetActorsInLevel(const TSharedPtr<FJsonObject>& Params)
@@ -295,10 +1446,15 @@ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetActorsInLevel(const T
295
1446
  TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleFindActorsByName(const TSharedPtr<FJsonObject>& Params)
296
1447
  {
297
1448
  FString Pattern;
298
- if (!Params->TryGetStringField(TEXT("pattern"), Pattern))
1449
+ if (!Params->TryGetStringField(TEXT("pattern"), Pattern) && !Params->TryGetStringField(TEXT("name"), Pattern))
299
1450
  {
300
1451
  return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'pattern' parameter"));
301
1452
  }
1453
+ Pattern = Pattern.TrimStartAndEnd();
1454
+ if (Pattern.IsEmpty())
1455
+ {
1456
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("pattern is empty"));
1457
+ }
302
1458
 
303
1459
  TArray<AActor*> AllActors;
304
1460
  UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
@@ -306,7 +1462,14 @@ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleFindActorsByName(const T
306
1462
  TArray<TSharedPtr<FJsonValue>> MatchingActors;
307
1463
  for (AActor* Actor : AllActors)
308
1464
  {
309
- if (Actor && Actor->GetName().Contains(Pattern))
1465
+ if (!Actor) continue;
1466
+ const bool bNameMatch = Actor->GetName().Contains(Pattern);
1467
+ #if WITH_EDITOR
1468
+ const bool bLabelMatch = Actor->GetActorLabel().Contains(Pattern);
1469
+ #else
1470
+ const bool bLabelMatch = false;
1471
+ #endif
1472
+ if (bNameMatch || bLabelMatch)
310
1473
  {
311
1474
  MatchingActors.Add(FUnrealMCPCommonUtils::ActorToJson(Actor));
312
1475
  }
@@ -453,6 +1616,196 @@ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSpawnActor(const TShared
453
1616
  return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to create actor"));
454
1617
  }
455
1618
 
1619
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleCreateLandscape(const TSharedPtr<FJsonObject>& Params)
1620
+ {
1621
+ {
1622
+ FString Err;
1623
+ if (!IsSafeToModifyEditorWorld(Err))
1624
+ {
1625
+ return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
1626
+ }
1627
+ }
1628
+
1629
+ UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
1630
+ if (!World)
1631
+ {
1632
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world"));
1633
+ }
1634
+
1635
+ FString LandscapeName;
1636
+ if (!Params->TryGetStringField(TEXT("name"), LandscapeName))
1637
+ {
1638
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
1639
+ }
1640
+ LandscapeName = LandscapeName.TrimStartAndEnd();
1641
+ if (LandscapeName.IsEmpty())
1642
+ {
1643
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Invalid 'name' parameter (empty)"));
1644
+ }
1645
+
1646
+ // Ensure actor name uniqueness.
1647
+ {
1648
+ TArray<AActor*> AllActors;
1649
+ UGameplayStatics::GetAllActorsOfClass(World, AActor::StaticClass(), AllActors);
1650
+ for (AActor* Actor : AllActors)
1651
+ {
1652
+ if (Actor && Actor->GetName() == LandscapeName)
1653
+ {
1654
+ return FUnrealMCPCommonUtils::CreateErrorResponse(
1655
+ FString::Printf(TEXT("Actor with name '%s' already exists"), *LandscapeName));
1656
+ }
1657
+ }
1658
+ }
1659
+
1660
+ FVector Location(0.0f, 0.0f, 0.0f);
1661
+ FRotator Rotation(0.0f, 0.0f, 0.0f);
1662
+ FVector Scale(100.0f, 100.0f, 100.0f);
1663
+
1664
+ if (Params->HasField(TEXT("location")))
1665
+ {
1666
+ Location = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("location"));
1667
+ }
1668
+ if (Params->HasField(TEXT("rotation")))
1669
+ {
1670
+ Rotation = FUnrealMCPCommonUtils::GetRotatorFromJson(Params, TEXT("rotation"));
1671
+ }
1672
+ if (Params->HasField(TEXT("scale")))
1673
+ {
1674
+ Scale = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("scale"));
1675
+ }
1676
+
1677
+ int32 ComponentCountX = 8;
1678
+ int32 ComponentCountY = 8;
1679
+ int32 SectionsPerComponent = 1;
1680
+ int32 QuadsPerSection = 63;
1681
+
1682
+ {
1683
+ double Value = 0;
1684
+ if (Params->TryGetNumberField(TEXT("componentCountX"), Value)) ComponentCountX = (int32)Value;
1685
+ if (Params->TryGetNumberField(TEXT("componentCountY"), Value)) ComponentCountY = (int32)Value;
1686
+ if (Params->TryGetNumberField(TEXT("sectionsPerComponent"), Value)) SectionsPerComponent = (int32)Value;
1687
+ if (Params->TryGetNumberField(TEXT("quadsPerSection"), Value)) QuadsPerSection = (int32)Value;
1688
+ }
1689
+
1690
+ if (ComponentCountX <= 0 || ComponentCountY <= 0)
1691
+ {
1692
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("componentCountX/componentCountY must be positive integers"));
1693
+ }
1694
+ if (SectionsPerComponent <= 0 || SectionsPerComponent > 2)
1695
+ {
1696
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("sectionsPerComponent must be 1 or 2"));
1697
+ }
1698
+ if (QuadsPerSection <= 0)
1699
+ {
1700
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("quadsPerSection must be a positive integer"));
1701
+ }
1702
+
1703
+ const int32 QuadsPerComponent = SectionsPerComponent * QuadsPerSection;
1704
+ const int32 SizeX = ComponentCountX * QuadsPerComponent + 1;
1705
+ const int32 SizeY = ComponentCountY * QuadsPerComponent + 1;
1706
+
1707
+ const int64 TotalVerts = (int64)SizeX * (int64)SizeY;
1708
+ if (TotalVerts > 16ll * 1024ll * 1024ll)
1709
+ {
1710
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Landscape is too large (vertex count limit exceeded)"));
1711
+ }
1712
+
1713
+ // Optional landscape material.
1714
+ UMaterialInterface* LandscapeMaterial = nullptr;
1715
+ {
1716
+ FString MaterialPath;
1717
+ if (Params->TryGetStringField(TEXT("materialPath"), MaterialPath) ||
1718
+ Params->TryGetStringField(TEXT("material"), MaterialPath) ||
1719
+ Params->TryGetStringField(TEXT("landscapeMaterial"), MaterialPath))
1720
+ {
1721
+ MaterialPath = MaterialPath.TrimStartAndEnd();
1722
+ if (!MaterialPath.IsEmpty())
1723
+ {
1724
+ LandscapeMaterial = LoadObject<UMaterialInterface>(nullptr, *MaterialPath);
1725
+ if (!LandscapeMaterial && !MaterialPath.StartsWith(TEXT("Material'")))
1726
+ {
1727
+ LandscapeMaterial = LoadObject<UMaterialInterface>(nullptr, *FString::Printf(TEXT("Material'%s'"), *MaterialPath));
1728
+ }
1729
+ if (!LandscapeMaterial)
1730
+ {
1731
+ return FUnrealMCPCommonUtils::CreateErrorResponse(
1732
+ FString::Printf(TEXT("Landscape material not found: %s"), *MaterialPath));
1733
+ }
1734
+ }
1735
+ }
1736
+ }
1737
+
1738
+ // Center on Location (similar to the editor create landscape UX).
1739
+ const FVector Offset =
1740
+ FTransform(Rotation, FVector::ZeroVector, Scale)
1741
+ .TransformVector(FVector(-ComponentCountX * QuadsPerComponent / 2, -ComponentCountY * QuadsPerComponent / 2, 0));
1742
+
1743
+ FActorSpawnParameters SpawnParams;
1744
+ SpawnParams.Name = *LandscapeName;
1745
+ ALandscape* Landscape = World->SpawnActor<ALandscape>(Location + Offset, Rotation, SpawnParams);
1746
+ if (!Landscape)
1747
+ {
1748
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to spawn Landscape actor"));
1749
+ }
1750
+
1751
+ Landscape->SetActorRelativeScale3D(Scale);
1752
+ if (LandscapeMaterial)
1753
+ {
1754
+ Landscape->LandscapeMaterial = LandscapeMaterial;
1755
+ }
1756
+
1757
+ Landscape->StaticLightingLOD = FMath::DivideAndRoundUp(FMath::CeilLogTwo((SizeX * SizeY) / (2048 * 2048) + 1), (uint32)2);
1758
+
1759
+ // Flat heightmap.
1760
+ TArray<uint16> HeightData;
1761
+ HeightData.SetNumUninitialized(SizeX * SizeY);
1762
+ for (int32 i = 0; i < HeightData.Num(); i++)
1763
+ {
1764
+ HeightData[i] = 32768;
1765
+ }
1766
+
1767
+ TMap<FGuid, TArray<uint16>> HeightDataPerLayers;
1768
+ HeightDataPerLayers.Add(FGuid(), MoveTemp(HeightData));
1769
+
1770
+ TMap<FGuid, TArray<FLandscapeImportLayerInfo>> MaterialLayerDataPerLayers;
1771
+ MaterialLayerDataPerLayers.Add(FGuid(), TArray<FLandscapeImportLayerInfo>());
1772
+
1773
+ FString ReimportHeightmapFilePath;
1774
+
1775
+ Landscape->Import(
1776
+ FGuid::NewGuid(),
1777
+ 0,
1778
+ 0,
1779
+ SizeX - 1,
1780
+ SizeY - 1,
1781
+ SectionsPerComponent,
1782
+ QuadsPerSection,
1783
+ HeightDataPerLayers,
1784
+ *ReimportHeightmapFilePath,
1785
+ MaterialLayerDataPerLayers,
1786
+ ELandscapeImportAlphamapType::Additive,
1787
+ TArrayView<const FLandscapeLayer>());
1788
+
1789
+ ULandscapeInfo* LandscapeInfo = Landscape->GetLandscapeInfo();
1790
+ if (!LandscapeInfo)
1791
+ {
1792
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Landscape import failed (LandscapeInfo missing)"));
1793
+ }
1794
+
1795
+ FActorLabelUtilities::SetActorLabelUnique(Landscape, LandscapeName);
1796
+ LandscapeInfo->UpdateLayerInfoMap(Landscape);
1797
+
1798
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
1799
+ ResultObj->SetObjectField(TEXT("landscape"), FUnrealMCPCommonUtils::ActorToJsonObject(Landscape));
1800
+ ResultObj->SetNumberField(TEXT("componentCountX"), ComponentCountX);
1801
+ ResultObj->SetNumberField(TEXT("componentCountY"), ComponentCountY);
1802
+ ResultObj->SetNumberField(TEXT("sectionsPerComponent"), SectionsPerComponent);
1803
+ ResultObj->SetNumberField(TEXT("quadsPerSection"), QuadsPerSection);
1804
+ ResultObj->SetNumberField(TEXT("sizeX"), SizeX);
1805
+ ResultObj->SetNumberField(TEXT("sizeY"), SizeY);
1806
+ return ResultObj;
1807
+ }
1808
+
456
1809
  TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleDeleteActor(const TSharedPtr<FJsonObject>& Params)
457
1810
  {
458
1811
  {
@@ -554,7 +1907,7 @@ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetActorProperties(const
554
1907
  {
555
1908
  // Get actor name
556
1909
  FString ActorName;
557
- if (!Params->TryGetStringField(TEXT("name"), ActorName))
1910
+ if (!Params->TryGetStringField(TEXT("name"), ActorName) && !Params->TryGetStringField(TEXT("actor"), ActorName))
558
1911
  {
559
1912
  return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
560
1913
  }
@@ -594,7 +1947,7 @@ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSetActorProperty(const T
594
1947
 
595
1948
  // Get actor name
596
1949
  FString ActorName;
597
- if (!Params->TryGetStringField(TEXT("name"), ActorName))
1950
+ if (!Params->TryGetStringField(TEXT("name"), ActorName) && !Params->TryGetStringField(TEXT("actor"), ActorName))
598
1951
  {
599
1952
  return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
600
1953
  }
@@ -620,18 +1973,20 @@ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSetActorProperty(const T
620
1973
 
621
1974
  // Get property name
622
1975
  FString PropertyName;
623
- if (!Params->TryGetStringField(TEXT("property_name"), PropertyName))
1976
+ if (!Params->TryGetStringField(TEXT("property_name"), PropertyName) && !Params->TryGetStringField(TEXT("property"), PropertyName))
624
1977
  {
625
1978
  return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'property_name' parameter"));
626
1979
  }
627
1980
 
628
1981
  // Get property value
629
- if (!Params->HasField(TEXT("property_value")))
1982
+ if (!Params->HasField(TEXT("property_value")) && !Params->HasField(TEXT("value")))
630
1983
  {
631
1984
  return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'property_value' parameter"));
632
1985
  }
633
1986
 
634
- TSharedPtr<FJsonValue> PropertyValue = Params->Values.FindRef(TEXT("property_value"));
1987
+ TSharedPtr<FJsonValue> PropertyValue = Params->HasField(TEXT("property_value"))
1988
+ ? Params->Values.FindRef(TEXT("property_value"))
1989
+ : Params->Values.FindRef(TEXT("value"));
635
1990
 
636
1991
  // Set the property using our utility function
637
1992
  FString ErrorMessage;
@@ -665,13 +2020,17 @@ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSpawnBlueprintActor(cons
665
2020
 
666
2021
  // Get required parameters
667
2022
  FString BlueprintName;
668
- if (!Params->TryGetStringField(TEXT("blueprint_name"), BlueprintName))
2023
+ if (!Params->TryGetStringField(TEXT("blueprint_name"), BlueprintName) &&
2024
+ !Params->TryGetStringField(TEXT("blueprint"), BlueprintName) &&
2025
+ !Params->TryGetStringField(TEXT("blueprint_path"), BlueprintName) &&
2026
+ !Params->TryGetStringField(TEXT("objectPath"), BlueprintName) &&
2027
+ !Params->TryGetStringField(TEXT("object_path"), BlueprintName))
669
2028
  {
670
2029
  return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'blueprint_name' parameter"));
671
2030
  }
672
2031
 
673
2032
  FString ActorName;
674
- if (!Params->TryGetStringField(TEXT("actor_name"), ActorName))
2033
+ if (!Params->TryGetStringField(TEXT("actor_name"), ActorName) && !Params->TryGetStringField(TEXT("name"), ActorName))
675
2034
  {
676
2035
  return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'actor_name' parameter"));
677
2036
  }
@@ -681,19 +2040,51 @@ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSpawnBlueprintActor(cons
681
2040
  {
682
2041
  return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Blueprint name is empty"));
683
2042
  }
2043
+ BlueprintName = BlueprintName.TrimStartAndEnd();
2044
+ ActorName = ActorName.TrimStartAndEnd();
2045
+
2046
+ const auto SanitizeObjectName = [](const FString& In) -> FString {
2047
+ FString Out;
2048
+ Out.Reserve(In.Len());
2049
+ for (int32 i = 0; i < In.Len(); i++)
2050
+ {
2051
+ const TCHAR Ch = In[i];
2052
+ const bool bOk =
2053
+ (Ch >= '0' && Ch <= '9') ||
2054
+ (Ch >= 'A' && Ch <= 'Z') ||
2055
+ (Ch >= 'a' && Ch <= 'z') ||
2056
+ Ch == '_' || Ch == '-';
2057
+ Out.AppendChar(bOk ? Ch : '_');
2058
+ }
2059
+ Out = Out.TrimStartAndEnd();
2060
+ while (Out.Contains(TEXT("__")))
2061
+ {
2062
+ Out = Out.Replace(TEXT("__"), TEXT("_"));
2063
+ }
2064
+ if (Out.IsEmpty()) Out = TEXT("BPActor");
2065
+ return Out;
2066
+ };
684
2067
 
685
- FString Root = TEXT("/Game/Blueprints/");
686
- FString AssetPath = Root + BlueprintName;
2068
+ const FString SafeActorBaseName = SanitizeObjectName(ActorName);
2069
+
2070
+ // Resolve an asset/object path:
2071
+ // - If caller passes /Game/... treat as an object path/package path.
2072
+ // - Otherwise assume /Game/Blueprints/<BlueprintName> (compat default).
2073
+ FString AssetPath = BlueprintName;
2074
+ if (!AssetPath.StartsWith(TEXT("/")))
2075
+ {
2076
+ AssetPath = TEXT("/Game/Blueprints/") + AssetPath;
2077
+ }
687
2078
 
688
- if (!FPackageName::DoesPackageExist(AssetPath))
2079
+ UObject* AssetObj = LoadObject<UObject>(nullptr, *AssetPath);
2080
+ if (!AssetObj && !AssetPath.Contains(TEXT("'")))
689
2081
  {
690
- return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Blueprint '%s' not found – it must reside under /Game/Blueprints"), *BlueprintName));
2082
+ AssetObj = LoadObject<UObject>(nullptr, *FString::Printf(TEXT("Blueprint'%s'"), *AssetPath));
691
2083
  }
692
2084
 
693
- UBlueprint* Blueprint = LoadObject<UBlueprint>(nullptr, *AssetPath);
694
- if (!Blueprint)
2085
+ if (!AssetObj)
695
2086
  {
696
- return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Blueprint not found: %s"), *BlueprintName));
2087
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Failed to load blueprint asset: %s"), *AssetPath));
697
2088
  }
698
2089
 
699
2090
  // Get transform parameters
@@ -727,12 +2118,45 @@ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSpawnBlueprintActor(cons
727
2118
  SpawnTransform.SetScale3D(Scale);
728
2119
 
729
2120
  FActorSpawnParameters SpawnParams;
730
- SpawnParams.Name = *ActorName;
2121
+ SpawnParams.Name = MakeUniqueObjectName(World, AActor::StaticClass(), FName(*SafeActorBaseName));
2122
+
2123
+ UClass* ActorClass = nullptr;
2124
+ if (UBlueprint* Blueprint = Cast<UBlueprint>(AssetObj))
2125
+ {
2126
+ ActorClass = Blueprint->GeneratedClass;
2127
+ }
2128
+ else if (UBlueprintGeneratedClass* BPGC = Cast<UBlueprintGeneratedClass>(AssetObj))
2129
+ {
2130
+ ActorClass = BPGC;
2131
+ }
2132
+ else if (UClass* Class = Cast<UClass>(AssetObj))
2133
+ {
2134
+ ActorClass = Class;
2135
+ }
2136
+
2137
+ if (!ActorClass || !ActorClass->IsChildOf(AActor::StaticClass()))
2138
+ {
2139
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Loaded asset is not a spawnable Actor Blueprint/Class. Compile the Blueprint and retry."));
2140
+ }
731
2141
 
732
- AActor* NewActor = World->SpawnActor<AActor>(Blueprint->GeneratedClass, SpawnTransform, SpawnParams);
2142
+ AActor* NewActor = World->SpawnActor<AActor>(ActorClass, SpawnTransform, SpawnParams);
733
2143
  if (NewActor)
734
2144
  {
735
- return FUnrealMCPCommonUtils::ActorToJsonObject(NewActor, true);
2145
+ bool bDetailed = true;
2146
+ if (Params->HasField(TEXT("include_details")))
2147
+ {
2148
+ bDetailed = Params->GetBoolField(TEXT("include_details"));
2149
+ }
2150
+ else if (Params->HasField(TEXT("detailed")))
2151
+ {
2152
+ bDetailed = Params->GetBoolField(TEXT("detailed"));
2153
+ }
2154
+ else if (Params->HasField(TEXT("includeProperties")))
2155
+ {
2156
+ bDetailed = Params->GetBoolField(TEXT("includeProperties"));
2157
+ }
2158
+
2159
+ return FUnrealMCPCommonUtils::ActorToJsonObject(NewActor, bDetailed);
736
2160
  }
737
2161
 
738
2162
  return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to spawn blueprint actor"));