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.
- package/README.md +729 -0
- package/dist/build/error-parser.js +51 -0
- package/dist/build/fix-suggester.js +84 -0
- package/dist/build/ubt-runner.js +146 -0
- package/dist/cli.js +13 -0
- package/dist/config.js +8 -0
- package/dist/docs/data/ue57-api.js +228 -0
- package/dist/docs/doc-index.js +110 -0
- package/dist/docs/types.js +4 -0
- package/dist/generators/class-generator.js +363 -0
- package/dist/generators/file-modifier.js +276 -0
- package/dist/generators/uht-validator.js +177 -0
- package/dist/index.js +89 -0
- package/dist/parsers/cpp-class-index.js +230 -0
- package/dist/parsers/cpp-parser.js +369 -0
- package/dist/parsers/ini-parser.js +216 -0
- package/dist/parsers/uproject-parser.js +130 -0
- package/dist/plugin-bridge/client.js +217 -0
- package/dist/plugin-bridge/protocol.js +6 -0
- package/dist/plugin-bridge/retry.js +23 -0
- package/dist/setup.js +209 -0
- package/dist/tools/ai-systems/index.js +247 -0
- package/dist/tools/ai-systems/types.js +4 -0
- package/dist/tools/animation/index.js +241 -0
- package/dist/tools/animation/types.js +4 -0
- package/dist/tools/audio/index.js +204 -0
- package/dist/tools/audio/types.js +4 -0
- package/dist/tools/blueprint/index.js +495 -0
- package/dist/tools/blueprint/types.js +4 -0
- package/dist/tools/build/index.js +163 -0
- package/dist/tools/chaos/index.js +230 -0
- package/dist/tools/chaos/types.js +4 -0
- package/dist/tools/collision-physics/index.js +211 -0
- package/dist/tools/config/index.js +288 -0
- package/dist/tools/cpp/index.js +305 -0
- package/dist/tools/docs/index.js +251 -0
- package/dist/tools/editor/index.js +242 -0
- package/dist/tools/gas/index.js +222 -0
- package/dist/tools/gas/types.js +5 -0
- package/dist/tools/import-export/index.js +218 -0
- package/dist/tools/input/index.js +146 -0
- package/dist/tools/known-issues/index.js +88 -0
- package/dist/tools/known-issues/middleware.js +55 -0
- package/dist/tools/known-issues/store.js +125 -0
- package/dist/tools/livelink/index.js +203 -0
- package/dist/tools/livelink/types.js +4 -0
- package/dist/tools/material/index.js +190 -0
- package/dist/tools/motion-design/index.js +251 -0
- package/dist/tools/motion-design/types.js +6 -0
- package/dist/tools/movie-render/index.js +220 -0
- package/dist/tools/networking/index.js +149 -0
- package/dist/tools/pcg/index.js +164 -0
- package/dist/tools/selection/index.js +180 -0
- package/dist/tools/sequencer/index.js +218 -0
- package/dist/tools/validation/index.js +183 -0
- package/dist/tools/validation/types.js +4 -0
- package/dist/tools/viewport/index.js +310 -0
- package/dist/tools/worldpartition/index.js +226 -0
- package/dist/tools/worldpartition/types.js +4 -0
- package/dist/utils/execFileNoThrow.js +40 -0
- package/dist/utils/logger.js +27 -0
- package/dist/utils/path-guard.js +26 -0
- package/package.json +40 -0
- package/unreal-plugin/MCPBridge/MCPBridge.uplugin +29 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/MCPBridgeEditor.Build.cs +68 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.cpp +919 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.h +23 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.cpp +415 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.cpp +653 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.h +24 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.cpp +290 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.h +17 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.cpp +624 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.cpp +616 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.h +25 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.cpp +744 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.h +24 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeEditor.cpp +23 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.cpp +149 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.h +38 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.cpp +771 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.cpp +749 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.cpp +172 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.cpp +715 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.cpp +679 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.cpp +381 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.h +24 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.cpp +504 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.cpp +511 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.cpp +1110 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.h +28 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.cpp +590 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.cpp +482 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.cpp +338 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.cpp +677 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.cpp +721 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.cpp +368 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.cpp +1208 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.h +29 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.cpp +822 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.h +23 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Public/MCPBridgeEditor.h +14 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/MCPBridgeRuntime.Build.cs +28 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPBridgeRuntime.cpp +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPCommandRouter.cpp +118 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPTcpServer.cpp +196 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPBridgeRuntime.h +15 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPCommandRouter.h +55 -0
- 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
|
+
}
|