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,449 +1,449 @@
1
- // Landscape sculpting and weight painting (#742).
2
- //
3
- // The landscape category has always advertised "sculpting, painting, heightmap
4
- // import", but no such action existed - the description promised capability the
5
- // surface did not have, which is worse than a gap because a caller plans around
6
- // it. These are the missing writes, done through FLandscapeEditDataInterface,
7
- // the same path the landscape editor tools use.
8
- //
9
- // Translation-unit partition of FLandscapeHandlers; registrations live in
10
- // LandscapeHandlers.cpp.
11
-
12
- #include "LandscapeHandlers.h"
13
-
14
- #include "HandlerUtils.h"
15
-
16
- #include "Editor.h"
17
- #include "Engine/World.h"
18
- #include "EngineUtils.h"
19
- #include "Landscape.h"
20
- #include "LandscapeComponent.h"
21
- #include "LandscapeEdit.h"
22
- #include "LandscapeInfo.h"
23
- #include "LandscapeLayerInfoObject.h"
24
- #include "LandscapeProxy.h"
25
- #include "LandscapeEditLayer.h"
26
- #include "ScopedTransaction.h"
27
-
28
- namespace
29
- {
30
- // Landscape heights are uint16 with 32768 as "zero". One unit of height is
31
- // LANDSCAPE_ZSCALE (1/128) cm before the actor's own Z scale.
32
-
33
- /** The landscape to edit: by actor label, else the only one in the world. */
34
- ALandscape* ResolveLandscape(UWorld* World, const FString& ActorLabel, FString& OutError)
35
- {
36
- TArray<ALandscape*> Found;
37
- for (TActorIterator<ALandscape> It(World); It; ++It)
38
- {
39
- ALandscape* Candidate = *It;
40
- if (!Candidate) continue;
41
- if (!ActorLabel.IsEmpty() && !Candidate->GetActorLabel().Equals(ActorLabel, ESearchCase::IgnoreCase)) continue;
42
- Found.Add(Candidate);
43
- }
44
- if (Found.Num() == 0)
45
- {
46
- OutError = ActorLabel.IsEmpty()
47
- ? TEXT("No Landscape actor in the current level. Create one with landscape(create).")
48
- : FString::Printf(TEXT("No Landscape actor labelled '%s'"), *ActorLabel);
49
- return nullptr;
50
- }
51
- if (Found.Num() > 1 && ActorLabel.IsEmpty())
52
- {
53
- TArray<FString> Labels;
54
- for (ALandscape* L : Found) Labels.Add(L->GetActorLabel());
55
- OutError = FString::Printf(
56
- TEXT("%d Landscape actors in the level; pass actorLabel to choose. Available: [%s]"),
57
- Found.Num(), *FString::Join(Labels, TEXT(", ")));
58
- return nullptr;
59
- }
60
- return Found[0];
61
- }
62
-
63
- /**
64
- * Convert a world-space XY position and radius into the landscape's own
65
- * quad grid, clamped to the loaded extent. Working in world units is what a
66
- * caller has; working in quads is what the edit interface needs.
67
- */
68
- bool WorldToLandscapeRect(
69
- ALandscape* Landscape,
70
- ULandscapeInfo* Info,
71
- const FVector2D& Center,
72
- double Radius,
73
- int32& OutX1, int32& OutY1, int32& OutX2, int32& OutY2,
74
- FString& OutError)
75
- {
76
- const FTransform ActorToWorld = Landscape->ActorToWorld();
77
- const FVector LocalCenter = ActorToWorld.InverseTransformPosition(FVector(Center.X, Center.Y, 0.0));
78
- const FVector Scale = ActorToWorld.GetScale3D();
79
- if (FMath::IsNearlyZero(Scale.X) || FMath::IsNearlyZero(Scale.Y))
80
- {
81
- OutError = TEXT("Landscape has a zero XY scale");
82
- return false;
83
- }
84
- const double RadiusQuadsX = FMath::Abs(Radius / Scale.X);
85
- const double RadiusQuadsY = FMath::Abs(Radius / Scale.Y);
86
-
87
- int32 MinX, MinY, MaxX, MaxY;
88
- if (!Info->GetLandscapeExtent(MinX, MinY, MaxX, MaxY))
89
- {
90
- OutError = TEXT("Could not read the landscape extent (are its components loaded?)");
91
- return false;
92
- }
93
-
94
- OutX1 = FMath::Clamp(FMath::FloorToInt(LocalCenter.X - RadiusQuadsX), MinX, MaxX);
95
- OutY1 = FMath::Clamp(FMath::FloorToInt(LocalCenter.Y - RadiusQuadsY), MinY, MaxY);
96
- OutX2 = FMath::Clamp(FMath::CeilToInt(LocalCenter.X + RadiusQuadsX), MinX, MaxX);
97
- OutY2 = FMath::Clamp(FMath::CeilToInt(LocalCenter.Y + RadiusQuadsY), MinY, MaxY);
98
-
99
- if (OutX2 < OutX1 || OutY2 < OutY1)
100
- {
101
- OutError = TEXT("The requested area does not overlap the landscape");
102
- return false;
103
- }
104
- return true;
105
- }
106
-
107
- /**
108
- * Resolve which edit layer to write into. UE 5.8 landscapes ALWAYS have
109
- * layer content (HasLayersContent/CanHaveLayersContent return true
110
- * unconditionally), so writing without a layer GUID targets the merged
111
- * heightmap - which the layer system then regenerates from the untouched
112
- * layer stack on the next tick, silently erasing the edit. Outside
113
- * Landscape Mode GetEditingLayer() is an invalid GUID, which is exactly
114
- * the case a headless bridge is always in.
115
- */
116
- bool ResolveEditLayerGuid(ALandscape* Landscape, const TSharedPtr<FJsonObject>& Params, FGuid& OutGuid, FString& OutName, FString& OutError)
117
- {
118
- const FString WantedName = OptionalString(Params, TEXT("editLayer"));
119
- const int32 WantedIndex = OptionalInt(Params, TEXT("editLayerIndex"), 0);
120
-
121
- const TArray<const ULandscapeEditLayerBase*> Layers = Landscape->GetEditLayersConst();
122
- if (Layers.Num() == 0)
123
- {
124
- OutError = TEXT("Landscape has no edit layers; cannot write a sculpt/paint edit that would survive the next layer update");
125
- return false;
126
- }
127
-
128
- if (!WantedName.IsEmpty())
129
- {
130
- for (const ULandscapeEditLayerBase* Layer : Layers)
131
- {
132
- if (Layer && Layer->GetName().ToString().Equals(WantedName, ESearchCase::IgnoreCase))
133
- {
134
- OutGuid = Layer->GetGuid();
135
- OutName = Layer->GetName().ToString();
136
- return true;
137
- }
138
- }
139
- TArray<FString> Names;
140
- for (const ULandscapeEditLayerBase* Layer : Layers) { if (Layer) Names.Add(Layer->GetName().ToString()); }
141
- OutError = FString::Printf(TEXT("Edit layer '%s' not found. Available: [%s]"), *WantedName, *FString::Join(Names, TEXT(", ")));
142
- return false;
143
- }
144
-
145
- if (!Layers.IsValidIndex(WantedIndex) || !Layers[WantedIndex])
146
- {
147
- OutError = FString::Printf(TEXT("editLayerIndex %d is out of range (%d edit layers)"), WantedIndex, Layers.Num());
148
- return false;
149
- }
150
- OutGuid = Layers[WantedIndex]->GetGuid();
151
- OutName = Layers[WantedIndex]->GetName().ToString();
152
- return true;
153
- }
154
-
155
- /** Smooth 0..1 brush weight for a point, given a falloff fraction. */
156
- double BrushWeight(double DistanceFraction, double Falloff)
157
- {
158
- if (DistanceFraction >= 1.0) return 0.0;
159
- const double Inner = FMath::Clamp(1.0 - Falloff, 0.0, 1.0);
160
- if (DistanceFraction <= Inner) return 1.0;
161
- const double T = (DistanceFraction - Inner) / FMath::Max(1.0 - Inner, KINDA_SMALL_NUMBER);
162
- // Smoothstep so a brush edge does not leave a visible ring.
163
- return 1.0 - (T * T * (3.0 - 2.0 * T));
164
- }
165
- }
166
-
167
- // landscape(sculpt): raise, lower or flatten a circular brush footprint.
168
- TSharedPtr<FJsonValue> FLandscapeHandlers::Sculpt(const TSharedPtr<FJsonObject>& Params)
169
- {
170
- REQUIRE_EDITOR_WORLD(World);
171
-
172
- FString ResolveError;
173
- ALandscape* Landscape = ResolveLandscape(World, OptionalString(Params, TEXT("actorLabel")), ResolveError);
174
- if (!Landscape) return MCPError(ResolveError);
175
-
176
- ULandscapeInfo* Info = Landscape->GetLandscapeInfo();
177
- if (!Info) return MCPError(TEXT("Landscape has no LandscapeInfo (not registered yet)"));
178
-
179
- const TSharedPtr<FJsonObject>* CenterObj = nullptr;
180
- if (!Params->TryGetObjectField(TEXT("center"), CenterObj) || !CenterObj)
181
- {
182
- return MCPError(TEXT("Missing 'center' ({x, y} in world space)"));
183
- }
184
- FVector2D Center(0.0, 0.0);
185
- (*CenterObj)->TryGetNumberField(TEXT("x"), Center.X);
186
- (*CenterObj)->TryGetNumberField(TEXT("y"), Center.Y);
187
-
188
- const double Radius = OptionalNumber(Params, TEXT("radius"), 500.0);
189
- if (Radius <= 0.0) return MCPError(TEXT("'radius' must be positive"));
190
- const double Falloff = FMath::Clamp(OptionalNumber(Params, TEXT("falloff"), 0.5), 0.0, 1.0);
191
- const FString Mode = OptionalString(Params, TEXT("mode"), TEXT("raise")).ToLower();
192
- if (Mode != TEXT("raise") && Mode != TEXT("lower") && Mode != TEXT("flatten"))
193
- {
194
- return MCPError(TEXT("'mode' must be raise, lower or flatten"));
195
- }
196
- // World-space centimetres to move the surface at full brush strength.
197
- const double Amount = OptionalNumber(Params, TEXT("amount"), 100.0);
198
-
199
- int32 X1, Y1, X2, Y2;
200
- FString RectError;
201
- if (!WorldToLandscapeRect(Landscape, Info, Center, Radius, X1, Y1, X2, Y2, RectError))
202
- {
203
- return MCPError(RectError);
204
- }
205
-
206
- const int32 Width = X2 - X1 + 1;
207
- const int32 Height = Y2 - Y1 + 1;
208
- // Cap the working rect: the extent clamp alone lets an 8k landscape with a
209
- // huge radius allocate hundreds of MB and iterate tens of millions of verts.
210
- const int64 VertexCount = (int64)Width * (int64)Height;
211
- const int64 MaxVertices = (int64)FMath::Clamp(OptionalInt(Params, TEXT("maxVertices"), 4'000'000), 1024, 64'000'000);
212
- if (VertexCount > MaxVertices)
213
- {
214
- return MCPError(FString::Printf(
215
- TEXT("Brush covers %lld vertices, above the %lld limit. Reduce 'radius' or raise 'maxVertices' deliberately."),
216
- VertexCount, MaxVertices));
217
- }
218
- TArray<uint16> Heights;
219
- Heights.SetNumZeroed(Width * Height);
220
-
221
- FGuid EditLayerGuid;
222
- FString EditLayerName;
223
- FString LayerError;
224
- if (!ResolveEditLayerGuid(Landscape, Params, EditLayerGuid, EditLayerName, LayerError))
225
- {
226
- return MCPError(LayerError);
227
- }
228
-
229
- FLandscapeEditDataInterface EditData(Info);
230
- EditData.SetEditLayer(EditLayerGuid);
231
- // GetHeightData takes the rect by non-const reference and REWRITES it with
232
- // the valid sub-range, so pass copies: the buffer is sized and indexed
233
- // against the original rect, and an empty region returns an inverted
234
- // INT_MAX/INT_MIN rect that would overflow the vertex count downstream.
235
- {
236
- int32 GX1 = X1, GY1 = Y1, GX2 = X2, GY2 = Y2;
237
- EditData.GetHeightData(GX1, GY1, GX2, GY2, Heights.GetData(), 0);
238
- if (GX2 < GX1 || GY2 < GY1)
239
- {
240
- return MCPError(TEXT("No landscape height data in the requested area (unloaded or absent components). Move the brush or load the region first."));
241
- }
242
- }
243
-
244
- const FVector Scale = Landscape->ActorToWorld().GetScale3D();
245
- // uint16 height units per world centimetre.
246
- const double ZScale = FMath::IsNearlyZero(Scale.Z) ? 0.0 : (1.0 / (LANDSCAPE_ZSCALE * Scale.Z));
247
- if (ZScale == 0.0) return MCPError(TEXT("Landscape has a zero Z scale; heights cannot be written"));
248
-
249
- const FVector LocalCenter = Landscape->ActorToWorld().InverseTransformPosition(FVector(Center.X, Center.Y, 0.0));
250
- const double RadiusQuadsX = FMath::Abs(Radius / Scale.X);
251
- const double RadiusQuadsY = FMath::Abs(Radius / Scale.Y);
252
-
253
- // Flatten needs a target: the height under the brush centre before editing.
254
- double FlattenTarget = 0.0;
255
- if (Mode == TEXT("flatten"))
256
- {
257
- const int32 CX = FMath::Clamp(FMath::RoundToInt(LocalCenter.X), X1, X2);
258
- const int32 CY = FMath::Clamp(FMath::RoundToInt(LocalCenter.Y), Y1, Y2);
259
- FlattenTarget = (double)Heights[(CY - Y1) * Width + (CX - X1)];
260
- }
261
-
262
- // Resolve the mode once instead of comparing strings per vertex.
263
- const int32 SculptMode = Mode == TEXT("raise") ? 0 : (Mode == TEXT("lower") ? 1 : 2);
264
-
265
- int32 Touched = 0;
266
- for (int32 Y = Y1; Y <= Y2; ++Y)
267
- {
268
- for (int32 X = X1; X <= X2; ++X)
269
- {
270
- const double DX = (X - LocalCenter.X) / FMath::Max(RadiusQuadsX, KINDA_SMALL_NUMBER);
271
- const double DY = (Y - LocalCenter.Y) / FMath::Max(RadiusQuadsY, KINDA_SMALL_NUMBER);
272
- const double Fraction = FMath::Sqrt(DX * DX + DY * DY);
273
- const double Weight = BrushWeight(Fraction, Falloff);
274
- if (Weight <= 0.0) continue;
275
-
276
- const int32 Index = (Y - Y1) * Width + (X - X1);
277
- const double Current = (double)Heights[Index];
278
- double Next = Current;
279
- if (SculptMode == 0) Next = Current + Amount * ZScale * Weight;
280
- else if (SculptMode == 1) Next = Current - Amount * ZScale * Weight;
281
- else Next = FMath::Lerp(Current, FlattenTarget, Weight);
282
-
283
- Heights[Index] = (uint16)FMath::Clamp(FMath::RoundToInt(Next), 0, 65535);
284
- ++Touched;
285
- }
286
- }
287
-
288
- {
289
- const FScopedTransaction Transaction(FText::FromString(TEXT("MCP landscape sculpt")));
290
- Landscape->Modify();
291
- FScopedSetLandscapeEditingLayer EditingLayer(Landscape, EditLayerGuid);
292
- int32 SX1 = X1, SY1 = Y1, SX2 = X2, SY2 = Y2;
293
- EditData.SetHeightData(SX1, SY1, SX2, SY2, Heights.GetData(), 0, /*InCalcNormals=*/true);
294
- EditData.Flush();
295
- }
296
- Landscape->PostEditChange();
297
-
298
- auto Result = MCPSuccess();
299
- MCPSetUpdated(Result);
300
- Result->SetStringField(TEXT("landscape"), Landscape->GetActorLabel());
301
- Result->SetStringField(TEXT("mode"), Mode);
302
- Result->SetNumberField(TEXT("radius"), Radius);
303
- Result->SetNumberField(TEXT("amount"), Amount);
304
- Result->SetNumberField(TEXT("verticesTouched"), Touched);
305
- Result->SetStringField(TEXT("editLayer"), EditLayerName);
306
- Result->SetNumberField(TEXT("rectX1"), X1);
307
- Result->SetNumberField(TEXT("rectY1"), Y1);
308
- Result->SetNumberField(TEXT("rectX2"), X2);
309
- Result->SetNumberField(TEXT("rectY2"), Y2);
310
- Result->SetStringField(TEXT("note"), TEXT("The level is left dirty and unsaved; save it when ready."));
311
- return MCPResult(Result);
312
- }
313
-
314
- // landscape(paint_layer): paint a weight layer over a circular footprint.
315
- TSharedPtr<FJsonValue> FLandscapeHandlers::PaintLayer(const TSharedPtr<FJsonObject>& Params)
316
- {
317
- REQUIRE_EDITOR_WORLD(World);
318
-
319
- FString ResolveError;
320
- ALandscape* Landscape = ResolveLandscape(World, OptionalString(Params, TEXT("actorLabel")), ResolveError);
321
- if (!Landscape) return MCPError(ResolveError);
322
-
323
- ULandscapeInfo* Info = Landscape->GetLandscapeInfo();
324
- if (!Info) return MCPError(TEXT("Landscape has no LandscapeInfo (not registered yet)"));
325
-
326
- FString LayerName;
327
- if (auto Err = RequireString(Params, TEXT("layerName"), LayerName)) return Err;
328
-
329
- // Resolve the layer against the landscape's registered target layers, so a
330
- // typo names the layers that DO exist instead of silently painting nothing.
331
- ULandscapeLayerInfoObject* LayerInfo = nullptr;
332
- TArray<FString> KnownLayers;
333
- for (const FLandscapeInfoLayerSettings& Layer : Info->Layers)
334
- {
335
- KnownLayers.Add(Layer.GetLayerName().ToString());
336
- if (Layer.GetLayerName().ToString().Equals(LayerName, ESearchCase::IgnoreCase))
337
- {
338
- LayerInfo = Layer.LayerInfoObj;
339
- }
340
- }
341
- if (!LayerInfo)
342
- {
343
- return MCPError(FString::Printf(
344
- TEXT("Paint layer '%s' has no LayerInfo on this landscape. Registered layers: [%s]. Use landscape(add_layer_info) first."),
345
- *LayerName, *FString::Join(KnownLayers, TEXT(", "))));
346
- }
347
-
348
- const TSharedPtr<FJsonObject>* CenterObj = nullptr;
349
- if (!Params->TryGetObjectField(TEXT("center"), CenterObj) || !CenterObj)
350
- {
351
- return MCPError(TEXT("Missing 'center' ({x, y} in world space)"));
352
- }
353
- FVector2D Center(0.0, 0.0);
354
- (*CenterObj)->TryGetNumberField(TEXT("x"), Center.X);
355
- (*CenterObj)->TryGetNumberField(TEXT("y"), Center.Y);
356
-
357
- const double Radius = OptionalNumber(Params, TEXT("radius"), 500.0);
358
- if (Radius <= 0.0) return MCPError(TEXT("'radius' must be positive"));
359
- const double Falloff = FMath::Clamp(OptionalNumber(Params, TEXT("falloff"), 0.5), 0.0, 1.0);
360
- // 0..1 target weight at full brush strength.
361
- const double Strength = FMath::Clamp(OptionalNumber(Params, TEXT("strength"), 1.0), 0.0, 1.0);
362
-
363
- int32 X1, Y1, X2, Y2;
364
- FString RectError;
365
- if (!WorldToLandscapeRect(Landscape, Info, Center, Radius, X1, Y1, X2, Y2, RectError))
366
- {
367
- return MCPError(RectError);
368
- }
369
-
370
- const int32 Width = X2 - X1 + 1;
371
- const int32 Height = Y2 - Y1 + 1;
372
- const int64 VertexCount = (int64)Width * (int64)Height;
373
- const int64 MaxVertices = (int64)FMath::Clamp(OptionalInt(Params, TEXT("maxVertices"), 4'000'000), 1024, 64'000'000);
374
- if (VertexCount > MaxVertices)
375
- {
376
- return MCPError(FString::Printf(
377
- TEXT("Brush covers %lld vertices, above the %lld limit. Reduce 'radius' or raise 'maxVertices' deliberately."),
378
- VertexCount, MaxVertices));
379
- }
380
- TArray<uint8> Weights;
381
- Weights.SetNumZeroed(Width * Height);
382
-
383
- FGuid EditLayerGuid;
384
- FString EditLayerName;
385
- FString LayerError;
386
- if (!ResolveEditLayerGuid(Landscape, Params, EditLayerGuid, EditLayerName, LayerError))
387
- {
388
- return MCPError(LayerError);
389
- }
390
-
391
- FLandscapeEditDataInterface EditData(Info);
392
- EditData.SetEditLayer(EditLayerGuid);
393
- {
394
- int32 GX1 = X1, GY1 = Y1, GX2 = X2, GY2 = Y2;
395
- EditData.GetWeightData(LayerInfo, GX1, GY1, GX2, GY2, Weights.GetData(), 0);
396
- if (GX2 < GX1 || GY2 < GY1)
397
- {
398
- return MCPError(TEXT("No landscape weight data in the requested area (unloaded or absent components). Move the brush or load the region first."));
399
- }
400
- }
401
-
402
- const FVector Scale = Landscape->ActorToWorld().GetScale3D();
403
- const FVector LocalCenter = Landscape->ActorToWorld().InverseTransformPosition(FVector(Center.X, Center.Y, 0.0));
404
- const double RadiusQuadsX = FMath::Abs(Radius / Scale.X);
405
- const double RadiusQuadsY = FMath::Abs(Radius / Scale.Y);
406
- const double TargetWeight = Strength * 255.0;
407
-
408
- int32 Touched = 0;
409
- for (int32 Y = Y1; Y <= Y2; ++Y)
410
- {
411
- for (int32 X = X1; X <= X2; ++X)
412
- {
413
- const double DX = (X - LocalCenter.X) / FMath::Max(RadiusQuadsX, KINDA_SMALL_NUMBER);
414
- const double DY = (Y - LocalCenter.Y) / FMath::Max(RadiusQuadsY, KINDA_SMALL_NUMBER);
415
- const double Weight = BrushWeight(FMath::Sqrt(DX * DX + DY * DY), Falloff);
416
- if (Weight <= 0.0) continue;
417
-
418
- const int32 Index = (Y - Y1) * Width + (X - X1);
419
- const double Blended = FMath::Lerp((double)Weights[Index], TargetWeight, Weight);
420
- Weights[Index] = (uint8)FMath::Clamp(FMath::RoundToInt(Blended), 0, 255);
421
- ++Touched;
422
- }
423
- }
424
-
425
- {
426
- const FScopedTransaction Transaction(FText::FromString(TEXT("MCP landscape paint")));
427
- Landscape->Modify();
428
- // The 9-arg overload taking bWeightAdjust/bTotalWeightAdjust is
429
- // deprecated in 5.7 and its body DISCARDS both flags, so calling it
430
- // would let us claim a renormalisation that never happened.
431
- FScopedSetLandscapeEditingLayer EditingLayer(Landscape, EditLayerGuid);
432
- int32 SX1 = X1, SY1 = Y1, SX2 = X2, SY2 = Y2;
433
- EditData.SetAlphaData(LayerInfo, SX1, SY1, SX2, SY2, Weights.GetData(), 0,
434
- ELandscapeLayerPaintingRestriction::None);
435
- EditData.Flush();
436
- }
437
- Landscape->PostEditChange();
438
-
439
- auto Result = MCPSuccess();
440
- MCPSetUpdated(Result);
441
- Result->SetStringField(TEXT("landscape"), Landscape->GetActorLabel());
442
- Result->SetStringField(TEXT("layerName"), LayerName);
443
- Result->SetNumberField(TEXT("strength"), Strength);
444
- Result->SetNumberField(TEXT("radius"), Radius);
445
- Result->SetNumberField(TEXT("verticesTouched"), Touched);
446
- Result->SetStringField(TEXT("editLayer"), EditLayerName);
447
- Result->SetStringField(TEXT("note"), TEXT("Weights are written as given; the engine no longer renormalises other layers for you, so set them explicitly if they must sum to 1. The level is left dirty and unsaved."));
448
- return MCPResult(Result);
449
- }
1
+ // Landscape sculpting and weight painting (#742).
2
+ //
3
+ // The landscape category has always advertised "sculpting, painting, heightmap
4
+ // import", but no such action existed - the description promised capability the
5
+ // surface did not have, which is worse than a gap because a caller plans around
6
+ // it. These are the missing writes, done through FLandscapeEditDataInterface,
7
+ // the same path the landscape editor tools use.
8
+ //
9
+ // Translation-unit partition of FLandscapeHandlers; registrations live in
10
+ // LandscapeHandlers.cpp.
11
+
12
+ #include "LandscapeHandlers.h"
13
+
14
+ #include "HandlerUtils.h"
15
+
16
+ #include "Editor.h"
17
+ #include "Engine/World.h"
18
+ #include "EngineUtils.h"
19
+ #include "Landscape.h"
20
+ #include "LandscapeComponent.h"
21
+ #include "LandscapeEdit.h"
22
+ #include "LandscapeInfo.h"
23
+ #include "LandscapeLayerInfoObject.h"
24
+ #include "LandscapeProxy.h"
25
+ #include "LandscapeEditLayer.h"
26
+ #include "ScopedTransaction.h"
27
+
28
+ namespace
29
+ {
30
+ // Landscape heights are uint16 with 32768 as "zero". One unit of height is
31
+ // LANDSCAPE_ZSCALE (1/128) cm before the actor's own Z scale.
32
+
33
+ /** The landscape to edit: by actor label, else the only one in the world. */
34
+ ALandscape* ResolveLandscape(UWorld* World, const FString& ActorLabel, FString& OutError)
35
+ {
36
+ TArray<ALandscape*> Found;
37
+ for (TActorIterator<ALandscape> It(World); It; ++It)
38
+ {
39
+ ALandscape* Candidate = *It;
40
+ if (!Candidate) continue;
41
+ if (!ActorLabel.IsEmpty() && !Candidate->GetActorLabel().Equals(ActorLabel, ESearchCase::IgnoreCase)) continue;
42
+ Found.Add(Candidate);
43
+ }
44
+ if (Found.Num() == 0)
45
+ {
46
+ OutError = ActorLabel.IsEmpty()
47
+ ? TEXT("No Landscape actor in the current level. Create one with landscape(create).")
48
+ : FString::Printf(TEXT("No Landscape actor labelled '%s'"), *ActorLabel);
49
+ return nullptr;
50
+ }
51
+ if (Found.Num() > 1 && ActorLabel.IsEmpty())
52
+ {
53
+ TArray<FString> Labels;
54
+ for (ALandscape* L : Found) Labels.Add(L->GetActorLabel());
55
+ OutError = FString::Printf(
56
+ TEXT("%d Landscape actors in the level; pass actorLabel to choose. Available: [%s]"),
57
+ Found.Num(), *FString::Join(Labels, TEXT(", ")));
58
+ return nullptr;
59
+ }
60
+ return Found[0];
61
+ }
62
+
63
+ /**
64
+ * Convert a world-space XY position and radius into the landscape's own
65
+ * quad grid, clamped to the loaded extent. Working in world units is what a
66
+ * caller has; working in quads is what the edit interface needs.
67
+ */
68
+ bool WorldToLandscapeRect(
69
+ ALandscape* Landscape,
70
+ ULandscapeInfo* Info,
71
+ const FVector2D& Center,
72
+ double Radius,
73
+ int32& OutX1, int32& OutY1, int32& OutX2, int32& OutY2,
74
+ FString& OutError)
75
+ {
76
+ const FTransform ActorToWorld = Landscape->ActorToWorld();
77
+ const FVector LocalCenter = ActorToWorld.InverseTransformPosition(FVector(Center.X, Center.Y, 0.0));
78
+ const FVector Scale = ActorToWorld.GetScale3D();
79
+ if (FMath::IsNearlyZero(Scale.X) || FMath::IsNearlyZero(Scale.Y))
80
+ {
81
+ OutError = TEXT("Landscape has a zero XY scale");
82
+ return false;
83
+ }
84
+ const double RadiusQuadsX = FMath::Abs(Radius / Scale.X);
85
+ const double RadiusQuadsY = FMath::Abs(Radius / Scale.Y);
86
+
87
+ int32 MinX, MinY, MaxX, MaxY;
88
+ if (!Info->GetLandscapeExtent(MinX, MinY, MaxX, MaxY))
89
+ {
90
+ OutError = TEXT("Could not read the landscape extent (are its components loaded?)");
91
+ return false;
92
+ }
93
+
94
+ OutX1 = FMath::Clamp(FMath::FloorToInt(LocalCenter.X - RadiusQuadsX), MinX, MaxX);
95
+ OutY1 = FMath::Clamp(FMath::FloorToInt(LocalCenter.Y - RadiusQuadsY), MinY, MaxY);
96
+ OutX2 = FMath::Clamp(FMath::CeilToInt(LocalCenter.X + RadiusQuadsX), MinX, MaxX);
97
+ OutY2 = FMath::Clamp(FMath::CeilToInt(LocalCenter.Y + RadiusQuadsY), MinY, MaxY);
98
+
99
+ if (OutX2 < OutX1 || OutY2 < OutY1)
100
+ {
101
+ OutError = TEXT("The requested area does not overlap the landscape");
102
+ return false;
103
+ }
104
+ return true;
105
+ }
106
+
107
+ /**
108
+ * Resolve which edit layer to write into. UE 5.8 landscapes ALWAYS have
109
+ * layer content (HasLayersContent/CanHaveLayersContent return true
110
+ * unconditionally), so writing without a layer GUID targets the merged
111
+ * heightmap - which the layer system then regenerates from the untouched
112
+ * layer stack on the next tick, silently erasing the edit. Outside
113
+ * Landscape Mode GetEditingLayer() is an invalid GUID, which is exactly
114
+ * the case a headless bridge is always in.
115
+ */
116
+ bool ResolveEditLayerGuid(ALandscape* Landscape, const TSharedPtr<FJsonObject>& Params, FGuid& OutGuid, FString& OutName, FString& OutError)
117
+ {
118
+ const FString WantedName = OptionalString(Params, TEXT("editLayer"));
119
+ const int32 WantedIndex = OptionalInt(Params, TEXT("editLayerIndex"), 0);
120
+
121
+ const TArray<const ULandscapeEditLayerBase*> Layers = Landscape->GetEditLayersConst();
122
+ if (Layers.Num() == 0)
123
+ {
124
+ OutError = TEXT("Landscape has no edit layers; cannot write a sculpt/paint edit that would survive the next layer update");
125
+ return false;
126
+ }
127
+
128
+ if (!WantedName.IsEmpty())
129
+ {
130
+ for (const ULandscapeEditLayerBase* Layer : Layers)
131
+ {
132
+ if (Layer && Layer->GetName().ToString().Equals(WantedName, ESearchCase::IgnoreCase))
133
+ {
134
+ OutGuid = Layer->GetGuid();
135
+ OutName = Layer->GetName().ToString();
136
+ return true;
137
+ }
138
+ }
139
+ TArray<FString> Names;
140
+ for (const ULandscapeEditLayerBase* Layer : Layers) { if (Layer) Names.Add(Layer->GetName().ToString()); }
141
+ OutError = FString::Printf(TEXT("Edit layer '%s' not found. Available: [%s]"), *WantedName, *FString::Join(Names, TEXT(", ")));
142
+ return false;
143
+ }
144
+
145
+ if (!Layers.IsValidIndex(WantedIndex) || !Layers[WantedIndex])
146
+ {
147
+ OutError = FString::Printf(TEXT("editLayerIndex %d is out of range (%d edit layers)"), WantedIndex, Layers.Num());
148
+ return false;
149
+ }
150
+ OutGuid = Layers[WantedIndex]->GetGuid();
151
+ OutName = Layers[WantedIndex]->GetName().ToString();
152
+ return true;
153
+ }
154
+
155
+ /** Smooth 0..1 brush weight for a point, given a falloff fraction. */
156
+ double BrushWeight(double DistanceFraction, double Falloff)
157
+ {
158
+ if (DistanceFraction >= 1.0) return 0.0;
159
+ const double Inner = FMath::Clamp(1.0 - Falloff, 0.0, 1.0);
160
+ if (DistanceFraction <= Inner) return 1.0;
161
+ const double T = (DistanceFraction - Inner) / FMath::Max(1.0 - Inner, KINDA_SMALL_NUMBER);
162
+ // Smoothstep so a brush edge does not leave a visible ring.
163
+ return 1.0 - (T * T * (3.0 - 2.0 * T));
164
+ }
165
+ }
166
+
167
+ // landscape(sculpt): raise, lower or flatten a circular brush footprint.
168
+ TSharedPtr<FJsonValue> FLandscapeHandlers::Sculpt(const TSharedPtr<FJsonObject>& Params)
169
+ {
170
+ REQUIRE_EDITOR_WORLD(World);
171
+
172
+ FString ResolveError;
173
+ ALandscape* Landscape = ResolveLandscape(World, OptionalString(Params, TEXT("actorLabel")), ResolveError);
174
+ if (!Landscape) return MCPError(ResolveError);
175
+
176
+ ULandscapeInfo* Info = Landscape->GetLandscapeInfo();
177
+ if (!Info) return MCPError(TEXT("Landscape has no LandscapeInfo (not registered yet)"));
178
+
179
+ const TSharedPtr<FJsonObject>* CenterObj = nullptr;
180
+ if (!Params->TryGetObjectField(TEXT("center"), CenterObj) || !CenterObj)
181
+ {
182
+ return MCPError(TEXT("Missing 'center' ({x, y} in world space)"));
183
+ }
184
+ FVector2D Center(0.0, 0.0);
185
+ (*CenterObj)->TryGetNumberField(TEXT("x"), Center.X);
186
+ (*CenterObj)->TryGetNumberField(TEXT("y"), Center.Y);
187
+
188
+ const double Radius = OptionalNumber(Params, TEXT("radius"), 500.0);
189
+ if (Radius <= 0.0) return MCPError(TEXT("'radius' must be positive"));
190
+ const double Falloff = FMath::Clamp(OptionalNumber(Params, TEXT("falloff"), 0.5), 0.0, 1.0);
191
+ const FString Mode = OptionalString(Params, TEXT("mode"), TEXT("raise")).ToLower();
192
+ if (Mode != TEXT("raise") && Mode != TEXT("lower") && Mode != TEXT("flatten"))
193
+ {
194
+ return MCPError(TEXT("'mode' must be raise, lower or flatten"));
195
+ }
196
+ // World-space centimetres to move the surface at full brush strength.
197
+ const double Amount = OptionalNumber(Params, TEXT("amount"), 100.0);
198
+
199
+ int32 X1, Y1, X2, Y2;
200
+ FString RectError;
201
+ if (!WorldToLandscapeRect(Landscape, Info, Center, Radius, X1, Y1, X2, Y2, RectError))
202
+ {
203
+ return MCPError(RectError);
204
+ }
205
+
206
+ const int32 Width = X2 - X1 + 1;
207
+ const int32 Height = Y2 - Y1 + 1;
208
+ // Cap the working rect: the extent clamp alone lets an 8k landscape with a
209
+ // huge radius allocate hundreds of MB and iterate tens of millions of verts.
210
+ const int64 VertexCount = (int64)Width * (int64)Height;
211
+ const int64 MaxVertices = (int64)FMath::Clamp(OptionalInt(Params, TEXT("maxVertices"), 4'000'000), 1024, 64'000'000);
212
+ if (VertexCount > MaxVertices)
213
+ {
214
+ return MCPError(FString::Printf(
215
+ TEXT("Brush covers %lld vertices, above the %lld limit. Reduce 'radius' or raise 'maxVertices' deliberately."),
216
+ VertexCount, MaxVertices));
217
+ }
218
+ TArray<uint16> Heights;
219
+ Heights.SetNumZeroed(Width * Height);
220
+
221
+ FGuid EditLayerGuid;
222
+ FString EditLayerName;
223
+ FString LayerError;
224
+ if (!ResolveEditLayerGuid(Landscape, Params, EditLayerGuid, EditLayerName, LayerError))
225
+ {
226
+ return MCPError(LayerError);
227
+ }
228
+
229
+ FLandscapeEditDataInterface EditData(Info);
230
+ EditData.SetEditLayer(EditLayerGuid);
231
+ // GetHeightData takes the rect by non-const reference and REWRITES it with
232
+ // the valid sub-range, so pass copies: the buffer is sized and indexed
233
+ // against the original rect, and an empty region returns an inverted
234
+ // INT_MAX/INT_MIN rect that would overflow the vertex count downstream.
235
+ {
236
+ int32 GX1 = X1, GY1 = Y1, GX2 = X2, GY2 = Y2;
237
+ EditData.GetHeightData(GX1, GY1, GX2, GY2, Heights.GetData(), 0);
238
+ if (GX2 < GX1 || GY2 < GY1)
239
+ {
240
+ return MCPError(TEXT("No landscape height data in the requested area (unloaded or absent components). Move the brush or load the region first."));
241
+ }
242
+ }
243
+
244
+ const FVector Scale = Landscape->ActorToWorld().GetScale3D();
245
+ // uint16 height units per world centimetre.
246
+ const double ZScale = FMath::IsNearlyZero(Scale.Z) ? 0.0 : (1.0 / (LANDSCAPE_ZSCALE * Scale.Z));
247
+ if (ZScale == 0.0) return MCPError(TEXT("Landscape has a zero Z scale; heights cannot be written"));
248
+
249
+ const FVector LocalCenter = Landscape->ActorToWorld().InverseTransformPosition(FVector(Center.X, Center.Y, 0.0));
250
+ const double RadiusQuadsX = FMath::Abs(Radius / Scale.X);
251
+ const double RadiusQuadsY = FMath::Abs(Radius / Scale.Y);
252
+
253
+ // Flatten needs a target: the height under the brush centre before editing.
254
+ double FlattenTarget = 0.0;
255
+ if (Mode == TEXT("flatten"))
256
+ {
257
+ const int32 CX = FMath::Clamp(FMath::RoundToInt(LocalCenter.X), X1, X2);
258
+ const int32 CY = FMath::Clamp(FMath::RoundToInt(LocalCenter.Y), Y1, Y2);
259
+ FlattenTarget = (double)Heights[(CY - Y1) * Width + (CX - X1)];
260
+ }
261
+
262
+ // Resolve the mode once instead of comparing strings per vertex.
263
+ const int32 SculptMode = Mode == TEXT("raise") ? 0 : (Mode == TEXT("lower") ? 1 : 2);
264
+
265
+ int32 Touched = 0;
266
+ for (int32 Y = Y1; Y <= Y2; ++Y)
267
+ {
268
+ for (int32 X = X1; X <= X2; ++X)
269
+ {
270
+ const double DX = (X - LocalCenter.X) / FMath::Max(RadiusQuadsX, KINDA_SMALL_NUMBER);
271
+ const double DY = (Y - LocalCenter.Y) / FMath::Max(RadiusQuadsY, KINDA_SMALL_NUMBER);
272
+ const double Fraction = FMath::Sqrt(DX * DX + DY * DY);
273
+ const double Weight = BrushWeight(Fraction, Falloff);
274
+ if (Weight <= 0.0) continue;
275
+
276
+ const int32 Index = (Y - Y1) * Width + (X - X1);
277
+ const double Current = (double)Heights[Index];
278
+ double Next = Current;
279
+ if (SculptMode == 0) Next = Current + Amount * ZScale * Weight;
280
+ else if (SculptMode == 1) Next = Current - Amount * ZScale * Weight;
281
+ else Next = FMath::Lerp(Current, FlattenTarget, Weight);
282
+
283
+ Heights[Index] = (uint16)FMath::Clamp(FMath::RoundToInt(Next), 0, 65535);
284
+ ++Touched;
285
+ }
286
+ }
287
+
288
+ {
289
+ const FScopedTransaction Transaction(FText::FromString(TEXT("MCP landscape sculpt")));
290
+ Landscape->Modify();
291
+ FScopedSetLandscapeEditingLayer EditingLayer(Landscape, EditLayerGuid);
292
+ int32 SX1 = X1, SY1 = Y1, SX2 = X2, SY2 = Y2;
293
+ EditData.SetHeightData(SX1, SY1, SX2, SY2, Heights.GetData(), 0, /*InCalcNormals=*/true);
294
+ EditData.Flush();
295
+ }
296
+ Landscape->PostEditChange();
297
+
298
+ auto Result = MCPSuccess();
299
+ MCPSetUpdated(Result);
300
+ Result->SetStringField(TEXT("landscape"), Landscape->GetActorLabel());
301
+ Result->SetStringField(TEXT("mode"), Mode);
302
+ Result->SetNumberField(TEXT("radius"), Radius);
303
+ Result->SetNumberField(TEXT("amount"), Amount);
304
+ Result->SetNumberField(TEXT("verticesTouched"), Touched);
305
+ Result->SetStringField(TEXT("editLayer"), EditLayerName);
306
+ Result->SetNumberField(TEXT("rectX1"), X1);
307
+ Result->SetNumberField(TEXT("rectY1"), Y1);
308
+ Result->SetNumberField(TEXT("rectX2"), X2);
309
+ Result->SetNumberField(TEXT("rectY2"), Y2);
310
+ Result->SetStringField(TEXT("note"), TEXT("The level is left dirty and unsaved; save it when ready."));
311
+ return MCPResult(Result);
312
+ }
313
+
314
+ // landscape(paint_layer): paint a weight layer over a circular footprint.
315
+ TSharedPtr<FJsonValue> FLandscapeHandlers::PaintLayer(const TSharedPtr<FJsonObject>& Params)
316
+ {
317
+ REQUIRE_EDITOR_WORLD(World);
318
+
319
+ FString ResolveError;
320
+ ALandscape* Landscape = ResolveLandscape(World, OptionalString(Params, TEXT("actorLabel")), ResolveError);
321
+ if (!Landscape) return MCPError(ResolveError);
322
+
323
+ ULandscapeInfo* Info = Landscape->GetLandscapeInfo();
324
+ if (!Info) return MCPError(TEXT("Landscape has no LandscapeInfo (not registered yet)"));
325
+
326
+ FString LayerName;
327
+ if (auto Err = RequireString(Params, TEXT("layerName"), LayerName)) return Err;
328
+
329
+ // Resolve the layer against the landscape's registered target layers, so a
330
+ // typo names the layers that DO exist instead of silently painting nothing.
331
+ ULandscapeLayerInfoObject* LayerInfo = nullptr;
332
+ TArray<FString> KnownLayers;
333
+ for (const FLandscapeInfoLayerSettings& Layer : Info->Layers)
334
+ {
335
+ KnownLayers.Add(Layer.GetLayerName().ToString());
336
+ if (Layer.GetLayerName().ToString().Equals(LayerName, ESearchCase::IgnoreCase))
337
+ {
338
+ LayerInfo = Layer.LayerInfoObj;
339
+ }
340
+ }
341
+ if (!LayerInfo)
342
+ {
343
+ return MCPError(FString::Printf(
344
+ TEXT("Paint layer '%s' has no LayerInfo on this landscape. Registered layers: [%s]. Use landscape(add_layer_info) first."),
345
+ *LayerName, *FString::Join(KnownLayers, TEXT(", "))));
346
+ }
347
+
348
+ const TSharedPtr<FJsonObject>* CenterObj = nullptr;
349
+ if (!Params->TryGetObjectField(TEXT("center"), CenterObj) || !CenterObj)
350
+ {
351
+ return MCPError(TEXT("Missing 'center' ({x, y} in world space)"));
352
+ }
353
+ FVector2D Center(0.0, 0.0);
354
+ (*CenterObj)->TryGetNumberField(TEXT("x"), Center.X);
355
+ (*CenterObj)->TryGetNumberField(TEXT("y"), Center.Y);
356
+
357
+ const double Radius = OptionalNumber(Params, TEXT("radius"), 500.0);
358
+ if (Radius <= 0.0) return MCPError(TEXT("'radius' must be positive"));
359
+ const double Falloff = FMath::Clamp(OptionalNumber(Params, TEXT("falloff"), 0.5), 0.0, 1.0);
360
+ // 0..1 target weight at full brush strength.
361
+ const double Strength = FMath::Clamp(OptionalNumber(Params, TEXT("strength"), 1.0), 0.0, 1.0);
362
+
363
+ int32 X1, Y1, X2, Y2;
364
+ FString RectError;
365
+ if (!WorldToLandscapeRect(Landscape, Info, Center, Radius, X1, Y1, X2, Y2, RectError))
366
+ {
367
+ return MCPError(RectError);
368
+ }
369
+
370
+ const int32 Width = X2 - X1 + 1;
371
+ const int32 Height = Y2 - Y1 + 1;
372
+ const int64 VertexCount = (int64)Width * (int64)Height;
373
+ const int64 MaxVertices = (int64)FMath::Clamp(OptionalInt(Params, TEXT("maxVertices"), 4'000'000), 1024, 64'000'000);
374
+ if (VertexCount > MaxVertices)
375
+ {
376
+ return MCPError(FString::Printf(
377
+ TEXT("Brush covers %lld vertices, above the %lld limit. Reduce 'radius' or raise 'maxVertices' deliberately."),
378
+ VertexCount, MaxVertices));
379
+ }
380
+ TArray<uint8> Weights;
381
+ Weights.SetNumZeroed(Width * Height);
382
+
383
+ FGuid EditLayerGuid;
384
+ FString EditLayerName;
385
+ FString LayerError;
386
+ if (!ResolveEditLayerGuid(Landscape, Params, EditLayerGuid, EditLayerName, LayerError))
387
+ {
388
+ return MCPError(LayerError);
389
+ }
390
+
391
+ FLandscapeEditDataInterface EditData(Info);
392
+ EditData.SetEditLayer(EditLayerGuid);
393
+ {
394
+ int32 GX1 = X1, GY1 = Y1, GX2 = X2, GY2 = Y2;
395
+ EditData.GetWeightData(LayerInfo, GX1, GY1, GX2, GY2, Weights.GetData(), 0);
396
+ if (GX2 < GX1 || GY2 < GY1)
397
+ {
398
+ return MCPError(TEXT("No landscape weight data in the requested area (unloaded or absent components). Move the brush or load the region first."));
399
+ }
400
+ }
401
+
402
+ const FVector Scale = Landscape->ActorToWorld().GetScale3D();
403
+ const FVector LocalCenter = Landscape->ActorToWorld().InverseTransformPosition(FVector(Center.X, Center.Y, 0.0));
404
+ const double RadiusQuadsX = FMath::Abs(Radius / Scale.X);
405
+ const double RadiusQuadsY = FMath::Abs(Radius / Scale.Y);
406
+ const double TargetWeight = Strength * 255.0;
407
+
408
+ int32 Touched = 0;
409
+ for (int32 Y = Y1; Y <= Y2; ++Y)
410
+ {
411
+ for (int32 X = X1; X <= X2; ++X)
412
+ {
413
+ const double DX = (X - LocalCenter.X) / FMath::Max(RadiusQuadsX, KINDA_SMALL_NUMBER);
414
+ const double DY = (Y - LocalCenter.Y) / FMath::Max(RadiusQuadsY, KINDA_SMALL_NUMBER);
415
+ const double Weight = BrushWeight(FMath::Sqrt(DX * DX + DY * DY), Falloff);
416
+ if (Weight <= 0.0) continue;
417
+
418
+ const int32 Index = (Y - Y1) * Width + (X - X1);
419
+ const double Blended = FMath::Lerp((double)Weights[Index], TargetWeight, Weight);
420
+ Weights[Index] = (uint8)FMath::Clamp(FMath::RoundToInt(Blended), 0, 255);
421
+ ++Touched;
422
+ }
423
+ }
424
+
425
+ {
426
+ const FScopedTransaction Transaction(FText::FromString(TEXT("MCP landscape paint")));
427
+ Landscape->Modify();
428
+ // The 9-arg overload taking bWeightAdjust/bTotalWeightAdjust is
429
+ // deprecated in 5.7 and its body DISCARDS both flags, so calling it
430
+ // would let us claim a renormalisation that never happened.
431
+ FScopedSetLandscapeEditingLayer EditingLayer(Landscape, EditLayerGuid);
432
+ int32 SX1 = X1, SY1 = Y1, SX2 = X2, SY2 = Y2;
433
+ EditData.SetAlphaData(LayerInfo, SX1, SY1, SX2, SY2, Weights.GetData(), 0,
434
+ ELandscapeLayerPaintingRestriction::None);
435
+ EditData.Flush();
436
+ }
437
+ Landscape->PostEditChange();
438
+
439
+ auto Result = MCPSuccess();
440
+ MCPSetUpdated(Result);
441
+ Result->SetStringField(TEXT("landscape"), Landscape->GetActorLabel());
442
+ Result->SetStringField(TEXT("layerName"), LayerName);
443
+ Result->SetNumberField(TEXT("strength"), Strength);
444
+ Result->SetNumberField(TEXT("radius"), Radius);
445
+ Result->SetNumberField(TEXT("verticesTouched"), Touched);
446
+ Result->SetStringField(TEXT("editLayer"), EditLayerName);
447
+ Result->SetStringField(TEXT("note"), TEXT("Weights are written as given; the engine no longer renormalises other layers for you, so set them explicitly if they must sum to 1. The level is left dirty and unsaved."));
448
+ return MCPResult(Result);
449
+ }