ue-mcp 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/context-cli.js +16 -16
  2. package/dist/engine-observer.js +68 -68
  3. package/dist/flow/guard.d.ts +26 -37
  4. package/dist/flow/guard.js +40 -25
  5. package/dist/flow/guard.js.map +1 -1
  6. package/dist/flow/guarded-bridge.d.ts +0 -10
  7. package/dist/flow/guarded-bridge.js +18 -23
  8. package/dist/flow/guarded-bridge.js.map +1 -1
  9. package/dist/flow/schema.d.ts +276 -16
  10. package/dist/flow/task-guards.d.ts +1 -1
  11. package/dist/flow/task-guards.js +44 -68
  12. package/dist/flow/task-guards.js.map +1 -1
  13. package/dist/instructions.js +130 -130
  14. package/dist/plugin-cli.js +299 -299
  15. package/dist/tool-counts.json +2 -2
  16. package/package.json +2 -2
  17. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/EngineStatusHooks.cpp +76 -76
  18. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/EngineStatusHooks.h +27 -27
  19. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerRegistry.cpp +128 -128
  20. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerRegistry.h +63 -63
  21. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRig.cpp +230 -230
  22. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +323 -323
  23. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Internal.h +20 -20
  24. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/ChooserHandlers_Nested.cpp +537 -537
  25. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DemoHandlers.h +77 -77
  26. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +353 -353
  27. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.h +18 -18
  28. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_InputRuntime.cpp +246 -246
  29. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +597 -597
  30. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +51 -51
  31. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +878 -878
  32. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +449 -449
  33. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Function.cpp +275 -275
  34. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Graph.cpp +551 -551
  35. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NetworkingHandlers.cpp +531 -531
  36. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NetworkingHandlers.h +28 -28
  37. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +677 -677
  38. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +822 -822
  39. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.h +22 -22
  40. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +218 -218
  41. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.h +17 -17
  42. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/VolumeHelpers_Internal.cpp +46 -46
  43. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/VolumeHelpers_Internal.h +23 -23
  44. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/MCPExternalRegistry.cpp +83 -83
  45. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/UE_MCP_BridgeModule.h +14 -14
  46. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/UE_MCP_BridgeStatusModule.cpp +37 -37
  47. package/dist/manifest-signature.d.ts +0 -1
  48. package/dist/manifest-signature.js +0 -53
  49. package/dist/manifest-signature.js.map +0 -1
  50. package/dist/pie/schema.d.ts +0 -791
  51. package/dist/pie/schema.js +0 -151
  52. package/dist/pie/schema.js.map +0 -1
  53. package/dist/watch-cli.js.map +0 -1
  54. /package/plugin/ue_mcp_bridge/{ue_mcp_bridge.uplugin → UE_MCP_Bridge.uplugin} +0 -0
@@ -1,878 +1,878 @@
1
- #include "LandscapeHandlers.h"
2
- #include "HandlerRegistry.h"
3
- #include "HandlerUtils.h"
4
- #include "Dom/JsonObject.h"
5
- #include "Dom/JsonValue.h"
6
- #include "Editor.h"
7
- #include "Engine/World.h"
8
- #include "EngineUtils.h"
9
- #include "Landscape.h"
10
- #include "LandscapeEditTypes.h"
11
- #include "LandscapeProxy.h"
12
- #include "LandscapeStreamingProxy.h"
13
- #include "LandscapeInfo.h"
14
- #include "LandscapeComponent.h"
15
- #include "LandscapeSplineActor.h"
16
- #include "LandscapeSplinesComponent.h"
17
- #include "LandscapeSplineControlPoint.h"
18
- #include "LandscapeSplineSegment.h"
19
- #include "Kismet/KismetSystemLibrary.h"
20
- #include "Misc/FileHelper.h"
21
- #include "Materials/MaterialInterface.h"
22
- #include "EditorScriptingUtilities/Public/EditorAssetLibrary.h"
23
- #include "Components/PrimitiveComponent.h"
24
- #include "LandscapeLayerInfoObject.h"
25
- #include "UObject/Package.h"
26
- #include "AssetRegistry/AssetRegistryModule.h"
27
- #include "AssetToolsModule.h"
28
- #include "IAssetTools.h"
29
-
30
- void FLandscapeHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
31
- {
32
- Registry.RegisterHandler(TEXT("get_landscape_info"), &GetLandscapeInfo);
33
- Registry.RegisterHandler(TEXT("list_landscape_layers"), &ListLandscapeLayers);
34
- Registry.RegisterHandler(TEXT("sample_landscape"), &SampleLandscape);
35
- Registry.RegisterHandler(TEXT("list_landscape_splines"), &ListLandscapeSplines);
36
- Registry.RegisterHandler(TEXT("get_landscape_component"), &GetLandscapeComponent);
37
- Registry.RegisterHandler(TEXT("set_landscape_material"), &SetLandscapeMaterial);
38
- Registry.RegisterHandler(TEXT("add_landscape_layer_info"), &AddLandscapeLayerInfo);
39
- Registry.RegisterHandler(TEXT("create_landscape"), &CreateLandscape);
40
- Registry.RegisterHandler(TEXT("create_landscape_layer_info"), &CreateLandscapeLayerInfo);
41
- Registry.RegisterHandler(TEXT("get_landscape_material_usage_summary"), &GetMaterialUsageSummary);
42
- // #733: World Partition landscape streaming-proxy enumeration + spatial lookup.
43
- Registry.RegisterHandler(TEXT("list_landscape_proxies"), &ListLandscapeProxies);
44
- Registry.RegisterHandler(TEXT("find_landscape_proxy_at"), &FindLandscapeProxyAt);
45
- Registry.RegisterHandlerWithTimeout(TEXT("sculpt_landscape"), &Sculpt, 120.0f);
46
- Registry.RegisterHandlerWithTimeout(TEXT("paint_landscape_layer"), &PaintLayer, 120.0f);
47
- }
48
-
49
- TSharedPtr<FJsonValue> FLandscapeHandlers::GetLandscapeInfo(const TSharedPtr<FJsonObject>& Params)
50
- {
51
- REQUIRE_EDITOR_WORLD(World);
52
-
53
- // Find landscape proxies in the world
54
- TArray<TSharedPtr<FJsonValue>> LandscapeArray;
55
- for (TActorIterator<ALandscapeProxy> It(World); It; ++It)
56
- {
57
- ALandscapeProxy* Landscape = *It;
58
- if (!Landscape) continue;
59
-
60
- TSharedPtr<FJsonObject> LandscapeObj = MakeShared<FJsonObject>();
61
- LandscapeObj->SetStringField(TEXT("name"), Landscape->GetName());
62
- LandscapeObj->SetStringField(TEXT("class"), Landscape->GetClass()->GetName());
63
-
64
- // Get component count
65
- TArray<ULandscapeComponent*> LandscapeComponents;
66
- Landscape->GetComponents<ULandscapeComponent>(LandscapeComponents);
67
- LandscapeObj->SetNumberField(TEXT("componentCount"), LandscapeComponents.Num());
68
-
69
- // Get bounds
70
- FBox Bounds = Landscape->GetComponentsBoundingBox();
71
- if (Bounds.IsValid)
72
- {
73
- TSharedPtr<FJsonObject> BoundsObj = MakeShared<FJsonObject>();
74
- BoundsObj->SetNumberField(TEXT("minX"), Bounds.Min.X);
75
- BoundsObj->SetNumberField(TEXT("minY"), Bounds.Min.Y);
76
- BoundsObj->SetNumberField(TEXT("minZ"), Bounds.Min.Z);
77
- BoundsObj->SetNumberField(TEXT("maxX"), Bounds.Max.X);
78
- BoundsObj->SetNumberField(TEXT("maxY"), Bounds.Max.Y);
79
- BoundsObj->SetNumberField(TEXT("maxZ"), Bounds.Max.Z);
80
-
81
- FVector Size = Bounds.GetSize();
82
- BoundsObj->SetNumberField(TEXT("sizeX"), Size.X);
83
- BoundsObj->SetNumberField(TEXT("sizeY"), Size.Y);
84
- BoundsObj->SetNumberField(TEXT("sizeZ"), Size.Z);
85
- LandscapeObj->SetObjectField(TEXT("bounds"), BoundsObj);
86
- }
87
-
88
- // Get location
89
- FVector Location = Landscape->GetActorLocation();
90
- LandscapeObj->SetNumberField(TEXT("locationX"), Location.X);
91
- LandscapeObj->SetNumberField(TEXT("locationY"), Location.Y);
92
- LandscapeObj->SetNumberField(TEXT("locationZ"), Location.Z);
93
-
94
- LandscapeArray.Add(MakeShared<FJsonValueObject>(LandscapeObj));
95
- }
96
-
97
- auto Result = MCPSuccess();
98
- if (LandscapeArray.Num() == 0)
99
- {
100
- Result->SetStringField(TEXT("landscape"), TEXT("none"));
101
- }
102
- else
103
- {
104
- Result->SetArrayField(TEXT("landscapes"), LandscapeArray);
105
- }
106
-
107
- Result->SetNumberField(TEXT("count"), LandscapeArray.Num());
108
- return MCPResult(Result);
109
- }
110
-
111
- TSharedPtr<FJsonValue> FLandscapeHandlers::ListLandscapeLayers(const TSharedPtr<FJsonObject>& Params)
112
- {
113
- REQUIRE_EDITOR_WORLD(World);
114
-
115
- TArray<TSharedPtr<FJsonValue>> LayerArray;
116
- for (TActorIterator<ALandscapeProxy> It(World); It; ++It)
117
- {
118
- ALandscapeProxy* Landscape = *It;
119
- if (!Landscape) continue;
120
-
121
- ULandscapeInfo* LandscapeInfo = Landscape->GetLandscapeInfo();
122
- if (LandscapeInfo)
123
- {
124
- for (const FLandscapeInfoLayerSettings& LayerSettings : LandscapeInfo->Layers)
125
- {
126
- if (LayerSettings.LayerInfoObj)
127
- {
128
- TSharedPtr<FJsonObject> LayerObj = MakeShared<FJsonObject>();
129
- LayerObj->SetStringField(TEXT("name"), LayerSettings.GetLayerName().ToString());
130
- LayerObj->SetStringField(TEXT("landscapeName"), Landscape->GetName());
131
- LayerArray.Add(MakeShared<FJsonValueObject>(LayerObj));
132
- }
133
- }
134
- }
135
- }
136
-
137
- auto Result = MCPSuccess();
138
- Result->SetArrayField(TEXT("layers"), LayerArray);
139
- Result->SetNumberField(TEXT("count"), LayerArray.Num());
140
- return MCPResult(Result);
141
- }
142
-
143
- TSharedPtr<FJsonValue> FLandscapeHandlers::SampleLandscape(const TSharedPtr<FJsonObject>& Params)
144
- {
145
- const TSharedPtr<FJsonObject>* PointObj = nullptr;
146
- if (!Params->TryGetObjectField(TEXT("point"), PointObj))
147
- {
148
- return MCPError(TEXT("Missing 'point' parameter"));
149
- }
150
-
151
- FVector Point;
152
- Point.X = (*PointObj)->GetNumberField(TEXT("x"));
153
- Point.Y = (*PointObj)->GetNumberField(TEXT("y"));
154
- Point.Z = (*PointObj)->GetNumberField(TEXT("z"));
155
-
156
- REQUIRE_EDITOR_WORLD(World);
157
-
158
- // Find the first landscape and sample height
159
- for (TActorIterator<ALandscapeProxy> It(World); It; ++It)
160
- {
161
- ALandscapeProxy* Landscape = *It;
162
- if (!Landscape) continue;
163
-
164
- // Use line trace to get the landscape height at the given point
165
- FVector TraceStart(Point.X, Point.Y, Point.Z + 100000.0f);
166
- FVector TraceEnd(Point.X, Point.Y, Point.Z - 100000.0f);
167
-
168
- FHitResult HitResult;
169
- FCollisionQueryParams QueryParams;
170
- QueryParams.bTraceComplex = true;
171
-
172
- if (World->LineTraceSingleByChannel(HitResult, TraceStart, TraceEnd, ECC_WorldStatic, QueryParams))
173
- {
174
- if (HitResult.GetActor() && HitResult.GetActor()->IsA(ALandscapeProxy::StaticClass()))
175
- {
176
- auto Result = MCPSuccess();
177
- Result->SetNumberField(TEXT("height"), HitResult.Location.Z);
178
- TSharedPtr<FJsonObject> HitPoint = MakeShared<FJsonObject>();
179
- HitPoint->SetNumberField(TEXT("x"), HitResult.Location.X);
180
- HitPoint->SetNumberField(TEXT("y"), HitResult.Location.Y);
181
- HitPoint->SetNumberField(TEXT("z"), HitResult.Location.Z);
182
- Result->SetObjectField(TEXT("hitLocation"), HitPoint);
183
-
184
- TSharedPtr<FJsonObject> Normal = MakeShared<FJsonObject>();
185
- Normal->SetNumberField(TEXT("x"), HitResult.Normal.X);
186
- Normal->SetNumberField(TEXT("y"), HitResult.Normal.Y);
187
- Normal->SetNumberField(TEXT("z"), HitResult.Normal.Z);
188
- Result->SetObjectField(TEXT("normal"), Normal);
189
-
190
- Result->SetBoolField(TEXT("hit"), true);
191
- return MCPResult(Result);
192
- }
193
- }
194
- }
195
-
196
- auto Result = MCPSuccess();
197
- Result->SetBoolField(TEXT("hit"), false);
198
- return MCPResult(Result);
199
- }
200
-
201
- TSharedPtr<FJsonValue> FLandscapeHandlers::ListLandscapeSplines(const TSharedPtr<FJsonObject>& Params)
202
- {
203
- REQUIRE_EDITOR_WORLD(World);
204
-
205
- TArray<TSharedPtr<FJsonValue>> SplineArray;
206
-
207
- for (TActorIterator<ALandscapeProxy> It(World); It; ++It)
208
- {
209
- ALandscapeProxy* Landscape = *It;
210
- if (!Landscape) continue;
211
-
212
- ULandscapeSplinesComponent* SplinesComp = Landscape->GetSplinesComponent();
213
- if (!SplinesComp) continue;
214
-
215
- const TArray<TObjectPtr<ULandscapeSplineControlPoint>>& ControlPoints = SplinesComp->GetControlPoints();
216
- for (const TObjectPtr<ULandscapeSplineControlPoint>& CP : ControlPoints)
217
- {
218
- if (!CP) continue;
219
-
220
- TSharedPtr<FJsonObject> PointObj = MakeShared<FJsonObject>();
221
- FVector Location = CP->Location;
222
- PointObj->SetNumberField(TEXT("x"), Location.X);
223
- PointObj->SetNumberField(TEXT("y"), Location.Y);
224
- PointObj->SetNumberField(TEXT("z"), Location.Z);
225
- PointObj->SetStringField(TEXT("landscapeName"), Landscape->GetName());
226
- SplineArray.Add(MakeShared<FJsonValueObject>(PointObj));
227
- }
228
- }
229
-
230
- auto Result = MCPSuccess();
231
- Result->SetArrayField(TEXT("controlPoints"), SplineArray);
232
- Result->SetNumberField(TEXT("count"), SplineArray.Num());
233
- return MCPResult(Result);
234
- }
235
-
236
- TSharedPtr<FJsonValue> FLandscapeHandlers::GetLandscapeComponent(const TSharedPtr<FJsonObject>& Params)
237
- {
238
- int32 ComponentIndex = 0;
239
- if (Params->HasField(TEXT("componentIndex")))
240
- {
241
- ComponentIndex = static_cast<int32>(Params->GetNumberField(TEXT("componentIndex")));
242
- }
243
-
244
- REQUIRE_EDITOR_WORLD(World);
245
-
246
- // Collect all landscape components across all landscape proxies
247
- TArray<ULandscapeComponent*> AllComponents;
248
- for (TActorIterator<ALandscapeProxy> It(World); It; ++It)
249
- {
250
- ALandscapeProxy* Landscape = *It;
251
- if (!Landscape) continue;
252
-
253
- TArray<ULandscapeComponent*> LandscapeComponents;
254
- Landscape->GetComponents<ULandscapeComponent>(LandscapeComponents);
255
- AllComponents.Append(LandscapeComponents);
256
- }
257
-
258
- if (ComponentIndex < 0 || ComponentIndex >= AllComponents.Num())
259
- {
260
- return MCPError(FString::Printf(TEXT("Component index %d out of range (0-%d)"), ComponentIndex, AllComponents.Num() - 1));
261
- }
262
-
263
- ULandscapeComponent* Comp = AllComponents[ComponentIndex];
264
- if (!Comp)
265
- {
266
- return MCPError(TEXT("Component is null"));
267
- }
268
-
269
- auto Result = MCPSuccess();
270
- Result->SetNumberField(TEXT("componentIndex"), ComponentIndex);
271
- Result->SetStringField(TEXT("name"), Comp->GetName());
272
-
273
- FVector CompLocation = Comp->GetComponentLocation();
274
- Result->SetNumberField(TEXT("locationX"), CompLocation.X);
275
- Result->SetNumberField(TEXT("locationY"), CompLocation.Y);
276
- Result->SetNumberField(TEXT("locationZ"), CompLocation.Z);
277
-
278
- Result->SetNumberField(TEXT("sectionBaseX"), Comp->SectionBaseX);
279
- Result->SetNumberField(TEXT("sectionBaseY"), Comp->SectionBaseY);
280
- Result->SetNumberField(TEXT("componentSizeQuads"), Comp->ComponentSizeQuads);
281
- Result->SetNumberField(TEXT("subSections"), Comp->NumSubsections);
282
-
283
- FBox CompBounds = Comp->Bounds.GetBox();
284
- if (CompBounds.IsValid)
285
- {
286
- TSharedPtr<FJsonObject> BoundsObj = MakeShared<FJsonObject>();
287
- BoundsObj->SetNumberField(TEXT("minX"), CompBounds.Min.X);
288
- BoundsObj->SetNumberField(TEXT("minY"), CompBounds.Min.Y);
289
- BoundsObj->SetNumberField(TEXT("minZ"), CompBounds.Min.Z);
290
- BoundsObj->SetNumberField(TEXT("maxX"), CompBounds.Max.X);
291
- BoundsObj->SetNumberField(TEXT("maxY"), CompBounds.Max.Y);
292
- BoundsObj->SetNumberField(TEXT("maxZ"), CompBounds.Max.Z);
293
- Result->SetObjectField(TEXT("bounds"), BoundsObj);
294
- }
295
-
296
- return MCPResult(Result);
297
- }
298
-
299
- TSharedPtr<FJsonValue> FLandscapeHandlers::SetLandscapeMaterial(const TSharedPtr<FJsonObject>& Params)
300
- {
301
- FString MaterialPath;
302
- if (!Params->TryGetStringField(TEXT("materialPath"), MaterialPath) && !Params->TryGetStringField(TEXT("path"), MaterialPath) && !Params->TryGetStringField(TEXT("assetPath"), MaterialPath))
303
- {
304
- return MCPError(TEXT("Missing 'materialPath', 'path', or 'assetPath' parameter"));
305
- }
306
-
307
- REQUIRE_EDITOR_WORLD(World);
308
-
309
- // Find the target landscape
310
- ALandscapeProxy* TargetLandscape = nullptr;
311
- FString LandscapeName = OptionalString(Params, TEXT("landscapeName"));
312
-
313
- for (TActorIterator<ALandscapeProxy> It(World); It; ++It)
314
- {
315
- ALandscapeProxy* Landscape = *It;
316
- if (!Landscape) continue;
317
-
318
- if (LandscapeName.IsEmpty() || Landscape->GetName() == LandscapeName)
319
- {
320
- TargetLandscape = Landscape;
321
- break;
322
- }
323
- }
324
-
325
- if (!TargetLandscape)
326
- {
327
- return MCPError(TEXT("No landscape found in the current level"));
328
- }
329
-
330
- // Load the material
331
- UMaterialInterface* Material = LoadObject<UMaterialInterface>(nullptr, *MaterialPath);
332
- if (!Material)
333
- {
334
- return MCPError(FString::Printf(TEXT("Material not found: %s"), *MaterialPath));
335
- }
336
-
337
- // Capture previous material for rollback and idempotency
338
- UMaterialInterface* PrevMaterial = TargetLandscape->LandscapeMaterial;
339
- if (PrevMaterial == Material)
340
- {
341
- auto Noop = MCPSuccess();
342
- MCPSetExisted(Noop);
343
- Noop->SetStringField(TEXT("landscapeName"), TargetLandscape->GetName());
344
- Noop->SetStringField(TEXT("materialPath"), MaterialPath);
345
- return MCPResult(Noop);
346
- }
347
-
348
- // Set the landscape material
349
- TargetLandscape->LandscapeMaterial = Material;
350
-
351
- // Update all landscape components to use the new material
352
- TArray<ULandscapeComponent*> LandscapeComponents;
353
- TargetLandscape->GetComponents<ULandscapeComponent>(LandscapeComponents);
354
- for (ULandscapeComponent* Comp : LandscapeComponents)
355
- {
356
- if (Comp)
357
- {
358
- Comp->SetMaterial(0, Material);
359
- Comp->MarkRenderStateDirty();
360
- }
361
- }
362
-
363
- // Mark the landscape as modified
364
- TargetLandscape->MarkPackageDirty();
365
-
366
- auto Result = MCPSuccess();
367
- MCPSetUpdated(Result);
368
- Result->SetStringField(TEXT("landscapeName"), TargetLandscape->GetName());
369
- Result->SetStringField(TEXT("materialPath"), MaterialPath);
370
- Result->SetStringField(TEXT("materialName"), Material->GetName());
371
- Result->SetNumberField(TEXT("componentsUpdated"), LandscapeComponents.Num());
372
-
373
- // Rollback: restore previous material path if any
374
- if (PrevMaterial)
375
- {
376
- TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
377
- Payload->SetStringField(TEXT("landscapeName"), TargetLandscape->GetName());
378
- Payload->SetStringField(TEXT("materialPath"), PrevMaterial->GetPathName());
379
- MCPSetRollback(Result, TEXT("set_landscape_material"), Payload);
380
- }
381
-
382
- return MCPResult(Result);
383
- }
384
-
385
- TSharedPtr<FJsonValue> FLandscapeHandlers::AddLandscapeLayerInfo(const TSharedPtr<FJsonObject>& Params)
386
- {
387
- FString LayerName;
388
- if (auto Err = RequireString(Params, TEXT("layerName"), LayerName)) return Err;
389
-
390
- REQUIRE_EDITOR_WORLD(World);
391
-
392
- // Find the target landscape
393
- ALandscapeProxy* TargetLandscape = nullptr;
394
- FString LandscapeName = OptionalString(Params, TEXT("landscapeName"));
395
-
396
- for (TActorIterator<ALandscapeProxy> It(World); It; ++It)
397
- {
398
- ALandscapeProxy* Landscape = *It;
399
- if (!Landscape) continue;
400
-
401
- if (LandscapeName.IsEmpty() || Landscape->GetName() == LandscapeName)
402
- {
403
- TargetLandscape = Landscape;
404
- break;
405
- }
406
- }
407
-
408
- if (!TargetLandscape)
409
- {
410
- return MCPError(TEXT("No landscape found in the current level"));
411
- }
412
-
413
- ULandscapeInfo* LandscapeInfo = TargetLandscape->GetLandscapeInfo();
414
- if (!LandscapeInfo)
415
- {
416
- return MCPError(TEXT("Failed to get landscape info"));
417
- }
418
-
419
- // Check if a layer with this name already exists
420
- for (const FLandscapeInfoLayerSettings& ExistingLayer : LandscapeInfo->Layers)
421
- {
422
- if (ExistingLayer.LayerInfoObj && ExistingLayer.GetLayerName().ToString() == LayerName)
423
- {
424
- auto Result = MCPSuccess();
425
- Result->SetStringField(TEXT("layerName"), LayerName);
426
- Result->SetStringField(TEXT("path"), ExistingLayer.LayerInfoObj->GetPathName());
427
- Result->SetStringField(TEXT("note"), TEXT("Layer already exists on this landscape"));
428
- return MCPResult(Result);
429
- }
430
- }
431
-
432
- // Create a new ULandscapeLayerInfoObject asset
433
- FString PackagePath = OptionalString(Params, TEXT("packagePath"), TEXT("/Game/Landscape/LayerInfos"));
434
-
435
- FString AssetName = FString::Printf(TEXT("LI_%s"), *LayerName);
436
- FString PackageFullPath = PackagePath / AssetName;
437
-
438
- // Check if the asset already exists
439
- ULandscapeLayerInfoObject* LayerInfoObj = LoadObject<ULandscapeLayerInfoObject>(nullptr, *(PackageFullPath + TEXT(".") + AssetName));
440
- if (!LayerInfoObj)
441
- {
442
- UPackage* Package = CreatePackage(*PackageFullPath);
443
- if (!Package)
444
- {
445
- return MCPError(FString::Printf(TEXT("Failed to create package: %s"), *PackageFullPath));
446
- }
447
-
448
- LayerInfoObj = NewObject<ULandscapeLayerInfoObject>(Package, *AssetName, RF_Public | RF_Standalone);
449
- if (!LayerInfoObj)
450
- {
451
- return MCPError(TEXT("Failed to create LandscapeLayerInfoObject"));
452
- }
453
-
454
- PRAGMA_DISABLE_DEPRECATION_WARNINGS
455
- LayerInfoObj->LayerName = FName(*LayerName);
456
- PRAGMA_ENABLE_DEPRECATION_WARNINGS
457
-
458
- // There is no weight-blend toggle to set here any more: bNoWeightBlend
459
- // was removed in 5.7 and blending is controlled per-layer through
460
- // landscape settings. The old 'weightBlended' param read into a unused
461
- // local and the response hardcoded true, so both are gone rather than
462
- // left implying a setting that is not being applied.
463
-
464
- // Notify asset registry and save
465
- FAssetRegistryModule::AssetCreated(LayerInfoObj);
466
- Package->MarkPackageDirty();
467
- UEditorAssetLibrary::SaveAsset(PackageFullPath, false);
468
- }
469
-
470
- // Register the layer info with the landscape
471
- int32 LayerIndex = LandscapeInfo->Layers.Num();
472
- FLandscapeInfoLayerSettings NewLayerSettings(LayerInfoObj, TargetLandscape);
473
- LandscapeInfo->Layers.Add(NewLayerSettings);
474
-
475
- // Mark the landscape as dirty
476
- TargetLandscape->MarkPackageDirty();
477
-
478
- auto Result = MCPSuccess();
479
- Result->SetStringField(TEXT("layerName"), LayerName);
480
- Result->SetStringField(TEXT("path"), LayerInfoObj->GetPathName());
481
- Result->SetStringField(TEXT("landscapeName"), TargetLandscape->GetName());
482
- Result->SetNumberField(TEXT("layerIndex"), LayerIndex);
483
-
484
- return MCPResult(Result);
485
- }
486
-
487
- // ─── #150 get_landscape_material_usage_summary ──────────────────────
488
- // Compact per-proxy dump: class, label, material paths, grass / Nanite /
489
- // landscape component counts. Avoids the big "get all components" blob
490
- // get_actor_details produces when you only need materials + counts.
491
- // #303: spawn an ALandscape with a default flat heightmap at mid-elevation
492
- // (uint16 32768 = no offset). Section/quad defaults match the Editor's
493
- // Landscape Mode "create new" form: 63 quads/subsection, 2 subsections/component
494
- // = 127 quads/component. ComponentCount X/Y default to 8x8 producing a
495
- // 1016x1016 quad landscape (~1 km at default actor scale 100,100,100).
496
- TSharedPtr<FJsonValue> FLandscapeHandlers::CreateLandscape(const TSharedPtr<FJsonObject>& Params)
497
- {
498
- REQUIRE_EDITOR_WORLD(World);
499
-
500
- const int32 SubsectionSizeQuads = OptionalInt(Params, TEXT("subsectionSizeQuads"), 63);
501
- const int32 NumSubsections = OptionalInt(Params, TEXT("numSubsections"), 2);
502
- const int32 ComponentCountX = OptionalInt(Params, TEXT("componentCountX"), 8);
503
- const int32 ComponentCountY = OptionalInt(Params, TEXT("componentCountY"), 8);
504
-
505
- // Bounds checks: SubsectionSizeQuads must be one of the engine's supported
506
- // values (7, 15, 31, 63, 127, 255), NumSubsections is 1 or 2, and the
507
- // component grid has to be at least 1x1.
508
- auto IsPowOf2Minus1 = [](int32 v) {
509
- const int32 p = v + 1;
510
- return v >= 7 && v <= 255 && (p & (p - 1)) == 0;
511
- };
512
- if (!IsPowOf2Minus1(SubsectionSizeQuads))
513
- {
514
- return MCPError(FString::Printf(
515
- TEXT("subsectionSizeQuads must be one of 7, 15, 31, 63, 127, 255 (got %d)"),
516
- SubsectionSizeQuads));
517
- }
518
- if (NumSubsections != 1 && NumSubsections != 2)
519
- {
520
- return MCPError(FString::Printf(TEXT("numSubsections must be 1 or 2 (got %d)"), NumSubsections));
521
- }
522
- if (ComponentCountX < 1 || ComponentCountY < 1)
523
- {
524
- return MCPError(TEXT("componentCountX and componentCountY must be >= 1"));
525
- }
526
-
527
- const int32 ComponentSizeQuads = SubsectionSizeQuads * NumSubsections;
528
- const int32 SizeX = (ComponentCountX * ComponentSizeQuads) + 1;
529
- const int32 SizeY = (ComponentCountY * ComponentSizeQuads) + 1;
530
-
531
- const int32 HeightOffset = OptionalInt(Params, TEXT("heightOffset"), 32768);
532
- if (HeightOffset < 0 || HeightOffset > 65535)
533
- {
534
- return MCPError(TEXT("heightOffset must be in [0, 65535] (uint16 elevation)"));
535
- }
536
-
537
- const FVector Location = OptionalVec3(Params, TEXT("location"));
538
- const FVector Scale = OptionalVec3(Params, TEXT("scale"), FVector(100.0, 100.0, 100.0));
539
-
540
- const FString Label = OptionalString(Params, TEXT("label"));
541
-
542
- // Idempotency by label.
543
- if (auto Existing = MCPCheckActorLabelExists(World, Label, TEXT("skip"), TEXT("Landscape")))
544
- {
545
- return Existing;
546
- }
547
-
548
- FActorSpawnParameters SpawnParams;
549
- SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
550
- ALandscape* Landscape = World->SpawnActor<ALandscape>(Location, FRotator::ZeroRotator, SpawnParams);
551
- if (!Landscape)
552
- {
553
- return MCPError(TEXT("Failed to spawn ALandscape actor"));
554
- }
555
- Landscape->SetActorScale3D(Scale);
556
-
557
- // Allocate a flat heightmap. Layer 0 (FGuid()) is the only edit layer for a
558
- // non-edit-layer landscape, which is what gets created by the Editor's
559
- // "create new landscape" defaults.
560
- TArray<uint16> HeightData;
561
- HeightData.SetNumUninitialized(SizeX * SizeY);
562
- for (int32 i = 0; i < HeightData.Num(); ++i)
563
- {
564
- HeightData[i] = static_cast<uint16>(HeightOffset);
565
- }
566
-
567
- TMap<FGuid, TArray<uint16>> ImportHeightData;
568
- ImportHeightData.Add(FGuid(), MoveTemp(HeightData));
569
-
570
- TMap<FGuid, TArray<FLandscapeImportLayerInfo>> ImportLayerInfo;
571
- ImportLayerInfo.Add(FGuid(), TArray<FLandscapeImportLayerInfo>());
572
-
573
- TArray<FLandscapeLayer> EmptyLayers;
574
- Landscape->Import(
575
- FGuid::NewGuid(),
576
- 0, 0,
577
- SizeX - 1, SizeY - 1,
578
- NumSubsections,
579
- SubsectionSizeQuads,
580
- ImportHeightData,
581
- nullptr,
582
- ImportLayerInfo,
583
- ELandscapeImportAlphamapType::Additive,
584
- #if UE_MCP_HAS_5_5_API
585
- MakeArrayView(EmptyLayers)
586
- #else
587
- // 5.4: last arg is const TArray<FLandscapeLayer>* (TArrayView signature added in 5.5).
588
- &EmptyLayers
589
- #endif
590
- );
591
-
592
- if (!Label.IsEmpty())
593
- {
594
- Landscape->SetActorLabel(Label);
595
- }
596
-
597
- // Register so subsequent get_landscape_info / sample_landscape calls find it.
598
- if (ULandscapeInfo* LI = Landscape->GetLandscapeInfo())
599
- {
600
- LI->FixupProxiesTransform();
601
- LI->RecreateCollisionComponents();
602
- }
603
-
604
- auto Result = MCPSuccess();
605
- MCPSetCreated(Result);
606
- Result->SetStringField(TEXT("actorLabel"), Landscape->GetActorLabel());
607
- Result->SetStringField(TEXT("actorPath"), Landscape->GetPathName());
608
- Result->SetNumberField(TEXT("componentCountX"), ComponentCountX);
609
- Result->SetNumberField(TEXT("componentCountY"), ComponentCountY);
610
- Result->SetNumberField(TEXT("componentSizeQuads"), ComponentSizeQuads);
611
- Result->SetNumberField(TEXT("subsectionSizeQuads"), SubsectionSizeQuads);
612
- Result->SetNumberField(TEXT("numSubsections"), NumSubsections);
613
- Result->SetNumberField(TEXT("sizeX"), SizeX);
614
- Result->SetNumberField(TEXT("sizeY"), SizeY);
615
-
616
- TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
617
- Payload->SetStringField(TEXT("actorLabel"), Landscape->GetActorLabel());
618
- MCPSetRollback(Result, TEXT("delete_actor"), Payload);
619
-
620
- return MCPResult(Result);
621
- }
622
-
623
- // #251: standalone LayerInfo asset creation. Unlike add_landscape_layer_info
624
- // (which requires a landscape in the world to register the layer against),
625
- // this creates the ULandscapeLayerInfoObject asset in the content browser
626
- // so paint workflows can pre-author layer assets before the landscape exists.
627
- TSharedPtr<FJsonValue> FLandscapeHandlers::CreateLandscapeLayerInfo(const TSharedPtr<FJsonObject>& Params)
628
- {
629
- FString LayerName;
630
- if (auto Err = RequireString(Params, TEXT("layerName"), LayerName)) return Err;
631
-
632
- const FString Name = OptionalString(Params, TEXT("name"), FString::Printf(TEXT("LI_%s"), *LayerName));
633
- const FString PackagePath = OptionalString(Params, TEXT("packagePath"), TEXT("/Game/Landscape/LayerInfos"));
634
- const FString OnConflict = OptionalString(Params, TEXT("onConflict"), TEXT("skip"));
635
-
636
- TSharedPtr<FJsonValue> Existing = MCPCheckAssetExists(PackagePath, Name, OnConflict, TEXT("LandscapeLayerInfoObject"));
637
- if (Existing.IsValid()) return Existing;
638
-
639
- const FString PackageFullPath = PackagePath / Name;
640
- UPackage* Package = CreatePackage(*PackageFullPath);
641
- if (!Package)
642
- {
643
- return MCPError(FString::Printf(TEXT("Failed to create package: %s"), *PackageFullPath));
644
- }
645
-
646
- ULandscapeLayerInfoObject* LayerInfo = NewObject<ULandscapeLayerInfoObject>(
647
- Package, *Name, RF_Public | RF_Standalone);
648
- if (!LayerInfo)
649
- {
650
- return MCPError(TEXT("Failed to create LandscapeLayerInfoObject"));
651
- }
652
-
653
- PRAGMA_DISABLE_DEPRECATION_WARNINGS
654
- LayerInfo->LayerName = FName(*LayerName);
655
- PRAGMA_ENABLE_DEPRECATION_WARNINGS
656
-
657
- // physMaterial was documented here and never applied - the caller had to
658
- // discover for themselves that it needed a second call. PhysicsCore is not
659
- // a hard dependency of this module, so the class is reached by path and the
660
- // property set through reflection rather than a link-time include.
661
- const FString PhysMaterialPath = OptionalString(Params, TEXT("physMaterial"));
662
- if (!PhysMaterialPath.IsEmpty())
663
- {
664
- UObject* PhysMat = LoadAssetByPath<UObject>(PhysMaterialPath);
665
- if (!PhysMat)
666
- {
667
- // Both failure paths here run after the package and object exist, so
668
- // bail out without leaving a half-made asset in memory.
669
- LayerInfo->MarkAsGarbage();
670
- return MCPError(FString::Printf(TEXT("physMaterial not found: %s"), *PhysMaterialPath));
671
- }
672
- FObjectProperty* Prop = CastField<FObjectProperty>(
673
- ULandscapeLayerInfoObject::StaticClass()->FindPropertyByName(TEXT("PhysMaterial")));
674
- if (!Prop || !Prop->PropertyClass || !PhysMat->IsA(Prop->PropertyClass))
675
- {
676
- LayerInfo->MarkAsGarbage();
677
- return MCPError(FString::Printf(
678
- TEXT("'%s' is a %s, not a PhysicalMaterial."),
679
- *PhysMaterialPath, *PhysMat->GetClass()->GetName()));
680
- }
681
- Prop->SetObjectPropertyValue_InContainer(LayerInfo, PhysMat);
682
- }
683
-
684
- double Hardness = 0.0;
685
- if (Params->TryGetNumberField(TEXT("hardness"), Hardness))
686
- {
687
- // Hardness is becoming private; the setter also handles Modify() and the
688
- // property-change notification the direct write skipped.
689
- LayerInfo->SetHardness(static_cast<float>(Hardness), /*bInModify=*/true, EPropertyChangeType::ValueSet);
690
- }
691
-
692
- FAssetRegistryModule::AssetCreated(LayerInfo);
693
- Package->MarkPackageDirty();
694
- SaveAssetPackage(LayerInfo);
695
-
696
- auto Result = MCPSuccess();
697
- MCPSetCreated(Result);
698
- Result->SetStringField(TEXT("path"), LayerInfo->GetPathName());
699
- Result->SetStringField(TEXT("layerName"), LayerName);
700
- Result->SetStringField(TEXT("packagePath"), PackagePath);
701
- if (!PhysMaterialPath.IsEmpty()) Result->SetStringField(TEXT("physMaterial"), PhysMaterialPath);
702
- MCPSetDeleteAssetRollback(Result, LayerInfo->GetPathName());
703
-
704
- return MCPResult(Result);
705
- }
706
-
707
- TSharedPtr<FJsonValue> FLandscapeHandlers::GetMaterialUsageSummary(const TSharedPtr<FJsonObject>& Params)
708
- {
709
- REQUIRE_EDITOR_WORLD(World);
710
-
711
- TArray<TSharedPtr<FJsonValue>> ProxyArray;
712
- TSet<FString> UniqueMaterials;
713
- int32 TotalComponents = 0, TotalGrass = 0, TotalNanite = 0;
714
-
715
- for (TActorIterator<ALandscapeProxy> It(World); It; ++It)
716
- {
717
- ALandscapeProxy* Proxy = *It;
718
- if (!Proxy) continue;
719
-
720
- TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
721
- Obj->SetStringField(TEXT("label"), Proxy->GetActorLabel());
722
- Obj->SetStringField(TEXT("name"), Proxy->GetName());
723
- Obj->SetStringField(TEXT("class"), Proxy->GetClass()->GetName());
724
- Obj->SetStringField(TEXT("path"), Proxy->GetPathName());
725
-
726
- if (UMaterialInterface* Mat = Proxy->LandscapeMaterial)
727
- {
728
- Obj->SetStringField(TEXT("landscapeMaterial"), Mat->GetPathName());
729
- UniqueMaterials.Add(Mat->GetPathName());
730
- }
731
- if (UMaterialInterface* HoleMat = Proxy->LandscapeHoleMaterial)
732
- {
733
- Obj->SetStringField(TEXT("landscapeHoleMaterial"), HoleMat->GetPathName());
734
- }
735
-
736
- // Histogram components by class (grass / Nanite / regular landscape comps)
737
- int32 LandscapeComps = 0, GrassComps = 0, NaniteComps = 0;
738
- TArray<UActorComponent*> Comps;
739
- Proxy->GetComponents(Comps);
740
- for (UActorComponent* C : Comps)
741
- {
742
- if (!C) continue;
743
- const FString CName = C->GetClass()->GetName();
744
- if (CName == TEXT("LandscapeComponent")) LandscapeComps++;
745
- else if (CName == TEXT("GrassInstancedStaticMeshComponent")) GrassComps++;
746
- else if (CName == TEXT("LandscapeNaniteComponent")) NaniteComps++;
747
- }
748
- Obj->SetNumberField(TEXT("landscapeComponentCount"), LandscapeComps);
749
- Obj->SetNumberField(TEXT("grassComponentCount"), GrassComps);
750
- Obj->SetNumberField(TEXT("naniteComponentCount"), NaniteComps);
751
- TotalComponents += LandscapeComps;
752
- TotalGrass += GrassComps;
753
- TotalNanite += NaniteComps;
754
-
755
- const FVector Loc = Proxy->GetActorLocation();
756
- const FVector Scale = Proxy->GetActorScale3D();
757
- TSharedPtr<FJsonObject> LocObj = MakeShared<FJsonObject>();
758
- LocObj->SetNumberField(TEXT("x"), Loc.X);
759
- LocObj->SetNumberField(TEXT("y"), Loc.Y);
760
- LocObj->SetNumberField(TEXT("z"), Loc.Z);
761
- Obj->SetObjectField(TEXT("location"), LocObj);
762
- TSharedPtr<FJsonObject> ScaleObj = MakeShared<FJsonObject>();
763
- ScaleObj->SetNumberField(TEXT("x"), Scale.X);
764
- ScaleObj->SetNumberField(TEXT("y"), Scale.Y);
765
- ScaleObj->SetNumberField(TEXT("z"), Scale.Z);
766
- Obj->SetObjectField(TEXT("scale"), ScaleObj);
767
-
768
- ProxyArray.Add(MakeShared<FJsonValueObject>(Obj));
769
- }
770
-
771
- TArray<TSharedPtr<FJsonValue>> UniqueMatsArr;
772
- for (const FString& M : UniqueMaterials) UniqueMatsArr.Add(MakeShared<FJsonValueString>(M));
773
-
774
- auto Result = MCPSuccess();
775
- Result->SetArrayField(TEXT("proxies"), ProxyArray);
776
- Result->SetNumberField(TEXT("proxyCount"), ProxyArray.Num());
777
- Result->SetArrayField(TEXT("uniqueLandscapeMaterials"), UniqueMatsArr);
778
- Result->SetNumberField(TEXT("totalLandscapeComponents"), TotalComponents);
779
- Result->SetNumberField(TEXT("totalGrassComponents"), TotalGrass);
780
- Result->SetNumberField(TEXT("totalNaniteComponents"), TotalNanite);
781
- return MCPResult(Result);
782
- }
783
-
784
- // #733: enumerate LandscapeStreamingProxy actors currently loaded in the world,
785
- // with per-proxy world bounds and the parent Landscape count. On a World
786
- // Partition map, an unloaded proxy silently reads layer weights as 0, so a
787
- // measurement is only trustworthy once the covering proxy is confirmed loaded.
788
- // Unloaded proxies are not spawned as actors, so the actor iterator only yields
789
- // loaded ones - hence loaded is always true for enumerated entries; callers use
790
- // the count + bounds to reason about coverage.
791
- TSharedPtr<FJsonValue> FLandscapeHandlers::ListLandscapeProxies(const TSharedPtr<FJsonObject>& Params)
792
- {
793
- REQUIRE_EDITOR_WORLD(World);
794
-
795
- int32 ParentLandscapes = 0;
796
- TArray<TSharedPtr<FJsonValue>> Proxies;
797
- for (TActorIterator<AActor> It(World); It; ++It)
798
- {
799
- AActor* Actor = *It;
800
- if (!Actor) continue;
801
- if (Actor->IsA<ALandscape>())
802
- {
803
- ParentLandscapes++;
804
- continue;
805
- }
806
- ALandscapeStreamingProxy* Proxy = Cast<ALandscapeStreamingProxy>(Actor);
807
- if (!Proxy) continue;
808
-
809
- FVector Origin, Extent;
810
- Proxy->GetActorBounds(false, Origin, Extent);
811
-
812
- TSharedPtr<FJsonObject> ProxyObj = MakeShared<FJsonObject>();
813
- ProxyObj->SetStringField(TEXT("label"), Proxy->GetActorLabel());
814
- ProxyObj->SetBoolField(TEXT("loaded"), true);
815
-
816
- TSharedPtr<FJsonObject> Bounds = MakeShared<FJsonObject>();
817
- TSharedPtr<FJsonObject> OriginObj = MakeShared<FJsonObject>();
818
- OriginObj->SetNumberField(TEXT("x"), Origin.X);
819
- OriginObj->SetNumberField(TEXT("y"), Origin.Y);
820
- OriginObj->SetNumberField(TEXT("z"), Origin.Z);
821
- TSharedPtr<FJsonObject> ExtentObj = MakeShared<FJsonObject>();
822
- ExtentObj->SetNumberField(TEXT("x"), Extent.X);
823
- ExtentObj->SetNumberField(TEXT("y"), Extent.Y);
824
- ExtentObj->SetNumberField(TEXT("z"), Extent.Z);
825
- Bounds->SetObjectField(TEXT("origin"), OriginObj);
826
- Bounds->SetObjectField(TEXT("extent"), ExtentObj);
827
- ProxyObj->SetObjectField(TEXT("worldBounds"), Bounds);
828
-
829
- Proxies.Add(MakeShared<FJsonValueObject>(ProxyObj));
830
- }
831
-
832
- auto Result = MCPSuccess();
833
- Result->SetNumberField(TEXT("loadedProxies"), Proxies.Num());
834
- Result->SetNumberField(TEXT("parentLandscapes"), ParentLandscapes);
835
- Result->SetArrayField(TEXT("proxies"), Proxies);
836
- Result->SetStringField(TEXT("note"), TEXT("World Partition unloaded proxies are not spawned as actors, so only loaded proxies are listed."));
837
- return MCPResult(Result);
838
- }
839
-
840
- // #733: resolve which loaded LandscapeStreamingProxy's world bounds contain a
841
- // world X/Y. Returns the covering proxy (loaded:true) or loaded:false when no
842
- // loaded proxy covers the position - which usually means the covering proxy is
843
- // streamed out, making any 0-weight readback there ambiguous rather than real.
844
- TSharedPtr<FJsonValue> FLandscapeHandlers::FindLandscapeProxyAt(const TSharedPtr<FJsonObject>& Params)
845
- {
846
- REQUIRE_EDITOR_WORLD(World);
847
-
848
- if (!Params->HasField(TEXT("worldX")) || !Params->HasField(TEXT("worldY")))
849
- {
850
- return MCPError(TEXT("Missing 'worldX'/'worldY' world position"));
851
- }
852
- const double TargetX = OptionalNumber(Params, TEXT("worldX"), 0.0);
853
- const double TargetY = OptionalNumber(Params, TEXT("worldY"), 0.0);
854
-
855
- for (TActorIterator<AActor> It(World); It; ++It)
856
- {
857
- ALandscapeStreamingProxy* Proxy = Cast<ALandscapeStreamingProxy>(*It);
858
- if (!Proxy) continue;
859
-
860
- FVector Origin, Extent;
861
- Proxy->GetActorBounds(false, Origin, Extent);
862
- if (TargetX >= Origin.X - Extent.X && TargetX <= Origin.X + Extent.X &&
863
- TargetY >= Origin.Y - Extent.Y && TargetY <= Origin.Y + Extent.Y)
864
- {
865
- auto Result = MCPSuccess();
866
- Result->SetBoolField(TEXT("found"), true);
867
- Result->SetBoolField(TEXT("loaded"), true);
868
- Result->SetStringField(TEXT("label"), Proxy->GetActorLabel());
869
- return MCPResult(Result);
870
- }
871
- }
872
-
873
- auto Result = MCPSuccess();
874
- Result->SetBoolField(TEXT("found"), false);
875
- Result->SetBoolField(TEXT("loaded"), false);
876
- Result->SetStringField(TEXT("note"), TEXT("No loaded proxy covers this position; the covering proxy is likely streamed out, so weight/height readbacks here are ambiguous."));
877
- return MCPResult(Result);
878
- }
1
+ #include "LandscapeHandlers.h"
2
+ #include "HandlerRegistry.h"
3
+ #include "HandlerUtils.h"
4
+ #include "Dom/JsonObject.h"
5
+ #include "Dom/JsonValue.h"
6
+ #include "Editor.h"
7
+ #include "Engine/World.h"
8
+ #include "EngineUtils.h"
9
+ #include "Landscape.h"
10
+ #include "LandscapeEditTypes.h"
11
+ #include "LandscapeProxy.h"
12
+ #include "LandscapeStreamingProxy.h"
13
+ #include "LandscapeInfo.h"
14
+ #include "LandscapeComponent.h"
15
+ #include "LandscapeSplineActor.h"
16
+ #include "LandscapeSplinesComponent.h"
17
+ #include "LandscapeSplineControlPoint.h"
18
+ #include "LandscapeSplineSegment.h"
19
+ #include "Kismet/KismetSystemLibrary.h"
20
+ #include "Misc/FileHelper.h"
21
+ #include "Materials/MaterialInterface.h"
22
+ #include "EditorScriptingUtilities/Public/EditorAssetLibrary.h"
23
+ #include "Components/PrimitiveComponent.h"
24
+ #include "LandscapeLayerInfoObject.h"
25
+ #include "UObject/Package.h"
26
+ #include "AssetRegistry/AssetRegistryModule.h"
27
+ #include "AssetToolsModule.h"
28
+ #include "IAssetTools.h"
29
+
30
+ void FLandscapeHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
31
+ {
32
+ Registry.RegisterHandler(TEXT("get_landscape_info"), &GetLandscapeInfo);
33
+ Registry.RegisterHandler(TEXT("list_landscape_layers"), &ListLandscapeLayers);
34
+ Registry.RegisterHandler(TEXT("sample_landscape"), &SampleLandscape);
35
+ Registry.RegisterHandler(TEXT("list_landscape_splines"), &ListLandscapeSplines);
36
+ Registry.RegisterHandler(TEXT("get_landscape_component"), &GetLandscapeComponent);
37
+ Registry.RegisterHandler(TEXT("set_landscape_material"), &SetLandscapeMaterial);
38
+ Registry.RegisterHandler(TEXT("add_landscape_layer_info"), &AddLandscapeLayerInfo);
39
+ Registry.RegisterHandler(TEXT("create_landscape"), &CreateLandscape);
40
+ Registry.RegisterHandler(TEXT("create_landscape_layer_info"), &CreateLandscapeLayerInfo);
41
+ Registry.RegisterHandler(TEXT("get_landscape_material_usage_summary"), &GetMaterialUsageSummary);
42
+ // #733: World Partition landscape streaming-proxy enumeration + spatial lookup.
43
+ Registry.RegisterHandler(TEXT("list_landscape_proxies"), &ListLandscapeProxies);
44
+ Registry.RegisterHandler(TEXT("find_landscape_proxy_at"), &FindLandscapeProxyAt);
45
+ Registry.RegisterHandlerWithTimeout(TEXT("sculpt_landscape"), &Sculpt, 120.0f);
46
+ Registry.RegisterHandlerWithTimeout(TEXT("paint_landscape_layer"), &PaintLayer, 120.0f);
47
+ }
48
+
49
+ TSharedPtr<FJsonValue> FLandscapeHandlers::GetLandscapeInfo(const TSharedPtr<FJsonObject>& Params)
50
+ {
51
+ REQUIRE_EDITOR_WORLD(World);
52
+
53
+ // Find landscape proxies in the world
54
+ TArray<TSharedPtr<FJsonValue>> LandscapeArray;
55
+ for (TActorIterator<ALandscapeProxy> It(World); It; ++It)
56
+ {
57
+ ALandscapeProxy* Landscape = *It;
58
+ if (!Landscape) continue;
59
+
60
+ TSharedPtr<FJsonObject> LandscapeObj = MakeShared<FJsonObject>();
61
+ LandscapeObj->SetStringField(TEXT("name"), Landscape->GetName());
62
+ LandscapeObj->SetStringField(TEXT("class"), Landscape->GetClass()->GetName());
63
+
64
+ // Get component count
65
+ TArray<ULandscapeComponent*> LandscapeComponents;
66
+ Landscape->GetComponents<ULandscapeComponent>(LandscapeComponents);
67
+ LandscapeObj->SetNumberField(TEXT("componentCount"), LandscapeComponents.Num());
68
+
69
+ // Get bounds
70
+ FBox Bounds = Landscape->GetComponentsBoundingBox();
71
+ if (Bounds.IsValid)
72
+ {
73
+ TSharedPtr<FJsonObject> BoundsObj = MakeShared<FJsonObject>();
74
+ BoundsObj->SetNumberField(TEXT("minX"), Bounds.Min.X);
75
+ BoundsObj->SetNumberField(TEXT("minY"), Bounds.Min.Y);
76
+ BoundsObj->SetNumberField(TEXT("minZ"), Bounds.Min.Z);
77
+ BoundsObj->SetNumberField(TEXT("maxX"), Bounds.Max.X);
78
+ BoundsObj->SetNumberField(TEXT("maxY"), Bounds.Max.Y);
79
+ BoundsObj->SetNumberField(TEXT("maxZ"), Bounds.Max.Z);
80
+
81
+ FVector Size = Bounds.GetSize();
82
+ BoundsObj->SetNumberField(TEXT("sizeX"), Size.X);
83
+ BoundsObj->SetNumberField(TEXT("sizeY"), Size.Y);
84
+ BoundsObj->SetNumberField(TEXT("sizeZ"), Size.Z);
85
+ LandscapeObj->SetObjectField(TEXT("bounds"), BoundsObj);
86
+ }
87
+
88
+ // Get location
89
+ FVector Location = Landscape->GetActorLocation();
90
+ LandscapeObj->SetNumberField(TEXT("locationX"), Location.X);
91
+ LandscapeObj->SetNumberField(TEXT("locationY"), Location.Y);
92
+ LandscapeObj->SetNumberField(TEXT("locationZ"), Location.Z);
93
+
94
+ LandscapeArray.Add(MakeShared<FJsonValueObject>(LandscapeObj));
95
+ }
96
+
97
+ auto Result = MCPSuccess();
98
+ if (LandscapeArray.Num() == 0)
99
+ {
100
+ Result->SetStringField(TEXT("landscape"), TEXT("none"));
101
+ }
102
+ else
103
+ {
104
+ Result->SetArrayField(TEXT("landscapes"), LandscapeArray);
105
+ }
106
+
107
+ Result->SetNumberField(TEXT("count"), LandscapeArray.Num());
108
+ return MCPResult(Result);
109
+ }
110
+
111
+ TSharedPtr<FJsonValue> FLandscapeHandlers::ListLandscapeLayers(const TSharedPtr<FJsonObject>& Params)
112
+ {
113
+ REQUIRE_EDITOR_WORLD(World);
114
+
115
+ TArray<TSharedPtr<FJsonValue>> LayerArray;
116
+ for (TActorIterator<ALandscapeProxy> It(World); It; ++It)
117
+ {
118
+ ALandscapeProxy* Landscape = *It;
119
+ if (!Landscape) continue;
120
+
121
+ ULandscapeInfo* LandscapeInfo = Landscape->GetLandscapeInfo();
122
+ if (LandscapeInfo)
123
+ {
124
+ for (const FLandscapeInfoLayerSettings& LayerSettings : LandscapeInfo->Layers)
125
+ {
126
+ if (LayerSettings.LayerInfoObj)
127
+ {
128
+ TSharedPtr<FJsonObject> LayerObj = MakeShared<FJsonObject>();
129
+ LayerObj->SetStringField(TEXT("name"), LayerSettings.GetLayerName().ToString());
130
+ LayerObj->SetStringField(TEXT("landscapeName"), Landscape->GetName());
131
+ LayerArray.Add(MakeShared<FJsonValueObject>(LayerObj));
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ auto Result = MCPSuccess();
138
+ Result->SetArrayField(TEXT("layers"), LayerArray);
139
+ Result->SetNumberField(TEXT("count"), LayerArray.Num());
140
+ return MCPResult(Result);
141
+ }
142
+
143
+ TSharedPtr<FJsonValue> FLandscapeHandlers::SampleLandscape(const TSharedPtr<FJsonObject>& Params)
144
+ {
145
+ const TSharedPtr<FJsonObject>* PointObj = nullptr;
146
+ if (!Params->TryGetObjectField(TEXT("point"), PointObj))
147
+ {
148
+ return MCPError(TEXT("Missing 'point' parameter"));
149
+ }
150
+
151
+ FVector Point;
152
+ Point.X = (*PointObj)->GetNumberField(TEXT("x"));
153
+ Point.Y = (*PointObj)->GetNumberField(TEXT("y"));
154
+ Point.Z = (*PointObj)->GetNumberField(TEXT("z"));
155
+
156
+ REQUIRE_EDITOR_WORLD(World);
157
+
158
+ // Find the first landscape and sample height
159
+ for (TActorIterator<ALandscapeProxy> It(World); It; ++It)
160
+ {
161
+ ALandscapeProxy* Landscape = *It;
162
+ if (!Landscape) continue;
163
+
164
+ // Use line trace to get the landscape height at the given point
165
+ FVector TraceStart(Point.X, Point.Y, Point.Z + 100000.0f);
166
+ FVector TraceEnd(Point.X, Point.Y, Point.Z - 100000.0f);
167
+
168
+ FHitResult HitResult;
169
+ FCollisionQueryParams QueryParams;
170
+ QueryParams.bTraceComplex = true;
171
+
172
+ if (World->LineTraceSingleByChannel(HitResult, TraceStart, TraceEnd, ECC_WorldStatic, QueryParams))
173
+ {
174
+ if (HitResult.GetActor() && HitResult.GetActor()->IsA(ALandscapeProxy::StaticClass()))
175
+ {
176
+ auto Result = MCPSuccess();
177
+ Result->SetNumberField(TEXT("height"), HitResult.Location.Z);
178
+ TSharedPtr<FJsonObject> HitPoint = MakeShared<FJsonObject>();
179
+ HitPoint->SetNumberField(TEXT("x"), HitResult.Location.X);
180
+ HitPoint->SetNumberField(TEXT("y"), HitResult.Location.Y);
181
+ HitPoint->SetNumberField(TEXT("z"), HitResult.Location.Z);
182
+ Result->SetObjectField(TEXT("hitLocation"), HitPoint);
183
+
184
+ TSharedPtr<FJsonObject> Normal = MakeShared<FJsonObject>();
185
+ Normal->SetNumberField(TEXT("x"), HitResult.Normal.X);
186
+ Normal->SetNumberField(TEXT("y"), HitResult.Normal.Y);
187
+ Normal->SetNumberField(TEXT("z"), HitResult.Normal.Z);
188
+ Result->SetObjectField(TEXT("normal"), Normal);
189
+
190
+ Result->SetBoolField(TEXT("hit"), true);
191
+ return MCPResult(Result);
192
+ }
193
+ }
194
+ }
195
+
196
+ auto Result = MCPSuccess();
197
+ Result->SetBoolField(TEXT("hit"), false);
198
+ return MCPResult(Result);
199
+ }
200
+
201
+ TSharedPtr<FJsonValue> FLandscapeHandlers::ListLandscapeSplines(const TSharedPtr<FJsonObject>& Params)
202
+ {
203
+ REQUIRE_EDITOR_WORLD(World);
204
+
205
+ TArray<TSharedPtr<FJsonValue>> SplineArray;
206
+
207
+ for (TActorIterator<ALandscapeProxy> It(World); It; ++It)
208
+ {
209
+ ALandscapeProxy* Landscape = *It;
210
+ if (!Landscape) continue;
211
+
212
+ ULandscapeSplinesComponent* SplinesComp = Landscape->GetSplinesComponent();
213
+ if (!SplinesComp) continue;
214
+
215
+ const TArray<TObjectPtr<ULandscapeSplineControlPoint>>& ControlPoints = SplinesComp->GetControlPoints();
216
+ for (const TObjectPtr<ULandscapeSplineControlPoint>& CP : ControlPoints)
217
+ {
218
+ if (!CP) continue;
219
+
220
+ TSharedPtr<FJsonObject> PointObj = MakeShared<FJsonObject>();
221
+ FVector Location = CP->Location;
222
+ PointObj->SetNumberField(TEXT("x"), Location.X);
223
+ PointObj->SetNumberField(TEXT("y"), Location.Y);
224
+ PointObj->SetNumberField(TEXT("z"), Location.Z);
225
+ PointObj->SetStringField(TEXT("landscapeName"), Landscape->GetName());
226
+ SplineArray.Add(MakeShared<FJsonValueObject>(PointObj));
227
+ }
228
+ }
229
+
230
+ auto Result = MCPSuccess();
231
+ Result->SetArrayField(TEXT("controlPoints"), SplineArray);
232
+ Result->SetNumberField(TEXT("count"), SplineArray.Num());
233
+ return MCPResult(Result);
234
+ }
235
+
236
+ TSharedPtr<FJsonValue> FLandscapeHandlers::GetLandscapeComponent(const TSharedPtr<FJsonObject>& Params)
237
+ {
238
+ int32 ComponentIndex = 0;
239
+ if (Params->HasField(TEXT("componentIndex")))
240
+ {
241
+ ComponentIndex = static_cast<int32>(Params->GetNumberField(TEXT("componentIndex")));
242
+ }
243
+
244
+ REQUIRE_EDITOR_WORLD(World);
245
+
246
+ // Collect all landscape components across all landscape proxies
247
+ TArray<ULandscapeComponent*> AllComponents;
248
+ for (TActorIterator<ALandscapeProxy> It(World); It; ++It)
249
+ {
250
+ ALandscapeProxy* Landscape = *It;
251
+ if (!Landscape) continue;
252
+
253
+ TArray<ULandscapeComponent*> LandscapeComponents;
254
+ Landscape->GetComponents<ULandscapeComponent>(LandscapeComponents);
255
+ AllComponents.Append(LandscapeComponents);
256
+ }
257
+
258
+ if (ComponentIndex < 0 || ComponentIndex >= AllComponents.Num())
259
+ {
260
+ return MCPError(FString::Printf(TEXT("Component index %d out of range (0-%d)"), ComponentIndex, AllComponents.Num() - 1));
261
+ }
262
+
263
+ ULandscapeComponent* Comp = AllComponents[ComponentIndex];
264
+ if (!Comp)
265
+ {
266
+ return MCPError(TEXT("Component is null"));
267
+ }
268
+
269
+ auto Result = MCPSuccess();
270
+ Result->SetNumberField(TEXT("componentIndex"), ComponentIndex);
271
+ Result->SetStringField(TEXT("name"), Comp->GetName());
272
+
273
+ FVector CompLocation = Comp->GetComponentLocation();
274
+ Result->SetNumberField(TEXT("locationX"), CompLocation.X);
275
+ Result->SetNumberField(TEXT("locationY"), CompLocation.Y);
276
+ Result->SetNumberField(TEXT("locationZ"), CompLocation.Z);
277
+
278
+ Result->SetNumberField(TEXT("sectionBaseX"), Comp->SectionBaseX);
279
+ Result->SetNumberField(TEXT("sectionBaseY"), Comp->SectionBaseY);
280
+ Result->SetNumberField(TEXT("componentSizeQuads"), Comp->ComponentSizeQuads);
281
+ Result->SetNumberField(TEXT("subSections"), Comp->NumSubsections);
282
+
283
+ FBox CompBounds = Comp->Bounds.GetBox();
284
+ if (CompBounds.IsValid)
285
+ {
286
+ TSharedPtr<FJsonObject> BoundsObj = MakeShared<FJsonObject>();
287
+ BoundsObj->SetNumberField(TEXT("minX"), CompBounds.Min.X);
288
+ BoundsObj->SetNumberField(TEXT("minY"), CompBounds.Min.Y);
289
+ BoundsObj->SetNumberField(TEXT("minZ"), CompBounds.Min.Z);
290
+ BoundsObj->SetNumberField(TEXT("maxX"), CompBounds.Max.X);
291
+ BoundsObj->SetNumberField(TEXT("maxY"), CompBounds.Max.Y);
292
+ BoundsObj->SetNumberField(TEXT("maxZ"), CompBounds.Max.Z);
293
+ Result->SetObjectField(TEXT("bounds"), BoundsObj);
294
+ }
295
+
296
+ return MCPResult(Result);
297
+ }
298
+
299
+ TSharedPtr<FJsonValue> FLandscapeHandlers::SetLandscapeMaterial(const TSharedPtr<FJsonObject>& Params)
300
+ {
301
+ FString MaterialPath;
302
+ if (!Params->TryGetStringField(TEXT("materialPath"), MaterialPath) && !Params->TryGetStringField(TEXT("path"), MaterialPath) && !Params->TryGetStringField(TEXT("assetPath"), MaterialPath))
303
+ {
304
+ return MCPError(TEXT("Missing 'materialPath', 'path', or 'assetPath' parameter"));
305
+ }
306
+
307
+ REQUIRE_EDITOR_WORLD(World);
308
+
309
+ // Find the target landscape
310
+ ALandscapeProxy* TargetLandscape = nullptr;
311
+ FString LandscapeName = OptionalString(Params, TEXT("landscapeName"));
312
+
313
+ for (TActorIterator<ALandscapeProxy> It(World); It; ++It)
314
+ {
315
+ ALandscapeProxy* Landscape = *It;
316
+ if (!Landscape) continue;
317
+
318
+ if (LandscapeName.IsEmpty() || Landscape->GetName() == LandscapeName)
319
+ {
320
+ TargetLandscape = Landscape;
321
+ break;
322
+ }
323
+ }
324
+
325
+ if (!TargetLandscape)
326
+ {
327
+ return MCPError(TEXT("No landscape found in the current level"));
328
+ }
329
+
330
+ // Load the material
331
+ UMaterialInterface* Material = LoadObject<UMaterialInterface>(nullptr, *MaterialPath);
332
+ if (!Material)
333
+ {
334
+ return MCPError(FString::Printf(TEXT("Material not found: %s"), *MaterialPath));
335
+ }
336
+
337
+ // Capture previous material for rollback and idempotency
338
+ UMaterialInterface* PrevMaterial = TargetLandscape->LandscapeMaterial;
339
+ if (PrevMaterial == Material)
340
+ {
341
+ auto Noop = MCPSuccess();
342
+ MCPSetExisted(Noop);
343
+ Noop->SetStringField(TEXT("landscapeName"), TargetLandscape->GetName());
344
+ Noop->SetStringField(TEXT("materialPath"), MaterialPath);
345
+ return MCPResult(Noop);
346
+ }
347
+
348
+ // Set the landscape material
349
+ TargetLandscape->LandscapeMaterial = Material;
350
+
351
+ // Update all landscape components to use the new material
352
+ TArray<ULandscapeComponent*> LandscapeComponents;
353
+ TargetLandscape->GetComponents<ULandscapeComponent>(LandscapeComponents);
354
+ for (ULandscapeComponent* Comp : LandscapeComponents)
355
+ {
356
+ if (Comp)
357
+ {
358
+ Comp->SetMaterial(0, Material);
359
+ Comp->MarkRenderStateDirty();
360
+ }
361
+ }
362
+
363
+ // Mark the landscape as modified
364
+ TargetLandscape->MarkPackageDirty();
365
+
366
+ auto Result = MCPSuccess();
367
+ MCPSetUpdated(Result);
368
+ Result->SetStringField(TEXT("landscapeName"), TargetLandscape->GetName());
369
+ Result->SetStringField(TEXT("materialPath"), MaterialPath);
370
+ Result->SetStringField(TEXT("materialName"), Material->GetName());
371
+ Result->SetNumberField(TEXT("componentsUpdated"), LandscapeComponents.Num());
372
+
373
+ // Rollback: restore previous material path if any
374
+ if (PrevMaterial)
375
+ {
376
+ TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
377
+ Payload->SetStringField(TEXT("landscapeName"), TargetLandscape->GetName());
378
+ Payload->SetStringField(TEXT("materialPath"), PrevMaterial->GetPathName());
379
+ MCPSetRollback(Result, TEXT("set_landscape_material"), Payload);
380
+ }
381
+
382
+ return MCPResult(Result);
383
+ }
384
+
385
+ TSharedPtr<FJsonValue> FLandscapeHandlers::AddLandscapeLayerInfo(const TSharedPtr<FJsonObject>& Params)
386
+ {
387
+ FString LayerName;
388
+ if (auto Err = RequireString(Params, TEXT("layerName"), LayerName)) return Err;
389
+
390
+ REQUIRE_EDITOR_WORLD(World);
391
+
392
+ // Find the target landscape
393
+ ALandscapeProxy* TargetLandscape = nullptr;
394
+ FString LandscapeName = OptionalString(Params, TEXT("landscapeName"));
395
+
396
+ for (TActorIterator<ALandscapeProxy> It(World); It; ++It)
397
+ {
398
+ ALandscapeProxy* Landscape = *It;
399
+ if (!Landscape) continue;
400
+
401
+ if (LandscapeName.IsEmpty() || Landscape->GetName() == LandscapeName)
402
+ {
403
+ TargetLandscape = Landscape;
404
+ break;
405
+ }
406
+ }
407
+
408
+ if (!TargetLandscape)
409
+ {
410
+ return MCPError(TEXT("No landscape found in the current level"));
411
+ }
412
+
413
+ ULandscapeInfo* LandscapeInfo = TargetLandscape->GetLandscapeInfo();
414
+ if (!LandscapeInfo)
415
+ {
416
+ return MCPError(TEXT("Failed to get landscape info"));
417
+ }
418
+
419
+ // Check if a layer with this name already exists
420
+ for (const FLandscapeInfoLayerSettings& ExistingLayer : LandscapeInfo->Layers)
421
+ {
422
+ if (ExistingLayer.LayerInfoObj && ExistingLayer.GetLayerName().ToString() == LayerName)
423
+ {
424
+ auto Result = MCPSuccess();
425
+ Result->SetStringField(TEXT("layerName"), LayerName);
426
+ Result->SetStringField(TEXT("path"), ExistingLayer.LayerInfoObj->GetPathName());
427
+ Result->SetStringField(TEXT("note"), TEXT("Layer already exists on this landscape"));
428
+ return MCPResult(Result);
429
+ }
430
+ }
431
+
432
+ // Create a new ULandscapeLayerInfoObject asset
433
+ FString PackagePath = OptionalString(Params, TEXT("packagePath"), TEXT("/Game/Landscape/LayerInfos"));
434
+
435
+ FString AssetName = FString::Printf(TEXT("LI_%s"), *LayerName);
436
+ FString PackageFullPath = PackagePath / AssetName;
437
+
438
+ // Check if the asset already exists
439
+ ULandscapeLayerInfoObject* LayerInfoObj = LoadObject<ULandscapeLayerInfoObject>(nullptr, *(PackageFullPath + TEXT(".") + AssetName));
440
+ if (!LayerInfoObj)
441
+ {
442
+ UPackage* Package = CreatePackage(*PackageFullPath);
443
+ if (!Package)
444
+ {
445
+ return MCPError(FString::Printf(TEXT("Failed to create package: %s"), *PackageFullPath));
446
+ }
447
+
448
+ LayerInfoObj = NewObject<ULandscapeLayerInfoObject>(Package, *AssetName, RF_Public | RF_Standalone);
449
+ if (!LayerInfoObj)
450
+ {
451
+ return MCPError(TEXT("Failed to create LandscapeLayerInfoObject"));
452
+ }
453
+
454
+ PRAGMA_DISABLE_DEPRECATION_WARNINGS
455
+ LayerInfoObj->LayerName = FName(*LayerName);
456
+ PRAGMA_ENABLE_DEPRECATION_WARNINGS
457
+
458
+ // There is no weight-blend toggle to set here any more: bNoWeightBlend
459
+ // was removed in 5.7 and blending is controlled per-layer through
460
+ // landscape settings. The old 'weightBlended' param read into a unused
461
+ // local and the response hardcoded true, so both are gone rather than
462
+ // left implying a setting that is not being applied.
463
+
464
+ // Notify asset registry and save
465
+ FAssetRegistryModule::AssetCreated(LayerInfoObj);
466
+ Package->MarkPackageDirty();
467
+ UEditorAssetLibrary::SaveAsset(PackageFullPath, false);
468
+ }
469
+
470
+ // Register the layer info with the landscape
471
+ int32 LayerIndex = LandscapeInfo->Layers.Num();
472
+ FLandscapeInfoLayerSettings NewLayerSettings(LayerInfoObj, TargetLandscape);
473
+ LandscapeInfo->Layers.Add(NewLayerSettings);
474
+
475
+ // Mark the landscape as dirty
476
+ TargetLandscape->MarkPackageDirty();
477
+
478
+ auto Result = MCPSuccess();
479
+ Result->SetStringField(TEXT("layerName"), LayerName);
480
+ Result->SetStringField(TEXT("path"), LayerInfoObj->GetPathName());
481
+ Result->SetStringField(TEXT("landscapeName"), TargetLandscape->GetName());
482
+ Result->SetNumberField(TEXT("layerIndex"), LayerIndex);
483
+
484
+ return MCPResult(Result);
485
+ }
486
+
487
+ // ─── #150 get_landscape_material_usage_summary ──────────────────────
488
+ // Compact per-proxy dump: class, label, material paths, grass / Nanite /
489
+ // landscape component counts. Avoids the big "get all components" blob
490
+ // get_actor_details produces when you only need materials + counts.
491
+ // #303: spawn an ALandscape with a default flat heightmap at mid-elevation
492
+ // (uint16 32768 = no offset). Section/quad defaults match the Editor's
493
+ // Landscape Mode "create new" form: 63 quads/subsection, 2 subsections/component
494
+ // = 127 quads/component. ComponentCount X/Y default to 8x8 producing a
495
+ // 1016x1016 quad landscape (~1 km at default actor scale 100,100,100).
496
+ TSharedPtr<FJsonValue> FLandscapeHandlers::CreateLandscape(const TSharedPtr<FJsonObject>& Params)
497
+ {
498
+ REQUIRE_EDITOR_WORLD(World);
499
+
500
+ const int32 SubsectionSizeQuads = OptionalInt(Params, TEXT("subsectionSizeQuads"), 63);
501
+ const int32 NumSubsections = OptionalInt(Params, TEXT("numSubsections"), 2);
502
+ const int32 ComponentCountX = OptionalInt(Params, TEXT("componentCountX"), 8);
503
+ const int32 ComponentCountY = OptionalInt(Params, TEXT("componentCountY"), 8);
504
+
505
+ // Bounds checks: SubsectionSizeQuads must be one of the engine's supported
506
+ // values (7, 15, 31, 63, 127, 255), NumSubsections is 1 or 2, and the
507
+ // component grid has to be at least 1x1.
508
+ auto IsPowOf2Minus1 = [](int32 v) {
509
+ const int32 p = v + 1;
510
+ return v >= 7 && v <= 255 && (p & (p - 1)) == 0;
511
+ };
512
+ if (!IsPowOf2Minus1(SubsectionSizeQuads))
513
+ {
514
+ return MCPError(FString::Printf(
515
+ TEXT("subsectionSizeQuads must be one of 7, 15, 31, 63, 127, 255 (got %d)"),
516
+ SubsectionSizeQuads));
517
+ }
518
+ if (NumSubsections != 1 && NumSubsections != 2)
519
+ {
520
+ return MCPError(FString::Printf(TEXT("numSubsections must be 1 or 2 (got %d)"), NumSubsections));
521
+ }
522
+ if (ComponentCountX < 1 || ComponentCountY < 1)
523
+ {
524
+ return MCPError(TEXT("componentCountX and componentCountY must be >= 1"));
525
+ }
526
+
527
+ const int32 ComponentSizeQuads = SubsectionSizeQuads * NumSubsections;
528
+ const int32 SizeX = (ComponentCountX * ComponentSizeQuads) + 1;
529
+ const int32 SizeY = (ComponentCountY * ComponentSizeQuads) + 1;
530
+
531
+ const int32 HeightOffset = OptionalInt(Params, TEXT("heightOffset"), 32768);
532
+ if (HeightOffset < 0 || HeightOffset > 65535)
533
+ {
534
+ return MCPError(TEXT("heightOffset must be in [0, 65535] (uint16 elevation)"));
535
+ }
536
+
537
+ const FVector Location = OptionalVec3(Params, TEXT("location"));
538
+ const FVector Scale = OptionalVec3(Params, TEXT("scale"), FVector(100.0, 100.0, 100.0));
539
+
540
+ const FString Label = OptionalString(Params, TEXT("label"));
541
+
542
+ // Idempotency by label.
543
+ if (auto Existing = MCPCheckActorLabelExists(World, Label, TEXT("skip"), TEXT("Landscape")))
544
+ {
545
+ return Existing;
546
+ }
547
+
548
+ FActorSpawnParameters SpawnParams;
549
+ SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
550
+ ALandscape* Landscape = World->SpawnActor<ALandscape>(Location, FRotator::ZeroRotator, SpawnParams);
551
+ if (!Landscape)
552
+ {
553
+ return MCPError(TEXT("Failed to spawn ALandscape actor"));
554
+ }
555
+ Landscape->SetActorScale3D(Scale);
556
+
557
+ // Allocate a flat heightmap. Layer 0 (FGuid()) is the only edit layer for a
558
+ // non-edit-layer landscape, which is what gets created by the Editor's
559
+ // "create new landscape" defaults.
560
+ TArray<uint16> HeightData;
561
+ HeightData.SetNumUninitialized(SizeX * SizeY);
562
+ for (int32 i = 0; i < HeightData.Num(); ++i)
563
+ {
564
+ HeightData[i] = static_cast<uint16>(HeightOffset);
565
+ }
566
+
567
+ TMap<FGuid, TArray<uint16>> ImportHeightData;
568
+ ImportHeightData.Add(FGuid(), MoveTemp(HeightData));
569
+
570
+ TMap<FGuid, TArray<FLandscapeImportLayerInfo>> ImportLayerInfo;
571
+ ImportLayerInfo.Add(FGuid(), TArray<FLandscapeImportLayerInfo>());
572
+
573
+ TArray<FLandscapeLayer> EmptyLayers;
574
+ Landscape->Import(
575
+ FGuid::NewGuid(),
576
+ 0, 0,
577
+ SizeX - 1, SizeY - 1,
578
+ NumSubsections,
579
+ SubsectionSizeQuads,
580
+ ImportHeightData,
581
+ nullptr,
582
+ ImportLayerInfo,
583
+ ELandscapeImportAlphamapType::Additive,
584
+ #if UE_MCP_HAS_5_5_API
585
+ MakeArrayView(EmptyLayers)
586
+ #else
587
+ // 5.4: last arg is const TArray<FLandscapeLayer>* (TArrayView signature added in 5.5).
588
+ &EmptyLayers
589
+ #endif
590
+ );
591
+
592
+ if (!Label.IsEmpty())
593
+ {
594
+ Landscape->SetActorLabel(Label);
595
+ }
596
+
597
+ // Register so subsequent get_landscape_info / sample_landscape calls find it.
598
+ if (ULandscapeInfo* LI = Landscape->GetLandscapeInfo())
599
+ {
600
+ LI->FixupProxiesTransform();
601
+ LI->RecreateCollisionComponents();
602
+ }
603
+
604
+ auto Result = MCPSuccess();
605
+ MCPSetCreated(Result);
606
+ Result->SetStringField(TEXT("actorLabel"), Landscape->GetActorLabel());
607
+ Result->SetStringField(TEXT("actorPath"), Landscape->GetPathName());
608
+ Result->SetNumberField(TEXT("componentCountX"), ComponentCountX);
609
+ Result->SetNumberField(TEXT("componentCountY"), ComponentCountY);
610
+ Result->SetNumberField(TEXT("componentSizeQuads"), ComponentSizeQuads);
611
+ Result->SetNumberField(TEXT("subsectionSizeQuads"), SubsectionSizeQuads);
612
+ Result->SetNumberField(TEXT("numSubsections"), NumSubsections);
613
+ Result->SetNumberField(TEXT("sizeX"), SizeX);
614
+ Result->SetNumberField(TEXT("sizeY"), SizeY);
615
+
616
+ TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
617
+ Payload->SetStringField(TEXT("actorLabel"), Landscape->GetActorLabel());
618
+ MCPSetRollback(Result, TEXT("delete_actor"), Payload);
619
+
620
+ return MCPResult(Result);
621
+ }
622
+
623
+ // #251: standalone LayerInfo asset creation. Unlike add_landscape_layer_info
624
+ // (which requires a landscape in the world to register the layer against),
625
+ // this creates the ULandscapeLayerInfoObject asset in the content browser
626
+ // so paint workflows can pre-author layer assets before the landscape exists.
627
+ TSharedPtr<FJsonValue> FLandscapeHandlers::CreateLandscapeLayerInfo(const TSharedPtr<FJsonObject>& Params)
628
+ {
629
+ FString LayerName;
630
+ if (auto Err = RequireString(Params, TEXT("layerName"), LayerName)) return Err;
631
+
632
+ const FString Name = OptionalString(Params, TEXT("name"), FString::Printf(TEXT("LI_%s"), *LayerName));
633
+ const FString PackagePath = OptionalString(Params, TEXT("packagePath"), TEXT("/Game/Landscape/LayerInfos"));
634
+ const FString OnConflict = OptionalString(Params, TEXT("onConflict"), TEXT("skip"));
635
+
636
+ TSharedPtr<FJsonValue> Existing = MCPCheckAssetExists(PackagePath, Name, OnConflict, TEXT("LandscapeLayerInfoObject"));
637
+ if (Existing.IsValid()) return Existing;
638
+
639
+ const FString PackageFullPath = PackagePath / Name;
640
+ UPackage* Package = CreatePackage(*PackageFullPath);
641
+ if (!Package)
642
+ {
643
+ return MCPError(FString::Printf(TEXT("Failed to create package: %s"), *PackageFullPath));
644
+ }
645
+
646
+ ULandscapeLayerInfoObject* LayerInfo = NewObject<ULandscapeLayerInfoObject>(
647
+ Package, *Name, RF_Public | RF_Standalone);
648
+ if (!LayerInfo)
649
+ {
650
+ return MCPError(TEXT("Failed to create LandscapeLayerInfoObject"));
651
+ }
652
+
653
+ PRAGMA_DISABLE_DEPRECATION_WARNINGS
654
+ LayerInfo->LayerName = FName(*LayerName);
655
+ PRAGMA_ENABLE_DEPRECATION_WARNINGS
656
+
657
+ // physMaterial was documented here and never applied - the caller had to
658
+ // discover for themselves that it needed a second call. PhysicsCore is not
659
+ // a hard dependency of this module, so the class is reached by path and the
660
+ // property set through reflection rather than a link-time include.
661
+ const FString PhysMaterialPath = OptionalString(Params, TEXT("physMaterial"));
662
+ if (!PhysMaterialPath.IsEmpty())
663
+ {
664
+ UObject* PhysMat = LoadAssetByPath<UObject>(PhysMaterialPath);
665
+ if (!PhysMat)
666
+ {
667
+ // Both failure paths here run after the package and object exist, so
668
+ // bail out without leaving a half-made asset in memory.
669
+ LayerInfo->MarkAsGarbage();
670
+ return MCPError(FString::Printf(TEXT("physMaterial not found: %s"), *PhysMaterialPath));
671
+ }
672
+ FObjectProperty* Prop = CastField<FObjectProperty>(
673
+ ULandscapeLayerInfoObject::StaticClass()->FindPropertyByName(TEXT("PhysMaterial")));
674
+ if (!Prop || !Prop->PropertyClass || !PhysMat->IsA(Prop->PropertyClass))
675
+ {
676
+ LayerInfo->MarkAsGarbage();
677
+ return MCPError(FString::Printf(
678
+ TEXT("'%s' is a %s, not a PhysicalMaterial."),
679
+ *PhysMaterialPath, *PhysMat->GetClass()->GetName()));
680
+ }
681
+ Prop->SetObjectPropertyValue_InContainer(LayerInfo, PhysMat);
682
+ }
683
+
684
+ double Hardness = 0.0;
685
+ if (Params->TryGetNumberField(TEXT("hardness"), Hardness))
686
+ {
687
+ // Hardness is becoming private; the setter also handles Modify() and the
688
+ // property-change notification the direct write skipped.
689
+ LayerInfo->SetHardness(static_cast<float>(Hardness), /*bInModify=*/true, EPropertyChangeType::ValueSet);
690
+ }
691
+
692
+ FAssetRegistryModule::AssetCreated(LayerInfo);
693
+ Package->MarkPackageDirty();
694
+ SaveAssetPackage(LayerInfo);
695
+
696
+ auto Result = MCPSuccess();
697
+ MCPSetCreated(Result);
698
+ Result->SetStringField(TEXT("path"), LayerInfo->GetPathName());
699
+ Result->SetStringField(TEXT("layerName"), LayerName);
700
+ Result->SetStringField(TEXT("packagePath"), PackagePath);
701
+ if (!PhysMaterialPath.IsEmpty()) Result->SetStringField(TEXT("physMaterial"), PhysMaterialPath);
702
+ MCPSetDeleteAssetRollback(Result, LayerInfo->GetPathName());
703
+
704
+ return MCPResult(Result);
705
+ }
706
+
707
+ TSharedPtr<FJsonValue> FLandscapeHandlers::GetMaterialUsageSummary(const TSharedPtr<FJsonObject>& Params)
708
+ {
709
+ REQUIRE_EDITOR_WORLD(World);
710
+
711
+ TArray<TSharedPtr<FJsonValue>> ProxyArray;
712
+ TSet<FString> UniqueMaterials;
713
+ int32 TotalComponents = 0, TotalGrass = 0, TotalNanite = 0;
714
+
715
+ for (TActorIterator<ALandscapeProxy> It(World); It; ++It)
716
+ {
717
+ ALandscapeProxy* Proxy = *It;
718
+ if (!Proxy) continue;
719
+
720
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
721
+ Obj->SetStringField(TEXT("label"), Proxy->GetActorLabel());
722
+ Obj->SetStringField(TEXT("name"), Proxy->GetName());
723
+ Obj->SetStringField(TEXT("class"), Proxy->GetClass()->GetName());
724
+ Obj->SetStringField(TEXT("path"), Proxy->GetPathName());
725
+
726
+ if (UMaterialInterface* Mat = Proxy->LandscapeMaterial)
727
+ {
728
+ Obj->SetStringField(TEXT("landscapeMaterial"), Mat->GetPathName());
729
+ UniqueMaterials.Add(Mat->GetPathName());
730
+ }
731
+ if (UMaterialInterface* HoleMat = Proxy->LandscapeHoleMaterial)
732
+ {
733
+ Obj->SetStringField(TEXT("landscapeHoleMaterial"), HoleMat->GetPathName());
734
+ }
735
+
736
+ // Histogram components by class (grass / Nanite / regular landscape comps)
737
+ int32 LandscapeComps = 0, GrassComps = 0, NaniteComps = 0;
738
+ TArray<UActorComponent*> Comps;
739
+ Proxy->GetComponents(Comps);
740
+ for (UActorComponent* C : Comps)
741
+ {
742
+ if (!C) continue;
743
+ const FString CName = C->GetClass()->GetName();
744
+ if (CName == TEXT("LandscapeComponent")) LandscapeComps++;
745
+ else if (CName == TEXT("GrassInstancedStaticMeshComponent")) GrassComps++;
746
+ else if (CName == TEXT("LandscapeNaniteComponent")) NaniteComps++;
747
+ }
748
+ Obj->SetNumberField(TEXT("landscapeComponentCount"), LandscapeComps);
749
+ Obj->SetNumberField(TEXT("grassComponentCount"), GrassComps);
750
+ Obj->SetNumberField(TEXT("naniteComponentCount"), NaniteComps);
751
+ TotalComponents += LandscapeComps;
752
+ TotalGrass += GrassComps;
753
+ TotalNanite += NaniteComps;
754
+
755
+ const FVector Loc = Proxy->GetActorLocation();
756
+ const FVector Scale = Proxy->GetActorScale3D();
757
+ TSharedPtr<FJsonObject> LocObj = MakeShared<FJsonObject>();
758
+ LocObj->SetNumberField(TEXT("x"), Loc.X);
759
+ LocObj->SetNumberField(TEXT("y"), Loc.Y);
760
+ LocObj->SetNumberField(TEXT("z"), Loc.Z);
761
+ Obj->SetObjectField(TEXT("location"), LocObj);
762
+ TSharedPtr<FJsonObject> ScaleObj = MakeShared<FJsonObject>();
763
+ ScaleObj->SetNumberField(TEXT("x"), Scale.X);
764
+ ScaleObj->SetNumberField(TEXT("y"), Scale.Y);
765
+ ScaleObj->SetNumberField(TEXT("z"), Scale.Z);
766
+ Obj->SetObjectField(TEXT("scale"), ScaleObj);
767
+
768
+ ProxyArray.Add(MakeShared<FJsonValueObject>(Obj));
769
+ }
770
+
771
+ TArray<TSharedPtr<FJsonValue>> UniqueMatsArr;
772
+ for (const FString& M : UniqueMaterials) UniqueMatsArr.Add(MakeShared<FJsonValueString>(M));
773
+
774
+ auto Result = MCPSuccess();
775
+ Result->SetArrayField(TEXT("proxies"), ProxyArray);
776
+ Result->SetNumberField(TEXT("proxyCount"), ProxyArray.Num());
777
+ Result->SetArrayField(TEXT("uniqueLandscapeMaterials"), UniqueMatsArr);
778
+ Result->SetNumberField(TEXT("totalLandscapeComponents"), TotalComponents);
779
+ Result->SetNumberField(TEXT("totalGrassComponents"), TotalGrass);
780
+ Result->SetNumberField(TEXT("totalNaniteComponents"), TotalNanite);
781
+ return MCPResult(Result);
782
+ }
783
+
784
+ // #733: enumerate LandscapeStreamingProxy actors currently loaded in the world,
785
+ // with per-proxy world bounds and the parent Landscape count. On a World
786
+ // Partition map, an unloaded proxy silently reads layer weights as 0, so a
787
+ // measurement is only trustworthy once the covering proxy is confirmed loaded.
788
+ // Unloaded proxies are not spawned as actors, so the actor iterator only yields
789
+ // loaded ones - hence loaded is always true for enumerated entries; callers use
790
+ // the count + bounds to reason about coverage.
791
+ TSharedPtr<FJsonValue> FLandscapeHandlers::ListLandscapeProxies(const TSharedPtr<FJsonObject>& Params)
792
+ {
793
+ REQUIRE_EDITOR_WORLD(World);
794
+
795
+ int32 ParentLandscapes = 0;
796
+ TArray<TSharedPtr<FJsonValue>> Proxies;
797
+ for (TActorIterator<AActor> It(World); It; ++It)
798
+ {
799
+ AActor* Actor = *It;
800
+ if (!Actor) continue;
801
+ if (Actor->IsA<ALandscape>())
802
+ {
803
+ ParentLandscapes++;
804
+ continue;
805
+ }
806
+ ALandscapeStreamingProxy* Proxy = Cast<ALandscapeStreamingProxy>(Actor);
807
+ if (!Proxy) continue;
808
+
809
+ FVector Origin, Extent;
810
+ Proxy->GetActorBounds(false, Origin, Extent);
811
+
812
+ TSharedPtr<FJsonObject> ProxyObj = MakeShared<FJsonObject>();
813
+ ProxyObj->SetStringField(TEXT("label"), Proxy->GetActorLabel());
814
+ ProxyObj->SetBoolField(TEXT("loaded"), true);
815
+
816
+ TSharedPtr<FJsonObject> Bounds = MakeShared<FJsonObject>();
817
+ TSharedPtr<FJsonObject> OriginObj = MakeShared<FJsonObject>();
818
+ OriginObj->SetNumberField(TEXT("x"), Origin.X);
819
+ OriginObj->SetNumberField(TEXT("y"), Origin.Y);
820
+ OriginObj->SetNumberField(TEXT("z"), Origin.Z);
821
+ TSharedPtr<FJsonObject> ExtentObj = MakeShared<FJsonObject>();
822
+ ExtentObj->SetNumberField(TEXT("x"), Extent.X);
823
+ ExtentObj->SetNumberField(TEXT("y"), Extent.Y);
824
+ ExtentObj->SetNumberField(TEXT("z"), Extent.Z);
825
+ Bounds->SetObjectField(TEXT("origin"), OriginObj);
826
+ Bounds->SetObjectField(TEXT("extent"), ExtentObj);
827
+ ProxyObj->SetObjectField(TEXT("worldBounds"), Bounds);
828
+
829
+ Proxies.Add(MakeShared<FJsonValueObject>(ProxyObj));
830
+ }
831
+
832
+ auto Result = MCPSuccess();
833
+ Result->SetNumberField(TEXT("loadedProxies"), Proxies.Num());
834
+ Result->SetNumberField(TEXT("parentLandscapes"), ParentLandscapes);
835
+ Result->SetArrayField(TEXT("proxies"), Proxies);
836
+ Result->SetStringField(TEXT("note"), TEXT("World Partition unloaded proxies are not spawned as actors, so only loaded proxies are listed."));
837
+ return MCPResult(Result);
838
+ }
839
+
840
+ // #733: resolve which loaded LandscapeStreamingProxy's world bounds contain a
841
+ // world X/Y. Returns the covering proxy (loaded:true) or loaded:false when no
842
+ // loaded proxy covers the position - which usually means the covering proxy is
843
+ // streamed out, making any 0-weight readback there ambiguous rather than real.
844
+ TSharedPtr<FJsonValue> FLandscapeHandlers::FindLandscapeProxyAt(const TSharedPtr<FJsonObject>& Params)
845
+ {
846
+ REQUIRE_EDITOR_WORLD(World);
847
+
848
+ if (!Params->HasField(TEXT("worldX")) || !Params->HasField(TEXT("worldY")))
849
+ {
850
+ return MCPError(TEXT("Missing 'worldX'/'worldY' world position"));
851
+ }
852
+ const double TargetX = OptionalNumber(Params, TEXT("worldX"), 0.0);
853
+ const double TargetY = OptionalNumber(Params, TEXT("worldY"), 0.0);
854
+
855
+ for (TActorIterator<AActor> It(World); It; ++It)
856
+ {
857
+ ALandscapeStreamingProxy* Proxy = Cast<ALandscapeStreamingProxy>(*It);
858
+ if (!Proxy) continue;
859
+
860
+ FVector Origin, Extent;
861
+ Proxy->GetActorBounds(false, Origin, Extent);
862
+ if (TargetX >= Origin.X - Extent.X && TargetX <= Origin.X + Extent.X &&
863
+ TargetY >= Origin.Y - Extent.Y && TargetY <= Origin.Y + Extent.Y)
864
+ {
865
+ auto Result = MCPSuccess();
866
+ Result->SetBoolField(TEXT("found"), true);
867
+ Result->SetBoolField(TEXT("loaded"), true);
868
+ Result->SetStringField(TEXT("label"), Proxy->GetActorLabel());
869
+ return MCPResult(Result);
870
+ }
871
+ }
872
+
873
+ auto Result = MCPSuccess();
874
+ Result->SetBoolField(TEXT("found"), false);
875
+ Result->SetBoolField(TEXT("loaded"), false);
876
+ Result->SetStringField(TEXT("note"), TEXT("No loaded proxy covers this position; the covering proxy is likely streamed out, so weight/height readbacks here are ambiguous."));
877
+ return MCPResult(Result);
878
+ }