ultimate-unreal-engine-mcp 0.1.0

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 (124) hide show
  1. package/README.md +729 -0
  2. package/dist/build/error-parser.js +51 -0
  3. package/dist/build/fix-suggester.js +84 -0
  4. package/dist/build/ubt-runner.js +146 -0
  5. package/dist/cli.js +13 -0
  6. package/dist/config.js +8 -0
  7. package/dist/docs/data/ue57-api.js +228 -0
  8. package/dist/docs/doc-index.js +110 -0
  9. package/dist/docs/types.js +4 -0
  10. package/dist/generators/class-generator.js +363 -0
  11. package/dist/generators/file-modifier.js +276 -0
  12. package/dist/generators/uht-validator.js +177 -0
  13. package/dist/index.js +89 -0
  14. package/dist/parsers/cpp-class-index.js +230 -0
  15. package/dist/parsers/cpp-parser.js +369 -0
  16. package/dist/parsers/ini-parser.js +216 -0
  17. package/dist/parsers/uproject-parser.js +130 -0
  18. package/dist/plugin-bridge/client.js +217 -0
  19. package/dist/plugin-bridge/protocol.js +6 -0
  20. package/dist/plugin-bridge/retry.js +23 -0
  21. package/dist/setup.js +209 -0
  22. package/dist/tools/ai-systems/index.js +247 -0
  23. package/dist/tools/ai-systems/types.js +4 -0
  24. package/dist/tools/animation/index.js +241 -0
  25. package/dist/tools/animation/types.js +4 -0
  26. package/dist/tools/audio/index.js +204 -0
  27. package/dist/tools/audio/types.js +4 -0
  28. package/dist/tools/blueprint/index.js +495 -0
  29. package/dist/tools/blueprint/types.js +4 -0
  30. package/dist/tools/build/index.js +163 -0
  31. package/dist/tools/chaos/index.js +230 -0
  32. package/dist/tools/chaos/types.js +4 -0
  33. package/dist/tools/collision-physics/index.js +211 -0
  34. package/dist/tools/config/index.js +288 -0
  35. package/dist/tools/cpp/index.js +305 -0
  36. package/dist/tools/docs/index.js +251 -0
  37. package/dist/tools/editor/index.js +242 -0
  38. package/dist/tools/gas/index.js +222 -0
  39. package/dist/tools/gas/types.js +5 -0
  40. package/dist/tools/import-export/index.js +218 -0
  41. package/dist/tools/input/index.js +146 -0
  42. package/dist/tools/known-issues/index.js +88 -0
  43. package/dist/tools/known-issues/middleware.js +55 -0
  44. package/dist/tools/known-issues/store.js +125 -0
  45. package/dist/tools/livelink/index.js +203 -0
  46. package/dist/tools/livelink/types.js +4 -0
  47. package/dist/tools/material/index.js +190 -0
  48. package/dist/tools/motion-design/index.js +251 -0
  49. package/dist/tools/motion-design/types.js +6 -0
  50. package/dist/tools/movie-render/index.js +220 -0
  51. package/dist/tools/networking/index.js +149 -0
  52. package/dist/tools/pcg/index.js +164 -0
  53. package/dist/tools/selection/index.js +180 -0
  54. package/dist/tools/sequencer/index.js +218 -0
  55. package/dist/tools/validation/index.js +183 -0
  56. package/dist/tools/validation/types.js +4 -0
  57. package/dist/tools/viewport/index.js +310 -0
  58. package/dist/tools/worldpartition/index.js +226 -0
  59. package/dist/tools/worldpartition/types.js +4 -0
  60. package/dist/utils/execFileNoThrow.js +40 -0
  61. package/dist/utils/logger.js +27 -0
  62. package/dist/utils/path-guard.js +26 -0
  63. package/package.json +40 -0
  64. package/unreal-plugin/MCPBridge/MCPBridge.uplugin +29 -0
  65. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/MCPBridgeEditor.Build.cs +68 -0
  66. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.cpp +919 -0
  67. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.h +23 -0
  68. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.cpp +415 -0
  69. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.h +16 -0
  70. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.cpp +653 -0
  71. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.h +24 -0
  72. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.cpp +290 -0
  73. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.h +17 -0
  74. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.cpp +624 -0
  75. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.h +22 -0
  76. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.cpp +616 -0
  77. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.h +25 -0
  78. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.cpp +744 -0
  79. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.h +24 -0
  80. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeEditor.cpp +23 -0
  81. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.cpp +149 -0
  82. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.h +38 -0
  83. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.cpp +771 -0
  84. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.h +22 -0
  85. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.cpp +749 -0
  86. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.h +22 -0
  87. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.cpp +172 -0
  88. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.h +16 -0
  89. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.cpp +715 -0
  90. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.h +22 -0
  91. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.cpp +679 -0
  92. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.h +22 -0
  93. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.cpp +381 -0
  94. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.h +24 -0
  95. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.cpp +504 -0
  96. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.h +22 -0
  97. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.cpp +511 -0
  98. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.h +22 -0
  99. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.cpp +1110 -0
  100. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.h +28 -0
  101. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.cpp +590 -0
  102. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.h +16 -0
  103. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.cpp +482 -0
  104. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.h +16 -0
  105. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.cpp +338 -0
  106. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.h +16 -0
  107. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.cpp +677 -0
  108. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.h +22 -0
  109. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.cpp +721 -0
  110. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.h +16 -0
  111. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.cpp +368 -0
  112. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.h +22 -0
  113. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.cpp +1208 -0
  114. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.h +29 -0
  115. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.cpp +822 -0
  116. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.h +23 -0
  117. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Public/MCPBridgeEditor.h +14 -0
  118. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/MCPBridgeRuntime.Build.cs +28 -0
  119. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPBridgeRuntime.cpp +22 -0
  120. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPCommandRouter.cpp +118 -0
  121. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPTcpServer.cpp +196 -0
  122. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPBridgeRuntime.h +15 -0
  123. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPCommandRouter.h +55 -0
  124. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPTcpServer.h +59 -0
@@ -0,0 +1,822 @@
1
+ // MCPWorldPartitionCommands.cpp (Plan 18-01)
2
+ // Implements four World Partition command handlers for the MCP bridge:
3
+ // worldpartition.settings -- read WP grid size, loading range, streaming config (WP-01)
4
+ // worldpartition.dataLayers -- list/create/toggle data layers, assign actors (WP-02)
5
+ // worldpartition.streamingSources -- inspect streaming sources with shape, priority, target state (WP-03)
6
+ // worldpartition.hlod -- inspect HLOD layer config and trigger HLOD generation (WP-04)
7
+ //
8
+ // All handlers run on the game thread via AsyncTask(ENamedThreads::GameThread).
9
+ // asset_path is validated to start with "/Game/" or "/Engine/" before any
10
+ // StaticLoadObject call to prevent path traversal (T-18-01).
11
+ // Modify() is called before all data layer write operations (T-18-02, T-18-03).
12
+
13
+ #include "MCPWorldPartitionCommands.h"
14
+
15
+ // World Partition headers
16
+ #include "WorldPartition/WorldPartition.h"
17
+ #include "WorldPartition/WorldPartitionSubsystem.h"
18
+ #include "WorldPartition/WorldPartitionStreamingSource.h"
19
+
20
+ // Data Layer headers
21
+ #include "WorldPartition/DataLayer/DataLayerInstance.h"
22
+ #include "WorldPartition/DataLayer/DataLayerSubsystem.h"
23
+ #include "WorldPartition/DataLayer/WorldDataLayers.h"
24
+
25
+ // HLOD headers
26
+ #include "WorldPartition/HLOD/HLODLayer.h"
27
+
28
+ // Asset Registry
29
+ #include "AssetRegistry/AssetRegistryModule.h"
30
+ #include "AssetRegistry/IAssetRegistry.h"
31
+
32
+ // JSON
33
+ #include "Serialization/JsonSerializer.h"
34
+ #include "Serialization/JsonWriter.h"
35
+ #include "Dom/JsonObject.h"
36
+ #include "Dom/JsonValue.h"
37
+
38
+ // Editor
39
+ #include "Editor.h"
40
+ #include "Engine/World.h"
41
+ #include "EngineUtils.h"
42
+ #include "GameFramework/Actor.h"
43
+ #include "Components/ActorComponent.h"
44
+ #include "Async/Async.h"
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Internal helpers
48
+ // ---------------------------------------------------------------------------
49
+
50
+ /** Returns a JSON success response string (without trailing newline). */
51
+ static FString BuildWPSuccessResponse(const FString& CorrId, TSharedPtr<FJsonObject> Data)
52
+ {
53
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
54
+ Obj->SetBoolField(TEXT("success"), true);
55
+ Obj->SetStringField(TEXT("correlationId"), CorrId);
56
+ if (Data.IsValid())
57
+ {
58
+ Obj->SetObjectField(TEXT("data"), Data);
59
+ }
60
+
61
+ FString Output;
62
+ TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
63
+ FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
64
+ return Output;
65
+ }
66
+
67
+ /** Returns a JSON error response string (without trailing newline). */
68
+ static FString BuildWPErrorResponse(const FString& CorrId, const FString& Error)
69
+ {
70
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
71
+ Obj->SetBoolField(TEXT("success"), false);
72
+ Obj->SetStringField(TEXT("correlationId"), CorrId);
73
+ Obj->SetStringField(TEXT("error"), Error);
74
+
75
+ FString Output;
76
+ TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
77
+ FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
78
+ return Output;
79
+ }
80
+
81
+ /**
82
+ * Validate that asset_path starts with "/Game/" or "/Engine/" to prevent
83
+ * path traversal attacks (T-18-01).
84
+ */
85
+ static bool IsValidAssetPath(const FString& AssetPath)
86
+ {
87
+ return AssetPath.StartsWith(TEXT("/Game/")) || AssetPath.StartsWith(TEXT("/Engine/"));
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // RegisterWorldPartitionCommands
92
+ // ---------------------------------------------------------------------------
93
+
94
+ void RegisterWorldPartitionCommands(FMCPCommandRouter& Router)
95
+ {
96
+ // -----------------------------------------------------------------------
97
+ // worldpartition.settings (WP-01)
98
+ // Reads World Partition settings from the currently open world.
99
+ // No required payload -- reads from current editor world context.
100
+ // Returns: grid_size, loading_range, enable_streaming, runtime_hash_name.
101
+ // -----------------------------------------------------------------------
102
+ Router.RegisterHandler(TEXT("worldpartition.settings"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
103
+ {
104
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
105
+
106
+ AsyncTask(ENamedThreads::GameThread, [CorrId, SendResponse]()
107
+ {
108
+ if (!GEditor)
109
+ {
110
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("editor_not_available")) + TEXT("\n"));
111
+ return;
112
+ }
113
+
114
+ UWorld* World = GEditor->GetEditorWorldContext().World();
115
+ if (!World)
116
+ {
117
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
118
+ return;
119
+ }
120
+
121
+ UWorldPartition* WorldPartition = World->GetWorldPartition();
122
+ if (!WorldPartition)
123
+ {
124
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("world_does_not_use_world_partition")) + TEXT("\n"));
125
+ return;
126
+ }
127
+
128
+ // Read World Partition settings.
129
+ // Default grid size is 12800 (12800 UU = 128m, the standard UE WP cell size).
130
+ double GridSize = 12800.0;
131
+ double LoadingRange = 25600.0;
132
+ bool bStreamingEnabled = WorldPartition->IsStreamingEnabled();
133
+
134
+ // Get the runtime hash class name for diagnostics.
135
+ FString RuntimeHashName = TEXT("none");
136
+ if (WorldPartition->RuntimeHash)
137
+ {
138
+ RuntimeHashName = WorldPartition->RuntimeHash->GetClass()->GetName();
139
+
140
+ // Try to read grid cell size from the runtime hash via reflection.
141
+ // UWorldPartitionRuntimeSpatialHash exposes a property "CellSize" or similar.
142
+ for (TFieldIterator<FDoubleProperty> PropIt(WorldPartition->RuntimeHash->GetClass()); PropIt; ++PropIt)
143
+ {
144
+ const FString PropName = PropIt->GetName();
145
+ if (PropName.Contains(TEXT("CellSize")) || PropName.Contains(TEXT("GridSize")))
146
+ {
147
+ GridSize = PropIt->GetPropertyValue_InContainer(WorldPartition->RuntimeHash);
148
+ break;
149
+ }
150
+ }
151
+ for (TFieldIterator<FFloatProperty> PropIt(WorldPartition->RuntimeHash->GetClass()); PropIt; ++PropIt)
152
+ {
153
+ const FString PropName = PropIt->GetName();
154
+ if (PropName.Contains(TEXT("LoadingRange")) || PropName.Contains(TEXT("StreamingRange")))
155
+ {
156
+ LoadingRange = static_cast<double>(PropIt->GetPropertyValue_InContainer(WorldPartition->RuntimeHash));
157
+ break;
158
+ }
159
+ if (PropName.Contains(TEXT("CellSize")) || PropName.Contains(TEXT("GridSize")))
160
+ {
161
+ GridSize = static_cast<double>(PropIt->GetPropertyValue_InContainer(WorldPartition->RuntimeHash));
162
+ break;
163
+ }
164
+ }
165
+ }
166
+
167
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
168
+ Data->SetNumberField(TEXT("grid_size"), GridSize);
169
+ Data->SetNumberField(TEXT("loading_range"), LoadingRange);
170
+ Data->SetBoolField(TEXT("enable_streaming"), bStreamingEnabled);
171
+ Data->SetStringField(TEXT("runtime_hash_name"), RuntimeHashName);
172
+
173
+ SendResponse(BuildWPSuccessResponse(CorrId, Data) + TEXT("\n"));
174
+ });
175
+ });
176
+
177
+ // -----------------------------------------------------------------------
178
+ // worldpartition.dataLayers (WP-02)
179
+ // Manages data layers: list, create, toggle state, and assign actors.
180
+ // Required payload field: action ("list" | "create" | "toggle" | "assign_actor")
181
+ //
182
+ // list: Returns array of data layer objects with name, type, state.
183
+ // create: Requires layer_name (string), layer_type ("Runtime"/"Editor").
184
+ // toggle: Requires layer_name (string), initial_state ("Unloaded"/"Loaded"/"Activated").
185
+ // assign_actor: Requires layer_name (string), actor_label (string).
186
+ // -----------------------------------------------------------------------
187
+ Router.RegisterHandler(TEXT("worldpartition.dataLayers"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
188
+ {
189
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
190
+
191
+ // Extract payload.
192
+ TSharedPtr<FJsonObject> Payload;
193
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
194
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
195
+ {
196
+ Payload = (*PayloadVal)->AsObject();
197
+ }
198
+
199
+ FString Action;
200
+ if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("action"), Action) || Action.IsEmpty())
201
+ {
202
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("missing_action")) + TEXT("\n"));
203
+ return;
204
+ }
205
+
206
+ // Capture payload fields before async dispatch.
207
+ FString LayerName;
208
+ FString LayerType;
209
+ FString InitialState;
210
+ FString ActorLabel;
211
+ if (Payload.IsValid())
212
+ {
213
+ Payload->TryGetStringField(TEXT("layer_name"), LayerName);
214
+ Payload->TryGetStringField(TEXT("layer_type"), LayerType);
215
+ Payload->TryGetStringField(TEXT("initial_state"), InitialState);
216
+ Payload->TryGetStringField(TEXT("actor_label"), ActorLabel);
217
+ }
218
+
219
+ AsyncTask(ENamedThreads::GameThread, [CorrId, SendResponse, Action, LayerName, LayerType, InitialState, ActorLabel]()
220
+ {
221
+ if (!GEditor)
222
+ {
223
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("editor_not_available")) + TEXT("\n"));
224
+ return;
225
+ }
226
+
227
+ UWorld* World = GEditor->GetEditorWorldContext().World();
228
+ if (!World)
229
+ {
230
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
231
+ return;
232
+ }
233
+
234
+ UWorldPartition* WorldPartition = World->GetWorldPartition();
235
+ if (!WorldPartition)
236
+ {
237
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("world_does_not_use_world_partition")) + TEXT("\n"));
238
+ return;
239
+ }
240
+
241
+ // ------------------------------------------------------------------
242
+ // action: list
243
+ // ------------------------------------------------------------------
244
+ if (Action == TEXT("list"))
245
+ {
246
+ TArray<TSharedPtr<FJsonValue>> LayersArray;
247
+
248
+ // Find AWorldDataLayers actor to enumerate data layer instances.
249
+ AWorldDataLayers* WorldDataLayers = nullptr;
250
+ for (TActorIterator<AWorldDataLayers> It(World); It; ++It)
251
+ {
252
+ WorldDataLayers = *It;
253
+ break;
254
+ }
255
+
256
+ if (WorldDataLayers)
257
+ {
258
+ WorldDataLayers->ForEachDataLayer([&LayersArray](UDataLayerInstance* DataLayerInstance) -> bool
259
+ {
260
+ if (!DataLayerInstance)
261
+ {
262
+ return true; // continue
263
+ }
264
+
265
+ FString TypeStr = TEXT("Editor");
266
+ const UDataLayerInstanceWithAsset* DLWithAsset = Cast<UDataLayerInstanceWithAsset>(DataLayerInstance);
267
+ if (DLWithAsset && DLWithAsset->GetAsset())
268
+ {
269
+ TypeStr = (DLWithAsset->GetAsset()->IsRuntime()) ? TEXT("Runtime") : TEXT("Editor");
270
+ }
271
+
272
+ FString StateStr = TEXT("Unloaded");
273
+ EDataLayerRuntimeState RuntimeState = DataLayerInstance->GetInitialRuntimeState();
274
+ if (RuntimeState == EDataLayerRuntimeState::Loaded)
275
+ {
276
+ StateStr = TEXT("Loaded");
277
+ }
278
+ else if (RuntimeState == EDataLayerRuntimeState::Activated)
279
+ {
280
+ StateStr = TEXT("Activated");
281
+ }
282
+
283
+ TSharedPtr<FJsonObject> LayerObj = MakeShared<FJsonObject>();
284
+ LayerObj->SetStringField(TEXT("name"), DataLayerInstance->GetDataLayerFullName());
285
+ LayerObj->SetStringField(TEXT("type"), TypeStr);
286
+ LayerObj->SetStringField(TEXT("initial_runtime_state"), StateStr);
287
+ LayerObj->SetBoolField(TEXT("is_initially_visible"), DataLayerInstance->IsInitiallyVisible());
288
+ LayersArray.Add(MakeShared<FJsonValueObject>(LayerObj));
289
+
290
+ return true; // continue iteration
291
+ });
292
+ }
293
+
294
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
295
+ Data->SetArrayField(TEXT("layers"), LayersArray);
296
+ Data->SetNumberField(TEXT("count"), static_cast<double>(LayersArray.Num()));
297
+
298
+ SendResponse(BuildWPSuccessResponse(CorrId, Data) + TEXT("\n"));
299
+ return;
300
+ }
301
+
302
+ // ------------------------------------------------------------------
303
+ // action: create
304
+ // ------------------------------------------------------------------
305
+ if (Action == TEXT("create"))
306
+ {
307
+ // Validate inputs (T-18-02).
308
+ if (LayerName.IsEmpty())
309
+ {
310
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("missing_layer_name")) + TEXT("\n"));
311
+ return;
312
+ }
313
+
314
+ // Find AWorldDataLayers actor and call Modify() before mutation.
315
+ AWorldDataLayers* WorldDataLayers = nullptr;
316
+ for (TActorIterator<AWorldDataLayers> It(World); It; ++It)
317
+ {
318
+ WorldDataLayers = *It;
319
+ break;
320
+ }
321
+
322
+ if (!WorldDataLayers)
323
+ {
324
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("no_world_data_layers_actor")) + TEXT("\n"));
325
+ return;
326
+ }
327
+
328
+ WorldDataLayers->Modify();
329
+
330
+ #if WITH_EDITOR
331
+ // Use UDataLayerEditorSubsystem if available.
332
+ UDataLayerEditorSubsystem* DataLayerSubsystem = GEditor->GetEditorSubsystem<UDataLayerEditorSubsystem>();
333
+ if (!DataLayerSubsystem)
334
+ {
335
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("data_layer_editor_subsystem_unavailable")) + TEXT("\n"));
336
+ return;
337
+ }
338
+
339
+ // Determine if we need a runtime layer.
340
+ bool bIsRuntime = (LayerType == TEXT("Runtime"));
341
+ UDataLayerAsset* NewAsset = DataLayerSubsystem->CreateDataLayerAsset(*LayerName, bIsRuntime);
342
+ if (!NewAsset)
343
+ {
344
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("failed_to_create_data_layer")) + TEXT("\n"));
345
+ return;
346
+ }
347
+
348
+ TSharedPtr<FJsonObject> LayerObj = MakeShared<FJsonObject>();
349
+ LayerObj->SetStringField(TEXT("name"), LayerName);
350
+ LayerObj->SetStringField(TEXT("type"), LayerType.IsEmpty() ? TEXT("Editor") : LayerType);
351
+ LayerObj->SetStringField(TEXT("asset_path"), NewAsset->GetPathName());
352
+
353
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
354
+ Data->SetObjectField(TEXT("created_layer"), LayerObj);
355
+
356
+ SendResponse(BuildWPSuccessResponse(CorrId, Data) + TEXT("\n"));
357
+ #else
358
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("data_layer_creation_requires_editor")) + TEXT("\n"));
359
+ #endif
360
+ return;
361
+ }
362
+
363
+ // ------------------------------------------------------------------
364
+ // action: toggle
365
+ // ------------------------------------------------------------------
366
+ if (Action == TEXT("toggle"))
367
+ {
368
+ // Validate inputs (T-18-02).
369
+ if (LayerName.IsEmpty())
370
+ {
371
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("missing_layer_name")) + TEXT("\n"));
372
+ return;
373
+ }
374
+ if (InitialState.IsEmpty())
375
+ {
376
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("missing_initial_state")) + TEXT("\n"));
377
+ return;
378
+ }
379
+
380
+ // Find the data layer instance by name.
381
+ AWorldDataLayers* WorldDataLayers = nullptr;
382
+ for (TActorIterator<AWorldDataLayers> It(World); It; ++It)
383
+ {
384
+ WorldDataLayers = *It;
385
+ break;
386
+ }
387
+
388
+ if (!WorldDataLayers)
389
+ {
390
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("no_world_data_layers_actor")) + TEXT("\n"));
391
+ return;
392
+ }
393
+
394
+ UDataLayerInstance* FoundLayer = nullptr;
395
+ WorldDataLayers->ForEachDataLayer([&FoundLayer, &LayerName](UDataLayerInstance* DataLayerInstance) -> bool
396
+ {
397
+ if (DataLayerInstance && DataLayerInstance->GetDataLayerFullName() == LayerName)
398
+ {
399
+ FoundLayer = DataLayerInstance;
400
+ return false; // stop iteration
401
+ }
402
+ return true;
403
+ });
404
+
405
+ if (!FoundLayer)
406
+ {
407
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("data_layer_not_found")) + TEXT("\n"));
408
+ return;
409
+ }
410
+
411
+ // Modify() before mutation (T-18-02).
412
+ FoundLayer->Modify();
413
+
414
+ // Parse the initial state string.
415
+ EDataLayerRuntimeState NewState = EDataLayerRuntimeState::Unloaded;
416
+ if (InitialState == TEXT("Loaded"))
417
+ {
418
+ NewState = EDataLayerRuntimeState::Loaded;
419
+ }
420
+ else if (InitialState == TEXT("Activated"))
421
+ {
422
+ NewState = EDataLayerRuntimeState::Activated;
423
+ }
424
+
425
+ FoundLayer->SetInitialRuntimeState(NewState);
426
+
427
+ TSharedPtr<FJsonObject> LayerObj = MakeShared<FJsonObject>();
428
+ LayerObj->SetStringField(TEXT("name"), LayerName);
429
+ LayerObj->SetStringField(TEXT("initial_runtime_state"), InitialState);
430
+
431
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
432
+ Data->SetObjectField(TEXT("updated_layer"), LayerObj);
433
+
434
+ SendResponse(BuildWPSuccessResponse(CorrId, Data) + TEXT("\n"));
435
+ return;
436
+ }
437
+
438
+ // ------------------------------------------------------------------
439
+ // action: assign_actor
440
+ // ------------------------------------------------------------------
441
+ if (Action == TEXT("assign_actor"))
442
+ {
443
+ // Validate inputs (T-18-03).
444
+ if (LayerName.IsEmpty())
445
+ {
446
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("missing_layer_name")) + TEXT("\n"));
447
+ return;
448
+ }
449
+ if (ActorLabel.IsEmpty())
450
+ {
451
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("missing_actor_label")) + TEXT("\n"));
452
+ return;
453
+ }
454
+
455
+ // Find actor by label.
456
+ AActor* FoundActor = nullptr;
457
+ for (TActorIterator<AActor> It(World); It; ++It)
458
+ {
459
+ if ((*It)->GetActorLabel() == ActorLabel)
460
+ {
461
+ FoundActor = *It;
462
+ break;
463
+ }
464
+ }
465
+
466
+ if (!FoundActor)
467
+ {
468
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("actor_not_found")) + TEXT("\n"));
469
+ return;
470
+ }
471
+
472
+ // Find the data layer instance.
473
+ AWorldDataLayers* WorldDataLayers = nullptr;
474
+ for (TActorIterator<AWorldDataLayers> It(World); It; ++It)
475
+ {
476
+ WorldDataLayers = *It;
477
+ break;
478
+ }
479
+
480
+ if (!WorldDataLayers)
481
+ {
482
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("no_world_data_layers_actor")) + TEXT("\n"));
483
+ return;
484
+ }
485
+
486
+ UDataLayerInstance* FoundLayer = nullptr;
487
+ WorldDataLayers->ForEachDataLayer([&FoundLayer, &LayerName](UDataLayerInstance* DataLayerInstance) -> bool
488
+ {
489
+ if (DataLayerInstance && DataLayerInstance->GetDataLayerFullName() == LayerName)
490
+ {
491
+ FoundLayer = DataLayerInstance;
492
+ return false;
493
+ }
494
+ return true;
495
+ });
496
+
497
+ if (!FoundLayer)
498
+ {
499
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("data_layer_not_found")) + TEXT("\n"));
500
+ return;
501
+ }
502
+
503
+ #if WITH_EDITOR
504
+ UDataLayerEditorSubsystem* DataLayerSubsystem = GEditor->GetEditorSubsystem<UDataLayerEditorSubsystem>();
505
+ if (!DataLayerSubsystem)
506
+ {
507
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("data_layer_editor_subsystem_unavailable")) + TEXT("\n"));
508
+ return;
509
+ }
510
+
511
+ // Modify() before mutation (T-18-03).
512
+ FoundActor->Modify();
513
+
514
+ bool bSuccess = DataLayerSubsystem->AddActorToDataLayer(FoundActor, FoundLayer);
515
+ if (!bSuccess)
516
+ {
517
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("failed_to_assign_actor_to_layer")) + TEXT("\n"));
518
+ return;
519
+ }
520
+
521
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
522
+ Data->SetStringField(TEXT("actor_label"), ActorLabel);
523
+ Data->SetStringField(TEXT("layer_name"), LayerName);
524
+ Data->SetBoolField(TEXT("success"), true);
525
+
526
+ SendResponse(BuildWPSuccessResponse(CorrId, Data) + TEXT("\n"));
527
+ #else
528
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("assign_actor_requires_editor")) + TEXT("\n"));
529
+ #endif
530
+ return;
531
+ }
532
+
533
+ // Unknown action.
534
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("unknown_action")) + TEXT("\n"));
535
+ });
536
+ });
537
+
538
+ // -----------------------------------------------------------------------
539
+ // worldpartition.streamingSources (WP-03)
540
+ // Inspects World Partition streaming source components on all actors.
541
+ // Optional payload field: actor_label (string) -- filter to a specific actor.
542
+ // Returns: streaming_sources array with actor_label, component_name,
543
+ // target_state, shapes, priority.
544
+ // -----------------------------------------------------------------------
545
+ Router.RegisterHandler(TEXT("worldpartition.streamingSources"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
546
+ {
547
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
548
+
549
+ // Extract optional actor_label filter.
550
+ FString ActorLabelFilter;
551
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
552
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
553
+ {
554
+ TSharedPtr<FJsonObject> Payload = (*PayloadVal)->AsObject();
555
+ Payload->TryGetStringField(TEXT("actor_label"), ActorLabelFilter);
556
+ }
557
+
558
+ AsyncTask(ENamedThreads::GameThread, [CorrId, SendResponse, ActorLabelFilter]()
559
+ {
560
+ if (!GEditor)
561
+ {
562
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("editor_not_available")) + TEXT("\n"));
563
+ return;
564
+ }
565
+
566
+ UWorld* World = GEditor->GetEditorWorldContext().World();
567
+ if (!World)
568
+ {
569
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
570
+ return;
571
+ }
572
+
573
+ TArray<TSharedPtr<FJsonValue>> SourcesArray;
574
+
575
+ // Iterate all actors and look for streaming source components.
576
+ for (TActorIterator<AActor> It(World); It; ++It)
577
+ {
578
+ AActor* Actor = *It;
579
+ if (!Actor)
580
+ {
581
+ continue;
582
+ }
583
+
584
+ // Apply actor_label filter if provided.
585
+ if (!ActorLabelFilter.IsEmpty() && Actor->GetActorLabel() != ActorLabelFilter)
586
+ {
587
+ continue;
588
+ }
589
+
590
+ // Find all UWorldPartitionStreamingSourceComponent on this actor.
591
+ TArray<UActorComponent*> Components;
592
+ Actor->GetComponents(UWorldPartitionStreamingSourceComponent::StaticClass(), Components);
593
+
594
+ for (UActorComponent* Comp : Components)
595
+ {
596
+ UWorldPartitionStreamingSourceComponent* SourceComp = Cast<UWorldPartitionStreamingSourceComponent>(Comp);
597
+ if (!SourceComp)
598
+ {
599
+ continue;
600
+ }
601
+
602
+ // Get target state as string.
603
+ FString TargetStateStr = TEXT("Loaded");
604
+ EStreamingSourceTargetState TargetState = SourceComp->TargetState;
605
+ if (TargetState == EStreamingSourceTargetState::Activated)
606
+ {
607
+ TargetStateStr = TEXT("Activated");
608
+ }
609
+ else if (TargetState == EStreamingSourceTargetState::Loaded)
610
+ {
611
+ TargetStateStr = TEXT("Loaded");
612
+ }
613
+
614
+ // Build shapes array.
615
+ TArray<TSharedPtr<FJsonValue>> ShapesArray;
616
+ for (const FWorldPartitionStreamingQuerySource& Shape : SourceComp->DebugSources)
617
+ {
618
+ TSharedPtr<FJsonObject> ShapeObj = MakeShared<FJsonObject>();
619
+ ShapeObj->SetNumberField(TEXT("radius"), static_cast<double>(Shape.Radius));
620
+ ShapeObj->SetNumberField(TEXT("pos_x"), static_cast<double>(Shape.Location.X));
621
+ ShapeObj->SetNumberField(TEXT("pos_y"), static_cast<double>(Shape.Location.Y));
622
+ ShapeObj->SetNumberField(TEXT("pos_z"), static_cast<double>(Shape.Location.Z));
623
+ ShapesArray.Add(MakeShared<FJsonValueObject>(ShapeObj));
624
+ }
625
+
626
+ TSharedPtr<FJsonObject> SourceObj = MakeShared<FJsonObject>();
627
+ SourceObj->SetStringField(TEXT("actor_label"), Actor->GetActorLabel());
628
+ SourceObj->SetStringField(TEXT("component_name"), SourceComp->GetName());
629
+ SourceObj->SetStringField(TEXT("target_state"), TargetStateStr);
630
+ SourceObj->SetNumberField(TEXT("priority"), static_cast<double>(static_cast<int32>(SourceComp->Priority)));
631
+ SourceObj->SetArrayField(TEXT("shapes"), ShapesArray);
632
+ SourcesArray.Add(MakeShared<FJsonValueObject>(SourceObj));
633
+ }
634
+ }
635
+
636
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
637
+ Data->SetArrayField(TEXT("streaming_sources"), SourcesArray);
638
+ Data->SetNumberField(TEXT("count"), static_cast<double>(SourcesArray.Num()));
639
+ if (!ActorLabelFilter.IsEmpty())
640
+ {
641
+ Data->SetStringField(TEXT("actor_label_filter"), ActorLabelFilter);
642
+ }
643
+
644
+ SendResponse(BuildWPSuccessResponse(CorrId, Data) + TEXT("\n"));
645
+ });
646
+ });
647
+
648
+ // -----------------------------------------------------------------------
649
+ // worldpartition.hlod (WP-04)
650
+ // Inspects HLOD layer configuration and triggers HLOD generation.
651
+ // Required payload field: action ("inspect" | "generate")
652
+ //
653
+ // inspect: Returns array of HLOD layers with name, cell_size, hlod_level, etc.
654
+ // generate: Fire-and-forget HLOD build trigger -- returns status "triggered".
655
+ // -----------------------------------------------------------------------
656
+ Router.RegisterHandler(TEXT("worldpartition.hlod"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
657
+ {
658
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
659
+
660
+ // Extract payload.
661
+ TSharedPtr<FJsonObject> Payload;
662
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
663
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
664
+ {
665
+ Payload = (*PayloadVal)->AsObject();
666
+ }
667
+
668
+ FString Action;
669
+ if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("action"), Action) || Action.IsEmpty())
670
+ {
671
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("missing_action")) + TEXT("\n"));
672
+ return;
673
+ }
674
+
675
+ // Capture optional asset_path for HLOD inspect filtering (validated below T-18-01).
676
+ FString AssetPath;
677
+ if (Payload.IsValid())
678
+ {
679
+ Payload->TryGetStringField(TEXT("asset_path"), AssetPath);
680
+ }
681
+
682
+ // Validate asset_path if provided (T-18-01).
683
+ if (!AssetPath.IsEmpty() && !IsValidAssetPath(AssetPath))
684
+ {
685
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("invalid_asset_path")) + TEXT("\n"));
686
+ return;
687
+ }
688
+
689
+ AsyncTask(ENamedThreads::GameThread, [CorrId, SendResponse, Action, AssetPath]()
690
+ {
691
+ if (!GEditor)
692
+ {
693
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("editor_not_available")) + TEXT("\n"));
694
+ return;
695
+ }
696
+
697
+ // ------------------------------------------------------------------
698
+ // action: inspect
699
+ // ------------------------------------------------------------------
700
+ if (Action == TEXT("inspect"))
701
+ {
702
+ // Get all UHLODLayer assets from the asset registry.
703
+ FAssetRegistryModule& RegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
704
+ IAssetRegistry& Registry = RegistryModule.Get();
705
+
706
+ TArray<FAssetData> HLODAssets;
707
+ FTopLevelAssetPath HLODLayerClass(TEXT("/Script/Engine"), TEXT("HLODLayer"));
708
+ Registry.GetAssetsByClass(HLODLayerClass, HLODAssets, /*bSearchSubClasses=*/true);
709
+
710
+ TArray<TSharedPtr<FJsonValue>> HLODLayersArray;
711
+
712
+ for (const FAssetData& AssetData : HLODAssets)
713
+ {
714
+ FString LayerAssetPath = AssetData.GetObjectPathString();
715
+
716
+ // Apply asset_path filter if provided.
717
+ if (!AssetPath.IsEmpty() && LayerAssetPath != AssetPath)
718
+ {
719
+ continue;
720
+ }
721
+
722
+ // Load the HLOD layer asset.
723
+ UHLODLayer* HLODLayer = Cast<UHLODLayer>(StaticLoadObject(UHLODLayer::StaticClass(), nullptr, *LayerAssetPath));
724
+ if (!HLODLayer)
725
+ {
726
+ continue;
727
+ }
728
+
729
+ TSharedPtr<FJsonObject> LayerObj = MakeShared<FJsonObject>();
730
+ LayerObj->SetStringField(TEXT("layer_name"), HLODLayer->GetName());
731
+ LayerObj->SetStringField(TEXT("asset_path"), LayerAssetPath);
732
+ LayerObj->SetBoolField(TEXT("is_spatially_loaded"), HLODLayer->IsSpatiallyLoaded());
733
+
734
+ // Read cell size via reflection -- UHLODLayer::CellSize or similar property.
735
+ double CellSize = 0.0;
736
+ int32 HLODLevel = 0;
737
+ double LoadingRange = 0.0;
738
+
739
+ for (TFieldIterator<FDoubleProperty> PropIt(HLODLayer->GetClass()); PropIt; ++PropIt)
740
+ {
741
+ const FString PropName = PropIt->GetName();
742
+ if (PropName.Contains(TEXT("CellSize")))
743
+ {
744
+ CellSize = PropIt->GetPropertyValue_InContainer(HLODLayer);
745
+ }
746
+ }
747
+ for (TFieldIterator<FFloatProperty> PropIt(HLODLayer->GetClass()); PropIt; ++PropIt)
748
+ {
749
+ const FString PropName = PropIt->GetName();
750
+ if (PropName.Contains(TEXT("CellSize")))
751
+ {
752
+ CellSize = static_cast<double>(PropIt->GetPropertyValue_InContainer(HLODLayer));
753
+ }
754
+ else if (PropName.Contains(TEXT("LoadingRange")))
755
+ {
756
+ LoadingRange = static_cast<double>(PropIt->GetPropertyValue_InContainer(HLODLayer));
757
+ }
758
+ }
759
+ for (TFieldIterator<FIntProperty> PropIt(HLODLayer->GetClass()); PropIt; ++PropIt)
760
+ {
761
+ const FString PropName = PropIt->GetName();
762
+ if (PropName.Contains(TEXT("HLODLevel")) || PropName.Contains(TEXT("Level")))
763
+ {
764
+ HLODLevel = PropIt->GetPropertyValue_InContainer(HLODLayer);
765
+ }
766
+ }
767
+
768
+ LayerObj->SetNumberField(TEXT("cell_size"), CellSize);
769
+ LayerObj->SetNumberField(TEXT("loading_range"), LoadingRange);
770
+ LayerObj->SetNumberField(TEXT("hlod_level"), static_cast<double>(HLODLevel));
771
+
772
+ HLODLayersArray.Add(MakeShared<FJsonValueObject>(LayerObj));
773
+ }
774
+
775
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
776
+ Data->SetArrayField(TEXT("hlod_layers"), HLODLayersArray);
777
+ Data->SetNumberField(TEXT("count"), static_cast<double>(HLODLayersArray.Num()));
778
+
779
+ SendResponse(BuildWPSuccessResponse(CorrId, Data) + TEXT("\n"));
780
+ return;
781
+ }
782
+
783
+ // ------------------------------------------------------------------
784
+ // action: generate
785
+ // ------------------------------------------------------------------
786
+ if (Action == TEXT("generate"))
787
+ {
788
+ UWorld* World = GEditor->GetEditorWorldContext().World();
789
+ if (!World)
790
+ {
791
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
792
+ return;
793
+ }
794
+
795
+ UWorldPartition* WorldPartition = World->GetWorldPartition();
796
+ if (!WorldPartition)
797
+ {
798
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("world_does_not_use_world_partition")) + TEXT("\n"));
799
+ return;
800
+ }
801
+
802
+ // Fire-and-forget HLOD generation trigger (T-18-04: accepted DoS risk).
803
+ // Use GEditor->Exec with the HLODBuilder commandlet to trigger generation.
804
+ // This is async -- the editor will show progress. We return "triggered" immediately.
805
+ if (GEditor)
806
+ {
807
+ GEditor->Exec(World, TEXT("wp.Runtime.BuildHLODs"), *GLog);
808
+ }
809
+
810
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
811
+ Data->SetStringField(TEXT("status"), TEXT("triggered"));
812
+ Data->SetStringField(TEXT("message"), TEXT("HLOD generation has been triggered. Check the editor Output Log for progress and completion status."));
813
+
814
+ SendResponse(BuildWPSuccessResponse(CorrId, Data) + TEXT("\n"));
815
+ return;
816
+ }
817
+
818
+ // Unknown action.
819
+ SendResponse(BuildWPErrorResponse(CorrId, TEXT("unknown_action")) + TEXT("\n"));
820
+ });
821
+ });
822
+ }