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