flockbay 0.10.49 → 0.10.52

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 (74) hide show
  1. package/README.md +60 -60
  2. package/bin/flockbay-mcp.mjs +62 -62
  3. package/bin/flockbay.mjs +95 -95
  4. package/dist/{index-r3VTdgFI.cjs → index-IntvnrmZ.cjs} +71 -14
  5. package/dist/{index-D8AeVbvv.mjs → index-wkeSi0iC.mjs} +70 -13
  6. package/dist/index.cjs +2 -2
  7. package/dist/index.mjs +2 -2
  8. package/dist/lib.cjs +1 -1
  9. package/dist/lib.mjs +1 -1
  10. package/dist/{migratePlugin-CEvGPul8.mjs → migratePlugin-C71hAoc5.mjs} +12 -2
  11. package/dist/{migratePlugin-wFK-k3Wk.cjs → migratePlugin-D1XjjWFq.cjs} +12 -2
  12. package/dist/{runCodex-DuyuYqoB.cjs → runCodex-B7i_ICoQ.cjs} +34 -23
  13. package/dist/{runCodex-ZfUyhHF6.mjs → runCodex-BSx-hCu6.mjs} +34 -23
  14. package/dist/{runGemini-DBU1mMdp.mjs → runGemini-Dwyh7_WI.mjs} +2 -2
  15. package/dist/{runGemini-D7j5Y5TF.cjs → runGemini-gBKR3rdL.cjs} +2 -2
  16. package/dist/{types-DNr0xwSy.cjs → types-5z8UADAi.cjs} +224 -15
  17. package/dist/{types-BRJuZQj_.mjs → types-Bgzap7qR.mjs} +224 -16
  18. package/package.json +131 -131
  19. package/scripts/claude_local_launcher.cjs +72 -72
  20. package/scripts/claude_remote_launcher.cjs +15 -15
  21. package/scripts/claude_version_utils.cjs +445 -445
  22. package/scripts/ripgrep_launcher.cjs +32 -32
  23. package/scripts/run-with-root-env.mjs +37 -32
  24. package/scripts/session_hook_forwarder.cjs +49 -49
  25. package/scripts/test-codex-abort-history.mjs +77 -77
  26. package/scripts/unpack-tools.cjs +244 -244
  27. package/tools/licenses/difftastic-LICENSE +21 -21
  28. package/tools/licenses/ripgrep-LICENSE +3 -3
  29. package/tools/unreal-mcp/UPSTREAM_VERSION.md +8 -8
  30. package/tools/unreal-mcp/upstream/Docs/README.md +8 -8
  31. package/tools/unreal-mcp/upstream/Docs/Tools/README.md +6 -6
  32. package/tools/unreal-mcp/upstream/Docs/Tools/actor_tools.md +183 -183
  33. package/tools/unreal-mcp/upstream/Docs/Tools/blueprint_tools.md +268 -268
  34. package/tools/unreal-mcp/upstream/Docs/Tools/editor_tools.md +104 -104
  35. package/tools/unreal-mcp/upstream/Docs/Tools/node_tools.md +274 -274
  36. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Config/FilterPlugin.ini +8 -8
  37. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/FlockbayMCP.uplugin +36 -36
  38. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/FlockbayMCP.Build.cs +82 -82
  39. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPBlueprintCommands.cpp +1364 -1364
  40. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPBlueprintNodeCommands.cpp +1310 -1310
  41. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPCommandSchema.cpp +756 -756
  42. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPCommonUtils.cpp +1177 -1177
  43. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPEditorCommands.cpp +2618 -2618
  44. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPProjectCommands.cpp +71 -71
  45. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPUMGCommands.cpp +543 -543
  46. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/MCPServerRunnable.cpp +372 -372
  47. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/UnrealMCPBridge.cpp +457 -457
  48. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/UnrealMCPModule.cpp +21 -21
  49. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPBlueprintCommands.h +35 -35
  50. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPBlueprintNodeCommands.h +30 -30
  51. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPCommandSchema.h +41 -41
  52. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPCommonUtils.h +59 -59
  53. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPEditorCommands.h +61 -61
  54. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPProjectCommands.h +20 -20
  55. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPUMGCommands.h +82 -82
  56. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/MCPServerRunnable.h +33 -33
  57. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/UnrealMCPBridge.h +64 -64
  58. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/UnrealMCPModule.h +22 -22
  59. package/tools/unreal-mcp/upstream/Python/README.md +39 -39
  60. package/tools/unreal-mcp/upstream/Python/pyproject.toml +22 -22
  61. package/tools/unreal-mcp/upstream/Python/scripts/actors/test_cube.py +202 -202
  62. package/tools/unreal-mcp/upstream/Python/scripts/blueprints/test_create_and_spawn_blueprints_with_different_components.py +496 -496
  63. package/tools/unreal-mcp/upstream/Python/scripts/blueprints/test_create_and_spawn_cube_blueprint.py +193 -193
  64. package/tools/unreal-mcp/upstream/Python/scripts/node/test_component_reference.py +266 -266
  65. package/tools/unreal-mcp/upstream/Python/scripts/node/test_create_bird_blueprint_with_input_and_camera.py +617 -617
  66. package/tools/unreal-mcp/upstream/Python/scripts/node/test_input_mapping.py +365 -365
  67. package/tools/unreal-mcp/upstream/Python/scripts/node/test_physics_variables.py +389 -389
  68. package/tools/unreal-mcp/upstream/Python/tools/blueprint_tools.py +419 -419
  69. package/tools/unreal-mcp/upstream/Python/tools/editor_tools.py +369 -369
  70. package/tools/unreal-mcp/upstream/Python/tools/node_tools.py +429 -429
  71. package/tools/unreal-mcp/upstream/Python/tools/project_tools.py +63 -63
  72. package/tools/unreal-mcp/upstream/Python/tools/umg_tools.py +332 -332
  73. package/tools/unreal-mcp/upstream/Python/unreal_mcp_server.py +398 -398
  74. package/tools/unreal-mcp/upstream/Python/uv.lock +521 -521
@@ -1,2618 +1,2618 @@
1
- #include "Commands/UnrealMCPEditorCommands.h"
2
- #include "Commands/UnrealMCPCommonUtils.h"
3
- #include "Editor.h"
4
- #include "Editor/EditorEngine.h"
5
- #include "EditorViewportClient.h"
6
- #include "LevelEditorViewport.h"
7
- #include "IAssetViewport.h"
8
- #include "ImageUtils.h"
9
- #include "HighResScreenshot.h"
10
- #include "Engine/GameViewportClient.h"
11
- #include "Misc/FileHelper.h"
12
- #include "Misc/Paths.h"
13
- #include "HAL/FileManager.h"
14
- #include "GameFramework/Actor.h"
15
- #include "Engine/Selection.h"
16
- #include "Kismet/GameplayStatics.h"
17
- #include "Engine/StaticMeshActor.h"
18
- #include "Engine/StaticMesh.h"
19
- #include "Engine/Texture2D.h"
20
- #include "Engine/SkeletalMesh.h"
21
- #include "Engine/DirectionalLight.h"
22
- #include "Engine/PointLight.h"
23
- #include "Engine/SpotLight.h"
24
- #include "Camera/CameraActor.h"
25
- #include "Landscape.h"
26
- #include "LandscapeImportHelper.h"
27
- #include "Components/StaticMeshComponent.h"
28
- #include "Materials/MaterialInterface.h"
29
- #include "Sound/SoundWave.h"
30
- #include "IImageWrapper.h"
31
- #include "IImageWrapperModule.h"
32
- #include "EditorSubsystem.h"
33
- #include "Subsystems/EditorActorSubsystem.h"
34
- #include "Engine/Blueprint.h"
35
- #include "Engine/BlueprintGeneratedClass.h"
36
- #include "UnrealEdGlobals.h"
37
- #include "Editor/UnrealEdEngine.h"
38
- #include "UObject/UnrealType.h"
39
- #include "Modules/ModuleManager.h"
40
- #include "LevelEditor.h"
41
- #include "PlayInEditorDataTypes.h"
42
- #include "Settings/LevelEditorPlaySettings.h"
43
- #include "Framework/Application/SlateApplication.h"
44
- #include "AssetRegistry/AssetRegistryModule.h"
45
- #include "UObject/SoftObjectPath.h"
46
- #include "UObject/Package.h"
47
- #include "UObject/UObjectGlobals.h"
48
- #include "FileHelpers.h"
49
- #include "MessageLogModule.h"
50
- #include "IMessageLogListing.h"
51
- #include "Engine/World.h"
52
- #include "Engine/EngineTypes.h"
53
- #include "GameFramework/PlayerController.h"
54
- #include "Camera/PlayerCameraManager.h"
55
- #include "Engine/LocalPlayer.h"
56
- #include "Engine/SceneCapture2D.h"
57
- #include "Components/SceneCaptureComponent2D.h"
58
- #include "Engine/TextureRenderTarget2D.h"
59
-
60
- static UClass* FindMcpClassByName(const TCHAR* ClassName)
61
- {
62
- #if defined(ANY_PACKAGE)
63
- return FindObject<UClass>(ANY_PACKAGE, ClassName);
64
- #else
65
- return FindFirstObject<UClass>(ClassName);
66
- #endif
67
- }
68
-
69
- static UClass* FindMcpClassByName(const FString& ClassName)
70
- {
71
- return FindMcpClassByName(*ClassName);
72
- }
73
-
74
- static bool SavePngToFile(const FString& FilePath, int32 Width, int32 Height, TArray<FColor>& Bitmap)
75
- {
76
- IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
77
- TSharedPtr<IImageWrapper> Wrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG);
78
- if (!Wrapper.IsValid())
79
- {
80
- return false;
81
- }
82
-
83
- // Normalize alpha: editor backbuffer alpha can be 0/undefined, which can make PNGs look dark/grey in viewers.
84
- for (FColor& Pixel : Bitmap)
85
- {
86
- Pixel.A = 255;
87
- }
88
-
89
- if (!Wrapper->SetRaw(Bitmap.GetData(), Bitmap.Num() * sizeof(FColor), Width, Height, ERGBFormat::BGRA, 8))
90
- {
91
- return false;
92
- }
93
-
94
- const TArray64<uint8>& Compressed = Wrapper->GetCompressed(100);
95
- if (Compressed.Num() <= 0 || Compressed.Num() > MAX_int32)
96
- {
97
- return false;
98
- }
99
-
100
- TArray<uint8> Out;
101
- Out.Append(Compressed.GetData(), static_cast<int32>(Compressed.Num()));
102
- return FFileHelper::SaveArrayToFile(Out, *FilePath);
103
- }
104
-
105
- FUnrealMCPEditorCommands::FUnrealMCPEditorCommands()
106
- {
107
- }
108
-
109
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleCommand(const FString& CommandType, const TSharedPtr<FJsonObject>& Params)
110
- {
111
- // Actor manipulation commands
112
- if (CommandType == TEXT("get_actors_in_level"))
113
- {
114
- return HandleGetActorsInLevel(Params);
115
- }
116
- else if (CommandType == TEXT("find_actors_by_name"))
117
- {
118
- return HandleFindActorsByName(Params);
119
- }
120
- else if (CommandType == TEXT("spawn_actor") || CommandType == TEXT("create_actor"))
121
- {
122
- if (CommandType == TEXT("create_actor"))
123
- {
124
- UE_LOG(LogTemp, Warning, TEXT("'create_actor' command is deprecated and will be removed in a future version. Please use 'spawn_actor' instead."));
125
- }
126
- return HandleSpawnActor(Params);
127
- }
128
- else if (CommandType == TEXT("create_landscape"))
129
- {
130
- return HandleCreateLandscape(Params);
131
- }
132
- else if (CommandType == TEXT("delete_actor"))
133
- {
134
- return HandleDeleteActor(Params);
135
- }
136
- else if (CommandType == TEXT("set_actor_transform"))
137
- {
138
- return HandleSetActorTransform(Params);
139
- }
140
- else if (CommandType == TEXT("get_actor_properties"))
141
- {
142
- return HandleGetActorProperties(Params);
143
- }
144
- else if (CommandType == TEXT("set_actor_property"))
145
- {
146
- return HandleSetActorProperty(Params);
147
- }
148
- // Blueprint actor spawning
149
- else if (CommandType == TEXT("spawn_blueprint_actor"))
150
- {
151
- return HandleSpawnBlueprintActor(Params);
152
- }
153
- // Editor viewport commands
154
- else if (CommandType == TEXT("focus_viewport"))
155
- {
156
- return HandleFocusViewport(Params);
157
- }
158
- else if (CommandType == TEXT("take_screenshot"))
159
- {
160
- return HandleTakeScreenshot(Params);
161
- }
162
- else if (CommandType == TEXT("save_all"))
163
- {
164
- return HandleSaveAll(Params);
165
- }
166
- else if (CommandType == TEXT("get_play_in_editor_status"))
167
- {
168
- return HandleGetPlayInEditorStatus(Params);
169
- }
170
- else if (CommandType == TEXT("play_in_editor"))
171
- {
172
- return HandlePlayInEditor(Params);
173
- }
174
- else if (CommandType == TEXT("play_in_editor_windowed"))
175
- {
176
- return HandlePlayInEditorWindowed(Params);
177
- }
178
- else if (CommandType == TEXT("stop_play_in_editor"))
179
- {
180
- return HandleStopPlayInEditor(Params);
181
- }
182
- else if (CommandType == TEXT("search_assets"))
183
- {
184
- return HandleSearchAssets(Params);
185
- }
186
- else if (CommandType == TEXT("get_asset_info"))
187
- {
188
- return HandleGetAssetInfo(Params);
189
- }
190
- else if (CommandType == TEXT("list_asset_packs"))
191
- {
192
- return HandleListAssetPacks(Params);
193
- }
194
- else if (CommandType == TEXT("place_asset"))
195
- {
196
- return HandlePlaceAsset(Params);
197
- }
198
- else if (CommandType == TEXT("map_check"))
199
- {
200
- return HandleMapCheck(Params);
201
- }
202
- else if (CommandType == TEXT("get_editor_context"))
203
- {
204
- return HandleGetEditorContext(Params);
205
- }
206
- else if (CommandType == TEXT("get_player_context"))
207
- {
208
- return HandleGetPlayerContext(Params);
209
- }
210
- else if (CommandType == TEXT("raycast_from_camera"))
211
- {
212
- return HandleRaycastFromCamera(Params);
213
- }
214
- else if (CommandType == TEXT("raycast_down"))
215
- {
216
- return HandleRaycastDown(Params);
217
- }
218
- else if (CommandType == TEXT("get_actor_transform"))
219
- {
220
- return HandleGetActorTransform(Params);
221
- }
222
- else if (CommandType == TEXT("get_actor_bounds"))
223
- {
224
- return HandleGetActorBounds(Params);
225
- }
226
-
227
- return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Unknown editor command: %s"), *CommandType));
228
- }
229
-
230
- static TSharedPtr<FJsonObject> CreatePlayStatusResponse()
231
- {
232
- if (!GEditor)
233
- {
234
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
235
- }
236
-
237
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
238
- ResultObj->SetBoolField(TEXT("isPlaySessionInProgress"), GEditor->IsPlaySessionInProgress());
239
- ResultObj->SetBoolField(TEXT("isPlaySessionRequestQueued"), GEditor->IsPlaySessionRequestQueued());
240
- ResultObj->SetBoolField(TEXT("isPlayingSessionInEditor"), GEditor->IsPlayingSessionInEditor());
241
- return ResultObj;
242
- }
243
-
244
- static bool IsSafeToModifyEditorWorld(FString& OutError)
245
- {
246
- if (!GEditor)
247
- {
248
- OutError = TEXT("Editor is not available (GEditor is null).");
249
- return false;
250
- }
251
-
252
- if (GEditor->IsPlaySessionInProgress())
253
- {
254
- OutError = TEXT("Play-In-Editor is running or queued. Stop PIE before modifying the editor world (use stop_play_in_editor).");
255
- return false;
256
- }
257
-
258
- return true;
259
- }
260
-
261
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSaveAll(const TSharedPtr<FJsonObject>& Params)
262
- {
263
- FString Err;
264
- if (!IsSafeToModifyEditorWorld(Err))
265
- {
266
- return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
267
- }
268
-
269
- TArray<UPackage*> DirtyBefore;
270
- FEditorFileUtils::GetDirtyPackages(DirtyBefore);
271
-
272
- TSet<FString> DirtyBeforeNames;
273
- for (UPackage* Pkg : DirtyBefore)
274
- {
275
- if (!Pkg) continue;
276
- DirtyBeforeNames.Add(Pkg->GetName());
277
- }
278
-
279
- const int32 DirtyBeforeCount = DirtyBeforeNames.Num();
280
-
281
- // Save without prompting; fail-hard if anything remains dirty.
282
- const bool bSaveOk = FEditorFileUtils::SaveDirtyPackages(
283
- /*bPromptUserToSave=*/false,
284
- /*bSaveMapPackages=*/true,
285
- /*bSaveContentPackages=*/true,
286
- /*bFastSave=*/false
287
- );
288
-
289
- TArray<UPackage*> DirtyAfter;
290
- FEditorFileUtils::GetDirtyPackages(DirtyAfter);
291
-
292
- TSet<FString> DirtyAfterNames;
293
- for (UPackage* Pkg : DirtyAfter)
294
- {
295
- if (!Pkg) continue;
296
- DirtyAfterNames.Add(Pkg->GetName());
297
- }
298
-
299
- TArray<TSharedPtr<FJsonValue>> Attempted;
300
- for (const FString& Name : DirtyBeforeNames)
301
- {
302
- Attempted.Add(MakeShareable(new FJsonValueString(Name)));
303
- }
304
-
305
- TArray<TSharedPtr<FJsonValue>> StillDirty;
306
- for (const FString& Name : DirtyAfterNames)
307
- {
308
- StillDirty.Add(MakeShareable(new FJsonValueString(Name)));
309
- }
310
-
311
- TArray<TSharedPtr<FJsonValue>> Saved;
312
- for (const FString& Name : DirtyBeforeNames)
313
- {
314
- if (!DirtyAfterNames.Contains(Name))
315
- {
316
- Saved.Add(MakeShareable(new FJsonValueString(Name)));
317
- }
318
- }
319
-
320
- if (DirtyBeforeCount == 0)
321
- {
322
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
323
- ResultObj->SetBoolField(TEXT("success"), true);
324
- ResultObj->SetNumberField(TEXT("dirtyBefore"), DirtyBeforeCount);
325
- ResultObj->SetNumberField(TEXT("dirtyAfter"), DirtyAfterNames.Num());
326
- ResultObj->SetArrayField(TEXT("attemptedPackages"), Attempted);
327
- ResultObj->SetArrayField(TEXT("savedPackages"), Saved);
328
- ResultObj->SetArrayField(TEXT("stillDirtyPackages"), StillDirty);
329
- ResultObj->SetStringField(TEXT("message"), TEXT("No unsaved changes were detected."));
330
- return ResultObj;
331
- }
332
-
333
- if (DirtyAfterNames.Num() > 0)
334
- {
335
- TSharedPtr<FJsonObject> Details = MakeShared<FJsonObject>();
336
- Details->SetBoolField(TEXT("saveOk"), bSaveOk);
337
- Details->SetNumberField(TEXT("dirtyBefore"), DirtyBeforeCount);
338
- Details->SetNumberField(TEXT("dirtyAfter"), DirtyAfterNames.Num());
339
- Details->SetArrayField(TEXT("attemptedPackages"), Attempted);
340
- Details->SetArrayField(TEXT("savedPackages"), Saved);
341
- Details->SetArrayField(TEXT("stillDirtyPackages"), StillDirty);
342
-
343
- TSharedPtr<FJsonObject> ErrObj = FUnrealMCPCommonUtils::CreateErrorResponse(
344
- 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."));
345
- ErrObj->SetObjectField(TEXT("details"), Details);
346
- return ErrObj;
347
- }
348
-
349
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
350
- ResultObj->SetBoolField(TEXT("success"), true);
351
- ResultObj->SetNumberField(TEXT("dirtyBefore"), DirtyBeforeCount);
352
- ResultObj->SetNumberField(TEXT("dirtyAfter"), DirtyAfterNames.Num());
353
- ResultObj->SetArrayField(TEXT("attemptedPackages"), Attempted);
354
- ResultObj->SetArrayField(TEXT("savedPackages"), Saved);
355
- ResultObj->SetArrayField(TEXT("stillDirtyPackages"), StillDirty);
356
- ResultObj->SetStringField(TEXT("message"), TEXT("Saved all dirty packages."));
357
- return ResultObj;
358
- }
359
-
360
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetPlayInEditorStatus(const TSharedPtr<FJsonObject>& Params)
361
- {
362
- return CreatePlayStatusResponse();
363
- }
364
-
365
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandlePlayInEditor(const TSharedPtr<FJsonObject>& Params)
366
- {
367
- if (!GEditor)
368
- {
369
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
370
- }
371
-
372
- if (GEditor->IsPlaySessionInProgress())
373
- {
374
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Play session already running or queued."));
375
- }
376
-
377
- // Use the active editor viewport, matching how the LevelEditor subsystem triggers PIE.
378
- FLevelEditorModule* LevelEditorModule = FModuleManager::LoadModulePtr<FLevelEditorModule>(TEXT("LevelEditor"));
379
- if (!LevelEditorModule)
380
- {
381
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("LevelEditor module not available."));
382
- }
383
-
384
- TSharedPtr<IAssetViewport> ActiveViewport = LevelEditorModule->GetFirstActiveViewport();
385
- if (!ActiveViewport.IsValid())
386
- {
387
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("No active level viewport. Click the viewport and retry."));
388
- }
389
-
390
- FRequestPlaySessionParams SessionParams;
391
- SessionParams.WorldType = EPlaySessionWorldType::PlayInEditor;
392
- SessionParams.DestinationSlateViewport = ActiveViewport;
393
-
394
- GEditor->RequestPlaySession(SessionParams);
395
- const bool bQueued = GEditor->IsPlaySessionRequestQueued();
396
-
397
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
398
- ResultObj->SetBoolField(TEXT("requested"), true);
399
- ResultObj->SetBoolField(TEXT("queued"), bQueued);
400
- ResultObj->SetBoolField(TEXT("started"), bQueued);
401
- ResultObj->SetStringField(TEXT("mode"), TEXT("pie"));
402
- return ResultObj;
403
- }
404
-
405
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandlePlayInEditorWindowed(const TSharedPtr<FJsonObject>& Params)
406
- {
407
- if (!GEditor)
408
- {
409
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
410
- }
411
-
412
- if (GEditor->IsPlaySessionInProgress())
413
- {
414
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Play session already running or queued."));
415
- }
416
-
417
- // Match the editor's built-in "New Editor Window (PIE)" behavior by setting the play mode type and
418
- // not specifying a DestinationSlateViewport.
419
- ULevelEditorPlaySettings* PlaySettings = GetMutableDefault<ULevelEditorPlaySettings>();
420
- if (PlaySettings)
421
- {
422
- PlaySettings->LastExecutedPlayModeType = EPlayModeType::PlayMode_InEditorFloating;
423
-
424
- if (FProperty* Prop = ULevelEditorPlaySettings::StaticClass()->FindPropertyByName(
425
- GET_MEMBER_NAME_CHECKED(ULevelEditorPlaySettings, LastExecutedPlayModeType)))
426
- {
427
- FPropertyChangedEvent PropChangeEvent(Prop);
428
- PlaySettings->PostEditChangeProperty(PropChangeEvent);
429
- }
430
-
431
- PlaySettings->SaveConfig();
432
- }
433
-
434
- const bool bAtPlayerStart =
435
- PlaySettings && PlaySettings->LastExecutedPlayModeLocation == EPlayModeLocations::PlayLocation_DefaultPlayerStart;
436
-
437
- FRequestPlaySessionParams SessionParams;
438
- SessionParams.WorldType = EPlaySessionWorldType::PlayInEditor;
439
-
440
- // If the user is playing from current camera location, use the active viewport camera as the start transform.
441
- if (!bAtPlayerStart)
442
- {
443
- FLevelEditorModule* LevelEditorModule = FModuleManager::LoadModulePtr<FLevelEditorModule>(TEXT("LevelEditor"));
444
- if (LevelEditorModule)
445
- {
446
- TSharedPtr<IAssetViewport> ActiveViewport = LevelEditorModule->GetFirstActiveViewport();
447
- if (ActiveViewport.IsValid() && FSlateApplication::IsInitialized() &&
448
- FSlateApplication::Get().FindWidgetWindow(ActiveViewport->AsWidget()).IsValid())
449
- {
450
- SessionParams.StartLocation = ActiveViewport->GetAssetViewportClient().GetViewLocation();
451
- SessionParams.StartRotation = ActiveViewport->GetAssetViewportClient().GetViewRotation();
452
- }
453
- }
454
- }
455
-
456
- GEditor->RequestPlaySession(SessionParams);
457
- const bool bQueued = GEditor->IsPlaySessionRequestQueued();
458
-
459
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
460
- ResultObj->SetBoolField(TEXT("requested"), true);
461
- ResultObj->SetBoolField(TEXT("queued"), bQueued);
462
- ResultObj->SetBoolField(TEXT("started"), bQueued);
463
- ResultObj->SetStringField(TEXT("mode"), TEXT("pie_new_window"));
464
- return ResultObj;
465
- }
466
-
467
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleStopPlayInEditor(const TSharedPtr<FJsonObject>& Params)
468
- {
469
- if (!GEditor)
470
- {
471
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
472
- }
473
-
474
- if (GEditor->IsPlaySessionRequestQueued() && !GEditor->IsPlayingSessionInEditor())
475
- {
476
- GEditor->CancelRequestPlaySession();
477
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
478
- ResultObj->SetBoolField(TEXT("stopped"), true);
479
- ResultObj->SetBoolField(TEXT("canceled"), true);
480
- return ResultObj;
481
- }
482
-
483
- if (!GEditor->IsPlayingSessionInEditor())
484
- {
485
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("No play session is running."));
486
- }
487
-
488
- GEditor->RequestEndPlayMap();
489
-
490
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
491
- ResultObj->SetBoolField(TEXT("stopped"), true);
492
- return ResultObj;
493
- }
494
-
495
- static FString NormalizeContentRoot(const FString& Root)
496
- {
497
- FString Out = Root.TrimStartAndEnd();
498
- if (Out.IsEmpty())
499
- {
500
- return TEXT("/Game/");
501
- }
502
- if (!Out.StartsWith(TEXT("/")))
503
- {
504
- Out = TEXT("/") + Out;
505
- }
506
- if (!Out.EndsWith(TEXT("/")))
507
- {
508
- Out += TEXT("/");
509
- }
510
- return Out;
511
- }
512
-
513
- static bool TryResolveAssetClassFilter(const TSharedPtr<FJsonObject>& Params, FARFilter& Filter, FString& OutError)
514
- {
515
- if (!Params.IsValid() || !Params->HasField(TEXT("class")))
516
- {
517
- return true;
518
- }
519
-
520
- TArray<FString> ClassNames;
521
-
522
- FString Single;
523
- if (Params->TryGetStringField(TEXT("class"), Single))
524
- {
525
- Single = Single.TrimStartAndEnd();
526
- if (!Single.IsEmpty())
527
- {
528
- ClassNames.Add(Single);
529
- }
530
- }
531
- else
532
- {
533
- const TArray<TSharedPtr<FJsonValue>>* Arr = nullptr;
534
- if (Params->TryGetArrayField(TEXT("class"), Arr) && Arr)
535
- {
536
- for (const TSharedPtr<FJsonValue>& V : *Arr)
537
- {
538
- if (!V.IsValid() || V->Type != EJson::String) continue;
539
- FString S = V->AsString().TrimStartAndEnd();
540
- if (!S.IsEmpty()) ClassNames.Add(S);
541
- }
542
- }
543
- }
544
-
545
- for (const FString& ClassNameRaw : ClassNames)
546
- {
547
- const FString ClassName = ClassNameRaw.TrimStartAndEnd();
548
- if (ClassName.IsEmpty()) continue;
549
-
550
- UClass* Class = nullptr;
551
-
552
- if (ClassName.Equals(TEXT("StaticMesh"), ESearchCase::IgnoreCase) || ClassName.Equals(TEXT("UStaticMesh"), ESearchCase::IgnoreCase))
553
- {
554
- Class = UStaticMesh::StaticClass();
555
- }
556
- else if (ClassName.Equals(TEXT("Material"), ESearchCase::IgnoreCase) || ClassName.Equals(TEXT("MaterialInterface"), ESearchCase::IgnoreCase))
557
- {
558
- Class = UMaterialInterface::StaticClass();
559
- }
560
- else if (ClassName.Equals(TEXT("Texture2D"), ESearchCase::IgnoreCase))
561
- {
562
- Class = UTexture2D::StaticClass();
563
- }
564
- else if (ClassName.Equals(TEXT("Blueprint"), ESearchCase::IgnoreCase) || ClassName.Equals(TEXT("UBlueprint"), ESearchCase::IgnoreCase))
565
- {
566
- Class = UBlueprint::StaticClass();
567
- }
568
- else if (ClassName.Equals(TEXT("SkeletalMesh"), ESearchCase::IgnoreCase))
569
- {
570
- Class = USkeletalMesh::StaticClass();
571
- }
572
- else if (ClassName.Equals(TEXT("SoundWave"), ESearchCase::IgnoreCase))
573
- {
574
- Class = USoundWave::StaticClass();
575
- }
576
- else
577
- {
578
- Class = FindMcpClassByName(ClassName);
579
- if (!Class && !ClassName.StartsWith(TEXT("U")))
580
- {
581
- Class = FindMcpClassByName(TEXT("U") + ClassName);
582
- }
583
- }
584
-
585
- if (!Class)
586
- {
587
- OutError = FString::Printf(TEXT("Unknown asset class filter: %s"), *ClassNameRaw);
588
- return false;
589
- }
590
-
591
- Filter.ClassPaths.Add(Class->GetClassPathName());
592
- }
593
-
594
- return true;
595
- }
596
-
597
- static int32 ScoreAssetMatch(const FString& Query, const FAssetData& Asset)
598
- {
599
- if (Query.IsEmpty()) return 0;
600
-
601
- const FString Name = Asset.AssetName.ToString();
602
- const FString Path = Asset.GetObjectPathString();
603
-
604
- if (Name.Equals(Query, ESearchCase::IgnoreCase)) return 0;
605
- if (Name.StartsWith(Query, ESearchCase::IgnoreCase)) return 1;
606
- if (Path.EndsWith(Query, ESearchCase::IgnoreCase)) return 2;
607
- if (Name.Contains(Query, ESearchCase::IgnoreCase)) return 3;
608
- if (Path.Contains(Query, ESearchCase::IgnoreCase)) return 4;
609
- return 100;
610
- }
611
-
612
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSearchAssets(const TSharedPtr<FJsonObject>& Params)
613
- {
614
- FString Root = TEXT("/Game/");
615
- Params->TryGetStringField(TEXT("root"), Root);
616
- Root = NormalizeContentRoot(Root);
617
-
618
- FString Query;
619
- Params->TryGetStringField(TEXT("query"), Query);
620
- Query = Query.TrimStartAndEnd();
621
-
622
- int32 Limit = 25;
623
- if (Params->HasField(TEXT("limit")))
624
- {
625
- Limit = (int32)Params->GetNumberField(TEXT("limit"));
626
- }
627
- Limit = FMath::Clamp(Limit, 1, 200);
628
-
629
- FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
630
- IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
631
-
632
- FARFilter Filter;
633
- Filter.PackagePaths.Add(*Root);
634
- Filter.bRecursivePaths = true;
635
-
636
- FString ClassErr;
637
- if (!TryResolveAssetClassFilter(Params, Filter, ClassErr))
638
- {
639
- return FUnrealMCPCommonUtils::CreateErrorResponse(ClassErr);
640
- }
641
-
642
- TArray<FAssetData> Assets;
643
- AssetRegistry.GetAssets(Filter, Assets);
644
-
645
- if (!Query.IsEmpty())
646
- {
647
- Assets.RemoveAll([&Query](const FAssetData& Asset) {
648
- const FString Name = Asset.AssetName.ToString();
649
- const FString Path = Asset.GetObjectPathString();
650
- return !(Name.Contains(Query, ESearchCase::IgnoreCase) || Path.Contains(Query, ESearchCase::IgnoreCase));
651
- });
652
-
653
- Assets.Sort([&Query](const FAssetData& A, const FAssetData& B) {
654
- const int32 SA = ScoreAssetMatch(Query, A);
655
- const int32 SB = ScoreAssetMatch(Query, B);
656
- if (SA != SB) return SA < SB;
657
- return A.AssetName.LexicalLess(B.AssetName);
658
- });
659
- }
660
- else
661
- {
662
- Assets.Sort([](const FAssetData& A, const FAssetData& B) {
663
- return A.AssetName.LexicalLess(B.AssetName);
664
- });
665
- }
666
-
667
- const int32 Total = Assets.Num();
668
- if (Assets.Num() > Limit)
669
- {
670
- Assets.SetNum(Limit);
671
- }
672
-
673
- TArray<TSharedPtr<FJsonValue>> OutAssets;
674
- OutAssets.Reserve(Assets.Num());
675
- for (const FAssetData& Asset : Assets)
676
- {
677
- TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
678
- Obj->SetStringField(TEXT("name"), Asset.AssetName.ToString());
679
- Obj->SetStringField(TEXT("objectPath"), Asset.GetObjectPathString());
680
- Obj->SetStringField(TEXT("packagePath"), Asset.PackagePath.ToString());
681
- Obj->SetStringField(TEXT("class"), Asset.AssetClassPath.ToString());
682
- OutAssets.Add(MakeShared<FJsonValueObject>(Obj));
683
- }
684
-
685
- TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
686
- Result->SetStringField(TEXT("root"), Root);
687
- Result->SetStringField(TEXT("query"), Query);
688
- Result->SetNumberField(TEXT("count"), OutAssets.Num());
689
- Result->SetNumberField(TEXT("totalMatches"), Total);
690
- Result->SetArrayField(TEXT("assets"), OutAssets);
691
- return Result;
692
- }
693
-
694
- static FString InferPlacementKindFromAssetClassPath(const FString& ClassPath)
695
- {
696
- if (ClassPath.Contains(TEXT("StaticMesh"), ESearchCase::IgnoreCase)) return TEXT("static_mesh");
697
- if (ClassPath.Contains(TEXT("Blueprint"), ESearchCase::IgnoreCase)) return TEXT("blueprint");
698
- if (ClassPath.Contains(TEXT("Material"), ESearchCase::IgnoreCase)) return TEXT("material");
699
- if (ClassPath.Contains(TEXT("Texture"), ESearchCase::IgnoreCase)) return TEXT("texture");
700
- return TEXT("unknown");
701
- }
702
-
703
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetAssetInfo(const TSharedPtr<FJsonObject>& Params)
704
- {
705
- FString ObjectPath;
706
- if (!Params->TryGetStringField(TEXT("objectPath"), ObjectPath) && !Params->TryGetStringField(TEXT("object_path"), ObjectPath))
707
- {
708
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'objectPath' parameter"));
709
- }
710
- ObjectPath = ObjectPath.TrimStartAndEnd();
711
- if (ObjectPath.IsEmpty())
712
- {
713
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("objectPath is empty"));
714
- }
715
-
716
- FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
717
- IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
718
-
719
- const FAssetData Asset = AssetRegistry.GetAssetByObjectPath(FSoftObjectPath(ObjectPath));
720
- if (!Asset.IsValid())
721
- {
722
- return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Asset not found: %s"), *ObjectPath));
723
- }
724
-
725
- const FString ClassPath = Asset.AssetClassPath.ToString();
726
-
727
- TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
728
- Result->SetStringField(TEXT("name"), Asset.AssetName.ToString());
729
- Result->SetStringField(TEXT("objectPath"), Asset.GetObjectPathString());
730
- Result->SetStringField(TEXT("packageName"), Asset.PackageName.ToString());
731
- Result->SetStringField(TEXT("packagePath"), Asset.PackagePath.ToString());
732
- Result->SetStringField(TEXT("class"), ClassPath);
733
- Result->SetStringField(TEXT("placementKind"), InferPlacementKindFromAssetClassPath(ClassPath));
734
-
735
- if (Params->HasField(TEXT("includeDependencies")) && Params->GetBoolField(TEXT("includeDependencies")))
736
- {
737
- TArray<FName> Deps;
738
- AssetRegistry.GetDependencies(Asset.PackageName, Deps);
739
- TArray<TSharedPtr<FJsonValue>> DepArr;
740
- DepArr.Reserve(Deps.Num());
741
- for (const FName& Dep : Deps)
742
- {
743
- DepArr.Add(MakeShared<FJsonValueString>(Dep.ToString()));
744
- }
745
- Result->SetArrayField(TEXT("dependencies"), DepArr);
746
- }
747
-
748
- return Result;
749
- }
750
-
751
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleListAssetPacks(const TSharedPtr<FJsonObject>& Params)
752
- {
753
- int32 Limit = 200;
754
- if (Params->HasField(TEXT("limit")))
755
- {
756
- Limit = (int32)Params->GetNumberField(TEXT("limit"));
757
- }
758
- Limit = FMath::Clamp(Limit, 1, 500);
759
-
760
- FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
761
- IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
762
-
763
- TArray<FString> SubPaths;
764
- AssetRegistry.GetSubPaths(TEXT("/Game"), SubPaths, false);
765
- SubPaths.Sort();
766
-
767
- TArray<TSharedPtr<FJsonValue>> Packs;
768
- for (const FString& Path : SubPaths)
769
- {
770
- if (Packs.Num() >= Limit) break;
771
- if (Path.StartsWith(TEXT("/Game/__External"), ESearchCase::IgnoreCase)) continue;
772
- if (Path.StartsWith(TEXT("/Game/Developers"), ESearchCase::IgnoreCase)) continue;
773
-
774
- FString Name = Path;
775
- int32 SlashIndex;
776
- if (Name.FindLastChar(TEXT('/'), SlashIndex))
777
- {
778
- Name = Name.Mid(SlashIndex + 1);
779
- }
780
-
781
- TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
782
- Obj->SetStringField(TEXT("name"), Name);
783
- Obj->SetStringField(TEXT("path"), Path + TEXT("/"));
784
- Packs.Add(MakeShared<FJsonValueObject>(Obj));
785
- }
786
-
787
- TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
788
- Result->SetNumberField(TEXT("count"), Packs.Num());
789
- Result->SetArrayField(TEXT("packs"), Packs);
790
- return Result;
791
- }
792
-
793
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandlePlaceAsset(const TSharedPtr<FJsonObject>& Params)
794
- {
795
- {
796
- FString Err;
797
- if (!IsSafeToModifyEditorWorld(Err))
798
- {
799
- return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
800
- }
801
- }
802
-
803
- FString ObjectPath;
804
- if (!Params->TryGetStringField(TEXT("objectPath"), ObjectPath) && !Params->TryGetStringField(TEXT("object_path"), ObjectPath))
805
- {
806
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'objectPath' parameter"));
807
- }
808
- ObjectPath = ObjectPath.TrimStartAndEnd();
809
- if (ObjectPath.IsEmpty())
810
- {
811
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("objectPath is empty"));
812
- }
813
-
814
- FVector Location(0.0f, 0.0f, 0.0f);
815
- FRotator Rotation(0.0f, 0.0f, 0.0f);
816
- FVector Scale(1.0f, 1.0f, 1.0f);
817
-
818
- if (Params->HasField(TEXT("location")))
819
- {
820
- Location = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("location"));
821
- }
822
- if (Params->HasField(TEXT("rotation")))
823
- {
824
- Rotation = FUnrealMCPCommonUtils::GetRotatorFromJson(Params, TEXT("rotation"));
825
- }
826
- if (Params->HasField(TEXT("scale")))
827
- {
828
- Scale = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("scale"));
829
- }
830
-
831
- UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
832
- if (!World)
833
- {
834
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world"));
835
- }
836
-
837
- FString AssetClassPath;
838
- FString AssetName;
839
- {
840
- FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
841
- IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
842
- const FAssetData Asset = AssetRegistry.GetAssetByObjectPath(FSoftObjectPath(ObjectPath));
843
- if (Asset.IsValid())
844
- {
845
- AssetClassPath = Asset.AssetClassPath.ToString();
846
- AssetName = Asset.AssetName.ToString();
847
- }
848
- }
849
-
850
- FString RequestedName;
851
- Params->TryGetStringField(TEXT("name"), RequestedName);
852
- RequestedName = RequestedName.TrimStartAndEnd();
853
- if (RequestedName.IsEmpty())
854
- {
855
- RequestedName = !AssetName.IsEmpty() ? AssetName : TEXT("PlacedAsset");
856
- }
857
-
858
- AActor* NewActor = nullptr;
859
- FString PlacementKind = !AssetClassPath.IsEmpty() ? InferPlacementKindFromAssetClassPath(AssetClassPath) : TEXT("unknown");
860
-
861
- UObject* AssetObj = LoadObject<UObject>(nullptr, *ObjectPath);
862
- if (!AssetObj && !ObjectPath.Contains(TEXT("'")))
863
- {
864
- AssetObj = LoadObject<UObject>(nullptr, *FString::Printf(TEXT("StaticMesh'%s'"), *ObjectPath));
865
- if (!AssetObj)
866
- {
867
- AssetObj = LoadObject<UObject>(nullptr, *FString::Printf(TEXT("Blueprint'%s'"), *ObjectPath));
868
- }
869
- }
870
-
871
- if (!AssetObj)
872
- {
873
- return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Failed to load asset: %s"), *ObjectPath));
874
- }
875
-
876
- FActorSpawnParameters SpawnParams;
877
- SpawnParams.Name = MakeUniqueObjectName(World, AActor::StaticClass(), FName(*RequestedName));
878
-
879
- if (UStaticMesh* StaticMesh = Cast<UStaticMesh>(AssetObj))
880
- {
881
- PlacementKind = TEXT("static_mesh");
882
- AStaticMeshActor* StaticMeshActor = World->SpawnActor<AStaticMeshActor>(AStaticMeshActor::StaticClass(), Location, Rotation, SpawnParams);
883
- if (!StaticMeshActor || !StaticMeshActor->GetStaticMeshComponent())
884
- {
885
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to spawn StaticMeshActor"));
886
- }
887
-
888
- StaticMeshActor->GetStaticMeshComponent()->SetStaticMesh(StaticMesh);
889
- StaticMeshActor->GetStaticMeshComponent()->MarkRenderStateDirty();
890
- NewActor = StaticMeshActor;
891
- }
892
- else if (UBlueprint* Blueprint = Cast<UBlueprint>(AssetObj))
893
- {
894
- PlacementKind = TEXT("blueprint");
895
- if (!Blueprint->GeneratedClass)
896
- {
897
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Blueprint has no GeneratedClass (compile it and retry)."));
898
- }
899
- const FTransform SpawnTransform(FQuat(Rotation), Location, Scale);
900
- NewActor = World->SpawnActor<AActor>(Blueprint->GeneratedClass, SpawnTransform, SpawnParams);
901
- }
902
- else if (UBlueprintGeneratedClass* GeneratedClass = Cast<UBlueprintGeneratedClass>(AssetObj))
903
- {
904
- PlacementKind = TEXT("blueprint");
905
- const FTransform SpawnTransform(FQuat(Rotation), Location, Scale);
906
- NewActor = World->SpawnActor<AActor>(GeneratedClass, SpawnTransform, SpawnParams);
907
- }
908
- else if (UClass* Class = Cast<UClass>(AssetObj))
909
- {
910
- if (!Class->IsChildOf(AActor::StaticClass()))
911
- {
912
- return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Loaded class is not an Actor class: %s"), *Class->GetName()));
913
- }
914
- PlacementKind = TEXT("blueprint");
915
- const FTransform SpawnTransform(FQuat(Rotation), Location, Scale);
916
- NewActor = World->SpawnActor<AActor>(Class, SpawnTransform, SpawnParams);
917
- }
918
- else
919
- {
920
- const FString ClassName = AssetObj->GetClass() ? AssetObj->GetClass()->GetName() : TEXT("UnknownClass");
921
- return FUnrealMCPCommonUtils::CreateErrorResponse(
922
- FString::Printf(TEXT("Asset is not placeable (class: %s). Placeable: StaticMesh, Blueprint. Asset: %s"), *ClassName, *ObjectPath));
923
- }
924
-
925
- if (!NewActor)
926
- {
927
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to spawn actor for asset."));
928
- }
929
-
930
- NewActor->SetActorScale3D(Scale);
931
-
932
- TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
933
- Result->SetStringField(TEXT("placementKind"), PlacementKind);
934
- Result->SetStringField(TEXT("requestedObjectPath"), ObjectPath);
935
- Result->SetStringField(TEXT("spawnedName"), NewActor->GetName());
936
- Result->SetObjectField(TEXT("actor"), FUnrealMCPCommonUtils::ActorToJsonObject(NewActor, true));
937
- return Result;
938
- }
939
-
940
- static FString MessageSeverityToString(EMessageSeverity::Type Severity)
941
- {
942
- switch (Severity)
943
- {
944
- case EMessageSeverity::CriticalError:
945
- case EMessageSeverity::Error:
946
- return TEXT("error");
947
- case EMessageSeverity::Warning:
948
- case EMessageSeverity::PerformanceWarning:
949
- return TEXT("warning");
950
- case EMessageSeverity::Info:
951
- return TEXT("info");
952
- default:
953
- return TEXT("info");
954
- }
955
- }
956
-
957
- static TArray<TSharedPtr<FJsonValue>> VectorToJsonArray(const FVector& V)
958
- {
959
- TArray<TSharedPtr<FJsonValue>> Arr;
960
- Arr.Add(MakeShared<FJsonValueNumber>(V.X));
961
- Arr.Add(MakeShared<FJsonValueNumber>(V.Y));
962
- Arr.Add(MakeShared<FJsonValueNumber>(V.Z));
963
- return Arr;
964
- }
965
-
966
- static TArray<TSharedPtr<FJsonValue>> RotatorToJsonArray(const FRotator& R)
967
- {
968
- TArray<TSharedPtr<FJsonValue>> Arr;
969
- Arr.Add(MakeShared<FJsonValueNumber>(R.Pitch));
970
- Arr.Add(MakeShared<FJsonValueNumber>(R.Yaw));
971
- Arr.Add(MakeShared<FJsonValueNumber>(R.Roll));
972
- return Arr;
973
- }
974
-
975
- static AActor* FindActorByNameOrLabel(UWorld* World, const FString& NameOrLabel)
976
- {
977
- if (!World) return nullptr;
978
- if (NameOrLabel.IsEmpty()) return nullptr;
979
-
980
- TArray<AActor*> AllActors;
981
- UGameplayStatics::GetAllActorsOfClass(World, AActor::StaticClass(), AllActors);
982
- for (AActor* Actor : AllActors)
983
- {
984
- if (!Actor) continue;
985
- if (Actor->GetName().Equals(NameOrLabel, ESearchCase::CaseSensitive) ||
986
- Actor->GetName().Equals(NameOrLabel, ESearchCase::IgnoreCase))
987
- {
988
- return Actor;
989
- }
990
- #if WITH_EDITOR
991
- if (Actor->GetActorLabel().Equals(NameOrLabel, ESearchCase::IgnoreCase))
992
- {
993
- return Actor;
994
- }
995
- #endif
996
- }
997
-
998
- return nullptr;
999
- }
1000
-
1001
- static TSharedPtr<FJsonObject> RaycastInWorld(UWorld* World, const FVector& Start, const FVector& Dir, float MaxDistance, const TArray<FString>& IgnoreActorNames)
1002
- {
1003
- if (!World)
1004
- {
1005
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("No world available for raycast."));
1006
- }
1007
-
1008
- const FVector Direction = Dir.GetSafeNormal();
1009
- if (Direction.IsNearlyZero())
1010
- {
1011
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Raycast direction is zero."));
1012
- }
1013
-
1014
- MaxDistance = FMath::Clamp(MaxDistance, 1.0f, 1.0e7f);
1015
- const FVector End = Start + Direction * MaxDistance;
1016
-
1017
- FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(UnrealMCP_Raycast), true);
1018
- QueryParams.bReturnPhysicalMaterial = false;
1019
-
1020
- for (const FString& Name : IgnoreActorNames)
1021
- {
1022
- if (AActor* Actor = FindActorByNameOrLabel(World, Name))
1023
- {
1024
- QueryParams.AddIgnoredActor(Actor);
1025
- }
1026
- }
1027
-
1028
- FHitResult Hit;
1029
- const bool bHit = World->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, QueryParams);
1030
-
1031
- TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1032
- Result->SetBoolField(TEXT("hit"), bHit);
1033
- Result->SetArrayField(TEXT("start"), VectorToJsonArray(Start));
1034
- Result->SetArrayField(TEXT("end"), VectorToJsonArray(End));
1035
- Result->SetNumberField(TEXT("maxDistance"), MaxDistance);
1036
-
1037
- if (!bHit)
1038
- {
1039
- return Result;
1040
- }
1041
-
1042
- Result->SetArrayField(TEXT("location"), VectorToJsonArray(Hit.ImpactPoint));
1043
- Result->SetArrayField(TEXT("normal"), VectorToJsonArray(Hit.ImpactNormal));
1044
- Result->SetNumberField(TEXT("distance"), Hit.Distance);
1045
- if (AActor* HitActor = Hit.GetActor())
1046
- {
1047
- Result->SetStringField(TEXT("actor"), HitActor->GetName());
1048
- #if WITH_EDITOR
1049
- Result->SetStringField(TEXT("actorLabel"), HitActor->GetActorLabel());
1050
- #endif
1051
- }
1052
- if (UPrimitiveComponent* HitComp = Hit.GetComponent())
1053
- {
1054
- Result->SetStringField(TEXT("component"), HitComp->GetName());
1055
- }
1056
-
1057
- return Result;
1058
- }
1059
-
1060
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleMapCheck(const TSharedPtr<FJsonObject>& Params)
1061
- {
1062
- {
1063
- FString Err;
1064
- if (!IsSafeToModifyEditorWorld(Err))
1065
- {
1066
- return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
1067
- }
1068
- }
1069
-
1070
- const bool bIncludeWarnings = !Params->HasField(TEXT("includeWarnings")) || Params->GetBoolField(TEXT("includeWarnings"));
1071
- int32 Limit = 200;
1072
- if (Params->HasField(TEXT("limit")))
1073
- {
1074
- Limit = (int32)Params->GetNumberField(TEXT("limit"));
1075
- }
1076
- Limit = FMath::Clamp(Limit, 1, 1000);
1077
-
1078
- if (!GEditor)
1079
- {
1080
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
1081
- }
1082
-
1083
- UWorld* World = GEditor->GetEditorWorldContext().World();
1084
- if (!World)
1085
- {
1086
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world"));
1087
- }
1088
-
1089
- FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>(TEXT("MessageLog"));
1090
- const TSharedRef<IMessageLogListing> Listing = MessageLogModule.GetLogListing(TEXT("MapCheck"));
1091
- Listing->ClearMessages();
1092
-
1093
- FOutputDeviceNull Ar;
1094
- // Use the editor's exec pathway so we can request "don't display dialog" without relying on private enums.
1095
- const bool bOk = GEditor->Exec(World, TEXT("MAP CHECK DONTDISPLAYDIALOG"), Ar);
1096
- if (!bOk)
1097
- {
1098
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to run Map Check."));
1099
- }
1100
-
1101
- const TArray<TSharedRef<FTokenizedMessage>>& Messages = Listing->GetFilteredMessages();
1102
- int32 ErrorCount = 0;
1103
- int32 WarningCount = 0;
1104
- int32 IncludedMessageCount = 0;
1105
- bool bTruncated = false;
1106
-
1107
- TArray<TSharedPtr<FJsonValue>> Issues;
1108
- for (const TSharedRef<FTokenizedMessage>& Message : Messages)
1109
- {
1110
- const EMessageSeverity::Type Severity = Message->GetSeverity();
1111
- const bool bIsWarning =
1112
- Severity == EMessageSeverity::Warning || Severity == EMessageSeverity::PerformanceWarning;
1113
- const bool bIsError = Severity == EMessageSeverity::Error || Severity == EMessageSeverity::CriticalError;
1114
-
1115
- if (bIsError) ErrorCount++;
1116
- if (bIsWarning) WarningCount++;
1117
-
1118
- if (!bIncludeWarnings && bIsWarning) continue;
1119
-
1120
- IncludedMessageCount++;
1121
- if (Issues.Num() >= Limit)
1122
- {
1123
- bTruncated = true;
1124
- continue;
1125
- }
1126
-
1127
- TSharedPtr<FJsonObject> Issue = MakeShared<FJsonObject>();
1128
- Issue->SetStringField(TEXT("severity"), MessageSeverityToString(Severity));
1129
- Issue->SetStringField(TEXT("message"), Message->ToText().ToString());
1130
- Issues.Add(MakeShared<FJsonValueObject>(Issue));
1131
- }
1132
-
1133
- TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1134
- Result->SetBoolField(TEXT("ok"), ErrorCount == 0);
1135
- Result->SetNumberField(TEXT("errors"), ErrorCount);
1136
- Result->SetNumberField(TEXT("warnings"), WarningCount);
1137
- Result->SetBoolField(TEXT("includeWarnings"), bIncludeWarnings);
1138
- Result->SetArrayField(TEXT("issues"), Issues);
1139
- Result->SetBoolField(TEXT("truncated"), bTruncated || IncludedMessageCount > Issues.Num());
1140
- return Result;
1141
- }
1142
-
1143
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetEditorContext(const TSharedPtr<FJsonObject>& Params)
1144
- {
1145
- if (!GEditor)
1146
- {
1147
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
1148
- }
1149
-
1150
- UWorld* World = GEditor->GetEditorWorldContext().World();
1151
- if (!World)
1152
- {
1153
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world"));
1154
- }
1155
-
1156
- const FDateTime Now = FDateTime::UtcNow();
1157
- const int64 TimestampMs = Now.ToUnixTimestamp() * 1000 + Now.GetMillisecond();
1158
-
1159
- TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1160
- Result->SetNumberField(TEXT("timestampMs"), (double)TimestampMs);
1161
-
1162
- const FString MapPackage = World->GetOutermost() ? World->GetOutermost()->GetName() : World->GetMapName();
1163
- Result->SetStringField(TEXT("map"), MapPackage);
1164
-
1165
- // Selection (editor)
1166
- TArray<TSharedPtr<FJsonValue>> Selection;
1167
- if (USelection* SelectedActors = GEditor->GetSelectedActors())
1168
- {
1169
- for (FSelectionIterator It(*SelectedActors); It; ++It)
1170
- {
1171
- if (AActor* Actor = Cast<AActor>(*It))
1172
- {
1173
- TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
1174
- Obj->SetStringField(TEXT("name"), Actor->GetName());
1175
- #if WITH_EDITOR
1176
- Obj->SetStringField(TEXT("label"), Actor->GetActorLabel());
1177
- #endif
1178
- Obj->SetArrayField(TEXT("location"), VectorToJsonArray(Actor->GetActorLocation()));
1179
- Obj->SetArrayField(TEXT("rotation"), RotatorToJsonArray(Actor->GetActorRotation()));
1180
- Selection.Add(MakeShared<FJsonValueObject>(Obj));
1181
- }
1182
- }
1183
- }
1184
- Result->SetArrayField(TEXT("selection"), Selection);
1185
-
1186
- // Active viewport camera (best-effort)
1187
- TSharedPtr<FJsonObject> ViewportCamera;
1188
- FLevelEditorModule* LevelEditorModule = FModuleManager::LoadModulePtr<FLevelEditorModule>(TEXT("LevelEditor"));
1189
- if (LevelEditorModule)
1190
- {
1191
- TSharedPtr<IAssetViewport> ActiveViewport = LevelEditorModule->GetFirstActiveViewport();
1192
- if (ActiveViewport.IsValid())
1193
- {
1194
- const FEditorViewportClient& VC = ActiveViewport->GetAssetViewportClient();
1195
- ViewportCamera = MakeShared<FJsonObject>();
1196
- ViewportCamera->SetArrayField(TEXT("location"), VectorToJsonArray(VC.GetViewLocation()));
1197
- ViewportCamera->SetArrayField(TEXT("rotation"), RotatorToJsonArray(VC.GetViewRotation()));
1198
- ViewportCamera->SetNumberField(TEXT("fov"), VC.ViewFOV);
1199
- ViewportCamera->SetArrayField(TEXT("forward"), VectorToJsonArray(VC.GetViewRotation().Vector()));
1200
- }
1201
- }
1202
- if (ViewportCamera.IsValid())
1203
- {
1204
- Result->SetObjectField(TEXT("viewportCamera"), ViewportCamera);
1205
- }
1206
- else
1207
- {
1208
- Result->SetField(TEXT("viewportCamera"), MakeShared<FJsonValueNull>());
1209
- }
1210
-
1211
- return Result;
1212
- }
1213
-
1214
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetPlayerContext(const TSharedPtr<FJsonObject>& Params)
1215
- {
1216
- if (!GEditor)
1217
- {
1218
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
1219
- }
1220
-
1221
- UWorld* PlayWorld = GEditor->PlayWorld;
1222
- const bool bIsPlaying = PlayWorld != nullptr;
1223
-
1224
- TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1225
- Result->SetBoolField(TEXT("isPlaying"), bIsPlaying);
1226
-
1227
- if (!bIsPlaying)
1228
- {
1229
- Result->SetField(TEXT("pawn"), MakeShared<FJsonValueNull>());
1230
- Result->SetField(TEXT("camera"), MakeShared<FJsonValueNull>());
1231
- return Result;
1232
- }
1233
-
1234
- APlayerController* PC = PlayWorld->GetFirstPlayerController();
1235
- if (!PC)
1236
- {
1237
- Result->SetField(TEXT("pawn"), MakeShared<FJsonValueNull>());
1238
- Result->SetField(TEXT("camera"), MakeShared<FJsonValueNull>());
1239
- return Result;
1240
- }
1241
-
1242
- Result->SetStringField(TEXT("controller"), PC->GetName());
1243
-
1244
- if (APawn* Pawn = PC->GetPawn())
1245
- {
1246
- TSharedPtr<FJsonObject> PawnObj = MakeShared<FJsonObject>();
1247
- PawnObj->SetStringField(TEXT("name"), Pawn->GetName());
1248
- PawnObj->SetArrayField(TEXT("location"), VectorToJsonArray(Pawn->GetActorLocation()));
1249
- PawnObj->SetArrayField(TEXT("rotation"), RotatorToJsonArray(Pawn->GetActorRotation()));
1250
- PawnObj->SetArrayField(TEXT("scale"), VectorToJsonArray(Pawn->GetActorScale3D()));
1251
- Result->SetObjectField(TEXT("pawn"), PawnObj);
1252
- }
1253
- else
1254
- {
1255
- Result->SetField(TEXT("pawn"), MakeShared<FJsonValueNull>());
1256
- }
1257
-
1258
- FVector CamLoc;
1259
- FRotator CamRot;
1260
- PC->GetPlayerViewPoint(CamLoc, CamRot);
1261
-
1262
- TSharedPtr<FJsonObject> CamObj = MakeShared<FJsonObject>();
1263
- CamObj->SetArrayField(TEXT("location"), VectorToJsonArray(CamLoc));
1264
- CamObj->SetArrayField(TEXT("rotation"), RotatorToJsonArray(CamRot));
1265
- CamObj->SetArrayField(TEXT("forward"), VectorToJsonArray(CamRot.Vector()));
1266
- Result->SetObjectField(TEXT("camera"), CamObj);
1267
-
1268
- return Result;
1269
- }
1270
-
1271
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleRaycastFromCamera(const TSharedPtr<FJsonObject>& Params)
1272
- {
1273
- const FString Source = Params->HasField(TEXT("source")) ? Params->GetStringField(TEXT("source")).ToLower() : TEXT("editor");
1274
- const float MaxDistance = Params->HasField(TEXT("maxDistance")) ? (float)Params->GetNumberField(TEXT("maxDistance")) : 10000.0f;
1275
-
1276
- TArray<FString> Ignore;
1277
- if (Params->HasField(TEXT("ignoreActors")))
1278
- {
1279
- const TArray<TSharedPtr<FJsonValue>> Arr = Params->GetArrayField(TEXT("ignoreActors"));
1280
- for (const TSharedPtr<FJsonValue>& V : Arr)
1281
- {
1282
- if (V.IsValid() && V->Type == EJson::String)
1283
- {
1284
- const FString S = V->AsString().TrimStartAndEnd();
1285
- if (!S.IsEmpty()) Ignore.Add(S);
1286
- }
1287
- }
1288
- }
1289
-
1290
- if (!GEditor)
1291
- {
1292
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
1293
- }
1294
-
1295
- if (Source == TEXT("pie") || Source == TEXT("player") || Source == TEXT("runtime"))
1296
- {
1297
- UWorld* PlayWorld = GEditor->PlayWorld;
1298
- if (!PlayWorld)
1299
- {
1300
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("PIE is not running (no PlayWorld)."));
1301
- }
1302
- APlayerController* PC = PlayWorld->GetFirstPlayerController();
1303
- if (!PC)
1304
- {
1305
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("PIE is running but no player controller was found."));
1306
- }
1307
- FVector CamLoc;
1308
- FRotator CamRot;
1309
- PC->GetPlayerViewPoint(CamLoc, CamRot);
1310
- return RaycastInWorld(PlayWorld, CamLoc, CamRot.Vector(), MaxDistance, Ignore);
1311
- }
1312
-
1313
- // Default: editor viewport camera.
1314
- FLevelEditorModule* LevelEditorModule = FModuleManager::LoadModulePtr<FLevelEditorModule>(TEXT("LevelEditor"));
1315
- if (!LevelEditorModule)
1316
- {
1317
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("LevelEditor module not available."));
1318
- }
1319
- TSharedPtr<IAssetViewport> ActiveViewport = LevelEditorModule->GetFirstActiveViewport();
1320
- if (!ActiveViewport.IsValid())
1321
- {
1322
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("No active level viewport. Click the viewport and retry."));
1323
- }
1324
-
1325
- const FEditorViewportClient& VC = ActiveViewport->GetAssetViewportClient();
1326
- UWorld* World = GEditor->GetEditorWorldContext().World();
1327
- return RaycastInWorld(World, VC.GetViewLocation(), VC.GetViewRotation().Vector(), MaxDistance, Ignore);
1328
- }
1329
-
1330
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleRaycastDown(const TSharedPtr<FJsonObject>& Params)
1331
- {
1332
- if (!GEditor)
1333
- {
1334
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
1335
- }
1336
-
1337
- FString WorldSource = TEXT("editor");
1338
- if (Params->HasField(TEXT("source")))
1339
- {
1340
- WorldSource = Params->GetStringField(TEXT("source")).ToLower();
1341
- }
1342
-
1343
- FVector Start;
1344
- if (Params->HasField(TEXT("startLocation")))
1345
- {
1346
- Start = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("startLocation"));
1347
- }
1348
- else
1349
- {
1350
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'startLocation' parameter"));
1351
- }
1352
-
1353
- const float MaxDistance = Params->HasField(TEXT("maxDistance")) ? (float)Params->GetNumberField(TEXT("maxDistance")) : 100000.0f;
1354
-
1355
- UWorld* World = nullptr;
1356
- if (WorldSource == TEXT("pie"))
1357
- {
1358
- World = GEditor->PlayWorld;
1359
- if (!World)
1360
- {
1361
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("PIE is not running (no PlayWorld)."));
1362
- }
1363
- }
1364
- else
1365
- {
1366
- World = GEditor->GetEditorWorldContext().World();
1367
- }
1368
-
1369
- return RaycastInWorld(World, Start, FVector(0.0f, 0.0f, -1.0f), MaxDistance, {});
1370
- }
1371
-
1372
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetActorTransform(const TSharedPtr<FJsonObject>& Params)
1373
- {
1374
- if (!GEditor)
1375
- {
1376
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
1377
- }
1378
-
1379
- FString Name;
1380
- if (!Params->TryGetStringField(TEXT("name"), Name) && !Params->TryGetStringField(TEXT("actor"), Name))
1381
- {
1382
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
1383
- }
1384
- Name = Name.TrimStartAndEnd();
1385
- if (Name.IsEmpty())
1386
- {
1387
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Actor name is empty"));
1388
- }
1389
-
1390
- FString Source = Params->HasField(TEXT("source")) ? Params->GetStringField(TEXT("source")).ToLower() : TEXT("editor");
1391
- UWorld* World = nullptr;
1392
- if (Source == TEXT("pie"))
1393
- {
1394
- World = GEditor->PlayWorld;
1395
- }
1396
- else
1397
- {
1398
- World = GEditor->GetEditorWorldContext().World();
1399
- }
1400
- if (!World)
1401
- {
1402
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("World is not available."));
1403
- }
1404
-
1405
- AActor* Actor = FindActorByNameOrLabel(World, Name);
1406
- if (!Actor)
1407
- {
1408
- return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *Name));
1409
- }
1410
-
1411
- TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1412
- Result->SetStringField(TEXT("name"), Actor->GetName());
1413
- #if WITH_EDITOR
1414
- Result->SetStringField(TEXT("label"), Actor->GetActorLabel());
1415
- #endif
1416
- Result->SetArrayField(TEXT("location"), VectorToJsonArray(Actor->GetActorLocation()));
1417
- Result->SetArrayField(TEXT("rotation"), RotatorToJsonArray(Actor->GetActorRotation()));
1418
- Result->SetArrayField(TEXT("scale"), VectorToJsonArray(Actor->GetActorScale3D()));
1419
- return Result;
1420
- }
1421
-
1422
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetActorBounds(const TSharedPtr<FJsonObject>& Params)
1423
- {
1424
- if (!GEditor)
1425
- {
1426
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
1427
- }
1428
-
1429
- FString Name;
1430
- if (!Params->TryGetStringField(TEXT("name"), Name) && !Params->TryGetStringField(TEXT("actor"), Name))
1431
- {
1432
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
1433
- }
1434
- Name = Name.TrimStartAndEnd();
1435
- if (Name.IsEmpty())
1436
- {
1437
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Actor name is empty"));
1438
- }
1439
-
1440
- FString Source = Params->HasField(TEXT("source")) ? Params->GetStringField(TEXT("source")).ToLower() : TEXT("editor");
1441
- UWorld* World = nullptr;
1442
- if (Source == TEXT("pie"))
1443
- {
1444
- World = GEditor->PlayWorld;
1445
- }
1446
- else
1447
- {
1448
- World = GEditor->GetEditorWorldContext().World();
1449
- }
1450
- if (!World)
1451
- {
1452
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("World is not available."));
1453
- }
1454
-
1455
- AActor* Actor = FindActorByNameOrLabel(World, Name);
1456
- if (!Actor)
1457
- {
1458
- return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *Name));
1459
- }
1460
-
1461
- FVector Origin;
1462
- FVector Extent;
1463
- Actor->GetActorBounds(true, Origin, Extent, false);
1464
-
1465
- TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1466
- Result->SetStringField(TEXT("name"), Actor->GetName());
1467
- #if WITH_EDITOR
1468
- Result->SetStringField(TEXT("label"), Actor->GetActorLabel());
1469
- #endif
1470
- Result->SetArrayField(TEXT("origin"), VectorToJsonArray(Origin));
1471
- Result->SetArrayField(TEXT("extent"), VectorToJsonArray(Extent));
1472
- Result->SetArrayField(TEXT("min"), VectorToJsonArray(Origin - Extent));
1473
- Result->SetArrayField(TEXT("max"), VectorToJsonArray(Origin + Extent));
1474
- return Result;
1475
- }
1476
-
1477
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetActorsInLevel(const TSharedPtr<FJsonObject>& Params)
1478
- {
1479
- TArray<AActor*> AllActors;
1480
- UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
1481
-
1482
- TArray<TSharedPtr<FJsonValue>> ActorArray;
1483
- for (AActor* Actor : AllActors)
1484
- {
1485
- if (Actor)
1486
- {
1487
- ActorArray.Add(FUnrealMCPCommonUtils::ActorToJson(Actor));
1488
- }
1489
- }
1490
-
1491
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
1492
- ResultObj->SetArrayField(TEXT("actors"), ActorArray);
1493
-
1494
- return ResultObj;
1495
- }
1496
-
1497
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleFindActorsByName(const TSharedPtr<FJsonObject>& Params)
1498
- {
1499
- FString Pattern;
1500
- if (!Params->TryGetStringField(TEXT("pattern"), Pattern) && !Params->TryGetStringField(TEXT("name"), Pattern))
1501
- {
1502
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'pattern' parameter"));
1503
- }
1504
- Pattern = Pattern.TrimStartAndEnd();
1505
- if (Pattern.IsEmpty())
1506
- {
1507
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("pattern is empty"));
1508
- }
1509
-
1510
- TArray<AActor*> AllActors;
1511
- UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
1512
-
1513
- TArray<TSharedPtr<FJsonValue>> MatchingActors;
1514
- for (AActor* Actor : AllActors)
1515
- {
1516
- if (!Actor) continue;
1517
- const bool bNameMatch = Actor->GetName().Contains(Pattern);
1518
- #if WITH_EDITOR
1519
- const bool bLabelMatch = Actor->GetActorLabel().Contains(Pattern);
1520
- #else
1521
- const bool bLabelMatch = false;
1522
- #endif
1523
- if (bNameMatch || bLabelMatch)
1524
- {
1525
- MatchingActors.Add(FUnrealMCPCommonUtils::ActorToJson(Actor));
1526
- }
1527
- }
1528
-
1529
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
1530
- ResultObj->SetArrayField(TEXT("actors"), MatchingActors);
1531
-
1532
- return ResultObj;
1533
- }
1534
-
1535
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSpawnActor(const TSharedPtr<FJsonObject>& Params)
1536
- {
1537
- {
1538
- FString Err;
1539
- if (!IsSafeToModifyEditorWorld(Err))
1540
- {
1541
- return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
1542
- }
1543
- }
1544
-
1545
- // Get required parameters
1546
- FString ActorType;
1547
- if (!Params->TryGetStringField(TEXT("type"), ActorType))
1548
- {
1549
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'type' parameter"));
1550
- }
1551
-
1552
- // Get actor name (required parameter)
1553
- FString ActorName;
1554
- if (!Params->TryGetStringField(TEXT("name"), ActorName))
1555
- {
1556
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
1557
- }
1558
-
1559
- // Get optional transform parameters
1560
- FVector Location(0.0f, 0.0f, 0.0f);
1561
- FRotator Rotation(0.0f, 0.0f, 0.0f);
1562
- FVector Scale(1.0f, 1.0f, 1.0f);
1563
-
1564
- if (Params->HasField(TEXT("location")))
1565
- {
1566
- Location = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("location"));
1567
- }
1568
- if (Params->HasField(TEXT("rotation")))
1569
- {
1570
- Rotation = FUnrealMCPCommonUtils::GetRotatorFromJson(Params, TEXT("rotation"));
1571
- }
1572
- if (Params->HasField(TEXT("scale")))
1573
- {
1574
- Scale = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("scale"));
1575
- }
1576
-
1577
- // Create the actor based on type
1578
- AActor* NewActor = nullptr;
1579
- UWorld* World = GEditor->GetEditorWorldContext().World();
1580
-
1581
- if (!World)
1582
- {
1583
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world"));
1584
- }
1585
-
1586
- // Check if an actor with this name already exists
1587
- TArray<AActor*> AllActors;
1588
- UGameplayStatics::GetAllActorsOfClass(World, AActor::StaticClass(), AllActors);
1589
- for (AActor* Actor : AllActors)
1590
- {
1591
- if (Actor && Actor->GetName() == ActorName)
1592
- {
1593
- return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor with name '%s' already exists"), *ActorName));
1594
- }
1595
- }
1596
-
1597
- FActorSpawnParameters SpawnParams;
1598
- SpawnParams.Name = *ActorName;
1599
-
1600
- if (ActorType == TEXT("StaticMeshActor"))
1601
- {
1602
- NewActor = World->SpawnActor<AStaticMeshActor>(AStaticMeshActor::StaticClass(), Location, Rotation, SpawnParams);
1603
-
1604
- // Optional: assign a StaticMesh at spawn time (preferred, since nested component property setting is limited).
1605
- FString StaticMeshPath;
1606
- if (Params->TryGetStringField(TEXT("static_mesh_path"), StaticMeshPath) ||
1607
- Params->TryGetStringField(TEXT("static_mesh"), StaticMeshPath))
1608
- {
1609
- StaticMeshPath = StaticMeshPath.TrimStartAndEnd();
1610
- if (!StaticMeshPath.IsEmpty())
1611
- {
1612
- UStaticMesh* StaticMesh = LoadObject<UStaticMesh>(nullptr, *StaticMeshPath);
1613
- if (!StaticMesh && !StaticMeshPath.StartsWith(TEXT("StaticMesh'")))
1614
- {
1615
- StaticMesh = LoadObject<UStaticMesh>(nullptr, *FString::Printf(TEXT("StaticMesh'%s'"), *StaticMeshPath));
1616
- }
1617
-
1618
- if (!StaticMesh)
1619
- {
1620
- return FUnrealMCPCommonUtils::CreateErrorResponse(
1621
- FString::Printf(TEXT("Static mesh not found: %s"), *StaticMeshPath));
1622
- }
1623
-
1624
- AStaticMeshActor* StaticMeshActor = Cast<AStaticMeshActor>(NewActor);
1625
- if (!StaticMeshActor || !StaticMeshActor->GetStaticMeshComponent())
1626
- {
1627
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Spawned StaticMeshActor has no StaticMeshComponent."));
1628
- }
1629
-
1630
- StaticMeshActor->GetStaticMeshComponent()->SetStaticMesh(StaticMesh);
1631
- StaticMeshActor->GetStaticMeshComponent()->MarkRenderStateDirty();
1632
- }
1633
- }
1634
- }
1635
- else if (ActorType == TEXT("PointLight"))
1636
- {
1637
- NewActor = World->SpawnActor<APointLight>(APointLight::StaticClass(), Location, Rotation, SpawnParams);
1638
- }
1639
- else if (ActorType == TEXT("SpotLight"))
1640
- {
1641
- NewActor = World->SpawnActor<ASpotLight>(ASpotLight::StaticClass(), Location, Rotation, SpawnParams);
1642
- }
1643
- else if (ActorType == TEXT("DirectionalLight"))
1644
- {
1645
- NewActor = World->SpawnActor<ADirectionalLight>(ADirectionalLight::StaticClass(), Location, Rotation, SpawnParams);
1646
- }
1647
- else if (ActorType == TEXT("CameraActor"))
1648
- {
1649
- NewActor = World->SpawnActor<ACameraActor>(ACameraActor::StaticClass(), Location, Rotation, SpawnParams);
1650
- }
1651
- else
1652
- {
1653
- return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Unknown actor type: %s"), *ActorType));
1654
- }
1655
-
1656
- if (NewActor)
1657
- {
1658
- // Set scale (since SpawnActor only takes location and rotation)
1659
- FTransform Transform = NewActor->GetTransform();
1660
- Transform.SetScale3D(Scale);
1661
- NewActor->SetActorTransform(Transform);
1662
-
1663
- // Return the created actor's details
1664
- return FUnrealMCPCommonUtils::ActorToJsonObject(NewActor, true);
1665
- }
1666
-
1667
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to create actor"));
1668
- }
1669
-
1670
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleCreateLandscape(const TSharedPtr<FJsonObject>& Params)
1671
- {
1672
- {
1673
- FString Err;
1674
- if (!IsSafeToModifyEditorWorld(Err))
1675
- {
1676
- return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
1677
- }
1678
- }
1679
-
1680
- UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
1681
- if (!World)
1682
- {
1683
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world"));
1684
- }
1685
-
1686
- FString LandscapeName;
1687
- if (!Params->TryGetStringField(TEXT("name"), LandscapeName))
1688
- {
1689
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
1690
- }
1691
- LandscapeName = LandscapeName.TrimStartAndEnd();
1692
- if (LandscapeName.IsEmpty())
1693
- {
1694
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Invalid 'name' parameter (empty)"));
1695
- }
1696
-
1697
- // Ensure actor name uniqueness.
1698
- {
1699
- TArray<AActor*> AllActors;
1700
- UGameplayStatics::GetAllActorsOfClass(World, AActor::StaticClass(), AllActors);
1701
- for (AActor* Actor : AllActors)
1702
- {
1703
- if (Actor && Actor->GetName() == LandscapeName)
1704
- {
1705
- return FUnrealMCPCommonUtils::CreateErrorResponse(
1706
- FString::Printf(TEXT("Actor with name '%s' already exists"), *LandscapeName));
1707
- }
1708
- }
1709
- }
1710
-
1711
- FVector Location(0.0f, 0.0f, 0.0f);
1712
- FRotator Rotation(0.0f, 0.0f, 0.0f);
1713
- FVector Scale(100.0f, 100.0f, 100.0f);
1714
-
1715
- if (Params->HasField(TEXT("location")))
1716
- {
1717
- Location = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("location"));
1718
- }
1719
- if (Params->HasField(TEXT("rotation")))
1720
- {
1721
- Rotation = FUnrealMCPCommonUtils::GetRotatorFromJson(Params, TEXT("rotation"));
1722
- }
1723
- if (Params->HasField(TEXT("scale")))
1724
- {
1725
- Scale = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("scale"));
1726
- }
1727
-
1728
- int32 ComponentCountX = 8;
1729
- int32 ComponentCountY = 8;
1730
- int32 SectionsPerComponent = 1;
1731
- int32 QuadsPerSection = 63;
1732
-
1733
- {
1734
- double Value = 0;
1735
- if (Params->TryGetNumberField(TEXT("componentCountX"), Value)) ComponentCountX = (int32)Value;
1736
- if (Params->TryGetNumberField(TEXT("componentCountY"), Value)) ComponentCountY = (int32)Value;
1737
- if (Params->TryGetNumberField(TEXT("sectionsPerComponent"), Value)) SectionsPerComponent = (int32)Value;
1738
- if (Params->TryGetNumberField(TEXT("quadsPerSection"), Value)) QuadsPerSection = (int32)Value;
1739
- }
1740
-
1741
- if (ComponentCountX <= 0 || ComponentCountY <= 0)
1742
- {
1743
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("componentCountX/componentCountY must be positive integers"));
1744
- }
1745
- if (SectionsPerComponent <= 0 || SectionsPerComponent > 2)
1746
- {
1747
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("sectionsPerComponent must be 1 or 2"));
1748
- }
1749
- if (QuadsPerSection <= 0)
1750
- {
1751
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("quadsPerSection must be a positive integer"));
1752
- }
1753
-
1754
- const int32 QuadsPerComponent = SectionsPerComponent * QuadsPerSection;
1755
- const int32 SizeX = ComponentCountX * QuadsPerComponent + 1;
1756
- const int32 SizeY = ComponentCountY * QuadsPerComponent + 1;
1757
-
1758
- const int64 TotalVerts = (int64)SizeX * (int64)SizeY;
1759
- if (TotalVerts > 16ll * 1024ll * 1024ll)
1760
- {
1761
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Landscape is too large (vertex count limit exceeded)"));
1762
- }
1763
-
1764
- // Optional landscape material.
1765
- UMaterialInterface* LandscapeMaterial = nullptr;
1766
- {
1767
- FString MaterialPath;
1768
- if (Params->TryGetStringField(TEXT("materialPath"), MaterialPath) ||
1769
- Params->TryGetStringField(TEXT("material"), MaterialPath) ||
1770
- Params->TryGetStringField(TEXT("landscapeMaterial"), MaterialPath))
1771
- {
1772
- MaterialPath = MaterialPath.TrimStartAndEnd();
1773
- if (!MaterialPath.IsEmpty())
1774
- {
1775
- LandscapeMaterial = LoadObject<UMaterialInterface>(nullptr, *MaterialPath);
1776
- if (!LandscapeMaterial && !MaterialPath.StartsWith(TEXT("Material'")))
1777
- {
1778
- LandscapeMaterial = LoadObject<UMaterialInterface>(nullptr, *FString::Printf(TEXT("Material'%s'"), *MaterialPath));
1779
- }
1780
- if (!LandscapeMaterial)
1781
- {
1782
- return FUnrealMCPCommonUtils::CreateErrorResponse(
1783
- FString::Printf(TEXT("Landscape material not found: %s"), *MaterialPath));
1784
- }
1785
- }
1786
- }
1787
- }
1788
-
1789
- // Center on Location (similar to the editor create landscape UX).
1790
- const FVector Offset =
1791
- FTransform(Rotation, FVector::ZeroVector, Scale)
1792
- .TransformVector(FVector(-ComponentCountX * QuadsPerComponent / 2, -ComponentCountY * QuadsPerComponent / 2, 0));
1793
-
1794
- FActorSpawnParameters SpawnParams;
1795
- SpawnParams.Name = *LandscapeName;
1796
- ALandscape* Landscape = World->SpawnActor<ALandscape>(Location + Offset, Rotation, SpawnParams);
1797
- if (!Landscape)
1798
- {
1799
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to spawn Landscape actor"));
1800
- }
1801
-
1802
- Landscape->SetActorRelativeScale3D(Scale);
1803
- if (LandscapeMaterial)
1804
- {
1805
- Landscape->LandscapeMaterial = LandscapeMaterial;
1806
- }
1807
-
1808
- Landscape->StaticLightingLOD = FMath::DivideAndRoundUp(FMath::CeilLogTwo((SizeX * SizeY) / (2048 * 2048) + 1), (uint32)2);
1809
-
1810
- // Flat heightmap.
1811
- TArray<uint16> HeightData;
1812
- HeightData.SetNumUninitialized(SizeX * SizeY);
1813
- for (int32 i = 0; i < HeightData.Num(); i++)
1814
- {
1815
- HeightData[i] = 32768;
1816
- }
1817
-
1818
- TMap<FGuid, TArray<uint16>> HeightDataPerLayers;
1819
- HeightDataPerLayers.Add(FGuid(), MoveTemp(HeightData));
1820
-
1821
- TMap<FGuid, TArray<FLandscapeImportLayerInfo>> MaterialLayerDataPerLayers;
1822
- MaterialLayerDataPerLayers.Add(FGuid(), TArray<FLandscapeImportLayerInfo>());
1823
-
1824
- FString ReimportHeightmapFilePath;
1825
-
1826
- Landscape->Import(
1827
- FGuid::NewGuid(),
1828
- 0,
1829
- 0,
1830
- SizeX - 1,
1831
- SizeY - 1,
1832
- SectionsPerComponent,
1833
- QuadsPerSection,
1834
- HeightDataPerLayers,
1835
- *ReimportHeightmapFilePath,
1836
- MaterialLayerDataPerLayers,
1837
- ELandscapeImportAlphamapType::Additive,
1838
- TArrayView<const FLandscapeLayer>());
1839
-
1840
- ULandscapeInfo* LandscapeInfo = Landscape->GetLandscapeInfo();
1841
- if (!LandscapeInfo)
1842
- {
1843
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Landscape import failed (LandscapeInfo missing)"));
1844
- }
1845
-
1846
- FActorLabelUtilities::SetActorLabelUnique(Landscape, LandscapeName);
1847
- LandscapeInfo->UpdateLayerInfoMap(Landscape);
1848
-
1849
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
1850
- ResultObj->SetObjectField(TEXT("landscape"), FUnrealMCPCommonUtils::ActorToJsonObject(Landscape));
1851
- ResultObj->SetNumberField(TEXT("componentCountX"), ComponentCountX);
1852
- ResultObj->SetNumberField(TEXT("componentCountY"), ComponentCountY);
1853
- ResultObj->SetNumberField(TEXT("sectionsPerComponent"), SectionsPerComponent);
1854
- ResultObj->SetNumberField(TEXT("quadsPerSection"), QuadsPerSection);
1855
- ResultObj->SetNumberField(TEXT("sizeX"), SizeX);
1856
- ResultObj->SetNumberField(TEXT("sizeY"), SizeY);
1857
- return ResultObj;
1858
- }
1859
-
1860
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleDeleteActor(const TSharedPtr<FJsonObject>& Params)
1861
- {
1862
- {
1863
- FString Err;
1864
- if (!IsSafeToModifyEditorWorld(Err))
1865
- {
1866
- return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
1867
- }
1868
- }
1869
-
1870
- FString ActorName;
1871
- if (!Params->TryGetStringField(TEXT("name"), ActorName))
1872
- {
1873
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
1874
- }
1875
-
1876
- TArray<AActor*> AllActors;
1877
- UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
1878
-
1879
- for (AActor* Actor : AllActors)
1880
- {
1881
- if (Actor && Actor->GetName() == ActorName)
1882
- {
1883
- // Store actor info before deletion for the response
1884
- TSharedPtr<FJsonObject> ActorInfo = FUnrealMCPCommonUtils::ActorToJsonObject(Actor);
1885
-
1886
- // Delete the actor
1887
- Actor->Destroy();
1888
-
1889
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
1890
- ResultObj->SetObjectField(TEXT("deleted_actor"), ActorInfo);
1891
- return ResultObj;
1892
- }
1893
- }
1894
-
1895
- return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *ActorName));
1896
- }
1897
-
1898
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSetActorTransform(const TSharedPtr<FJsonObject>& Params)
1899
- {
1900
- {
1901
- FString Err;
1902
- if (!IsSafeToModifyEditorWorld(Err))
1903
- {
1904
- return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
1905
- }
1906
- }
1907
-
1908
- // Get actor name
1909
- FString ActorName;
1910
- if (!Params->TryGetStringField(TEXT("name"), ActorName))
1911
- {
1912
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
1913
- }
1914
-
1915
- // Find the actor
1916
- AActor* TargetActor = nullptr;
1917
- TArray<AActor*> AllActors;
1918
- UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
1919
-
1920
- for (AActor* Actor : AllActors)
1921
- {
1922
- if (Actor && Actor->GetName() == ActorName)
1923
- {
1924
- TargetActor = Actor;
1925
- break;
1926
- }
1927
- }
1928
-
1929
- if (!TargetActor)
1930
- {
1931
- return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *ActorName));
1932
- }
1933
-
1934
- // Get transform parameters
1935
- FTransform NewTransform = TargetActor->GetTransform();
1936
-
1937
- if (Params->HasField(TEXT("location")))
1938
- {
1939
- NewTransform.SetLocation(FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("location")));
1940
- }
1941
- if (Params->HasField(TEXT("rotation")))
1942
- {
1943
- NewTransform.SetRotation(FQuat(FUnrealMCPCommonUtils::GetRotatorFromJson(Params, TEXT("rotation"))));
1944
- }
1945
- if (Params->HasField(TEXT("scale")))
1946
- {
1947
- NewTransform.SetScale3D(FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("scale")));
1948
- }
1949
-
1950
- // Set the new transform
1951
- TargetActor->SetActorTransform(NewTransform);
1952
-
1953
- // Return updated actor info
1954
- return FUnrealMCPCommonUtils::ActorToJsonObject(TargetActor, true);
1955
- }
1956
-
1957
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetActorProperties(const TSharedPtr<FJsonObject>& Params)
1958
- {
1959
- // Get actor name
1960
- FString ActorName;
1961
- if (!Params->TryGetStringField(TEXT("name"), ActorName) && !Params->TryGetStringField(TEXT("actor"), ActorName))
1962
- {
1963
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
1964
- }
1965
-
1966
- // Find the actor
1967
- AActor* TargetActor = nullptr;
1968
- TArray<AActor*> AllActors;
1969
- UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
1970
-
1971
- for (AActor* Actor : AllActors)
1972
- {
1973
- if (Actor && Actor->GetName() == ActorName)
1974
- {
1975
- TargetActor = Actor;
1976
- break;
1977
- }
1978
- }
1979
-
1980
- if (!TargetActor)
1981
- {
1982
- return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *ActorName));
1983
- }
1984
-
1985
- // Always return detailed properties for this command
1986
- return FUnrealMCPCommonUtils::ActorToJsonObject(TargetActor, true);
1987
- }
1988
-
1989
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSetActorProperty(const TSharedPtr<FJsonObject>& Params)
1990
- {
1991
- {
1992
- FString Err;
1993
- if (!IsSafeToModifyEditorWorld(Err))
1994
- {
1995
- return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
1996
- }
1997
- }
1998
-
1999
- // Get actor name
2000
- FString ActorName;
2001
- if (!Params->TryGetStringField(TEXT("name"), ActorName) && !Params->TryGetStringField(TEXT("actor"), ActorName))
2002
- {
2003
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
2004
- }
2005
-
2006
- // Find the actor
2007
- AActor* TargetActor = nullptr;
2008
- TArray<AActor*> AllActors;
2009
- UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
2010
-
2011
- for (AActor* Actor : AllActors)
2012
- {
2013
- if (Actor && Actor->GetName() == ActorName)
2014
- {
2015
- TargetActor = Actor;
2016
- break;
2017
- }
2018
- }
2019
-
2020
- if (!TargetActor)
2021
- {
2022
- return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *ActorName));
2023
- }
2024
-
2025
- // Get property name
2026
- FString PropertyName;
2027
- if (!Params->TryGetStringField(TEXT("property_name"), PropertyName) && !Params->TryGetStringField(TEXT("property"), PropertyName))
2028
- {
2029
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'property_name' parameter"));
2030
- }
2031
-
2032
- // Get property value
2033
- if (!Params->HasField(TEXT("property_value")) && !Params->HasField(TEXT("value")))
2034
- {
2035
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'property_value' parameter"));
2036
- }
2037
-
2038
- TSharedPtr<FJsonValue> PropertyValue = Params->HasField(TEXT("property_value"))
2039
- ? Params->Values.FindRef(TEXT("property_value"))
2040
- : Params->Values.FindRef(TEXT("value"));
2041
-
2042
- // Set the property using our utility function
2043
- FString ErrorMessage;
2044
- if (FUnrealMCPCommonUtils::SetObjectProperty(TargetActor, PropertyName, PropertyValue, ErrorMessage))
2045
- {
2046
- // Property set successfully
2047
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
2048
- ResultObj->SetStringField(TEXT("actor"), ActorName);
2049
- ResultObj->SetStringField(TEXT("property"), PropertyName);
2050
- ResultObj->SetBoolField(TEXT("success"), true);
2051
-
2052
- // Also include the full actor details
2053
- ResultObj->SetObjectField(TEXT("actor_details"), FUnrealMCPCommonUtils::ActorToJsonObject(TargetActor, true));
2054
- return ResultObj;
2055
- }
2056
- else
2057
- {
2058
- return FUnrealMCPCommonUtils::CreateErrorResponse(ErrorMessage);
2059
- }
2060
- }
2061
-
2062
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSpawnBlueprintActor(const TSharedPtr<FJsonObject>& Params)
2063
- {
2064
- {
2065
- FString Err;
2066
- if (!IsSafeToModifyEditorWorld(Err))
2067
- {
2068
- return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
2069
- }
2070
- }
2071
-
2072
- // Get required parameters
2073
- FString BlueprintName;
2074
- if (!Params->TryGetStringField(TEXT("blueprint_name"), BlueprintName) &&
2075
- !Params->TryGetStringField(TEXT("blueprint"), BlueprintName) &&
2076
- !Params->TryGetStringField(TEXT("blueprint_path"), BlueprintName) &&
2077
- !Params->TryGetStringField(TEXT("objectPath"), BlueprintName) &&
2078
- !Params->TryGetStringField(TEXT("object_path"), BlueprintName))
2079
- {
2080
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'blueprint_name' parameter"));
2081
- }
2082
-
2083
- FString ActorName;
2084
- if (!Params->TryGetStringField(TEXT("actor_name"), ActorName) && !Params->TryGetStringField(TEXT("name"), ActorName))
2085
- {
2086
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'actor_name' parameter"));
2087
- }
2088
-
2089
- // Find the blueprint
2090
- if (BlueprintName.IsEmpty())
2091
- {
2092
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Blueprint name is empty"));
2093
- }
2094
- BlueprintName = BlueprintName.TrimStartAndEnd();
2095
- ActorName = ActorName.TrimStartAndEnd();
2096
-
2097
- const auto SanitizeObjectName = [](const FString& In) -> FString {
2098
- FString Out;
2099
- Out.Reserve(In.Len());
2100
- for (int32 i = 0; i < In.Len(); i++)
2101
- {
2102
- const TCHAR Ch = In[i];
2103
- const bool bOk =
2104
- (Ch >= '0' && Ch <= '9') ||
2105
- (Ch >= 'A' && Ch <= 'Z') ||
2106
- (Ch >= 'a' && Ch <= 'z') ||
2107
- Ch == '_' || Ch == '-';
2108
- Out.AppendChar(bOk ? Ch : '_');
2109
- }
2110
- Out = Out.TrimStartAndEnd();
2111
- while (Out.Contains(TEXT("__")))
2112
- {
2113
- Out = Out.Replace(TEXT("__"), TEXT("_"));
2114
- }
2115
- if (Out.IsEmpty()) Out = TEXT("BPActor");
2116
- return Out;
2117
- };
2118
-
2119
- const FString SafeActorBaseName = SanitizeObjectName(ActorName);
2120
-
2121
- // Resolve an asset/object path:
2122
- // - If caller passes /Game/... treat as an object path/package path.
2123
- // - Otherwise assume /Game/Blueprints/<BlueprintName> (compat default).
2124
- FString AssetPath = BlueprintName;
2125
- if (!AssetPath.StartsWith(TEXT("/")))
2126
- {
2127
- AssetPath = TEXT("/Game/Blueprints/") + AssetPath;
2128
- }
2129
-
2130
- UObject* AssetObj = LoadObject<UObject>(nullptr, *AssetPath);
2131
- if (!AssetObj && !AssetPath.Contains(TEXT("'")))
2132
- {
2133
- AssetObj = LoadObject<UObject>(nullptr, *FString::Printf(TEXT("Blueprint'%s'"), *AssetPath));
2134
- }
2135
-
2136
- if (!AssetObj)
2137
- {
2138
- return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Failed to load blueprint asset: %s"), *AssetPath));
2139
- }
2140
-
2141
- // Get transform parameters
2142
- FVector Location(0.0f, 0.0f, 0.0f);
2143
- FRotator Rotation(0.0f, 0.0f, 0.0f);
2144
- FVector Scale(1.0f, 1.0f, 1.0f);
2145
-
2146
- if (Params->HasField(TEXT("location")))
2147
- {
2148
- Location = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("location"));
2149
- }
2150
- if (Params->HasField(TEXT("rotation")))
2151
- {
2152
- Rotation = FUnrealMCPCommonUtils::GetRotatorFromJson(Params, TEXT("rotation"));
2153
- }
2154
- if (Params->HasField(TEXT("scale")))
2155
- {
2156
- Scale = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("scale"));
2157
- }
2158
-
2159
- // Spawn the actor
2160
- UWorld* World = GEditor->GetEditorWorldContext().World();
2161
- if (!World)
2162
- {
2163
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world"));
2164
- }
2165
-
2166
- FTransform SpawnTransform;
2167
- SpawnTransform.SetLocation(Location);
2168
- SpawnTransform.SetRotation(FQuat(Rotation));
2169
- SpawnTransform.SetScale3D(Scale);
2170
-
2171
- FActorSpawnParameters SpawnParams;
2172
- SpawnParams.Name = MakeUniqueObjectName(World, AActor::StaticClass(), FName(*SafeActorBaseName));
2173
-
2174
- UClass* ActorClass = nullptr;
2175
- if (UBlueprint* Blueprint = Cast<UBlueprint>(AssetObj))
2176
- {
2177
- ActorClass = Blueprint->GeneratedClass;
2178
- }
2179
- else if (UBlueprintGeneratedClass* BPGC = Cast<UBlueprintGeneratedClass>(AssetObj))
2180
- {
2181
- ActorClass = BPGC;
2182
- }
2183
- else if (UClass* Class = Cast<UClass>(AssetObj))
2184
- {
2185
- ActorClass = Class;
2186
- }
2187
-
2188
- if (!ActorClass || !ActorClass->IsChildOf(AActor::StaticClass()))
2189
- {
2190
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Loaded asset is not a spawnable Actor Blueprint/Class. Compile the Blueprint and retry."));
2191
- }
2192
-
2193
- AActor* NewActor = World->SpawnActor<AActor>(ActorClass, SpawnTransform, SpawnParams);
2194
- if (NewActor)
2195
- {
2196
- bool bDetailed = true;
2197
- if (Params->HasField(TEXT("include_details")))
2198
- {
2199
- bDetailed = Params->GetBoolField(TEXT("include_details"));
2200
- }
2201
- else if (Params->HasField(TEXT("detailed")))
2202
- {
2203
- bDetailed = Params->GetBoolField(TEXT("detailed"));
2204
- }
2205
- else if (Params->HasField(TEXT("includeProperties")))
2206
- {
2207
- bDetailed = Params->GetBoolField(TEXT("includeProperties"));
2208
- }
2209
-
2210
- return FUnrealMCPCommonUtils::ActorToJsonObject(NewActor, bDetailed);
2211
- }
2212
-
2213
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to spawn blueprint actor"));
2214
- }
2215
-
2216
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleFocusViewport(const TSharedPtr<FJsonObject>& Params)
2217
- {
2218
- // Get target actor name if provided
2219
- FString TargetActorName;
2220
- bool HasTargetActor = Params->TryGetStringField(TEXT("target"), TargetActorName);
2221
-
2222
- // Get location if provided
2223
- FVector Location(0.0f, 0.0f, 0.0f);
2224
- bool HasLocation = false;
2225
- if (Params->HasField(TEXT("location")))
2226
- {
2227
- Location = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("location"));
2228
- HasLocation = true;
2229
- }
2230
-
2231
- // Get distance
2232
- float Distance = 1000.0f;
2233
- if (Params->HasField(TEXT("distance")))
2234
- {
2235
- Distance = Params->GetNumberField(TEXT("distance"));
2236
- }
2237
-
2238
- // Get orientation if provided
2239
- FRotator Orientation(0.0f, 0.0f, 0.0f);
2240
- bool HasOrientation = false;
2241
- if (Params->HasField(TEXT("orientation")))
2242
- {
2243
- Orientation = FUnrealMCPCommonUtils::GetRotatorFromJson(Params, TEXT("orientation"));
2244
- HasOrientation = true;
2245
- }
2246
-
2247
- // Get the active viewport
2248
- FLevelEditorViewportClient* ViewportClient = (FLevelEditorViewportClient*)GEditor->GetActiveViewport()->GetClient();
2249
- if (!ViewportClient)
2250
- {
2251
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get active viewport"));
2252
- }
2253
-
2254
- // If we have a target actor, focus on it
2255
- if (HasTargetActor)
2256
- {
2257
- // Find the actor
2258
- AActor* TargetActor = nullptr;
2259
- TArray<AActor*> AllActors;
2260
- UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
2261
-
2262
- for (AActor* Actor : AllActors)
2263
- {
2264
- if (Actor && Actor->GetName() == TargetActorName)
2265
- {
2266
- TargetActor = Actor;
2267
- break;
2268
- }
2269
- }
2270
-
2271
- if (!TargetActor)
2272
- {
2273
- return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *TargetActorName));
2274
- }
2275
-
2276
- // Focus on the actor
2277
- ViewportClient->SetViewLocation(TargetActor->GetActorLocation() - FVector(Distance, 0.0f, 0.0f));
2278
- }
2279
- // Otherwise use the provided location
2280
- else if (HasLocation)
2281
- {
2282
- ViewportClient->SetViewLocation(Location - FVector(Distance, 0.0f, 0.0f));
2283
- }
2284
- else
2285
- {
2286
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Either 'target' or 'location' must be provided"));
2287
- }
2288
-
2289
- // Set orientation if provided
2290
- if (HasOrientation)
2291
- {
2292
- ViewportClient->SetViewRotation(Orientation);
2293
- }
2294
-
2295
- // Force viewport to redraw
2296
- ViewportClient->Invalidate();
2297
-
2298
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
2299
- ResultObj->SetBoolField(TEXT("success"), true);
2300
- return ResultObj;
2301
- }
2302
-
2303
- TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleTakeScreenshot(const TSharedPtr<FJsonObject>& Params)
2304
- {
2305
- // Optional file path parameter.
2306
- // If omitted, save to <Project>/Saved/Screenshots/Flockbay with an auto-generated filename.
2307
- FString FilePath;
2308
- const bool bHasFilePath = Params->TryGetStringField(TEXT("filepath"), FilePath);
2309
-
2310
- FString FileName;
2311
- Params->TryGetStringField(TEXT("filename"), FileName);
2312
-
2313
- const FString DefaultDir = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Screenshots"), TEXT("Flockbay"));
2314
- IFileManager::Get().MakeDirectory(*DefaultDir, /*Tree*/ true);
2315
-
2316
- if (bHasFilePath)
2317
- {
2318
- FilePath = FilePath.TrimStartAndEnd();
2319
- if (FilePath.IsEmpty())
2320
- {
2321
- FilePath.Reset();
2322
- }
2323
- else if (!FPaths::IsRelative(FilePath))
2324
- {
2325
- // absolute path: use as-is
2326
- }
2327
- else
2328
- {
2329
- // relative path: resolve under our default screenshots folder
2330
- FilePath = FPaths::Combine(DefaultDir, FilePath);
2331
- }
2332
- }
2333
-
2334
- if (FilePath.IsEmpty())
2335
- {
2336
- if (FileName.IsEmpty())
2337
- {
2338
- FileName = FString::Printf(TEXT("mcp_screenshot_%s"), *FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S")));
2339
- }
2340
- FilePath = FPaths::Combine(DefaultDir, FileName);
2341
- }
2342
-
2343
- // Ensure the file path has a proper extension
2344
- if (!FilePath.EndsWith(TEXT(".png")))
2345
- {
2346
- FilePath += TEXT(".png");
2347
- }
2348
-
2349
- FString Source = TEXT("auto");
2350
- if (Params->TryGetStringField(TEXT("source"), Source))
2351
- {
2352
- Source = Source.TrimStartAndEnd().ToLower();
2353
- if (Source.IsEmpty())
2354
- {
2355
- Source = TEXT("auto");
2356
- }
2357
- }
2358
-
2359
- double WidthNum = 0.0;
2360
- double HeightNum = 0.0;
2361
- int32 RequestedWidth = 0;
2362
- int32 RequestedHeight = 0;
2363
- if (Params->TryGetNumberField(TEXT("width"), WidthNum))
2364
- {
2365
- RequestedWidth = static_cast<int32>(WidthNum);
2366
- }
2367
- else if (Params->TryGetNumberField(TEXT("resX"), WidthNum))
2368
- {
2369
- RequestedWidth = static_cast<int32>(WidthNum);
2370
- }
2371
-
2372
- if (Params->TryGetNumberField(TEXT("height"), HeightNum))
2373
- {
2374
- RequestedHeight = static_cast<int32>(HeightNum);
2375
- }
2376
- else if (Params->TryGetNumberField(TEXT("resY"), HeightNum))
2377
- {
2378
- RequestedHeight = static_cast<int32>(HeightNum);
2379
- }
2380
-
2381
- const bool bPieRunning = (GEditor && GEditor->PlayWorld);
2382
- const bool bWantsPie = (Source == TEXT("pie"));
2383
- const bool bWantsEditor = (Source == TEXT("editor"));
2384
- const bool bAuto = (Source == TEXT("auto"));
2385
- if (!bWantsPie && !bWantsEditor && !bAuto)
2386
- {
2387
- return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Invalid screenshot source: %s (expected: auto|pie|editor)"), *Source));
2388
- }
2389
-
2390
- const bool bUsePieCapture = bWantsPie || (bAuto && bPieRunning);
2391
- if (bUsePieCapture)
2392
- {
2393
- if (!bPieRunning)
2394
- {
2395
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("PIE is not running (PlayWorld is null)."));
2396
- }
2397
-
2398
- UWorld* PlayWorld = GEditor->PlayWorld;
2399
- APlayerController* PC = PlayWorld ? PlayWorld->GetFirstPlayerController() : nullptr;
2400
- if (!PC)
2401
- {
2402
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to find PlayerController in PlayWorld."));
2403
- }
2404
-
2405
- APlayerCameraManager* PCM = PC->PlayerCameraManager;
2406
- if (!PCM)
2407
- {
2408
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to find PlayerCameraManager (PlayerController.PlayerCameraManager is null)."));
2409
- }
2410
-
2411
- int32 Width = RequestedWidth;
2412
- int32 Height = RequestedHeight;
2413
- if (Width <= 0 || Height <= 0)
2414
- {
2415
- if (ULocalPlayer* LocalPlayer = PC->GetLocalPlayer())
2416
- {
2417
- if (LocalPlayer->ViewportClient && LocalPlayer->ViewportClient->Viewport)
2418
- {
2419
- const FIntPoint ViewSize = LocalPlayer->ViewportClient->Viewport->GetSizeXY();
2420
- if (ViewSize.X > 0 && ViewSize.Y > 0)
2421
- {
2422
- if (Width <= 0)
2423
- {
2424
- Width = ViewSize.X;
2425
- }
2426
- if (Height <= 0)
2427
- {
2428
- Height = ViewSize.Y;
2429
- }
2430
- }
2431
- }
2432
- }
2433
- }
2434
-
2435
- if (Width <= 0 || Height <= 0)
2436
- {
2437
- Width = 1280;
2438
- Height = 720;
2439
- }
2440
-
2441
- const FVector CamLocation = PCM->GetCameraLocation();
2442
- const FRotator CamRotation = PCM->GetCameraRotation();
2443
-
2444
- FActorSpawnParameters SpawnParams;
2445
- SpawnParams.ObjectFlags |= RF_Transient;
2446
- SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
2447
-
2448
- ASceneCapture2D* CaptureActor = PlayWorld->SpawnActor<ASceneCapture2D>(ASceneCapture2D::StaticClass(), CamLocation, CamRotation, SpawnParams);
2449
- if (!CaptureActor)
2450
- {
2451
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to spawn ASceneCapture2D for PIE screenshot capture."));
2452
- }
2453
-
2454
- USceneCaptureComponent2D* CaptureComp = CaptureActor->GetCaptureComponent2D();
2455
- if (!CaptureComp)
2456
- {
2457
- CaptureActor->Destroy();
2458
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get SceneCaptureComponent2D from ASceneCapture2D."));
2459
- }
2460
-
2461
- UTextureRenderTarget2D* RenderTarget = NewObject<UTextureRenderTarget2D>(CaptureComp);
2462
- if (!RenderTarget)
2463
- {
2464
- CaptureActor->Destroy();
2465
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to create UTextureRenderTarget2D."));
2466
- }
2467
- RenderTarget->InitCustomFormat(Width, Height, PF_B8G8R8A8, /*bForceLinearGamma*/ false);
2468
- RenderTarget->ClearColor = FLinearColor::Black;
2469
- RenderTarget->UpdateResourceImmediate(true);
2470
-
2471
- CaptureComp->TextureTarget = RenderTarget;
2472
- CaptureComp->bCaptureEveryFrame = false;
2473
- CaptureComp->bCaptureOnMovement = false;
2474
- CaptureComp->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR;
2475
- CaptureComp->FOVAngle = PCM->GetFOVAngle();
2476
-
2477
- CaptureComp->CaptureScene();
2478
-
2479
- FTextureRenderTargetResource* RenderResource = RenderTarget->GameThread_GetRenderTargetResource();
2480
- if (!RenderResource)
2481
- {
2482
- CaptureActor->Destroy();
2483
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get render target resource for PIE screenshot capture."));
2484
- }
2485
-
2486
- TArray<FColor> Bitmap;
2487
- if (!RenderResource->ReadPixels(Bitmap))
2488
- {
2489
- CaptureActor->Destroy();
2490
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to read pixels from PIE render target."));
2491
- }
2492
-
2493
- CaptureActor->Destroy();
2494
-
2495
- if (!SavePngToFile(FilePath, Width, Height, Bitmap))
2496
- {
2497
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to save screenshot PNG to disk."));
2498
- }
2499
-
2500
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
2501
- ResultObj->SetStringField(TEXT("filepath"), FilePath);
2502
- ResultObj->SetStringField(TEXT("source"), TEXT("pie"));
2503
- ResultObj->SetNumberField(TEXT("width"), Width);
2504
- ResultObj->SetNumberField(TEXT("height"), Height);
2505
- return ResultObj;
2506
- }
2507
-
2508
- // Editor screenshot (prefer offscreen SceneCapture2D; ReadPixels on the active viewport can be blank/stale when the OS window isn't focused).
2509
- if (GEditor && GEditor->GetActiveViewport())
2510
- {
2511
- GEditor->RedrawAllViewports(/*bInvalidateHitProxies*/ true);
2512
-
2513
- FViewport* Viewport = GEditor->GetActiveViewport();
2514
- FEditorViewportClient* ViewportClient = (FEditorViewportClient*)Viewport->GetClient();
2515
- if (!ViewportClient)
2516
- {
2517
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get active editor viewport client for editor screenshot capture."));
2518
- }
2519
-
2520
- UWorld* EditorWorld = ViewportClient->GetWorld();
2521
- if (!EditorWorld)
2522
- {
2523
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world for editor screenshot capture."));
2524
- }
2525
-
2526
- int32 Width = RequestedWidth;
2527
- int32 Height = RequestedHeight;
2528
- if (Width <= 0 || Height <= 0)
2529
- {
2530
- const FIntPoint ViewSize = Viewport->GetSizeXY();
2531
- if (ViewSize.X > 0 && ViewSize.Y > 0)
2532
- {
2533
- if (Width <= 0)
2534
- {
2535
- Width = ViewSize.X;
2536
- }
2537
- if (Height <= 0)
2538
- {
2539
- Height = ViewSize.Y;
2540
- }
2541
- }
2542
- }
2543
- if (Width <= 0 || Height <= 0)
2544
- {
2545
- Width = 1280;
2546
- Height = 720;
2547
- }
2548
-
2549
- const FVector CamLocation = ViewportClient->GetViewLocation();
2550
- const FRotator CamRotation = ViewportClient->GetViewRotation();
2551
-
2552
- FActorSpawnParameters SpawnParams;
2553
- SpawnParams.ObjectFlags |= RF_Transient;
2554
- SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
2555
-
2556
- ASceneCapture2D* CaptureActor =
2557
- EditorWorld->SpawnActor<ASceneCapture2D>(ASceneCapture2D::StaticClass(), CamLocation, CamRotation, SpawnParams);
2558
- if (!CaptureActor)
2559
- {
2560
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to spawn ASceneCapture2D for editor screenshot capture."));
2561
- }
2562
-
2563
- USceneCaptureComponent2D* CaptureComp = CaptureActor->GetCaptureComponent2D();
2564
- if (!CaptureComp)
2565
- {
2566
- CaptureActor->Destroy();
2567
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get SceneCaptureComponent2D from ASceneCapture2D (editor capture)."));
2568
- }
2569
-
2570
- UTextureRenderTarget2D* RenderTarget = NewObject<UTextureRenderTarget2D>(CaptureComp);
2571
- if (!RenderTarget)
2572
- {
2573
- CaptureActor->Destroy();
2574
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to create UTextureRenderTarget2D (editor capture)."));
2575
- }
2576
- RenderTarget->InitCustomFormat(Width, Height, PF_B8G8R8A8, /*bForceLinearGamma*/ false);
2577
- RenderTarget->ClearColor = FLinearColor::Black;
2578
- RenderTarget->UpdateResourceImmediate(true);
2579
-
2580
- CaptureComp->TextureTarget = RenderTarget;
2581
- CaptureComp->bCaptureEveryFrame = false;
2582
- CaptureComp->bCaptureOnMovement = false;
2583
- CaptureComp->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR;
2584
- CaptureComp->FOVAngle = ViewportClient->ViewFOV;
2585
-
2586
- CaptureComp->CaptureScene();
2587
-
2588
- FTextureRenderTargetResource* RenderResource = RenderTarget->GameThread_GetRenderTargetResource();
2589
- if (!RenderResource)
2590
- {
2591
- CaptureActor->Destroy();
2592
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get render target resource for editor screenshot capture."));
2593
- }
2594
-
2595
- TArray<FColor> Bitmap;
2596
- if (!RenderResource->ReadPixels(Bitmap))
2597
- {
2598
- CaptureActor->Destroy();
2599
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to read pixels from editor render target."));
2600
- }
2601
-
2602
- CaptureActor->Destroy();
2603
-
2604
- if (!SavePngToFile(FilePath, Width, Height, Bitmap))
2605
- {
2606
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to save editor screenshot PNG to disk."));
2607
- }
2608
-
2609
- TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
2610
- ResultObj->SetStringField(TEXT("filepath"), FilePath);
2611
- ResultObj->SetStringField(TEXT("source"), TEXT("editor"));
2612
- ResultObj->SetNumberField(TEXT("width"), Width);
2613
- ResultObj->SetNumberField(TEXT("height"), Height);
2614
- return ResultObj;
2615
- }
2616
-
2617
- return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to take screenshot."));
2618
- }
1
+ #include "Commands/UnrealMCPEditorCommands.h"
2
+ #include "Commands/UnrealMCPCommonUtils.h"
3
+ #include "Editor.h"
4
+ #include "Editor/EditorEngine.h"
5
+ #include "EditorViewportClient.h"
6
+ #include "LevelEditorViewport.h"
7
+ #include "IAssetViewport.h"
8
+ #include "ImageUtils.h"
9
+ #include "HighResScreenshot.h"
10
+ #include "Engine/GameViewportClient.h"
11
+ #include "Misc/FileHelper.h"
12
+ #include "Misc/Paths.h"
13
+ #include "HAL/FileManager.h"
14
+ #include "GameFramework/Actor.h"
15
+ #include "Engine/Selection.h"
16
+ #include "Kismet/GameplayStatics.h"
17
+ #include "Engine/StaticMeshActor.h"
18
+ #include "Engine/StaticMesh.h"
19
+ #include "Engine/Texture2D.h"
20
+ #include "Engine/SkeletalMesh.h"
21
+ #include "Engine/DirectionalLight.h"
22
+ #include "Engine/PointLight.h"
23
+ #include "Engine/SpotLight.h"
24
+ #include "Camera/CameraActor.h"
25
+ #include "Landscape.h"
26
+ #include "LandscapeImportHelper.h"
27
+ #include "Components/StaticMeshComponent.h"
28
+ #include "Materials/MaterialInterface.h"
29
+ #include "Sound/SoundWave.h"
30
+ #include "IImageWrapper.h"
31
+ #include "IImageWrapperModule.h"
32
+ #include "EditorSubsystem.h"
33
+ #include "Subsystems/EditorActorSubsystem.h"
34
+ #include "Engine/Blueprint.h"
35
+ #include "Engine/BlueprintGeneratedClass.h"
36
+ #include "UnrealEdGlobals.h"
37
+ #include "Editor/UnrealEdEngine.h"
38
+ #include "UObject/UnrealType.h"
39
+ #include "Modules/ModuleManager.h"
40
+ #include "LevelEditor.h"
41
+ #include "PlayInEditorDataTypes.h"
42
+ #include "Settings/LevelEditorPlaySettings.h"
43
+ #include "Framework/Application/SlateApplication.h"
44
+ #include "AssetRegistry/AssetRegistryModule.h"
45
+ #include "UObject/SoftObjectPath.h"
46
+ #include "UObject/Package.h"
47
+ #include "UObject/UObjectGlobals.h"
48
+ #include "FileHelpers.h"
49
+ #include "MessageLogModule.h"
50
+ #include "IMessageLogListing.h"
51
+ #include "Engine/World.h"
52
+ #include "Engine/EngineTypes.h"
53
+ #include "GameFramework/PlayerController.h"
54
+ #include "Camera/PlayerCameraManager.h"
55
+ #include "Engine/LocalPlayer.h"
56
+ #include "Engine/SceneCapture2D.h"
57
+ #include "Components/SceneCaptureComponent2D.h"
58
+ #include "Engine/TextureRenderTarget2D.h"
59
+
60
+ static UClass* FindMcpClassByName(const TCHAR* ClassName)
61
+ {
62
+ #if defined(ANY_PACKAGE)
63
+ return FindObject<UClass>(ANY_PACKAGE, ClassName);
64
+ #else
65
+ return FindFirstObject<UClass>(ClassName);
66
+ #endif
67
+ }
68
+
69
+ static UClass* FindMcpClassByName(const FString& ClassName)
70
+ {
71
+ return FindMcpClassByName(*ClassName);
72
+ }
73
+
74
+ static bool SavePngToFile(const FString& FilePath, int32 Width, int32 Height, TArray<FColor>& Bitmap)
75
+ {
76
+ IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
77
+ TSharedPtr<IImageWrapper> Wrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG);
78
+ if (!Wrapper.IsValid())
79
+ {
80
+ return false;
81
+ }
82
+
83
+ // Normalize alpha: editor backbuffer alpha can be 0/undefined, which can make PNGs look dark/grey in viewers.
84
+ for (FColor& Pixel : Bitmap)
85
+ {
86
+ Pixel.A = 255;
87
+ }
88
+
89
+ if (!Wrapper->SetRaw(Bitmap.GetData(), Bitmap.Num() * sizeof(FColor), Width, Height, ERGBFormat::BGRA, 8))
90
+ {
91
+ return false;
92
+ }
93
+
94
+ const TArray64<uint8>& Compressed = Wrapper->GetCompressed(100);
95
+ if (Compressed.Num() <= 0 || Compressed.Num() > MAX_int32)
96
+ {
97
+ return false;
98
+ }
99
+
100
+ TArray<uint8> Out;
101
+ Out.Append(Compressed.GetData(), static_cast<int32>(Compressed.Num()));
102
+ return FFileHelper::SaveArrayToFile(Out, *FilePath);
103
+ }
104
+
105
+ FUnrealMCPEditorCommands::FUnrealMCPEditorCommands()
106
+ {
107
+ }
108
+
109
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleCommand(const FString& CommandType, const TSharedPtr<FJsonObject>& Params)
110
+ {
111
+ // Actor manipulation commands
112
+ if (CommandType == TEXT("get_actors_in_level"))
113
+ {
114
+ return HandleGetActorsInLevel(Params);
115
+ }
116
+ else if (CommandType == TEXT("find_actors_by_name"))
117
+ {
118
+ return HandleFindActorsByName(Params);
119
+ }
120
+ else if (CommandType == TEXT("spawn_actor") || CommandType == TEXT("create_actor"))
121
+ {
122
+ if (CommandType == TEXT("create_actor"))
123
+ {
124
+ UE_LOG(LogTemp, Warning, TEXT("'create_actor' command is deprecated and will be removed in a future version. Please use 'spawn_actor' instead."));
125
+ }
126
+ return HandleSpawnActor(Params);
127
+ }
128
+ else if (CommandType == TEXT("create_landscape"))
129
+ {
130
+ return HandleCreateLandscape(Params);
131
+ }
132
+ else if (CommandType == TEXT("delete_actor"))
133
+ {
134
+ return HandleDeleteActor(Params);
135
+ }
136
+ else if (CommandType == TEXT("set_actor_transform"))
137
+ {
138
+ return HandleSetActorTransform(Params);
139
+ }
140
+ else if (CommandType == TEXT("get_actor_properties"))
141
+ {
142
+ return HandleGetActorProperties(Params);
143
+ }
144
+ else if (CommandType == TEXT("set_actor_property"))
145
+ {
146
+ return HandleSetActorProperty(Params);
147
+ }
148
+ // Blueprint actor spawning
149
+ else if (CommandType == TEXT("spawn_blueprint_actor"))
150
+ {
151
+ return HandleSpawnBlueprintActor(Params);
152
+ }
153
+ // Editor viewport commands
154
+ else if (CommandType == TEXT("focus_viewport"))
155
+ {
156
+ return HandleFocusViewport(Params);
157
+ }
158
+ else if (CommandType == TEXT("take_screenshot"))
159
+ {
160
+ return HandleTakeScreenshot(Params);
161
+ }
162
+ else if (CommandType == TEXT("save_all"))
163
+ {
164
+ return HandleSaveAll(Params);
165
+ }
166
+ else if (CommandType == TEXT("get_play_in_editor_status"))
167
+ {
168
+ return HandleGetPlayInEditorStatus(Params);
169
+ }
170
+ else if (CommandType == TEXT("play_in_editor"))
171
+ {
172
+ return HandlePlayInEditor(Params);
173
+ }
174
+ else if (CommandType == TEXT("play_in_editor_windowed"))
175
+ {
176
+ return HandlePlayInEditorWindowed(Params);
177
+ }
178
+ else if (CommandType == TEXT("stop_play_in_editor"))
179
+ {
180
+ return HandleStopPlayInEditor(Params);
181
+ }
182
+ else if (CommandType == TEXT("search_assets"))
183
+ {
184
+ return HandleSearchAssets(Params);
185
+ }
186
+ else if (CommandType == TEXT("get_asset_info"))
187
+ {
188
+ return HandleGetAssetInfo(Params);
189
+ }
190
+ else if (CommandType == TEXT("list_asset_packs"))
191
+ {
192
+ return HandleListAssetPacks(Params);
193
+ }
194
+ else if (CommandType == TEXT("place_asset"))
195
+ {
196
+ return HandlePlaceAsset(Params);
197
+ }
198
+ else if (CommandType == TEXT("map_check"))
199
+ {
200
+ return HandleMapCheck(Params);
201
+ }
202
+ else if (CommandType == TEXT("get_editor_context"))
203
+ {
204
+ return HandleGetEditorContext(Params);
205
+ }
206
+ else if (CommandType == TEXT("get_player_context"))
207
+ {
208
+ return HandleGetPlayerContext(Params);
209
+ }
210
+ else if (CommandType == TEXT("raycast_from_camera"))
211
+ {
212
+ return HandleRaycastFromCamera(Params);
213
+ }
214
+ else if (CommandType == TEXT("raycast_down"))
215
+ {
216
+ return HandleRaycastDown(Params);
217
+ }
218
+ else if (CommandType == TEXT("get_actor_transform"))
219
+ {
220
+ return HandleGetActorTransform(Params);
221
+ }
222
+ else if (CommandType == TEXT("get_actor_bounds"))
223
+ {
224
+ return HandleGetActorBounds(Params);
225
+ }
226
+
227
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Unknown editor command: %s"), *CommandType));
228
+ }
229
+
230
+ static TSharedPtr<FJsonObject> CreatePlayStatusResponse()
231
+ {
232
+ if (!GEditor)
233
+ {
234
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
235
+ }
236
+
237
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
238
+ ResultObj->SetBoolField(TEXT("isPlaySessionInProgress"), GEditor->IsPlaySessionInProgress());
239
+ ResultObj->SetBoolField(TEXT("isPlaySessionRequestQueued"), GEditor->IsPlaySessionRequestQueued());
240
+ ResultObj->SetBoolField(TEXT("isPlayingSessionInEditor"), GEditor->IsPlayingSessionInEditor());
241
+ return ResultObj;
242
+ }
243
+
244
+ static bool IsSafeToModifyEditorWorld(FString& OutError)
245
+ {
246
+ if (!GEditor)
247
+ {
248
+ OutError = TEXT("Editor is not available (GEditor is null).");
249
+ return false;
250
+ }
251
+
252
+ if (GEditor->IsPlaySessionInProgress())
253
+ {
254
+ OutError = TEXT("Play-In-Editor is running or queued. Stop PIE before modifying the editor world (use stop_play_in_editor).");
255
+ return false;
256
+ }
257
+
258
+ return true;
259
+ }
260
+
261
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSaveAll(const TSharedPtr<FJsonObject>& Params)
262
+ {
263
+ FString Err;
264
+ if (!IsSafeToModifyEditorWorld(Err))
265
+ {
266
+ return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
267
+ }
268
+
269
+ TArray<UPackage*> DirtyBefore;
270
+ FEditorFileUtils::GetDirtyPackages(DirtyBefore);
271
+
272
+ TSet<FString> DirtyBeforeNames;
273
+ for (UPackage* Pkg : DirtyBefore)
274
+ {
275
+ if (!Pkg) continue;
276
+ DirtyBeforeNames.Add(Pkg->GetName());
277
+ }
278
+
279
+ const int32 DirtyBeforeCount = DirtyBeforeNames.Num();
280
+
281
+ // Save without prompting; fail-hard if anything remains dirty.
282
+ const bool bSaveOk = FEditorFileUtils::SaveDirtyPackages(
283
+ /*bPromptUserToSave=*/false,
284
+ /*bSaveMapPackages=*/true,
285
+ /*bSaveContentPackages=*/true,
286
+ /*bFastSave=*/false
287
+ );
288
+
289
+ TArray<UPackage*> DirtyAfter;
290
+ FEditorFileUtils::GetDirtyPackages(DirtyAfter);
291
+
292
+ TSet<FString> DirtyAfterNames;
293
+ for (UPackage* Pkg : DirtyAfter)
294
+ {
295
+ if (!Pkg) continue;
296
+ DirtyAfterNames.Add(Pkg->GetName());
297
+ }
298
+
299
+ TArray<TSharedPtr<FJsonValue>> Attempted;
300
+ for (const FString& Name : DirtyBeforeNames)
301
+ {
302
+ Attempted.Add(MakeShareable(new FJsonValueString(Name)));
303
+ }
304
+
305
+ TArray<TSharedPtr<FJsonValue>> StillDirty;
306
+ for (const FString& Name : DirtyAfterNames)
307
+ {
308
+ StillDirty.Add(MakeShareable(new FJsonValueString(Name)));
309
+ }
310
+
311
+ TArray<TSharedPtr<FJsonValue>> Saved;
312
+ for (const FString& Name : DirtyBeforeNames)
313
+ {
314
+ if (!DirtyAfterNames.Contains(Name))
315
+ {
316
+ Saved.Add(MakeShareable(new FJsonValueString(Name)));
317
+ }
318
+ }
319
+
320
+ if (DirtyBeforeCount == 0)
321
+ {
322
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
323
+ ResultObj->SetBoolField(TEXT("success"), true);
324
+ ResultObj->SetNumberField(TEXT("dirtyBefore"), DirtyBeforeCount);
325
+ ResultObj->SetNumberField(TEXT("dirtyAfter"), DirtyAfterNames.Num());
326
+ ResultObj->SetArrayField(TEXT("attemptedPackages"), Attempted);
327
+ ResultObj->SetArrayField(TEXT("savedPackages"), Saved);
328
+ ResultObj->SetArrayField(TEXT("stillDirtyPackages"), StillDirty);
329
+ ResultObj->SetStringField(TEXT("message"), TEXT("No unsaved changes were detected."));
330
+ return ResultObj;
331
+ }
332
+
333
+ if (DirtyAfterNames.Num() > 0)
334
+ {
335
+ TSharedPtr<FJsonObject> Details = MakeShared<FJsonObject>();
336
+ Details->SetBoolField(TEXT("saveOk"), bSaveOk);
337
+ Details->SetNumberField(TEXT("dirtyBefore"), DirtyBeforeCount);
338
+ Details->SetNumberField(TEXT("dirtyAfter"), DirtyAfterNames.Num());
339
+ Details->SetArrayField(TEXT("attemptedPackages"), Attempted);
340
+ Details->SetArrayField(TEXT("savedPackages"), Saved);
341
+ Details->SetArrayField(TEXT("stillDirtyPackages"), StillDirty);
342
+
343
+ TSharedPtr<FJsonObject> ErrObj = FUnrealMCPCommonUtils::CreateErrorResponse(
344
+ 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."));
345
+ ErrObj->SetObjectField(TEXT("details"), Details);
346
+ return ErrObj;
347
+ }
348
+
349
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
350
+ ResultObj->SetBoolField(TEXT("success"), true);
351
+ ResultObj->SetNumberField(TEXT("dirtyBefore"), DirtyBeforeCount);
352
+ ResultObj->SetNumberField(TEXT("dirtyAfter"), DirtyAfterNames.Num());
353
+ ResultObj->SetArrayField(TEXT("attemptedPackages"), Attempted);
354
+ ResultObj->SetArrayField(TEXT("savedPackages"), Saved);
355
+ ResultObj->SetArrayField(TEXT("stillDirtyPackages"), StillDirty);
356
+ ResultObj->SetStringField(TEXT("message"), TEXT("Saved all dirty packages."));
357
+ return ResultObj;
358
+ }
359
+
360
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetPlayInEditorStatus(const TSharedPtr<FJsonObject>& Params)
361
+ {
362
+ return CreatePlayStatusResponse();
363
+ }
364
+
365
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandlePlayInEditor(const TSharedPtr<FJsonObject>& Params)
366
+ {
367
+ if (!GEditor)
368
+ {
369
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
370
+ }
371
+
372
+ if (GEditor->IsPlaySessionInProgress())
373
+ {
374
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Play session already running or queued."));
375
+ }
376
+
377
+ // Use the active editor viewport, matching how the LevelEditor subsystem triggers PIE.
378
+ FLevelEditorModule* LevelEditorModule = FModuleManager::LoadModulePtr<FLevelEditorModule>(TEXT("LevelEditor"));
379
+ if (!LevelEditorModule)
380
+ {
381
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("LevelEditor module not available."));
382
+ }
383
+
384
+ TSharedPtr<IAssetViewport> ActiveViewport = LevelEditorModule->GetFirstActiveViewport();
385
+ if (!ActiveViewport.IsValid())
386
+ {
387
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("No active level viewport. Click the viewport and retry."));
388
+ }
389
+
390
+ FRequestPlaySessionParams SessionParams;
391
+ SessionParams.WorldType = EPlaySessionWorldType::PlayInEditor;
392
+ SessionParams.DestinationSlateViewport = ActiveViewport;
393
+
394
+ GEditor->RequestPlaySession(SessionParams);
395
+ const bool bQueued = GEditor->IsPlaySessionRequestQueued();
396
+
397
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
398
+ ResultObj->SetBoolField(TEXT("requested"), true);
399
+ ResultObj->SetBoolField(TEXT("queued"), bQueued);
400
+ ResultObj->SetBoolField(TEXT("started"), bQueued);
401
+ ResultObj->SetStringField(TEXT("mode"), TEXT("pie"));
402
+ return ResultObj;
403
+ }
404
+
405
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandlePlayInEditorWindowed(const TSharedPtr<FJsonObject>& Params)
406
+ {
407
+ if (!GEditor)
408
+ {
409
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
410
+ }
411
+
412
+ if (GEditor->IsPlaySessionInProgress())
413
+ {
414
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Play session already running or queued."));
415
+ }
416
+
417
+ // Match the editor's built-in "New Editor Window (PIE)" behavior by setting the play mode type and
418
+ // not specifying a DestinationSlateViewport.
419
+ ULevelEditorPlaySettings* PlaySettings = GetMutableDefault<ULevelEditorPlaySettings>();
420
+ if (PlaySettings)
421
+ {
422
+ PlaySettings->LastExecutedPlayModeType = EPlayModeType::PlayMode_InEditorFloating;
423
+
424
+ if (FProperty* Prop = ULevelEditorPlaySettings::StaticClass()->FindPropertyByName(
425
+ GET_MEMBER_NAME_CHECKED(ULevelEditorPlaySettings, LastExecutedPlayModeType)))
426
+ {
427
+ FPropertyChangedEvent PropChangeEvent(Prop);
428
+ PlaySettings->PostEditChangeProperty(PropChangeEvent);
429
+ }
430
+
431
+ PlaySettings->SaveConfig();
432
+ }
433
+
434
+ const bool bAtPlayerStart =
435
+ PlaySettings && PlaySettings->LastExecutedPlayModeLocation == EPlayModeLocations::PlayLocation_DefaultPlayerStart;
436
+
437
+ FRequestPlaySessionParams SessionParams;
438
+ SessionParams.WorldType = EPlaySessionWorldType::PlayInEditor;
439
+
440
+ // If the user is playing from current camera location, use the active viewport camera as the start transform.
441
+ if (!bAtPlayerStart)
442
+ {
443
+ FLevelEditorModule* LevelEditorModule = FModuleManager::LoadModulePtr<FLevelEditorModule>(TEXT("LevelEditor"));
444
+ if (LevelEditorModule)
445
+ {
446
+ TSharedPtr<IAssetViewport> ActiveViewport = LevelEditorModule->GetFirstActiveViewport();
447
+ if (ActiveViewport.IsValid() && FSlateApplication::IsInitialized() &&
448
+ FSlateApplication::Get().FindWidgetWindow(ActiveViewport->AsWidget()).IsValid())
449
+ {
450
+ SessionParams.StartLocation = ActiveViewport->GetAssetViewportClient().GetViewLocation();
451
+ SessionParams.StartRotation = ActiveViewport->GetAssetViewportClient().GetViewRotation();
452
+ }
453
+ }
454
+ }
455
+
456
+ GEditor->RequestPlaySession(SessionParams);
457
+ const bool bQueued = GEditor->IsPlaySessionRequestQueued();
458
+
459
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
460
+ ResultObj->SetBoolField(TEXT("requested"), true);
461
+ ResultObj->SetBoolField(TEXT("queued"), bQueued);
462
+ ResultObj->SetBoolField(TEXT("started"), bQueued);
463
+ ResultObj->SetStringField(TEXT("mode"), TEXT("pie_new_window"));
464
+ return ResultObj;
465
+ }
466
+
467
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleStopPlayInEditor(const TSharedPtr<FJsonObject>& Params)
468
+ {
469
+ if (!GEditor)
470
+ {
471
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
472
+ }
473
+
474
+ if (GEditor->IsPlaySessionRequestQueued() && !GEditor->IsPlayingSessionInEditor())
475
+ {
476
+ GEditor->CancelRequestPlaySession();
477
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
478
+ ResultObj->SetBoolField(TEXT("stopped"), true);
479
+ ResultObj->SetBoolField(TEXT("canceled"), true);
480
+ return ResultObj;
481
+ }
482
+
483
+ if (!GEditor->IsPlayingSessionInEditor())
484
+ {
485
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("No play session is running."));
486
+ }
487
+
488
+ GEditor->RequestEndPlayMap();
489
+
490
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
491
+ ResultObj->SetBoolField(TEXT("stopped"), true);
492
+ return ResultObj;
493
+ }
494
+
495
+ static FString NormalizeContentRoot(const FString& Root)
496
+ {
497
+ FString Out = Root.TrimStartAndEnd();
498
+ if (Out.IsEmpty())
499
+ {
500
+ return TEXT("/Game/");
501
+ }
502
+ if (!Out.StartsWith(TEXT("/")))
503
+ {
504
+ Out = TEXT("/") + Out;
505
+ }
506
+ if (!Out.EndsWith(TEXT("/")))
507
+ {
508
+ Out += TEXT("/");
509
+ }
510
+ return Out;
511
+ }
512
+
513
+ static bool TryResolveAssetClassFilter(const TSharedPtr<FJsonObject>& Params, FARFilter& Filter, FString& OutError)
514
+ {
515
+ if (!Params.IsValid() || !Params->HasField(TEXT("class")))
516
+ {
517
+ return true;
518
+ }
519
+
520
+ TArray<FString> ClassNames;
521
+
522
+ FString Single;
523
+ if (Params->TryGetStringField(TEXT("class"), Single))
524
+ {
525
+ Single = Single.TrimStartAndEnd();
526
+ if (!Single.IsEmpty())
527
+ {
528
+ ClassNames.Add(Single);
529
+ }
530
+ }
531
+ else
532
+ {
533
+ const TArray<TSharedPtr<FJsonValue>>* Arr = nullptr;
534
+ if (Params->TryGetArrayField(TEXT("class"), Arr) && Arr)
535
+ {
536
+ for (const TSharedPtr<FJsonValue>& V : *Arr)
537
+ {
538
+ if (!V.IsValid() || V->Type != EJson::String) continue;
539
+ FString S = V->AsString().TrimStartAndEnd();
540
+ if (!S.IsEmpty()) ClassNames.Add(S);
541
+ }
542
+ }
543
+ }
544
+
545
+ for (const FString& ClassNameRaw : ClassNames)
546
+ {
547
+ const FString ClassName = ClassNameRaw.TrimStartAndEnd();
548
+ if (ClassName.IsEmpty()) continue;
549
+
550
+ UClass* Class = nullptr;
551
+
552
+ if (ClassName.Equals(TEXT("StaticMesh"), ESearchCase::IgnoreCase) || ClassName.Equals(TEXT("UStaticMesh"), ESearchCase::IgnoreCase))
553
+ {
554
+ Class = UStaticMesh::StaticClass();
555
+ }
556
+ else if (ClassName.Equals(TEXT("Material"), ESearchCase::IgnoreCase) || ClassName.Equals(TEXT("MaterialInterface"), ESearchCase::IgnoreCase))
557
+ {
558
+ Class = UMaterialInterface::StaticClass();
559
+ }
560
+ else if (ClassName.Equals(TEXT("Texture2D"), ESearchCase::IgnoreCase))
561
+ {
562
+ Class = UTexture2D::StaticClass();
563
+ }
564
+ else if (ClassName.Equals(TEXT("Blueprint"), ESearchCase::IgnoreCase) || ClassName.Equals(TEXT("UBlueprint"), ESearchCase::IgnoreCase))
565
+ {
566
+ Class = UBlueprint::StaticClass();
567
+ }
568
+ else if (ClassName.Equals(TEXT("SkeletalMesh"), ESearchCase::IgnoreCase))
569
+ {
570
+ Class = USkeletalMesh::StaticClass();
571
+ }
572
+ else if (ClassName.Equals(TEXT("SoundWave"), ESearchCase::IgnoreCase))
573
+ {
574
+ Class = USoundWave::StaticClass();
575
+ }
576
+ else
577
+ {
578
+ Class = FindMcpClassByName(ClassName);
579
+ if (!Class && !ClassName.StartsWith(TEXT("U")))
580
+ {
581
+ Class = FindMcpClassByName(TEXT("U") + ClassName);
582
+ }
583
+ }
584
+
585
+ if (!Class)
586
+ {
587
+ OutError = FString::Printf(TEXT("Unknown asset class filter: %s"), *ClassNameRaw);
588
+ return false;
589
+ }
590
+
591
+ Filter.ClassPaths.Add(Class->GetClassPathName());
592
+ }
593
+
594
+ return true;
595
+ }
596
+
597
+ static int32 ScoreAssetMatch(const FString& Query, const FAssetData& Asset)
598
+ {
599
+ if (Query.IsEmpty()) return 0;
600
+
601
+ const FString Name = Asset.AssetName.ToString();
602
+ const FString Path = Asset.GetObjectPathString();
603
+
604
+ if (Name.Equals(Query, ESearchCase::IgnoreCase)) return 0;
605
+ if (Name.StartsWith(Query, ESearchCase::IgnoreCase)) return 1;
606
+ if (Path.EndsWith(Query, ESearchCase::IgnoreCase)) return 2;
607
+ if (Name.Contains(Query, ESearchCase::IgnoreCase)) return 3;
608
+ if (Path.Contains(Query, ESearchCase::IgnoreCase)) return 4;
609
+ return 100;
610
+ }
611
+
612
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSearchAssets(const TSharedPtr<FJsonObject>& Params)
613
+ {
614
+ FString Root = TEXT("/Game/");
615
+ Params->TryGetStringField(TEXT("root"), Root);
616
+ Root = NormalizeContentRoot(Root);
617
+
618
+ FString Query;
619
+ Params->TryGetStringField(TEXT("query"), Query);
620
+ Query = Query.TrimStartAndEnd();
621
+
622
+ int32 Limit = 25;
623
+ if (Params->HasField(TEXT("limit")))
624
+ {
625
+ Limit = (int32)Params->GetNumberField(TEXT("limit"));
626
+ }
627
+ Limit = FMath::Clamp(Limit, 1, 200);
628
+
629
+ FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
630
+ IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
631
+
632
+ FARFilter Filter;
633
+ Filter.PackagePaths.Add(*Root);
634
+ Filter.bRecursivePaths = true;
635
+
636
+ FString ClassErr;
637
+ if (!TryResolveAssetClassFilter(Params, Filter, ClassErr))
638
+ {
639
+ return FUnrealMCPCommonUtils::CreateErrorResponse(ClassErr);
640
+ }
641
+
642
+ TArray<FAssetData> Assets;
643
+ AssetRegistry.GetAssets(Filter, Assets);
644
+
645
+ if (!Query.IsEmpty())
646
+ {
647
+ Assets.RemoveAll([&Query](const FAssetData& Asset) {
648
+ const FString Name = Asset.AssetName.ToString();
649
+ const FString Path = Asset.GetObjectPathString();
650
+ return !(Name.Contains(Query, ESearchCase::IgnoreCase) || Path.Contains(Query, ESearchCase::IgnoreCase));
651
+ });
652
+
653
+ Assets.Sort([&Query](const FAssetData& A, const FAssetData& B) {
654
+ const int32 SA = ScoreAssetMatch(Query, A);
655
+ const int32 SB = ScoreAssetMatch(Query, B);
656
+ if (SA != SB) return SA < SB;
657
+ return A.AssetName.LexicalLess(B.AssetName);
658
+ });
659
+ }
660
+ else
661
+ {
662
+ Assets.Sort([](const FAssetData& A, const FAssetData& B) {
663
+ return A.AssetName.LexicalLess(B.AssetName);
664
+ });
665
+ }
666
+
667
+ const int32 Total = Assets.Num();
668
+ if (Assets.Num() > Limit)
669
+ {
670
+ Assets.SetNum(Limit);
671
+ }
672
+
673
+ TArray<TSharedPtr<FJsonValue>> OutAssets;
674
+ OutAssets.Reserve(Assets.Num());
675
+ for (const FAssetData& Asset : Assets)
676
+ {
677
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
678
+ Obj->SetStringField(TEXT("name"), Asset.AssetName.ToString());
679
+ Obj->SetStringField(TEXT("objectPath"), Asset.GetObjectPathString());
680
+ Obj->SetStringField(TEXT("packagePath"), Asset.PackagePath.ToString());
681
+ Obj->SetStringField(TEXT("class"), Asset.AssetClassPath.ToString());
682
+ OutAssets.Add(MakeShared<FJsonValueObject>(Obj));
683
+ }
684
+
685
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
686
+ Result->SetStringField(TEXT("root"), Root);
687
+ Result->SetStringField(TEXT("query"), Query);
688
+ Result->SetNumberField(TEXT("count"), OutAssets.Num());
689
+ Result->SetNumberField(TEXT("totalMatches"), Total);
690
+ Result->SetArrayField(TEXT("assets"), OutAssets);
691
+ return Result;
692
+ }
693
+
694
+ static FString InferPlacementKindFromAssetClassPath(const FString& ClassPath)
695
+ {
696
+ if (ClassPath.Contains(TEXT("StaticMesh"), ESearchCase::IgnoreCase)) return TEXT("static_mesh");
697
+ if (ClassPath.Contains(TEXT("Blueprint"), ESearchCase::IgnoreCase)) return TEXT("blueprint");
698
+ if (ClassPath.Contains(TEXT("Material"), ESearchCase::IgnoreCase)) return TEXT("material");
699
+ if (ClassPath.Contains(TEXT("Texture"), ESearchCase::IgnoreCase)) return TEXT("texture");
700
+ return TEXT("unknown");
701
+ }
702
+
703
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetAssetInfo(const TSharedPtr<FJsonObject>& Params)
704
+ {
705
+ FString ObjectPath;
706
+ if (!Params->TryGetStringField(TEXT("objectPath"), ObjectPath) && !Params->TryGetStringField(TEXT("object_path"), ObjectPath))
707
+ {
708
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'objectPath' parameter"));
709
+ }
710
+ ObjectPath = ObjectPath.TrimStartAndEnd();
711
+ if (ObjectPath.IsEmpty())
712
+ {
713
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("objectPath is empty"));
714
+ }
715
+
716
+ FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
717
+ IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
718
+
719
+ const FAssetData Asset = AssetRegistry.GetAssetByObjectPath(FSoftObjectPath(ObjectPath));
720
+ if (!Asset.IsValid())
721
+ {
722
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Asset not found: %s"), *ObjectPath));
723
+ }
724
+
725
+ const FString ClassPath = Asset.AssetClassPath.ToString();
726
+
727
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
728
+ Result->SetStringField(TEXT("name"), Asset.AssetName.ToString());
729
+ Result->SetStringField(TEXT("objectPath"), Asset.GetObjectPathString());
730
+ Result->SetStringField(TEXT("packageName"), Asset.PackageName.ToString());
731
+ Result->SetStringField(TEXT("packagePath"), Asset.PackagePath.ToString());
732
+ Result->SetStringField(TEXT("class"), ClassPath);
733
+ Result->SetStringField(TEXT("placementKind"), InferPlacementKindFromAssetClassPath(ClassPath));
734
+
735
+ if (Params->HasField(TEXT("includeDependencies")) && Params->GetBoolField(TEXT("includeDependencies")))
736
+ {
737
+ TArray<FName> Deps;
738
+ AssetRegistry.GetDependencies(Asset.PackageName, Deps);
739
+ TArray<TSharedPtr<FJsonValue>> DepArr;
740
+ DepArr.Reserve(Deps.Num());
741
+ for (const FName& Dep : Deps)
742
+ {
743
+ DepArr.Add(MakeShared<FJsonValueString>(Dep.ToString()));
744
+ }
745
+ Result->SetArrayField(TEXT("dependencies"), DepArr);
746
+ }
747
+
748
+ return Result;
749
+ }
750
+
751
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleListAssetPacks(const TSharedPtr<FJsonObject>& Params)
752
+ {
753
+ int32 Limit = 200;
754
+ if (Params->HasField(TEXT("limit")))
755
+ {
756
+ Limit = (int32)Params->GetNumberField(TEXT("limit"));
757
+ }
758
+ Limit = FMath::Clamp(Limit, 1, 500);
759
+
760
+ FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
761
+ IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
762
+
763
+ TArray<FString> SubPaths;
764
+ AssetRegistry.GetSubPaths(TEXT("/Game"), SubPaths, false);
765
+ SubPaths.Sort();
766
+
767
+ TArray<TSharedPtr<FJsonValue>> Packs;
768
+ for (const FString& Path : SubPaths)
769
+ {
770
+ if (Packs.Num() >= Limit) break;
771
+ if (Path.StartsWith(TEXT("/Game/__External"), ESearchCase::IgnoreCase)) continue;
772
+ if (Path.StartsWith(TEXT("/Game/Developers"), ESearchCase::IgnoreCase)) continue;
773
+
774
+ FString Name = Path;
775
+ int32 SlashIndex;
776
+ if (Name.FindLastChar(TEXT('/'), SlashIndex))
777
+ {
778
+ Name = Name.Mid(SlashIndex + 1);
779
+ }
780
+
781
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
782
+ Obj->SetStringField(TEXT("name"), Name);
783
+ Obj->SetStringField(TEXT("path"), Path + TEXT("/"));
784
+ Packs.Add(MakeShared<FJsonValueObject>(Obj));
785
+ }
786
+
787
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
788
+ Result->SetNumberField(TEXT("count"), Packs.Num());
789
+ Result->SetArrayField(TEXT("packs"), Packs);
790
+ return Result;
791
+ }
792
+
793
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandlePlaceAsset(const TSharedPtr<FJsonObject>& Params)
794
+ {
795
+ {
796
+ FString Err;
797
+ if (!IsSafeToModifyEditorWorld(Err))
798
+ {
799
+ return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
800
+ }
801
+ }
802
+
803
+ FString ObjectPath;
804
+ if (!Params->TryGetStringField(TEXT("objectPath"), ObjectPath) && !Params->TryGetStringField(TEXT("object_path"), ObjectPath))
805
+ {
806
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'objectPath' parameter"));
807
+ }
808
+ ObjectPath = ObjectPath.TrimStartAndEnd();
809
+ if (ObjectPath.IsEmpty())
810
+ {
811
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("objectPath is empty"));
812
+ }
813
+
814
+ FVector Location(0.0f, 0.0f, 0.0f);
815
+ FRotator Rotation(0.0f, 0.0f, 0.0f);
816
+ FVector Scale(1.0f, 1.0f, 1.0f);
817
+
818
+ if (Params->HasField(TEXT("location")))
819
+ {
820
+ Location = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("location"));
821
+ }
822
+ if (Params->HasField(TEXT("rotation")))
823
+ {
824
+ Rotation = FUnrealMCPCommonUtils::GetRotatorFromJson(Params, TEXT("rotation"));
825
+ }
826
+ if (Params->HasField(TEXT("scale")))
827
+ {
828
+ Scale = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("scale"));
829
+ }
830
+
831
+ UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
832
+ if (!World)
833
+ {
834
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world"));
835
+ }
836
+
837
+ FString AssetClassPath;
838
+ FString AssetName;
839
+ {
840
+ FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
841
+ IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
842
+ const FAssetData Asset = AssetRegistry.GetAssetByObjectPath(FSoftObjectPath(ObjectPath));
843
+ if (Asset.IsValid())
844
+ {
845
+ AssetClassPath = Asset.AssetClassPath.ToString();
846
+ AssetName = Asset.AssetName.ToString();
847
+ }
848
+ }
849
+
850
+ FString RequestedName;
851
+ Params->TryGetStringField(TEXT("name"), RequestedName);
852
+ RequestedName = RequestedName.TrimStartAndEnd();
853
+ if (RequestedName.IsEmpty())
854
+ {
855
+ RequestedName = !AssetName.IsEmpty() ? AssetName : TEXT("PlacedAsset");
856
+ }
857
+
858
+ AActor* NewActor = nullptr;
859
+ FString PlacementKind = !AssetClassPath.IsEmpty() ? InferPlacementKindFromAssetClassPath(AssetClassPath) : TEXT("unknown");
860
+
861
+ UObject* AssetObj = LoadObject<UObject>(nullptr, *ObjectPath);
862
+ if (!AssetObj && !ObjectPath.Contains(TEXT("'")))
863
+ {
864
+ AssetObj = LoadObject<UObject>(nullptr, *FString::Printf(TEXT("StaticMesh'%s'"), *ObjectPath));
865
+ if (!AssetObj)
866
+ {
867
+ AssetObj = LoadObject<UObject>(nullptr, *FString::Printf(TEXT("Blueprint'%s'"), *ObjectPath));
868
+ }
869
+ }
870
+
871
+ if (!AssetObj)
872
+ {
873
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Failed to load asset: %s"), *ObjectPath));
874
+ }
875
+
876
+ FActorSpawnParameters SpawnParams;
877
+ SpawnParams.Name = MakeUniqueObjectName(World, AActor::StaticClass(), FName(*RequestedName));
878
+
879
+ if (UStaticMesh* StaticMesh = Cast<UStaticMesh>(AssetObj))
880
+ {
881
+ PlacementKind = TEXT("static_mesh");
882
+ AStaticMeshActor* StaticMeshActor = World->SpawnActor<AStaticMeshActor>(AStaticMeshActor::StaticClass(), Location, Rotation, SpawnParams);
883
+ if (!StaticMeshActor || !StaticMeshActor->GetStaticMeshComponent())
884
+ {
885
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to spawn StaticMeshActor"));
886
+ }
887
+
888
+ StaticMeshActor->GetStaticMeshComponent()->SetStaticMesh(StaticMesh);
889
+ StaticMeshActor->GetStaticMeshComponent()->MarkRenderStateDirty();
890
+ NewActor = StaticMeshActor;
891
+ }
892
+ else if (UBlueprint* Blueprint = Cast<UBlueprint>(AssetObj))
893
+ {
894
+ PlacementKind = TEXT("blueprint");
895
+ if (!Blueprint->GeneratedClass)
896
+ {
897
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Blueprint has no GeneratedClass (compile it and retry)."));
898
+ }
899
+ const FTransform SpawnTransform(FQuat(Rotation), Location, Scale);
900
+ NewActor = World->SpawnActor<AActor>(Blueprint->GeneratedClass, SpawnTransform, SpawnParams);
901
+ }
902
+ else if (UBlueprintGeneratedClass* GeneratedClass = Cast<UBlueprintGeneratedClass>(AssetObj))
903
+ {
904
+ PlacementKind = TEXT("blueprint");
905
+ const FTransform SpawnTransform(FQuat(Rotation), Location, Scale);
906
+ NewActor = World->SpawnActor<AActor>(GeneratedClass, SpawnTransform, SpawnParams);
907
+ }
908
+ else if (UClass* Class = Cast<UClass>(AssetObj))
909
+ {
910
+ if (!Class->IsChildOf(AActor::StaticClass()))
911
+ {
912
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Loaded class is not an Actor class: %s"), *Class->GetName()));
913
+ }
914
+ PlacementKind = TEXT("blueprint");
915
+ const FTransform SpawnTransform(FQuat(Rotation), Location, Scale);
916
+ NewActor = World->SpawnActor<AActor>(Class, SpawnTransform, SpawnParams);
917
+ }
918
+ else
919
+ {
920
+ const FString ClassName = AssetObj->GetClass() ? AssetObj->GetClass()->GetName() : TEXT("UnknownClass");
921
+ return FUnrealMCPCommonUtils::CreateErrorResponse(
922
+ FString::Printf(TEXT("Asset is not placeable (class: %s). Placeable: StaticMesh, Blueprint. Asset: %s"), *ClassName, *ObjectPath));
923
+ }
924
+
925
+ if (!NewActor)
926
+ {
927
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to spawn actor for asset."));
928
+ }
929
+
930
+ NewActor->SetActorScale3D(Scale);
931
+
932
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
933
+ Result->SetStringField(TEXT("placementKind"), PlacementKind);
934
+ Result->SetStringField(TEXT("requestedObjectPath"), ObjectPath);
935
+ Result->SetStringField(TEXT("spawnedName"), NewActor->GetName());
936
+ Result->SetObjectField(TEXT("actor"), FUnrealMCPCommonUtils::ActorToJsonObject(NewActor, true));
937
+ return Result;
938
+ }
939
+
940
+ static FString MessageSeverityToString(EMessageSeverity::Type Severity)
941
+ {
942
+ switch (Severity)
943
+ {
944
+ case EMessageSeverity::CriticalError:
945
+ case EMessageSeverity::Error:
946
+ return TEXT("error");
947
+ case EMessageSeverity::Warning:
948
+ case EMessageSeverity::PerformanceWarning:
949
+ return TEXT("warning");
950
+ case EMessageSeverity::Info:
951
+ return TEXT("info");
952
+ default:
953
+ return TEXT("info");
954
+ }
955
+ }
956
+
957
+ static TArray<TSharedPtr<FJsonValue>> VectorToJsonArray(const FVector& V)
958
+ {
959
+ TArray<TSharedPtr<FJsonValue>> Arr;
960
+ Arr.Add(MakeShared<FJsonValueNumber>(V.X));
961
+ Arr.Add(MakeShared<FJsonValueNumber>(V.Y));
962
+ Arr.Add(MakeShared<FJsonValueNumber>(V.Z));
963
+ return Arr;
964
+ }
965
+
966
+ static TArray<TSharedPtr<FJsonValue>> RotatorToJsonArray(const FRotator& R)
967
+ {
968
+ TArray<TSharedPtr<FJsonValue>> Arr;
969
+ Arr.Add(MakeShared<FJsonValueNumber>(R.Pitch));
970
+ Arr.Add(MakeShared<FJsonValueNumber>(R.Yaw));
971
+ Arr.Add(MakeShared<FJsonValueNumber>(R.Roll));
972
+ return Arr;
973
+ }
974
+
975
+ static AActor* FindActorByNameOrLabel(UWorld* World, const FString& NameOrLabel)
976
+ {
977
+ if (!World) return nullptr;
978
+ if (NameOrLabel.IsEmpty()) return nullptr;
979
+
980
+ TArray<AActor*> AllActors;
981
+ UGameplayStatics::GetAllActorsOfClass(World, AActor::StaticClass(), AllActors);
982
+ for (AActor* Actor : AllActors)
983
+ {
984
+ if (!Actor) continue;
985
+ if (Actor->GetName().Equals(NameOrLabel, ESearchCase::CaseSensitive) ||
986
+ Actor->GetName().Equals(NameOrLabel, ESearchCase::IgnoreCase))
987
+ {
988
+ return Actor;
989
+ }
990
+ #if WITH_EDITOR
991
+ if (Actor->GetActorLabel().Equals(NameOrLabel, ESearchCase::IgnoreCase))
992
+ {
993
+ return Actor;
994
+ }
995
+ #endif
996
+ }
997
+
998
+ return nullptr;
999
+ }
1000
+
1001
+ static TSharedPtr<FJsonObject> RaycastInWorld(UWorld* World, const FVector& Start, const FVector& Dir, float MaxDistance, const TArray<FString>& IgnoreActorNames)
1002
+ {
1003
+ if (!World)
1004
+ {
1005
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("No world available for raycast."));
1006
+ }
1007
+
1008
+ const FVector Direction = Dir.GetSafeNormal();
1009
+ if (Direction.IsNearlyZero())
1010
+ {
1011
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Raycast direction is zero."));
1012
+ }
1013
+
1014
+ MaxDistance = FMath::Clamp(MaxDistance, 1.0f, 1.0e7f);
1015
+ const FVector End = Start + Direction * MaxDistance;
1016
+
1017
+ FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(UnrealMCP_Raycast), true);
1018
+ QueryParams.bReturnPhysicalMaterial = false;
1019
+
1020
+ for (const FString& Name : IgnoreActorNames)
1021
+ {
1022
+ if (AActor* Actor = FindActorByNameOrLabel(World, Name))
1023
+ {
1024
+ QueryParams.AddIgnoredActor(Actor);
1025
+ }
1026
+ }
1027
+
1028
+ FHitResult Hit;
1029
+ const bool bHit = World->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, QueryParams);
1030
+
1031
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1032
+ Result->SetBoolField(TEXT("hit"), bHit);
1033
+ Result->SetArrayField(TEXT("start"), VectorToJsonArray(Start));
1034
+ Result->SetArrayField(TEXT("end"), VectorToJsonArray(End));
1035
+ Result->SetNumberField(TEXT("maxDistance"), MaxDistance);
1036
+
1037
+ if (!bHit)
1038
+ {
1039
+ return Result;
1040
+ }
1041
+
1042
+ Result->SetArrayField(TEXT("location"), VectorToJsonArray(Hit.ImpactPoint));
1043
+ Result->SetArrayField(TEXT("normal"), VectorToJsonArray(Hit.ImpactNormal));
1044
+ Result->SetNumberField(TEXT("distance"), Hit.Distance);
1045
+ if (AActor* HitActor = Hit.GetActor())
1046
+ {
1047
+ Result->SetStringField(TEXT("actor"), HitActor->GetName());
1048
+ #if WITH_EDITOR
1049
+ Result->SetStringField(TEXT("actorLabel"), HitActor->GetActorLabel());
1050
+ #endif
1051
+ }
1052
+ if (UPrimitiveComponent* HitComp = Hit.GetComponent())
1053
+ {
1054
+ Result->SetStringField(TEXT("component"), HitComp->GetName());
1055
+ }
1056
+
1057
+ return Result;
1058
+ }
1059
+
1060
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleMapCheck(const TSharedPtr<FJsonObject>& Params)
1061
+ {
1062
+ {
1063
+ FString Err;
1064
+ if (!IsSafeToModifyEditorWorld(Err))
1065
+ {
1066
+ return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
1067
+ }
1068
+ }
1069
+
1070
+ const bool bIncludeWarnings = !Params->HasField(TEXT("includeWarnings")) || Params->GetBoolField(TEXT("includeWarnings"));
1071
+ int32 Limit = 200;
1072
+ if (Params->HasField(TEXT("limit")))
1073
+ {
1074
+ Limit = (int32)Params->GetNumberField(TEXT("limit"));
1075
+ }
1076
+ Limit = FMath::Clamp(Limit, 1, 1000);
1077
+
1078
+ if (!GEditor)
1079
+ {
1080
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
1081
+ }
1082
+
1083
+ UWorld* World = GEditor->GetEditorWorldContext().World();
1084
+ if (!World)
1085
+ {
1086
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world"));
1087
+ }
1088
+
1089
+ FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>(TEXT("MessageLog"));
1090
+ const TSharedRef<IMessageLogListing> Listing = MessageLogModule.GetLogListing(TEXT("MapCheck"));
1091
+ Listing->ClearMessages();
1092
+
1093
+ FOutputDeviceNull Ar;
1094
+ // Use the editor's exec pathway so we can request "don't display dialog" without relying on private enums.
1095
+ const bool bOk = GEditor->Exec(World, TEXT("MAP CHECK DONTDISPLAYDIALOG"), Ar);
1096
+ if (!bOk)
1097
+ {
1098
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to run Map Check."));
1099
+ }
1100
+
1101
+ const TArray<TSharedRef<FTokenizedMessage>>& Messages = Listing->GetFilteredMessages();
1102
+ int32 ErrorCount = 0;
1103
+ int32 WarningCount = 0;
1104
+ int32 IncludedMessageCount = 0;
1105
+ bool bTruncated = false;
1106
+
1107
+ TArray<TSharedPtr<FJsonValue>> Issues;
1108
+ for (const TSharedRef<FTokenizedMessage>& Message : Messages)
1109
+ {
1110
+ const EMessageSeverity::Type Severity = Message->GetSeverity();
1111
+ const bool bIsWarning =
1112
+ Severity == EMessageSeverity::Warning || Severity == EMessageSeverity::PerformanceWarning;
1113
+ const bool bIsError = Severity == EMessageSeverity::Error || Severity == EMessageSeverity::CriticalError;
1114
+
1115
+ if (bIsError) ErrorCount++;
1116
+ if (bIsWarning) WarningCount++;
1117
+
1118
+ if (!bIncludeWarnings && bIsWarning) continue;
1119
+
1120
+ IncludedMessageCount++;
1121
+ if (Issues.Num() >= Limit)
1122
+ {
1123
+ bTruncated = true;
1124
+ continue;
1125
+ }
1126
+
1127
+ TSharedPtr<FJsonObject> Issue = MakeShared<FJsonObject>();
1128
+ Issue->SetStringField(TEXT("severity"), MessageSeverityToString(Severity));
1129
+ Issue->SetStringField(TEXT("message"), Message->ToText().ToString());
1130
+ Issues.Add(MakeShared<FJsonValueObject>(Issue));
1131
+ }
1132
+
1133
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1134
+ Result->SetBoolField(TEXT("ok"), ErrorCount == 0);
1135
+ Result->SetNumberField(TEXT("errors"), ErrorCount);
1136
+ Result->SetNumberField(TEXT("warnings"), WarningCount);
1137
+ Result->SetBoolField(TEXT("includeWarnings"), bIncludeWarnings);
1138
+ Result->SetArrayField(TEXT("issues"), Issues);
1139
+ Result->SetBoolField(TEXT("truncated"), bTruncated || IncludedMessageCount > Issues.Num());
1140
+ return Result;
1141
+ }
1142
+
1143
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetEditorContext(const TSharedPtr<FJsonObject>& Params)
1144
+ {
1145
+ if (!GEditor)
1146
+ {
1147
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
1148
+ }
1149
+
1150
+ UWorld* World = GEditor->GetEditorWorldContext().World();
1151
+ if (!World)
1152
+ {
1153
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world"));
1154
+ }
1155
+
1156
+ const FDateTime Now = FDateTime::UtcNow();
1157
+ const int64 TimestampMs = Now.ToUnixTimestamp() * 1000 + Now.GetMillisecond();
1158
+
1159
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1160
+ Result->SetNumberField(TEXT("timestampMs"), (double)TimestampMs);
1161
+
1162
+ const FString MapPackage = World->GetOutermost() ? World->GetOutermost()->GetName() : World->GetMapName();
1163
+ Result->SetStringField(TEXT("map"), MapPackage);
1164
+
1165
+ // Selection (editor)
1166
+ TArray<TSharedPtr<FJsonValue>> Selection;
1167
+ if (USelection* SelectedActors = GEditor->GetSelectedActors())
1168
+ {
1169
+ for (FSelectionIterator It(*SelectedActors); It; ++It)
1170
+ {
1171
+ if (AActor* Actor = Cast<AActor>(*It))
1172
+ {
1173
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
1174
+ Obj->SetStringField(TEXT("name"), Actor->GetName());
1175
+ #if WITH_EDITOR
1176
+ Obj->SetStringField(TEXT("label"), Actor->GetActorLabel());
1177
+ #endif
1178
+ Obj->SetArrayField(TEXT("location"), VectorToJsonArray(Actor->GetActorLocation()));
1179
+ Obj->SetArrayField(TEXT("rotation"), RotatorToJsonArray(Actor->GetActorRotation()));
1180
+ Selection.Add(MakeShared<FJsonValueObject>(Obj));
1181
+ }
1182
+ }
1183
+ }
1184
+ Result->SetArrayField(TEXT("selection"), Selection);
1185
+
1186
+ // Active viewport camera (best-effort)
1187
+ TSharedPtr<FJsonObject> ViewportCamera;
1188
+ FLevelEditorModule* LevelEditorModule = FModuleManager::LoadModulePtr<FLevelEditorModule>(TEXT("LevelEditor"));
1189
+ if (LevelEditorModule)
1190
+ {
1191
+ TSharedPtr<IAssetViewport> ActiveViewport = LevelEditorModule->GetFirstActiveViewport();
1192
+ if (ActiveViewport.IsValid())
1193
+ {
1194
+ const FEditorViewportClient& VC = ActiveViewport->GetAssetViewportClient();
1195
+ ViewportCamera = MakeShared<FJsonObject>();
1196
+ ViewportCamera->SetArrayField(TEXT("location"), VectorToJsonArray(VC.GetViewLocation()));
1197
+ ViewportCamera->SetArrayField(TEXT("rotation"), RotatorToJsonArray(VC.GetViewRotation()));
1198
+ ViewportCamera->SetNumberField(TEXT("fov"), VC.ViewFOV);
1199
+ ViewportCamera->SetArrayField(TEXT("forward"), VectorToJsonArray(VC.GetViewRotation().Vector()));
1200
+ }
1201
+ }
1202
+ if (ViewportCamera.IsValid())
1203
+ {
1204
+ Result->SetObjectField(TEXT("viewportCamera"), ViewportCamera);
1205
+ }
1206
+ else
1207
+ {
1208
+ Result->SetField(TEXT("viewportCamera"), MakeShared<FJsonValueNull>());
1209
+ }
1210
+
1211
+ return Result;
1212
+ }
1213
+
1214
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetPlayerContext(const TSharedPtr<FJsonObject>& Params)
1215
+ {
1216
+ if (!GEditor)
1217
+ {
1218
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
1219
+ }
1220
+
1221
+ UWorld* PlayWorld = GEditor->PlayWorld;
1222
+ const bool bIsPlaying = PlayWorld != nullptr;
1223
+
1224
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1225
+ Result->SetBoolField(TEXT("isPlaying"), bIsPlaying);
1226
+
1227
+ if (!bIsPlaying)
1228
+ {
1229
+ Result->SetField(TEXT("pawn"), MakeShared<FJsonValueNull>());
1230
+ Result->SetField(TEXT("camera"), MakeShared<FJsonValueNull>());
1231
+ return Result;
1232
+ }
1233
+
1234
+ APlayerController* PC = PlayWorld->GetFirstPlayerController();
1235
+ if (!PC)
1236
+ {
1237
+ Result->SetField(TEXT("pawn"), MakeShared<FJsonValueNull>());
1238
+ Result->SetField(TEXT("camera"), MakeShared<FJsonValueNull>());
1239
+ return Result;
1240
+ }
1241
+
1242
+ Result->SetStringField(TEXT("controller"), PC->GetName());
1243
+
1244
+ if (APawn* Pawn = PC->GetPawn())
1245
+ {
1246
+ TSharedPtr<FJsonObject> PawnObj = MakeShared<FJsonObject>();
1247
+ PawnObj->SetStringField(TEXT("name"), Pawn->GetName());
1248
+ PawnObj->SetArrayField(TEXT("location"), VectorToJsonArray(Pawn->GetActorLocation()));
1249
+ PawnObj->SetArrayField(TEXT("rotation"), RotatorToJsonArray(Pawn->GetActorRotation()));
1250
+ PawnObj->SetArrayField(TEXT("scale"), VectorToJsonArray(Pawn->GetActorScale3D()));
1251
+ Result->SetObjectField(TEXT("pawn"), PawnObj);
1252
+ }
1253
+ else
1254
+ {
1255
+ Result->SetField(TEXT("pawn"), MakeShared<FJsonValueNull>());
1256
+ }
1257
+
1258
+ FVector CamLoc;
1259
+ FRotator CamRot;
1260
+ PC->GetPlayerViewPoint(CamLoc, CamRot);
1261
+
1262
+ TSharedPtr<FJsonObject> CamObj = MakeShared<FJsonObject>();
1263
+ CamObj->SetArrayField(TEXT("location"), VectorToJsonArray(CamLoc));
1264
+ CamObj->SetArrayField(TEXT("rotation"), RotatorToJsonArray(CamRot));
1265
+ CamObj->SetArrayField(TEXT("forward"), VectorToJsonArray(CamRot.Vector()));
1266
+ Result->SetObjectField(TEXT("camera"), CamObj);
1267
+
1268
+ return Result;
1269
+ }
1270
+
1271
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleRaycastFromCamera(const TSharedPtr<FJsonObject>& Params)
1272
+ {
1273
+ const FString Source = Params->HasField(TEXT("source")) ? Params->GetStringField(TEXT("source")).ToLower() : TEXT("editor");
1274
+ const float MaxDistance = Params->HasField(TEXT("maxDistance")) ? (float)Params->GetNumberField(TEXT("maxDistance")) : 10000.0f;
1275
+
1276
+ TArray<FString> Ignore;
1277
+ if (Params->HasField(TEXT("ignoreActors")))
1278
+ {
1279
+ const TArray<TSharedPtr<FJsonValue>> Arr = Params->GetArrayField(TEXT("ignoreActors"));
1280
+ for (const TSharedPtr<FJsonValue>& V : Arr)
1281
+ {
1282
+ if (V.IsValid() && V->Type == EJson::String)
1283
+ {
1284
+ const FString S = V->AsString().TrimStartAndEnd();
1285
+ if (!S.IsEmpty()) Ignore.Add(S);
1286
+ }
1287
+ }
1288
+ }
1289
+
1290
+ if (!GEditor)
1291
+ {
1292
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
1293
+ }
1294
+
1295
+ if (Source == TEXT("pie") || Source == TEXT("player") || Source == TEXT("runtime"))
1296
+ {
1297
+ UWorld* PlayWorld = GEditor->PlayWorld;
1298
+ if (!PlayWorld)
1299
+ {
1300
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("PIE is not running (no PlayWorld)."));
1301
+ }
1302
+ APlayerController* PC = PlayWorld->GetFirstPlayerController();
1303
+ if (!PC)
1304
+ {
1305
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("PIE is running but no player controller was found."));
1306
+ }
1307
+ FVector CamLoc;
1308
+ FRotator CamRot;
1309
+ PC->GetPlayerViewPoint(CamLoc, CamRot);
1310
+ return RaycastInWorld(PlayWorld, CamLoc, CamRot.Vector(), MaxDistance, Ignore);
1311
+ }
1312
+
1313
+ // Default: editor viewport camera.
1314
+ FLevelEditorModule* LevelEditorModule = FModuleManager::LoadModulePtr<FLevelEditorModule>(TEXT("LevelEditor"));
1315
+ if (!LevelEditorModule)
1316
+ {
1317
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("LevelEditor module not available."));
1318
+ }
1319
+ TSharedPtr<IAssetViewport> ActiveViewport = LevelEditorModule->GetFirstActiveViewport();
1320
+ if (!ActiveViewport.IsValid())
1321
+ {
1322
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("No active level viewport. Click the viewport and retry."));
1323
+ }
1324
+
1325
+ const FEditorViewportClient& VC = ActiveViewport->GetAssetViewportClient();
1326
+ UWorld* World = GEditor->GetEditorWorldContext().World();
1327
+ return RaycastInWorld(World, VC.GetViewLocation(), VC.GetViewRotation().Vector(), MaxDistance, Ignore);
1328
+ }
1329
+
1330
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleRaycastDown(const TSharedPtr<FJsonObject>& Params)
1331
+ {
1332
+ if (!GEditor)
1333
+ {
1334
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
1335
+ }
1336
+
1337
+ FString WorldSource = TEXT("editor");
1338
+ if (Params->HasField(TEXT("source")))
1339
+ {
1340
+ WorldSource = Params->GetStringField(TEXT("source")).ToLower();
1341
+ }
1342
+
1343
+ FVector Start;
1344
+ if (Params->HasField(TEXT("startLocation")))
1345
+ {
1346
+ Start = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("startLocation"));
1347
+ }
1348
+ else
1349
+ {
1350
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'startLocation' parameter"));
1351
+ }
1352
+
1353
+ const float MaxDistance = Params->HasField(TEXT("maxDistance")) ? (float)Params->GetNumberField(TEXT("maxDistance")) : 100000.0f;
1354
+
1355
+ UWorld* World = nullptr;
1356
+ if (WorldSource == TEXT("pie"))
1357
+ {
1358
+ World = GEditor->PlayWorld;
1359
+ if (!World)
1360
+ {
1361
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("PIE is not running (no PlayWorld)."));
1362
+ }
1363
+ }
1364
+ else
1365
+ {
1366
+ World = GEditor->GetEditorWorldContext().World();
1367
+ }
1368
+
1369
+ return RaycastInWorld(World, Start, FVector(0.0f, 0.0f, -1.0f), MaxDistance, {});
1370
+ }
1371
+
1372
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetActorTransform(const TSharedPtr<FJsonObject>& Params)
1373
+ {
1374
+ if (!GEditor)
1375
+ {
1376
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
1377
+ }
1378
+
1379
+ FString Name;
1380
+ if (!Params->TryGetStringField(TEXT("name"), Name) && !Params->TryGetStringField(TEXT("actor"), Name))
1381
+ {
1382
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
1383
+ }
1384
+ Name = Name.TrimStartAndEnd();
1385
+ if (Name.IsEmpty())
1386
+ {
1387
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Actor name is empty"));
1388
+ }
1389
+
1390
+ FString Source = Params->HasField(TEXT("source")) ? Params->GetStringField(TEXT("source")).ToLower() : TEXT("editor");
1391
+ UWorld* World = nullptr;
1392
+ if (Source == TEXT("pie"))
1393
+ {
1394
+ World = GEditor->PlayWorld;
1395
+ }
1396
+ else
1397
+ {
1398
+ World = GEditor->GetEditorWorldContext().World();
1399
+ }
1400
+ if (!World)
1401
+ {
1402
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("World is not available."));
1403
+ }
1404
+
1405
+ AActor* Actor = FindActorByNameOrLabel(World, Name);
1406
+ if (!Actor)
1407
+ {
1408
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *Name));
1409
+ }
1410
+
1411
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1412
+ Result->SetStringField(TEXT("name"), Actor->GetName());
1413
+ #if WITH_EDITOR
1414
+ Result->SetStringField(TEXT("label"), Actor->GetActorLabel());
1415
+ #endif
1416
+ Result->SetArrayField(TEXT("location"), VectorToJsonArray(Actor->GetActorLocation()));
1417
+ Result->SetArrayField(TEXT("rotation"), RotatorToJsonArray(Actor->GetActorRotation()));
1418
+ Result->SetArrayField(TEXT("scale"), VectorToJsonArray(Actor->GetActorScale3D()));
1419
+ return Result;
1420
+ }
1421
+
1422
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetActorBounds(const TSharedPtr<FJsonObject>& Params)
1423
+ {
1424
+ if (!GEditor)
1425
+ {
1426
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Editor is not available (GEditor is null)."));
1427
+ }
1428
+
1429
+ FString Name;
1430
+ if (!Params->TryGetStringField(TEXT("name"), Name) && !Params->TryGetStringField(TEXT("actor"), Name))
1431
+ {
1432
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
1433
+ }
1434
+ Name = Name.TrimStartAndEnd();
1435
+ if (Name.IsEmpty())
1436
+ {
1437
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Actor name is empty"));
1438
+ }
1439
+
1440
+ FString Source = Params->HasField(TEXT("source")) ? Params->GetStringField(TEXT("source")).ToLower() : TEXT("editor");
1441
+ UWorld* World = nullptr;
1442
+ if (Source == TEXT("pie"))
1443
+ {
1444
+ World = GEditor->PlayWorld;
1445
+ }
1446
+ else
1447
+ {
1448
+ World = GEditor->GetEditorWorldContext().World();
1449
+ }
1450
+ if (!World)
1451
+ {
1452
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("World is not available."));
1453
+ }
1454
+
1455
+ AActor* Actor = FindActorByNameOrLabel(World, Name);
1456
+ if (!Actor)
1457
+ {
1458
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *Name));
1459
+ }
1460
+
1461
+ FVector Origin;
1462
+ FVector Extent;
1463
+ Actor->GetActorBounds(true, Origin, Extent, false);
1464
+
1465
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1466
+ Result->SetStringField(TEXT("name"), Actor->GetName());
1467
+ #if WITH_EDITOR
1468
+ Result->SetStringField(TEXT("label"), Actor->GetActorLabel());
1469
+ #endif
1470
+ Result->SetArrayField(TEXT("origin"), VectorToJsonArray(Origin));
1471
+ Result->SetArrayField(TEXT("extent"), VectorToJsonArray(Extent));
1472
+ Result->SetArrayField(TEXT("min"), VectorToJsonArray(Origin - Extent));
1473
+ Result->SetArrayField(TEXT("max"), VectorToJsonArray(Origin + Extent));
1474
+ return Result;
1475
+ }
1476
+
1477
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetActorsInLevel(const TSharedPtr<FJsonObject>& Params)
1478
+ {
1479
+ TArray<AActor*> AllActors;
1480
+ UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
1481
+
1482
+ TArray<TSharedPtr<FJsonValue>> ActorArray;
1483
+ for (AActor* Actor : AllActors)
1484
+ {
1485
+ if (Actor)
1486
+ {
1487
+ ActorArray.Add(FUnrealMCPCommonUtils::ActorToJson(Actor));
1488
+ }
1489
+ }
1490
+
1491
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
1492
+ ResultObj->SetArrayField(TEXT("actors"), ActorArray);
1493
+
1494
+ return ResultObj;
1495
+ }
1496
+
1497
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleFindActorsByName(const TSharedPtr<FJsonObject>& Params)
1498
+ {
1499
+ FString Pattern;
1500
+ if (!Params->TryGetStringField(TEXT("pattern"), Pattern) && !Params->TryGetStringField(TEXT("name"), Pattern))
1501
+ {
1502
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'pattern' parameter"));
1503
+ }
1504
+ Pattern = Pattern.TrimStartAndEnd();
1505
+ if (Pattern.IsEmpty())
1506
+ {
1507
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("pattern is empty"));
1508
+ }
1509
+
1510
+ TArray<AActor*> AllActors;
1511
+ UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
1512
+
1513
+ TArray<TSharedPtr<FJsonValue>> MatchingActors;
1514
+ for (AActor* Actor : AllActors)
1515
+ {
1516
+ if (!Actor) continue;
1517
+ const bool bNameMatch = Actor->GetName().Contains(Pattern);
1518
+ #if WITH_EDITOR
1519
+ const bool bLabelMatch = Actor->GetActorLabel().Contains(Pattern);
1520
+ #else
1521
+ const bool bLabelMatch = false;
1522
+ #endif
1523
+ if (bNameMatch || bLabelMatch)
1524
+ {
1525
+ MatchingActors.Add(FUnrealMCPCommonUtils::ActorToJson(Actor));
1526
+ }
1527
+ }
1528
+
1529
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
1530
+ ResultObj->SetArrayField(TEXT("actors"), MatchingActors);
1531
+
1532
+ return ResultObj;
1533
+ }
1534
+
1535
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSpawnActor(const TSharedPtr<FJsonObject>& Params)
1536
+ {
1537
+ {
1538
+ FString Err;
1539
+ if (!IsSafeToModifyEditorWorld(Err))
1540
+ {
1541
+ return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
1542
+ }
1543
+ }
1544
+
1545
+ // Get required parameters
1546
+ FString ActorType;
1547
+ if (!Params->TryGetStringField(TEXT("type"), ActorType))
1548
+ {
1549
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'type' parameter"));
1550
+ }
1551
+
1552
+ // Get actor name (required parameter)
1553
+ FString ActorName;
1554
+ if (!Params->TryGetStringField(TEXT("name"), ActorName))
1555
+ {
1556
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
1557
+ }
1558
+
1559
+ // Get optional transform parameters
1560
+ FVector Location(0.0f, 0.0f, 0.0f);
1561
+ FRotator Rotation(0.0f, 0.0f, 0.0f);
1562
+ FVector Scale(1.0f, 1.0f, 1.0f);
1563
+
1564
+ if (Params->HasField(TEXT("location")))
1565
+ {
1566
+ Location = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("location"));
1567
+ }
1568
+ if (Params->HasField(TEXT("rotation")))
1569
+ {
1570
+ Rotation = FUnrealMCPCommonUtils::GetRotatorFromJson(Params, TEXT("rotation"));
1571
+ }
1572
+ if (Params->HasField(TEXT("scale")))
1573
+ {
1574
+ Scale = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("scale"));
1575
+ }
1576
+
1577
+ // Create the actor based on type
1578
+ AActor* NewActor = nullptr;
1579
+ UWorld* World = GEditor->GetEditorWorldContext().World();
1580
+
1581
+ if (!World)
1582
+ {
1583
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world"));
1584
+ }
1585
+
1586
+ // Check if an actor with this name already exists
1587
+ TArray<AActor*> AllActors;
1588
+ UGameplayStatics::GetAllActorsOfClass(World, AActor::StaticClass(), AllActors);
1589
+ for (AActor* Actor : AllActors)
1590
+ {
1591
+ if (Actor && Actor->GetName() == ActorName)
1592
+ {
1593
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor with name '%s' already exists"), *ActorName));
1594
+ }
1595
+ }
1596
+
1597
+ FActorSpawnParameters SpawnParams;
1598
+ SpawnParams.Name = *ActorName;
1599
+
1600
+ if (ActorType == TEXT("StaticMeshActor"))
1601
+ {
1602
+ NewActor = World->SpawnActor<AStaticMeshActor>(AStaticMeshActor::StaticClass(), Location, Rotation, SpawnParams);
1603
+
1604
+ // Optional: assign a StaticMesh at spawn time (preferred, since nested component property setting is limited).
1605
+ FString StaticMeshPath;
1606
+ if (Params->TryGetStringField(TEXT("static_mesh_path"), StaticMeshPath) ||
1607
+ Params->TryGetStringField(TEXT("static_mesh"), StaticMeshPath))
1608
+ {
1609
+ StaticMeshPath = StaticMeshPath.TrimStartAndEnd();
1610
+ if (!StaticMeshPath.IsEmpty())
1611
+ {
1612
+ UStaticMesh* StaticMesh = LoadObject<UStaticMesh>(nullptr, *StaticMeshPath);
1613
+ if (!StaticMesh && !StaticMeshPath.StartsWith(TEXT("StaticMesh'")))
1614
+ {
1615
+ StaticMesh = LoadObject<UStaticMesh>(nullptr, *FString::Printf(TEXT("StaticMesh'%s'"), *StaticMeshPath));
1616
+ }
1617
+
1618
+ if (!StaticMesh)
1619
+ {
1620
+ return FUnrealMCPCommonUtils::CreateErrorResponse(
1621
+ FString::Printf(TEXT("Static mesh not found: %s"), *StaticMeshPath));
1622
+ }
1623
+
1624
+ AStaticMeshActor* StaticMeshActor = Cast<AStaticMeshActor>(NewActor);
1625
+ if (!StaticMeshActor || !StaticMeshActor->GetStaticMeshComponent())
1626
+ {
1627
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Spawned StaticMeshActor has no StaticMeshComponent."));
1628
+ }
1629
+
1630
+ StaticMeshActor->GetStaticMeshComponent()->SetStaticMesh(StaticMesh);
1631
+ StaticMeshActor->GetStaticMeshComponent()->MarkRenderStateDirty();
1632
+ }
1633
+ }
1634
+ }
1635
+ else if (ActorType == TEXT("PointLight"))
1636
+ {
1637
+ NewActor = World->SpawnActor<APointLight>(APointLight::StaticClass(), Location, Rotation, SpawnParams);
1638
+ }
1639
+ else if (ActorType == TEXT("SpotLight"))
1640
+ {
1641
+ NewActor = World->SpawnActor<ASpotLight>(ASpotLight::StaticClass(), Location, Rotation, SpawnParams);
1642
+ }
1643
+ else if (ActorType == TEXT("DirectionalLight"))
1644
+ {
1645
+ NewActor = World->SpawnActor<ADirectionalLight>(ADirectionalLight::StaticClass(), Location, Rotation, SpawnParams);
1646
+ }
1647
+ else if (ActorType == TEXT("CameraActor"))
1648
+ {
1649
+ NewActor = World->SpawnActor<ACameraActor>(ACameraActor::StaticClass(), Location, Rotation, SpawnParams);
1650
+ }
1651
+ else
1652
+ {
1653
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Unknown actor type: %s"), *ActorType));
1654
+ }
1655
+
1656
+ if (NewActor)
1657
+ {
1658
+ // Set scale (since SpawnActor only takes location and rotation)
1659
+ FTransform Transform = NewActor->GetTransform();
1660
+ Transform.SetScale3D(Scale);
1661
+ NewActor->SetActorTransform(Transform);
1662
+
1663
+ // Return the created actor's details
1664
+ return FUnrealMCPCommonUtils::ActorToJsonObject(NewActor, true);
1665
+ }
1666
+
1667
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to create actor"));
1668
+ }
1669
+
1670
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleCreateLandscape(const TSharedPtr<FJsonObject>& Params)
1671
+ {
1672
+ {
1673
+ FString Err;
1674
+ if (!IsSafeToModifyEditorWorld(Err))
1675
+ {
1676
+ return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
1677
+ }
1678
+ }
1679
+
1680
+ UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
1681
+ if (!World)
1682
+ {
1683
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world"));
1684
+ }
1685
+
1686
+ FString LandscapeName;
1687
+ if (!Params->TryGetStringField(TEXT("name"), LandscapeName))
1688
+ {
1689
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
1690
+ }
1691
+ LandscapeName = LandscapeName.TrimStartAndEnd();
1692
+ if (LandscapeName.IsEmpty())
1693
+ {
1694
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Invalid 'name' parameter (empty)"));
1695
+ }
1696
+
1697
+ // Ensure actor name uniqueness.
1698
+ {
1699
+ TArray<AActor*> AllActors;
1700
+ UGameplayStatics::GetAllActorsOfClass(World, AActor::StaticClass(), AllActors);
1701
+ for (AActor* Actor : AllActors)
1702
+ {
1703
+ if (Actor && Actor->GetName() == LandscapeName)
1704
+ {
1705
+ return FUnrealMCPCommonUtils::CreateErrorResponse(
1706
+ FString::Printf(TEXT("Actor with name '%s' already exists"), *LandscapeName));
1707
+ }
1708
+ }
1709
+ }
1710
+
1711
+ FVector Location(0.0f, 0.0f, 0.0f);
1712
+ FRotator Rotation(0.0f, 0.0f, 0.0f);
1713
+ FVector Scale(100.0f, 100.0f, 100.0f);
1714
+
1715
+ if (Params->HasField(TEXT("location")))
1716
+ {
1717
+ Location = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("location"));
1718
+ }
1719
+ if (Params->HasField(TEXT("rotation")))
1720
+ {
1721
+ Rotation = FUnrealMCPCommonUtils::GetRotatorFromJson(Params, TEXT("rotation"));
1722
+ }
1723
+ if (Params->HasField(TEXT("scale")))
1724
+ {
1725
+ Scale = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("scale"));
1726
+ }
1727
+
1728
+ int32 ComponentCountX = 8;
1729
+ int32 ComponentCountY = 8;
1730
+ int32 SectionsPerComponent = 1;
1731
+ int32 QuadsPerSection = 63;
1732
+
1733
+ {
1734
+ double Value = 0;
1735
+ if (Params->TryGetNumberField(TEXT("componentCountX"), Value)) ComponentCountX = (int32)Value;
1736
+ if (Params->TryGetNumberField(TEXT("componentCountY"), Value)) ComponentCountY = (int32)Value;
1737
+ if (Params->TryGetNumberField(TEXT("sectionsPerComponent"), Value)) SectionsPerComponent = (int32)Value;
1738
+ if (Params->TryGetNumberField(TEXT("quadsPerSection"), Value)) QuadsPerSection = (int32)Value;
1739
+ }
1740
+
1741
+ if (ComponentCountX <= 0 || ComponentCountY <= 0)
1742
+ {
1743
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("componentCountX/componentCountY must be positive integers"));
1744
+ }
1745
+ if (SectionsPerComponent <= 0 || SectionsPerComponent > 2)
1746
+ {
1747
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("sectionsPerComponent must be 1 or 2"));
1748
+ }
1749
+ if (QuadsPerSection <= 0)
1750
+ {
1751
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("quadsPerSection must be a positive integer"));
1752
+ }
1753
+
1754
+ const int32 QuadsPerComponent = SectionsPerComponent * QuadsPerSection;
1755
+ const int32 SizeX = ComponentCountX * QuadsPerComponent + 1;
1756
+ const int32 SizeY = ComponentCountY * QuadsPerComponent + 1;
1757
+
1758
+ const int64 TotalVerts = (int64)SizeX * (int64)SizeY;
1759
+ if (TotalVerts > 16ll * 1024ll * 1024ll)
1760
+ {
1761
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Landscape is too large (vertex count limit exceeded)"));
1762
+ }
1763
+
1764
+ // Optional landscape material.
1765
+ UMaterialInterface* LandscapeMaterial = nullptr;
1766
+ {
1767
+ FString MaterialPath;
1768
+ if (Params->TryGetStringField(TEXT("materialPath"), MaterialPath) ||
1769
+ Params->TryGetStringField(TEXT("material"), MaterialPath) ||
1770
+ Params->TryGetStringField(TEXT("landscapeMaterial"), MaterialPath))
1771
+ {
1772
+ MaterialPath = MaterialPath.TrimStartAndEnd();
1773
+ if (!MaterialPath.IsEmpty())
1774
+ {
1775
+ LandscapeMaterial = LoadObject<UMaterialInterface>(nullptr, *MaterialPath);
1776
+ if (!LandscapeMaterial && !MaterialPath.StartsWith(TEXT("Material'")))
1777
+ {
1778
+ LandscapeMaterial = LoadObject<UMaterialInterface>(nullptr, *FString::Printf(TEXT("Material'%s'"), *MaterialPath));
1779
+ }
1780
+ if (!LandscapeMaterial)
1781
+ {
1782
+ return FUnrealMCPCommonUtils::CreateErrorResponse(
1783
+ FString::Printf(TEXT("Landscape material not found: %s"), *MaterialPath));
1784
+ }
1785
+ }
1786
+ }
1787
+ }
1788
+
1789
+ // Center on Location (similar to the editor create landscape UX).
1790
+ const FVector Offset =
1791
+ FTransform(Rotation, FVector::ZeroVector, Scale)
1792
+ .TransformVector(FVector(-ComponentCountX * QuadsPerComponent / 2, -ComponentCountY * QuadsPerComponent / 2, 0));
1793
+
1794
+ FActorSpawnParameters SpawnParams;
1795
+ SpawnParams.Name = *LandscapeName;
1796
+ ALandscape* Landscape = World->SpawnActor<ALandscape>(Location + Offset, Rotation, SpawnParams);
1797
+ if (!Landscape)
1798
+ {
1799
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to spawn Landscape actor"));
1800
+ }
1801
+
1802
+ Landscape->SetActorRelativeScale3D(Scale);
1803
+ if (LandscapeMaterial)
1804
+ {
1805
+ Landscape->LandscapeMaterial = LandscapeMaterial;
1806
+ }
1807
+
1808
+ Landscape->StaticLightingLOD = FMath::DivideAndRoundUp(FMath::CeilLogTwo((SizeX * SizeY) / (2048 * 2048) + 1), (uint32)2);
1809
+
1810
+ // Flat heightmap.
1811
+ TArray<uint16> HeightData;
1812
+ HeightData.SetNumUninitialized(SizeX * SizeY);
1813
+ for (int32 i = 0; i < HeightData.Num(); i++)
1814
+ {
1815
+ HeightData[i] = 32768;
1816
+ }
1817
+
1818
+ TMap<FGuid, TArray<uint16>> HeightDataPerLayers;
1819
+ HeightDataPerLayers.Add(FGuid(), MoveTemp(HeightData));
1820
+
1821
+ TMap<FGuid, TArray<FLandscapeImportLayerInfo>> MaterialLayerDataPerLayers;
1822
+ MaterialLayerDataPerLayers.Add(FGuid(), TArray<FLandscapeImportLayerInfo>());
1823
+
1824
+ FString ReimportHeightmapFilePath;
1825
+
1826
+ Landscape->Import(
1827
+ FGuid::NewGuid(),
1828
+ 0,
1829
+ 0,
1830
+ SizeX - 1,
1831
+ SizeY - 1,
1832
+ SectionsPerComponent,
1833
+ QuadsPerSection,
1834
+ HeightDataPerLayers,
1835
+ *ReimportHeightmapFilePath,
1836
+ MaterialLayerDataPerLayers,
1837
+ ELandscapeImportAlphamapType::Additive,
1838
+ TArrayView<const FLandscapeLayer>());
1839
+
1840
+ ULandscapeInfo* LandscapeInfo = Landscape->GetLandscapeInfo();
1841
+ if (!LandscapeInfo)
1842
+ {
1843
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Landscape import failed (LandscapeInfo missing)"));
1844
+ }
1845
+
1846
+ FActorLabelUtilities::SetActorLabelUnique(Landscape, LandscapeName);
1847
+ LandscapeInfo->UpdateLayerInfoMap(Landscape);
1848
+
1849
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
1850
+ ResultObj->SetObjectField(TEXT("landscape"), FUnrealMCPCommonUtils::ActorToJsonObject(Landscape));
1851
+ ResultObj->SetNumberField(TEXT("componentCountX"), ComponentCountX);
1852
+ ResultObj->SetNumberField(TEXT("componentCountY"), ComponentCountY);
1853
+ ResultObj->SetNumberField(TEXT("sectionsPerComponent"), SectionsPerComponent);
1854
+ ResultObj->SetNumberField(TEXT("quadsPerSection"), QuadsPerSection);
1855
+ ResultObj->SetNumberField(TEXT("sizeX"), SizeX);
1856
+ ResultObj->SetNumberField(TEXT("sizeY"), SizeY);
1857
+ return ResultObj;
1858
+ }
1859
+
1860
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleDeleteActor(const TSharedPtr<FJsonObject>& Params)
1861
+ {
1862
+ {
1863
+ FString Err;
1864
+ if (!IsSafeToModifyEditorWorld(Err))
1865
+ {
1866
+ return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
1867
+ }
1868
+ }
1869
+
1870
+ FString ActorName;
1871
+ if (!Params->TryGetStringField(TEXT("name"), ActorName))
1872
+ {
1873
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
1874
+ }
1875
+
1876
+ TArray<AActor*> AllActors;
1877
+ UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
1878
+
1879
+ for (AActor* Actor : AllActors)
1880
+ {
1881
+ if (Actor && Actor->GetName() == ActorName)
1882
+ {
1883
+ // Store actor info before deletion for the response
1884
+ TSharedPtr<FJsonObject> ActorInfo = FUnrealMCPCommonUtils::ActorToJsonObject(Actor);
1885
+
1886
+ // Delete the actor
1887
+ Actor->Destroy();
1888
+
1889
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
1890
+ ResultObj->SetObjectField(TEXT("deleted_actor"), ActorInfo);
1891
+ return ResultObj;
1892
+ }
1893
+ }
1894
+
1895
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *ActorName));
1896
+ }
1897
+
1898
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSetActorTransform(const TSharedPtr<FJsonObject>& Params)
1899
+ {
1900
+ {
1901
+ FString Err;
1902
+ if (!IsSafeToModifyEditorWorld(Err))
1903
+ {
1904
+ return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
1905
+ }
1906
+ }
1907
+
1908
+ // Get actor name
1909
+ FString ActorName;
1910
+ if (!Params->TryGetStringField(TEXT("name"), ActorName))
1911
+ {
1912
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
1913
+ }
1914
+
1915
+ // Find the actor
1916
+ AActor* TargetActor = nullptr;
1917
+ TArray<AActor*> AllActors;
1918
+ UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
1919
+
1920
+ for (AActor* Actor : AllActors)
1921
+ {
1922
+ if (Actor && Actor->GetName() == ActorName)
1923
+ {
1924
+ TargetActor = Actor;
1925
+ break;
1926
+ }
1927
+ }
1928
+
1929
+ if (!TargetActor)
1930
+ {
1931
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *ActorName));
1932
+ }
1933
+
1934
+ // Get transform parameters
1935
+ FTransform NewTransform = TargetActor->GetTransform();
1936
+
1937
+ if (Params->HasField(TEXT("location")))
1938
+ {
1939
+ NewTransform.SetLocation(FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("location")));
1940
+ }
1941
+ if (Params->HasField(TEXT("rotation")))
1942
+ {
1943
+ NewTransform.SetRotation(FQuat(FUnrealMCPCommonUtils::GetRotatorFromJson(Params, TEXT("rotation"))));
1944
+ }
1945
+ if (Params->HasField(TEXT("scale")))
1946
+ {
1947
+ NewTransform.SetScale3D(FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("scale")));
1948
+ }
1949
+
1950
+ // Set the new transform
1951
+ TargetActor->SetActorTransform(NewTransform);
1952
+
1953
+ // Return updated actor info
1954
+ return FUnrealMCPCommonUtils::ActorToJsonObject(TargetActor, true);
1955
+ }
1956
+
1957
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleGetActorProperties(const TSharedPtr<FJsonObject>& Params)
1958
+ {
1959
+ // Get actor name
1960
+ FString ActorName;
1961
+ if (!Params->TryGetStringField(TEXT("name"), ActorName) && !Params->TryGetStringField(TEXT("actor"), ActorName))
1962
+ {
1963
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
1964
+ }
1965
+
1966
+ // Find the actor
1967
+ AActor* TargetActor = nullptr;
1968
+ TArray<AActor*> AllActors;
1969
+ UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
1970
+
1971
+ for (AActor* Actor : AllActors)
1972
+ {
1973
+ if (Actor && Actor->GetName() == ActorName)
1974
+ {
1975
+ TargetActor = Actor;
1976
+ break;
1977
+ }
1978
+ }
1979
+
1980
+ if (!TargetActor)
1981
+ {
1982
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *ActorName));
1983
+ }
1984
+
1985
+ // Always return detailed properties for this command
1986
+ return FUnrealMCPCommonUtils::ActorToJsonObject(TargetActor, true);
1987
+ }
1988
+
1989
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSetActorProperty(const TSharedPtr<FJsonObject>& Params)
1990
+ {
1991
+ {
1992
+ FString Err;
1993
+ if (!IsSafeToModifyEditorWorld(Err))
1994
+ {
1995
+ return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
1996
+ }
1997
+ }
1998
+
1999
+ // Get actor name
2000
+ FString ActorName;
2001
+ if (!Params->TryGetStringField(TEXT("name"), ActorName) && !Params->TryGetStringField(TEXT("actor"), ActorName))
2002
+ {
2003
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'name' parameter"));
2004
+ }
2005
+
2006
+ // Find the actor
2007
+ AActor* TargetActor = nullptr;
2008
+ TArray<AActor*> AllActors;
2009
+ UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
2010
+
2011
+ for (AActor* Actor : AllActors)
2012
+ {
2013
+ if (Actor && Actor->GetName() == ActorName)
2014
+ {
2015
+ TargetActor = Actor;
2016
+ break;
2017
+ }
2018
+ }
2019
+
2020
+ if (!TargetActor)
2021
+ {
2022
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *ActorName));
2023
+ }
2024
+
2025
+ // Get property name
2026
+ FString PropertyName;
2027
+ if (!Params->TryGetStringField(TEXT("property_name"), PropertyName) && !Params->TryGetStringField(TEXT("property"), PropertyName))
2028
+ {
2029
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'property_name' parameter"));
2030
+ }
2031
+
2032
+ // Get property value
2033
+ if (!Params->HasField(TEXT("property_value")) && !Params->HasField(TEXT("value")))
2034
+ {
2035
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'property_value' parameter"));
2036
+ }
2037
+
2038
+ TSharedPtr<FJsonValue> PropertyValue = Params->HasField(TEXT("property_value"))
2039
+ ? Params->Values.FindRef(TEXT("property_value"))
2040
+ : Params->Values.FindRef(TEXT("value"));
2041
+
2042
+ // Set the property using our utility function
2043
+ FString ErrorMessage;
2044
+ if (FUnrealMCPCommonUtils::SetObjectProperty(TargetActor, PropertyName, PropertyValue, ErrorMessage))
2045
+ {
2046
+ // Property set successfully
2047
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
2048
+ ResultObj->SetStringField(TEXT("actor"), ActorName);
2049
+ ResultObj->SetStringField(TEXT("property"), PropertyName);
2050
+ ResultObj->SetBoolField(TEXT("success"), true);
2051
+
2052
+ // Also include the full actor details
2053
+ ResultObj->SetObjectField(TEXT("actor_details"), FUnrealMCPCommonUtils::ActorToJsonObject(TargetActor, true));
2054
+ return ResultObj;
2055
+ }
2056
+ else
2057
+ {
2058
+ return FUnrealMCPCommonUtils::CreateErrorResponse(ErrorMessage);
2059
+ }
2060
+ }
2061
+
2062
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleSpawnBlueprintActor(const TSharedPtr<FJsonObject>& Params)
2063
+ {
2064
+ {
2065
+ FString Err;
2066
+ if (!IsSafeToModifyEditorWorld(Err))
2067
+ {
2068
+ return FUnrealMCPCommonUtils::CreateErrorResponse(Err);
2069
+ }
2070
+ }
2071
+
2072
+ // Get required parameters
2073
+ FString BlueprintName;
2074
+ if (!Params->TryGetStringField(TEXT("blueprint_name"), BlueprintName) &&
2075
+ !Params->TryGetStringField(TEXT("blueprint"), BlueprintName) &&
2076
+ !Params->TryGetStringField(TEXT("blueprint_path"), BlueprintName) &&
2077
+ !Params->TryGetStringField(TEXT("objectPath"), BlueprintName) &&
2078
+ !Params->TryGetStringField(TEXT("object_path"), BlueprintName))
2079
+ {
2080
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'blueprint_name' parameter"));
2081
+ }
2082
+
2083
+ FString ActorName;
2084
+ if (!Params->TryGetStringField(TEXT("actor_name"), ActorName) && !Params->TryGetStringField(TEXT("name"), ActorName))
2085
+ {
2086
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Missing 'actor_name' parameter"));
2087
+ }
2088
+
2089
+ // Find the blueprint
2090
+ if (BlueprintName.IsEmpty())
2091
+ {
2092
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Blueprint name is empty"));
2093
+ }
2094
+ BlueprintName = BlueprintName.TrimStartAndEnd();
2095
+ ActorName = ActorName.TrimStartAndEnd();
2096
+
2097
+ const auto SanitizeObjectName = [](const FString& In) -> FString {
2098
+ FString Out;
2099
+ Out.Reserve(In.Len());
2100
+ for (int32 i = 0; i < In.Len(); i++)
2101
+ {
2102
+ const TCHAR Ch = In[i];
2103
+ const bool bOk =
2104
+ (Ch >= '0' && Ch <= '9') ||
2105
+ (Ch >= 'A' && Ch <= 'Z') ||
2106
+ (Ch >= 'a' && Ch <= 'z') ||
2107
+ Ch == '_' || Ch == '-';
2108
+ Out.AppendChar(bOk ? Ch : '_');
2109
+ }
2110
+ Out = Out.TrimStartAndEnd();
2111
+ while (Out.Contains(TEXT("__")))
2112
+ {
2113
+ Out = Out.Replace(TEXT("__"), TEXT("_"));
2114
+ }
2115
+ if (Out.IsEmpty()) Out = TEXT("BPActor");
2116
+ return Out;
2117
+ };
2118
+
2119
+ const FString SafeActorBaseName = SanitizeObjectName(ActorName);
2120
+
2121
+ // Resolve an asset/object path:
2122
+ // - If caller passes /Game/... treat as an object path/package path.
2123
+ // - Otherwise assume /Game/Blueprints/<BlueprintName> (compat default).
2124
+ FString AssetPath = BlueprintName;
2125
+ if (!AssetPath.StartsWith(TEXT("/")))
2126
+ {
2127
+ AssetPath = TEXT("/Game/Blueprints/") + AssetPath;
2128
+ }
2129
+
2130
+ UObject* AssetObj = LoadObject<UObject>(nullptr, *AssetPath);
2131
+ if (!AssetObj && !AssetPath.Contains(TEXT("'")))
2132
+ {
2133
+ AssetObj = LoadObject<UObject>(nullptr, *FString::Printf(TEXT("Blueprint'%s'"), *AssetPath));
2134
+ }
2135
+
2136
+ if (!AssetObj)
2137
+ {
2138
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Failed to load blueprint asset: %s"), *AssetPath));
2139
+ }
2140
+
2141
+ // Get transform parameters
2142
+ FVector Location(0.0f, 0.0f, 0.0f);
2143
+ FRotator Rotation(0.0f, 0.0f, 0.0f);
2144
+ FVector Scale(1.0f, 1.0f, 1.0f);
2145
+
2146
+ if (Params->HasField(TEXT("location")))
2147
+ {
2148
+ Location = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("location"));
2149
+ }
2150
+ if (Params->HasField(TEXT("rotation")))
2151
+ {
2152
+ Rotation = FUnrealMCPCommonUtils::GetRotatorFromJson(Params, TEXT("rotation"));
2153
+ }
2154
+ if (Params->HasField(TEXT("scale")))
2155
+ {
2156
+ Scale = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("scale"));
2157
+ }
2158
+
2159
+ // Spawn the actor
2160
+ UWorld* World = GEditor->GetEditorWorldContext().World();
2161
+ if (!World)
2162
+ {
2163
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world"));
2164
+ }
2165
+
2166
+ FTransform SpawnTransform;
2167
+ SpawnTransform.SetLocation(Location);
2168
+ SpawnTransform.SetRotation(FQuat(Rotation));
2169
+ SpawnTransform.SetScale3D(Scale);
2170
+
2171
+ FActorSpawnParameters SpawnParams;
2172
+ SpawnParams.Name = MakeUniqueObjectName(World, AActor::StaticClass(), FName(*SafeActorBaseName));
2173
+
2174
+ UClass* ActorClass = nullptr;
2175
+ if (UBlueprint* Blueprint = Cast<UBlueprint>(AssetObj))
2176
+ {
2177
+ ActorClass = Blueprint->GeneratedClass;
2178
+ }
2179
+ else if (UBlueprintGeneratedClass* BPGC = Cast<UBlueprintGeneratedClass>(AssetObj))
2180
+ {
2181
+ ActorClass = BPGC;
2182
+ }
2183
+ else if (UClass* Class = Cast<UClass>(AssetObj))
2184
+ {
2185
+ ActorClass = Class;
2186
+ }
2187
+
2188
+ if (!ActorClass || !ActorClass->IsChildOf(AActor::StaticClass()))
2189
+ {
2190
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Loaded asset is not a spawnable Actor Blueprint/Class. Compile the Blueprint and retry."));
2191
+ }
2192
+
2193
+ AActor* NewActor = World->SpawnActor<AActor>(ActorClass, SpawnTransform, SpawnParams);
2194
+ if (NewActor)
2195
+ {
2196
+ bool bDetailed = true;
2197
+ if (Params->HasField(TEXT("include_details")))
2198
+ {
2199
+ bDetailed = Params->GetBoolField(TEXT("include_details"));
2200
+ }
2201
+ else if (Params->HasField(TEXT("detailed")))
2202
+ {
2203
+ bDetailed = Params->GetBoolField(TEXT("detailed"));
2204
+ }
2205
+ else if (Params->HasField(TEXT("includeProperties")))
2206
+ {
2207
+ bDetailed = Params->GetBoolField(TEXT("includeProperties"));
2208
+ }
2209
+
2210
+ return FUnrealMCPCommonUtils::ActorToJsonObject(NewActor, bDetailed);
2211
+ }
2212
+
2213
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to spawn blueprint actor"));
2214
+ }
2215
+
2216
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleFocusViewport(const TSharedPtr<FJsonObject>& Params)
2217
+ {
2218
+ // Get target actor name if provided
2219
+ FString TargetActorName;
2220
+ bool HasTargetActor = Params->TryGetStringField(TEXT("target"), TargetActorName);
2221
+
2222
+ // Get location if provided
2223
+ FVector Location(0.0f, 0.0f, 0.0f);
2224
+ bool HasLocation = false;
2225
+ if (Params->HasField(TEXT("location")))
2226
+ {
2227
+ Location = FUnrealMCPCommonUtils::GetVectorFromJson(Params, TEXT("location"));
2228
+ HasLocation = true;
2229
+ }
2230
+
2231
+ // Get distance
2232
+ float Distance = 1000.0f;
2233
+ if (Params->HasField(TEXT("distance")))
2234
+ {
2235
+ Distance = Params->GetNumberField(TEXT("distance"));
2236
+ }
2237
+
2238
+ // Get orientation if provided
2239
+ FRotator Orientation(0.0f, 0.0f, 0.0f);
2240
+ bool HasOrientation = false;
2241
+ if (Params->HasField(TEXT("orientation")))
2242
+ {
2243
+ Orientation = FUnrealMCPCommonUtils::GetRotatorFromJson(Params, TEXT("orientation"));
2244
+ HasOrientation = true;
2245
+ }
2246
+
2247
+ // Get the active viewport
2248
+ FLevelEditorViewportClient* ViewportClient = (FLevelEditorViewportClient*)GEditor->GetActiveViewport()->GetClient();
2249
+ if (!ViewportClient)
2250
+ {
2251
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get active viewport"));
2252
+ }
2253
+
2254
+ // If we have a target actor, focus on it
2255
+ if (HasTargetActor)
2256
+ {
2257
+ // Find the actor
2258
+ AActor* TargetActor = nullptr;
2259
+ TArray<AActor*> AllActors;
2260
+ UGameplayStatics::GetAllActorsOfClass(GWorld, AActor::StaticClass(), AllActors);
2261
+
2262
+ for (AActor* Actor : AllActors)
2263
+ {
2264
+ if (Actor && Actor->GetName() == TargetActorName)
2265
+ {
2266
+ TargetActor = Actor;
2267
+ break;
2268
+ }
2269
+ }
2270
+
2271
+ if (!TargetActor)
2272
+ {
2273
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *TargetActorName));
2274
+ }
2275
+
2276
+ // Focus on the actor
2277
+ ViewportClient->SetViewLocation(TargetActor->GetActorLocation() - FVector(Distance, 0.0f, 0.0f));
2278
+ }
2279
+ // Otherwise use the provided location
2280
+ else if (HasLocation)
2281
+ {
2282
+ ViewportClient->SetViewLocation(Location - FVector(Distance, 0.0f, 0.0f));
2283
+ }
2284
+ else
2285
+ {
2286
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Either 'target' or 'location' must be provided"));
2287
+ }
2288
+
2289
+ // Set orientation if provided
2290
+ if (HasOrientation)
2291
+ {
2292
+ ViewportClient->SetViewRotation(Orientation);
2293
+ }
2294
+
2295
+ // Force viewport to redraw
2296
+ ViewportClient->Invalidate();
2297
+
2298
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
2299
+ ResultObj->SetBoolField(TEXT("success"), true);
2300
+ return ResultObj;
2301
+ }
2302
+
2303
+ TSharedPtr<FJsonObject> FUnrealMCPEditorCommands::HandleTakeScreenshot(const TSharedPtr<FJsonObject>& Params)
2304
+ {
2305
+ // Optional file path parameter.
2306
+ // If omitted, save to <Project>/Saved/Screenshots/Flockbay with an auto-generated filename.
2307
+ FString FilePath;
2308
+ const bool bHasFilePath = Params->TryGetStringField(TEXT("filepath"), FilePath);
2309
+
2310
+ FString FileName;
2311
+ Params->TryGetStringField(TEXT("filename"), FileName);
2312
+
2313
+ const FString DefaultDir = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Screenshots"), TEXT("Flockbay"));
2314
+ IFileManager::Get().MakeDirectory(*DefaultDir, /*Tree*/ true);
2315
+
2316
+ if (bHasFilePath)
2317
+ {
2318
+ FilePath = FilePath.TrimStartAndEnd();
2319
+ if (FilePath.IsEmpty())
2320
+ {
2321
+ FilePath.Reset();
2322
+ }
2323
+ else if (!FPaths::IsRelative(FilePath))
2324
+ {
2325
+ // absolute path: use as-is
2326
+ }
2327
+ else
2328
+ {
2329
+ // relative path: resolve under our default screenshots folder
2330
+ FilePath = FPaths::Combine(DefaultDir, FilePath);
2331
+ }
2332
+ }
2333
+
2334
+ if (FilePath.IsEmpty())
2335
+ {
2336
+ if (FileName.IsEmpty())
2337
+ {
2338
+ FileName = FString::Printf(TEXT("mcp_screenshot_%s"), *FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S")));
2339
+ }
2340
+ FilePath = FPaths::Combine(DefaultDir, FileName);
2341
+ }
2342
+
2343
+ // Ensure the file path has a proper extension
2344
+ if (!FilePath.EndsWith(TEXT(".png")))
2345
+ {
2346
+ FilePath += TEXT(".png");
2347
+ }
2348
+
2349
+ FString Source = TEXT("auto");
2350
+ if (Params->TryGetStringField(TEXT("source"), Source))
2351
+ {
2352
+ Source = Source.TrimStartAndEnd().ToLower();
2353
+ if (Source.IsEmpty())
2354
+ {
2355
+ Source = TEXT("auto");
2356
+ }
2357
+ }
2358
+
2359
+ double WidthNum = 0.0;
2360
+ double HeightNum = 0.0;
2361
+ int32 RequestedWidth = 0;
2362
+ int32 RequestedHeight = 0;
2363
+ if (Params->TryGetNumberField(TEXT("width"), WidthNum))
2364
+ {
2365
+ RequestedWidth = static_cast<int32>(WidthNum);
2366
+ }
2367
+ else if (Params->TryGetNumberField(TEXT("resX"), WidthNum))
2368
+ {
2369
+ RequestedWidth = static_cast<int32>(WidthNum);
2370
+ }
2371
+
2372
+ if (Params->TryGetNumberField(TEXT("height"), HeightNum))
2373
+ {
2374
+ RequestedHeight = static_cast<int32>(HeightNum);
2375
+ }
2376
+ else if (Params->TryGetNumberField(TEXT("resY"), HeightNum))
2377
+ {
2378
+ RequestedHeight = static_cast<int32>(HeightNum);
2379
+ }
2380
+
2381
+ const bool bPieRunning = (GEditor && GEditor->PlayWorld);
2382
+ const bool bWantsPie = (Source == TEXT("pie"));
2383
+ const bool bWantsEditor = (Source == TEXT("editor"));
2384
+ const bool bAuto = (Source == TEXT("auto"));
2385
+ if (!bWantsPie && !bWantsEditor && !bAuto)
2386
+ {
2387
+ return FUnrealMCPCommonUtils::CreateErrorResponse(FString::Printf(TEXT("Invalid screenshot source: %s (expected: auto|pie|editor)"), *Source));
2388
+ }
2389
+
2390
+ const bool bUsePieCapture = bWantsPie || (bAuto && bPieRunning);
2391
+ if (bUsePieCapture)
2392
+ {
2393
+ if (!bPieRunning)
2394
+ {
2395
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("PIE is not running (PlayWorld is null)."));
2396
+ }
2397
+
2398
+ UWorld* PlayWorld = GEditor->PlayWorld;
2399
+ APlayerController* PC = PlayWorld ? PlayWorld->GetFirstPlayerController() : nullptr;
2400
+ if (!PC)
2401
+ {
2402
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to find PlayerController in PlayWorld."));
2403
+ }
2404
+
2405
+ APlayerCameraManager* PCM = PC->PlayerCameraManager;
2406
+ if (!PCM)
2407
+ {
2408
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to find PlayerCameraManager (PlayerController.PlayerCameraManager is null)."));
2409
+ }
2410
+
2411
+ int32 Width = RequestedWidth;
2412
+ int32 Height = RequestedHeight;
2413
+ if (Width <= 0 || Height <= 0)
2414
+ {
2415
+ if (ULocalPlayer* LocalPlayer = PC->GetLocalPlayer())
2416
+ {
2417
+ if (LocalPlayer->ViewportClient && LocalPlayer->ViewportClient->Viewport)
2418
+ {
2419
+ const FIntPoint ViewSize = LocalPlayer->ViewportClient->Viewport->GetSizeXY();
2420
+ if (ViewSize.X > 0 && ViewSize.Y > 0)
2421
+ {
2422
+ if (Width <= 0)
2423
+ {
2424
+ Width = ViewSize.X;
2425
+ }
2426
+ if (Height <= 0)
2427
+ {
2428
+ Height = ViewSize.Y;
2429
+ }
2430
+ }
2431
+ }
2432
+ }
2433
+ }
2434
+
2435
+ if (Width <= 0 || Height <= 0)
2436
+ {
2437
+ Width = 1280;
2438
+ Height = 720;
2439
+ }
2440
+
2441
+ const FVector CamLocation = PCM->GetCameraLocation();
2442
+ const FRotator CamRotation = PCM->GetCameraRotation();
2443
+
2444
+ FActorSpawnParameters SpawnParams;
2445
+ SpawnParams.ObjectFlags |= RF_Transient;
2446
+ SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
2447
+
2448
+ ASceneCapture2D* CaptureActor = PlayWorld->SpawnActor<ASceneCapture2D>(ASceneCapture2D::StaticClass(), CamLocation, CamRotation, SpawnParams);
2449
+ if (!CaptureActor)
2450
+ {
2451
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to spawn ASceneCapture2D for PIE screenshot capture."));
2452
+ }
2453
+
2454
+ USceneCaptureComponent2D* CaptureComp = CaptureActor->GetCaptureComponent2D();
2455
+ if (!CaptureComp)
2456
+ {
2457
+ CaptureActor->Destroy();
2458
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get SceneCaptureComponent2D from ASceneCapture2D."));
2459
+ }
2460
+
2461
+ UTextureRenderTarget2D* RenderTarget = NewObject<UTextureRenderTarget2D>(CaptureComp);
2462
+ if (!RenderTarget)
2463
+ {
2464
+ CaptureActor->Destroy();
2465
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to create UTextureRenderTarget2D."));
2466
+ }
2467
+ RenderTarget->InitCustomFormat(Width, Height, PF_B8G8R8A8, /*bForceLinearGamma*/ false);
2468
+ RenderTarget->ClearColor = FLinearColor::Black;
2469
+ RenderTarget->UpdateResourceImmediate(true);
2470
+
2471
+ CaptureComp->TextureTarget = RenderTarget;
2472
+ CaptureComp->bCaptureEveryFrame = false;
2473
+ CaptureComp->bCaptureOnMovement = false;
2474
+ CaptureComp->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR;
2475
+ CaptureComp->FOVAngle = PCM->GetFOVAngle();
2476
+
2477
+ CaptureComp->CaptureScene();
2478
+
2479
+ FTextureRenderTargetResource* RenderResource = RenderTarget->GameThread_GetRenderTargetResource();
2480
+ if (!RenderResource)
2481
+ {
2482
+ CaptureActor->Destroy();
2483
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get render target resource for PIE screenshot capture."));
2484
+ }
2485
+
2486
+ TArray<FColor> Bitmap;
2487
+ if (!RenderResource->ReadPixels(Bitmap))
2488
+ {
2489
+ CaptureActor->Destroy();
2490
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to read pixels from PIE render target."));
2491
+ }
2492
+
2493
+ CaptureActor->Destroy();
2494
+
2495
+ if (!SavePngToFile(FilePath, Width, Height, Bitmap))
2496
+ {
2497
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to save screenshot PNG to disk."));
2498
+ }
2499
+
2500
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
2501
+ ResultObj->SetStringField(TEXT("filepath"), FilePath);
2502
+ ResultObj->SetStringField(TEXT("source"), TEXT("pie"));
2503
+ ResultObj->SetNumberField(TEXT("width"), Width);
2504
+ ResultObj->SetNumberField(TEXT("height"), Height);
2505
+ return ResultObj;
2506
+ }
2507
+
2508
+ // Editor screenshot (prefer offscreen SceneCapture2D; ReadPixels on the active viewport can be blank/stale when the OS window isn't focused).
2509
+ if (GEditor && GEditor->GetActiveViewport())
2510
+ {
2511
+ GEditor->RedrawAllViewports(/*bInvalidateHitProxies*/ true);
2512
+
2513
+ FViewport* Viewport = GEditor->GetActiveViewport();
2514
+ FEditorViewportClient* ViewportClient = (FEditorViewportClient*)Viewport->GetClient();
2515
+ if (!ViewportClient)
2516
+ {
2517
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get active editor viewport client for editor screenshot capture."));
2518
+ }
2519
+
2520
+ UWorld* EditorWorld = ViewportClient->GetWorld();
2521
+ if (!EditorWorld)
2522
+ {
2523
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get editor world for editor screenshot capture."));
2524
+ }
2525
+
2526
+ int32 Width = RequestedWidth;
2527
+ int32 Height = RequestedHeight;
2528
+ if (Width <= 0 || Height <= 0)
2529
+ {
2530
+ const FIntPoint ViewSize = Viewport->GetSizeXY();
2531
+ if (ViewSize.X > 0 && ViewSize.Y > 0)
2532
+ {
2533
+ if (Width <= 0)
2534
+ {
2535
+ Width = ViewSize.X;
2536
+ }
2537
+ if (Height <= 0)
2538
+ {
2539
+ Height = ViewSize.Y;
2540
+ }
2541
+ }
2542
+ }
2543
+ if (Width <= 0 || Height <= 0)
2544
+ {
2545
+ Width = 1280;
2546
+ Height = 720;
2547
+ }
2548
+
2549
+ const FVector CamLocation = ViewportClient->GetViewLocation();
2550
+ const FRotator CamRotation = ViewportClient->GetViewRotation();
2551
+
2552
+ FActorSpawnParameters SpawnParams;
2553
+ SpawnParams.ObjectFlags |= RF_Transient;
2554
+ SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
2555
+
2556
+ ASceneCapture2D* CaptureActor =
2557
+ EditorWorld->SpawnActor<ASceneCapture2D>(ASceneCapture2D::StaticClass(), CamLocation, CamRotation, SpawnParams);
2558
+ if (!CaptureActor)
2559
+ {
2560
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to spawn ASceneCapture2D for editor screenshot capture."));
2561
+ }
2562
+
2563
+ USceneCaptureComponent2D* CaptureComp = CaptureActor->GetCaptureComponent2D();
2564
+ if (!CaptureComp)
2565
+ {
2566
+ CaptureActor->Destroy();
2567
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get SceneCaptureComponent2D from ASceneCapture2D (editor capture)."));
2568
+ }
2569
+
2570
+ UTextureRenderTarget2D* RenderTarget = NewObject<UTextureRenderTarget2D>(CaptureComp);
2571
+ if (!RenderTarget)
2572
+ {
2573
+ CaptureActor->Destroy();
2574
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to create UTextureRenderTarget2D (editor capture)."));
2575
+ }
2576
+ RenderTarget->InitCustomFormat(Width, Height, PF_B8G8R8A8, /*bForceLinearGamma*/ false);
2577
+ RenderTarget->ClearColor = FLinearColor::Black;
2578
+ RenderTarget->UpdateResourceImmediate(true);
2579
+
2580
+ CaptureComp->TextureTarget = RenderTarget;
2581
+ CaptureComp->bCaptureEveryFrame = false;
2582
+ CaptureComp->bCaptureOnMovement = false;
2583
+ CaptureComp->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR;
2584
+ CaptureComp->FOVAngle = ViewportClient->ViewFOV;
2585
+
2586
+ CaptureComp->CaptureScene();
2587
+
2588
+ FTextureRenderTargetResource* RenderResource = RenderTarget->GameThread_GetRenderTargetResource();
2589
+ if (!RenderResource)
2590
+ {
2591
+ CaptureActor->Destroy();
2592
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to get render target resource for editor screenshot capture."));
2593
+ }
2594
+
2595
+ TArray<FColor> Bitmap;
2596
+ if (!RenderResource->ReadPixels(Bitmap))
2597
+ {
2598
+ CaptureActor->Destroy();
2599
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to read pixels from editor render target."));
2600
+ }
2601
+
2602
+ CaptureActor->Destroy();
2603
+
2604
+ if (!SavePngToFile(FilePath, Width, Height, Bitmap))
2605
+ {
2606
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to save editor screenshot PNG to disk."));
2607
+ }
2608
+
2609
+ TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
2610
+ ResultObj->SetStringField(TEXT("filepath"), FilePath);
2611
+ ResultObj->SetStringField(TEXT("source"), TEXT("editor"));
2612
+ ResultObj->SetNumberField(TEXT("width"), Width);
2613
+ ResultObj->SetNumberField(TEXT("height"), Height);
2614
+ return ResultObj;
2615
+ }
2616
+
2617
+ return FUnrealMCPCommonUtils::CreateErrorResponse(TEXT("Failed to take screenshot."));
2618
+ }