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.
- package/README.md +60 -60
- package/bin/flockbay-mcp.mjs +62 -62
- package/bin/flockbay.mjs +95 -95
- package/dist/{index-r3VTdgFI.cjs → index-IntvnrmZ.cjs} +71 -14
- package/dist/{index-D8AeVbvv.mjs → index-wkeSi0iC.mjs} +70 -13
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/lib.cjs +1 -1
- package/dist/lib.mjs +1 -1
- package/dist/{migratePlugin-CEvGPul8.mjs → migratePlugin-C71hAoc5.mjs} +12 -2
- package/dist/{migratePlugin-wFK-k3Wk.cjs → migratePlugin-D1XjjWFq.cjs} +12 -2
- package/dist/{runCodex-DuyuYqoB.cjs → runCodex-B7i_ICoQ.cjs} +34 -23
- package/dist/{runCodex-ZfUyhHF6.mjs → runCodex-BSx-hCu6.mjs} +34 -23
- package/dist/{runGemini-DBU1mMdp.mjs → runGemini-Dwyh7_WI.mjs} +2 -2
- package/dist/{runGemini-D7j5Y5TF.cjs → runGemini-gBKR3rdL.cjs} +2 -2
- package/dist/{types-DNr0xwSy.cjs → types-5z8UADAi.cjs} +224 -15
- package/dist/{types-BRJuZQj_.mjs → types-Bgzap7qR.mjs} +224 -16
- package/package.json +131 -131
- package/scripts/claude_local_launcher.cjs +72 -72
- package/scripts/claude_remote_launcher.cjs +15 -15
- package/scripts/claude_version_utils.cjs +445 -445
- package/scripts/ripgrep_launcher.cjs +32 -32
- package/scripts/run-with-root-env.mjs +37 -32
- package/scripts/session_hook_forwarder.cjs +49 -49
- package/scripts/test-codex-abort-history.mjs +77 -77
- package/scripts/unpack-tools.cjs +244 -244
- package/tools/licenses/difftastic-LICENSE +21 -21
- package/tools/licenses/ripgrep-LICENSE +3 -3
- package/tools/unreal-mcp/UPSTREAM_VERSION.md +8 -8
- package/tools/unreal-mcp/upstream/Docs/README.md +8 -8
- package/tools/unreal-mcp/upstream/Docs/Tools/README.md +6 -6
- package/tools/unreal-mcp/upstream/Docs/Tools/actor_tools.md +183 -183
- package/tools/unreal-mcp/upstream/Docs/Tools/blueprint_tools.md +268 -268
- package/tools/unreal-mcp/upstream/Docs/Tools/editor_tools.md +104 -104
- package/tools/unreal-mcp/upstream/Docs/Tools/node_tools.md +274 -274
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Config/FilterPlugin.ini +8 -8
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/FlockbayMCP.uplugin +36 -36
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/FlockbayMCP.Build.cs +82 -82
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPBlueprintCommands.cpp +1364 -1364
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPBlueprintNodeCommands.cpp +1310 -1310
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPCommandSchema.cpp +756 -756
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPCommonUtils.cpp +1177 -1177
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPEditorCommands.cpp +2618 -2618
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPProjectCommands.cpp +71 -71
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/Commands/UnrealMCPUMGCommands.cpp +543 -543
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/MCPServerRunnable.cpp +372 -372
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/UnrealMCPBridge.cpp +457 -457
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Private/UnrealMCPModule.cpp +21 -21
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPBlueprintCommands.h +35 -35
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPBlueprintNodeCommands.h +30 -30
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPCommandSchema.h +41 -41
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPCommonUtils.h +59 -59
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPEditorCommands.h +61 -61
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPProjectCommands.h +20 -20
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/Commands/UnrealMCPUMGCommands.h +82 -82
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/MCPServerRunnable.h +33 -33
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/UnrealMCPBridge.h +64 -64
- package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/FlockbayMCP/Source/FlockbayMCP/Public/UnrealMCPModule.h +22 -22
- package/tools/unreal-mcp/upstream/Python/README.md +39 -39
- package/tools/unreal-mcp/upstream/Python/pyproject.toml +22 -22
- package/tools/unreal-mcp/upstream/Python/scripts/actors/test_cube.py +202 -202
- package/tools/unreal-mcp/upstream/Python/scripts/blueprints/test_create_and_spawn_blueprints_with_different_components.py +496 -496
- package/tools/unreal-mcp/upstream/Python/scripts/blueprints/test_create_and_spawn_cube_blueprint.py +193 -193
- package/tools/unreal-mcp/upstream/Python/scripts/node/test_component_reference.py +266 -266
- package/tools/unreal-mcp/upstream/Python/scripts/node/test_create_bird_blueprint_with_input_and_camera.py +617 -617
- package/tools/unreal-mcp/upstream/Python/scripts/node/test_input_mapping.py +365 -365
- package/tools/unreal-mcp/upstream/Python/scripts/node/test_physics_variables.py +389 -389
- package/tools/unreal-mcp/upstream/Python/tools/blueprint_tools.py +419 -419
- package/tools/unreal-mcp/upstream/Python/tools/editor_tools.py +369 -369
- package/tools/unreal-mcp/upstream/Python/tools/node_tools.py +429 -429
- package/tools/unreal-mcp/upstream/Python/tools/project_tools.py +63 -63
- package/tools/unreal-mcp/upstream/Python/tools/umg_tools.py +332 -332
- package/tools/unreal-mcp/upstream/Python/unreal_mcp_server.py +398 -398
- 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
|
+
}
|