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,677 @@
1
+ // MCPSelectionCommands.cpp (Plan 19-01)
2
+ // Implements four actor selection command handlers for the MCP bridge:
3
+ // selection.select -- select/deselect actors by name, label, or class filter (SEL-01)
4
+ // selection.get -- get current selection or apply all/none/invert bulk operation (SEL-02)
5
+ // selection.duplicate -- duplicate selected actors with optional position offset (SEL-03)
6
+ // selection.convert -- convert an actor to a different target class (SEL-04)
7
+ //
8
+ // All handlers run on the game thread via AsyncTask(ENamedThreads::GameThread).
9
+ // target_class is validated via FindObject/StaticLoadClass before any actor operation (T-19-01).
10
+ // Offset values are clamped to +/-1e7 to prevent floating point issues (T-19-02).
11
+ // Actor labels are validated via TActorIterator before conversion (T-19-04).
12
+ // Modify() is called before all write operations in duplicate and convert handlers.
13
+
14
+ #include "MCPSelectionCommands.h"
15
+
16
+ // Editor
17
+ #include "Editor.h"
18
+ #include "Engine/World.h"
19
+ #include "EngineUtils.h"
20
+ #include "GameFramework/Actor.h"
21
+
22
+ // Async
23
+ #include "Async/Async.h"
24
+
25
+ // JSON
26
+ #include "Serialization/JsonSerializer.h"
27
+ #include "Serialization/JsonWriter.h"
28
+ #include "Dom/JsonObject.h"
29
+ #include "Dom/JsonValue.h"
30
+
31
+ // Editor actor subsystem
32
+ #include "Subsystems/EditorActorSubsystem.h"
33
+
34
+ // Selection
35
+ #include "Selection.h"
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Internal helpers
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /** Returns a JSON success response string (without trailing newline). */
42
+ static FString BuildSelSuccessResponse(const FString& CorrId, TSharedPtr<FJsonObject> Data)
43
+ {
44
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
45
+ Obj->SetBoolField(TEXT("success"), true);
46
+ Obj->SetStringField(TEXT("correlationId"), CorrId);
47
+ if (Data.IsValid())
48
+ {
49
+ Obj->SetObjectField(TEXT("data"), Data);
50
+ }
51
+
52
+ FString Output;
53
+ TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
54
+ FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
55
+ return Output;
56
+ }
57
+
58
+ /** Returns a JSON error response string (without trailing newline). */
59
+ static FString BuildSelErrorResponse(const FString& CorrId, const FString& Error)
60
+ {
61
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
62
+ Obj->SetBoolField(TEXT("success"), false);
63
+ Obj->SetStringField(TEXT("correlationId"), CorrId);
64
+ Obj->SetStringField(TEXT("error"), Error);
65
+
66
+ FString Output;
67
+ TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
68
+ FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
69
+ return Output;
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // RegisterSelectionCommands
74
+ // ---------------------------------------------------------------------------
75
+
76
+ void RegisterSelectionCommands(FMCPCommandRouter& Router)
77
+ {
78
+ // -----------------------------------------------------------------------
79
+ // selection.select (SEL-01)
80
+ // Selects or deselects actors by label, name, or class filter.
81
+ //
82
+ // Optional payload fields:
83
+ // actors -- string array of actor labels/names (supports * wildcards)
84
+ // class_filter -- string class name to match (e.g. "StaticMeshActor")
85
+ // action -- "select" (default) or "deselect"
86
+ //
87
+ // Returns: selected_count and actors array with label, class, selected state.
88
+ // -----------------------------------------------------------------------
89
+ Router.RegisterHandler(TEXT("selection.select"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
90
+ {
91
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
92
+
93
+ // Extract payload.
94
+ TSharedPtr<FJsonObject> Payload;
95
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
96
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
97
+ {
98
+ Payload = (*PayloadVal)->AsObject();
99
+ }
100
+
101
+ // Extract actors array.
102
+ TArray<FString> ActorNames;
103
+ if (Payload.IsValid())
104
+ {
105
+ const TArray<TSharedPtr<FJsonValue>>* ActorsArray = nullptr;
106
+ if (Payload->TryGetArrayField(TEXT("actors"), ActorsArray) && ActorsArray)
107
+ {
108
+ for (const TSharedPtr<FJsonValue>& Val : *ActorsArray)
109
+ {
110
+ FString Name;
111
+ if (Val.IsValid() && Val->TryGetString(Name))
112
+ {
113
+ ActorNames.Add(Name);
114
+ }
115
+ }
116
+ }
117
+ }
118
+
119
+ // Extract class_filter and action.
120
+ FString ClassFilter;
121
+ FString Action = TEXT("select");
122
+ if (Payload.IsValid())
123
+ {
124
+ Payload->TryGetStringField(TEXT("class_filter"), ClassFilter);
125
+ FString ActionVal;
126
+ if (Payload->TryGetStringField(TEXT("action"), ActionVal) && !ActionVal.IsEmpty())
127
+ {
128
+ Action = ActionVal;
129
+ }
130
+ }
131
+
132
+ const bool bSelect = (Action != TEXT("deselect"));
133
+
134
+ AsyncTask(ENamedThreads::GameThread, [CorrId, SendResponse, ActorNames, ClassFilter, bSelect]()
135
+ {
136
+ if (!GEditor)
137
+ {
138
+ SendResponse(BuildSelErrorResponse(CorrId, TEXT("editor_not_available")) + TEXT("\n"));
139
+ return;
140
+ }
141
+
142
+ UWorld* World = GEditor->GetEditorWorldContext().World();
143
+ if (!World)
144
+ {
145
+ SendResponse(BuildSelErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
146
+ return;
147
+ }
148
+
149
+ TArray<TSharedPtr<FJsonValue>> ActorsResultArray;
150
+ int32 MatchCount = 0;
151
+
152
+ for (TActorIterator<AActor> It(World); It; ++It)
153
+ {
154
+ AActor* Actor = *It;
155
+ if (!Actor)
156
+ {
157
+ continue;
158
+ }
159
+
160
+ const FString ActorLabel = Actor->GetActorLabel();
161
+ const FString ActorName = Actor->GetName();
162
+ const FString ActorClass = Actor->GetClass()->GetName();
163
+
164
+ bool bMatches = false;
165
+
166
+ // Match against actors array (by label or name, supports wildcards).
167
+ if (ActorNames.Num() > 0)
168
+ {
169
+ for (const FString& Pattern : ActorNames)
170
+ {
171
+ if (Pattern.Contains(TEXT("*")))
172
+ {
173
+ // Wildcard matching.
174
+ if (ActorLabel.MatchesWildcard(Pattern) || ActorName.MatchesWildcard(Pattern))
175
+ {
176
+ bMatches = true;
177
+ break;
178
+ }
179
+ }
180
+ else
181
+ {
182
+ // Exact match by label or name.
183
+ if (ActorLabel == Pattern || ActorName == Pattern)
184
+ {
185
+ bMatches = true;
186
+ break;
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ // Match against class filter.
193
+ if (!ClassFilter.IsEmpty())
194
+ {
195
+ if (ClassFilter.Contains(TEXT("*")))
196
+ {
197
+ if (ActorClass.MatchesWildcard(ClassFilter))
198
+ {
199
+ bMatches = true;
200
+ }
201
+ }
202
+ else
203
+ {
204
+ if (ActorClass == ClassFilter)
205
+ {
206
+ bMatches = true;
207
+ }
208
+ }
209
+ }
210
+
211
+ // If no filter specified, match all actors.
212
+ if (ActorNames.Num() == 0 && ClassFilter.IsEmpty())
213
+ {
214
+ bMatches = true;
215
+ }
216
+
217
+ if (bMatches)
218
+ {
219
+ GEditor->SelectActor(Actor, bSelect, /*bNotify=*/true);
220
+ ++MatchCount;
221
+
222
+ const FTransform& T = Actor->GetActorTransform();
223
+ FVector Loc = T.GetLocation();
224
+ FRotator Rot = T.GetRotation().Rotator();
225
+ FVector Scale = T.GetScale3D();
226
+
227
+ TSharedPtr<FJsonObject> LocObj = MakeShared<FJsonObject>();
228
+ LocObj->SetNumberField(TEXT("x"), static_cast<double>(Loc.X));
229
+ LocObj->SetNumberField(TEXT("y"), static_cast<double>(Loc.Y));
230
+ LocObj->SetNumberField(TEXT("z"), static_cast<double>(Loc.Z));
231
+
232
+ TSharedPtr<FJsonObject> ActorObj = MakeShared<FJsonObject>();
233
+ ActorObj->SetStringField(TEXT("label"), ActorLabel);
234
+ ActorObj->SetStringField(TEXT("class"), ActorClass);
235
+ ActorObj->SetBoolField(TEXT("selected"), bSelect);
236
+ ActorObj->SetObjectField(TEXT("location"), LocObj);
237
+
238
+ ActorsResultArray.Add(MakeShared<FJsonValueObject>(ActorObj));
239
+ }
240
+ }
241
+
242
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
243
+ Data->SetNumberField(TEXT("selected_count"), static_cast<double>(MatchCount));
244
+ Data->SetArrayField(TEXT("actors"), ActorsResultArray);
245
+
246
+ SendResponse(BuildSelSuccessResponse(CorrId, Data) + TEXT("\n"));
247
+ });
248
+ });
249
+
250
+ // -----------------------------------------------------------------------
251
+ // selection.get (SEL-02)
252
+ // Gets the current editor selection or applies a bulk selection mode.
253
+ //
254
+ // Optional payload fields:
255
+ // mode -- "current" (default), "all", "none", "invert"
256
+ //
257
+ // Returns: count and actors array with label, class, and transform.
258
+ // -----------------------------------------------------------------------
259
+ Router.RegisterHandler(TEXT("selection.get"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
260
+ {
261
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
262
+
263
+ // Extract mode from payload.
264
+ FString Mode = TEXT("current");
265
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
266
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
267
+ {
268
+ TSharedPtr<FJsonObject> Payload = (*PayloadVal)->AsObject();
269
+ FString ModeVal;
270
+ if (Payload->TryGetStringField(TEXT("mode"), ModeVal) && !ModeVal.IsEmpty())
271
+ {
272
+ Mode = ModeVal;
273
+ }
274
+ }
275
+
276
+ AsyncTask(ENamedThreads::GameThread, [CorrId, SendResponse, Mode]()
277
+ {
278
+ if (!GEditor)
279
+ {
280
+ SendResponse(BuildSelErrorResponse(CorrId, TEXT("editor_not_available")) + TEXT("\n"));
281
+ return;
282
+ }
283
+
284
+ UWorld* World = GEditor->GetEditorWorldContext().World();
285
+ if (!World)
286
+ {
287
+ SendResponse(BuildSelErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
288
+ return;
289
+ }
290
+
291
+ // Apply bulk selection modes.
292
+ if (Mode == TEXT("all"))
293
+ {
294
+ for (TActorIterator<AActor> It(World); It; ++It)
295
+ {
296
+ AActor* Actor = *It;
297
+ if (Actor)
298
+ {
299
+ GEditor->SelectActor(Actor, /*bInSelected=*/true, /*bNotify=*/true, /*bSelectEvenIfHidden=*/false);
300
+ }
301
+ }
302
+ GEditor->NoteSelectionChange();
303
+ }
304
+ else if (Mode == TEXT("none"))
305
+ {
306
+ GEditor->SelectNone(/*bNoteSelectionChange=*/true, /*bDeselectBSPSurfs=*/true);
307
+ }
308
+ else if (Mode == TEXT("invert"))
309
+ {
310
+ for (TActorIterator<AActor> It(World); It; ++It)
311
+ {
312
+ AActor* Actor = *It;
313
+ if (Actor)
314
+ {
315
+ const bool bCurrentlySelected = Actor->IsSelected();
316
+ GEditor->SelectActor(Actor, !bCurrentlySelected, /*bNotify=*/true, /*bSelectEvenIfHidden=*/false);
317
+ }
318
+ }
319
+ GEditor->NoteSelectionChange();
320
+ }
321
+ // mode == "current": just read the existing selection without changes.
322
+
323
+ // Build response from current selection.
324
+ TArray<TSharedPtr<FJsonValue>> ActorsArray;
325
+
326
+ USelection* Selection = GEditor->GetSelectedActors();
327
+ if (Selection)
328
+ {
329
+ TArray<UObject*> SelectedObjects;
330
+ Selection->GetSelectedObjects(AActor::StaticClass(), SelectedObjects);
331
+
332
+ for (UObject* Obj : SelectedObjects)
333
+ {
334
+ AActor* Actor = Cast<AActor>(Obj);
335
+ if (!Actor)
336
+ {
337
+ continue;
338
+ }
339
+
340
+ const FTransform& T = Actor->GetActorTransform();
341
+ FVector Loc = T.GetLocation();
342
+ FRotator Rot = T.GetRotation().Rotator();
343
+ FVector Scale = T.GetScale3D();
344
+
345
+ TSharedPtr<FJsonObject> LocObj = MakeShared<FJsonObject>();
346
+ LocObj->SetNumberField(TEXT("x"), static_cast<double>(Loc.X));
347
+ LocObj->SetNumberField(TEXT("y"), static_cast<double>(Loc.Y));
348
+ LocObj->SetNumberField(TEXT("z"), static_cast<double>(Loc.Z));
349
+
350
+ TSharedPtr<FJsonObject> RotObj = MakeShared<FJsonObject>();
351
+ RotObj->SetNumberField(TEXT("pitch"), static_cast<double>(Rot.Pitch));
352
+ RotObj->SetNumberField(TEXT("yaw"), static_cast<double>(Rot.Yaw));
353
+ RotObj->SetNumberField(TEXT("roll"), static_cast<double>(Rot.Roll));
354
+
355
+ TSharedPtr<FJsonObject> ScaleObj = MakeShared<FJsonObject>();
356
+ ScaleObj->SetNumberField(TEXT("x"), static_cast<double>(Scale.X));
357
+ ScaleObj->SetNumberField(TEXT("y"), static_cast<double>(Scale.Y));
358
+ ScaleObj->SetNumberField(TEXT("z"), static_cast<double>(Scale.Z));
359
+
360
+ TSharedPtr<FJsonObject> TransformObj = MakeShared<FJsonObject>();
361
+ TransformObj->SetObjectField(TEXT("location"), LocObj);
362
+ TransformObj->SetObjectField(TEXT("rotation"), RotObj);
363
+ TransformObj->SetObjectField(TEXT("scale"), ScaleObj);
364
+
365
+ TSharedPtr<FJsonObject> ActorObj = MakeShared<FJsonObject>();
366
+ ActorObj->SetStringField(TEXT("label"), Actor->GetActorLabel());
367
+ ActorObj->SetStringField(TEXT("class"), Actor->GetClass()->GetName());
368
+ ActorObj->SetObjectField(TEXT("transform"), TransformObj);
369
+
370
+ ActorsArray.Add(MakeShared<FJsonValueObject>(ActorObj));
371
+ }
372
+ }
373
+
374
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
375
+ Data->SetStringField(TEXT("mode"), Mode);
376
+ Data->SetNumberField(TEXT("count"), static_cast<double>(ActorsArray.Num()));
377
+ Data->SetArrayField(TEXT("actors"), ActorsArray);
378
+
379
+ SendResponse(BuildSelSuccessResponse(CorrId, Data) + TEXT("\n"));
380
+ });
381
+ });
382
+
383
+ // -----------------------------------------------------------------------
384
+ // selection.duplicate (SEL-03)
385
+ // Duplicates all currently selected actors with an optional offset.
386
+ //
387
+ // Optional payload fields:
388
+ // offset -- object with x, y, z doubles (default: {x:100, y:0, z:0})
389
+ // Clamped to +/-1e7 to prevent floating point issues (T-19-02).
390
+ //
391
+ // Returns: duplicated_count and array of new actor labels, classes, locations.
392
+ // -----------------------------------------------------------------------
393
+ Router.RegisterHandler(TEXT("selection.duplicate"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
394
+ {
395
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
396
+
397
+ // Extract offset from payload.
398
+ double OffsetX = 100.0;
399
+ double OffsetY = 0.0;
400
+ double OffsetZ = 0.0;
401
+
402
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
403
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
404
+ {
405
+ TSharedPtr<FJsonObject> Payload = (*PayloadVal)->AsObject();
406
+ const TSharedPtr<FJsonValue>* OffsetVal = Payload->Values.Find(TEXT("offset"));
407
+ if (OffsetVal && (*OffsetVal)->Type == EJson::Object)
408
+ {
409
+ TSharedPtr<FJsonObject> OffsetObj = (*OffsetVal)->AsObject();
410
+ OffsetObj->TryGetNumberField(TEXT("x"), OffsetX);
411
+ OffsetObj->TryGetNumberField(TEXT("y"), OffsetY);
412
+ OffsetObj->TryGetNumberField(TEXT("z"), OffsetZ);
413
+ }
414
+ }
415
+
416
+ // Clamp offset values to prevent floating point issues (T-19-02).
417
+ constexpr double MaxOffset = 1e7;
418
+ OffsetX = FMath::Clamp(OffsetX, -MaxOffset, MaxOffset);
419
+ OffsetY = FMath::Clamp(OffsetY, -MaxOffset, MaxOffset);
420
+ OffsetZ = FMath::Clamp(OffsetZ, -MaxOffset, MaxOffset);
421
+
422
+ const FVector Offset(OffsetX, OffsetY, OffsetZ);
423
+
424
+ AsyncTask(ENamedThreads::GameThread, [CorrId, SendResponse, Offset]()
425
+ {
426
+ if (!GEditor)
427
+ {
428
+ SendResponse(BuildSelErrorResponse(CorrId, TEXT("editor_not_available")) + TEXT("\n"));
429
+ return;
430
+ }
431
+
432
+ UWorld* World = GEditor->GetEditorWorldContext().World();
433
+ if (!World)
434
+ {
435
+ SendResponse(BuildSelErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
436
+ return;
437
+ }
438
+
439
+ // Collect currently selected actors.
440
+ TArray<AActor*> SelectedActors;
441
+ USelection* Selection = GEditor->GetSelectedActors();
442
+ if (Selection)
443
+ {
444
+ TArray<UObject*> SelectedObjects;
445
+ Selection->GetSelectedObjects(AActor::StaticClass(), SelectedObjects);
446
+ for (UObject* Obj : SelectedObjects)
447
+ {
448
+ AActor* Actor = Cast<AActor>(Obj);
449
+ if (Actor)
450
+ {
451
+ SelectedActors.Add(Actor);
452
+ }
453
+ }
454
+ }
455
+
456
+ if (SelectedActors.Num() == 0)
457
+ {
458
+ SendResponse(BuildSelErrorResponse(CorrId, TEXT("no_actors_selected")) + TEXT("\n"));
459
+ return;
460
+ }
461
+
462
+ // Use UEditorActorSubsystem to duplicate actors.
463
+ UEditorActorSubsystem* EditorActorSubsystem = GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
464
+ if (!EditorActorSubsystem)
465
+ {
466
+ SendResponse(BuildSelErrorResponse(CorrId, TEXT("editor_actor_subsystem_unavailable")) + TEXT("\n"));
467
+ return;
468
+ }
469
+
470
+ TArray<AActor*> DuplicatedActors = EditorActorSubsystem->DuplicateActors(SelectedActors);
471
+
472
+ TArray<TSharedPtr<FJsonValue>> DuplicatesArray;
473
+ for (AActor* NewActor : DuplicatedActors)
474
+ {
475
+ if (!NewActor)
476
+ {
477
+ continue;
478
+ }
479
+
480
+ // Modify() before write operation.
481
+ NewActor->Modify();
482
+
483
+ // Apply offset.
484
+ FVector NewLocation = NewActor->GetActorLocation() + Offset;
485
+ NewActor->SetActorLocation(NewLocation);
486
+
487
+ TSharedPtr<FJsonObject> LocObj = MakeShared<FJsonObject>();
488
+ LocObj->SetNumberField(TEXT("x"), static_cast<double>(NewLocation.X));
489
+ LocObj->SetNumberField(TEXT("y"), static_cast<double>(NewLocation.Y));
490
+ LocObj->SetNumberField(TEXT("z"), static_cast<double>(NewLocation.Z));
491
+
492
+ TSharedPtr<FJsonObject> ActorObj = MakeShared<FJsonObject>();
493
+ ActorObj->SetStringField(TEXT("label"), NewActor->GetActorLabel());
494
+ ActorObj->SetStringField(TEXT("class"), NewActor->GetClass()->GetName());
495
+ ActorObj->SetObjectField(TEXT("location"), LocObj);
496
+
497
+ DuplicatesArray.Add(MakeShared<FJsonValueObject>(ActorObj));
498
+ }
499
+
500
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
501
+ Data->SetNumberField(TEXT("duplicated_count"), static_cast<double>(DuplicatesArray.Num()));
502
+ Data->SetArrayField(TEXT("actors"), DuplicatesArray);
503
+
504
+ SendResponse(BuildSelSuccessResponse(CorrId, Data) + TEXT("\n"));
505
+ });
506
+ });
507
+
508
+ // -----------------------------------------------------------------------
509
+ // selection.convert (SEL-04)
510
+ // Converts an actor to a different target class.
511
+ //
512
+ // Required payload fields:
513
+ // actor_label -- string label of the actor to convert (validated T-19-04)
514
+ // target_class -- string class name, e.g. "StaticMeshActor" or
515
+ // "Blueprint'/Game/BP_MyActor.BP_MyActor_C'"
516
+ // (validated via FindObject/StaticLoadClass T-19-01)
517
+ //
518
+ // Returns: success, new_label, new_class, transform.
519
+ // -----------------------------------------------------------------------
520
+ Router.RegisterHandler(TEXT("selection.convert"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
521
+ {
522
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
523
+
524
+ // Extract payload.
525
+ TSharedPtr<FJsonObject> Payload;
526
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
527
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
528
+ {
529
+ Payload = (*PayloadVal)->AsObject();
530
+ }
531
+
532
+ FString ActorLabel;
533
+ FString TargetClassName;
534
+ if (!Payload.IsValid() ||
535
+ !Payload->TryGetStringField(TEXT("actor_label"), ActorLabel) || ActorLabel.IsEmpty())
536
+ {
537
+ SendResponse(BuildSelErrorResponse(CorrId, TEXT("missing_actor_label")) + TEXT("\n"));
538
+ return;
539
+ }
540
+ if (!Payload->TryGetStringField(TEXT("target_class"), TargetClassName) || TargetClassName.IsEmpty())
541
+ {
542
+ SendResponse(BuildSelErrorResponse(CorrId, TEXT("missing_target_class")) + TEXT("\n"));
543
+ return;
544
+ }
545
+
546
+ AsyncTask(ENamedThreads::GameThread, [CorrId, SendResponse, ActorLabel, TargetClassName]()
547
+ {
548
+ if (!GEditor)
549
+ {
550
+ SendResponse(BuildSelErrorResponse(CorrId, TEXT("editor_not_available")) + TEXT("\n"));
551
+ return;
552
+ }
553
+
554
+ UWorld* World = GEditor->GetEditorWorldContext().World();
555
+ if (!World)
556
+ {
557
+ SendResponse(BuildSelErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
558
+ return;
559
+ }
560
+
561
+ // Find the actor by label (T-19-04: validate actor exists).
562
+ AActor* FoundActor = nullptr;
563
+ for (TActorIterator<AActor> It(World); It; ++It)
564
+ {
565
+ if ((*It)->GetActorLabel() == ActorLabel)
566
+ {
567
+ FoundActor = *It;
568
+ break;
569
+ }
570
+ }
571
+
572
+ if (!FoundActor)
573
+ {
574
+ SendResponse(BuildSelErrorResponse(CorrId, TEXT("actor_not_found")) + TEXT("\n"));
575
+ return;
576
+ }
577
+
578
+ // Resolve target class (T-19-01: validate target class before any actor operation).
579
+ UClass* TargetClass = nullptr;
580
+ if (TargetClassName.StartsWith(TEXT("/")) || TargetClassName.Contains(TEXT("Blueprint'")))
581
+ {
582
+ // Blueprint class path -- use StaticLoadClass.
583
+ TargetClass = StaticLoadClass(AActor::StaticClass(), nullptr, *TargetClassName);
584
+ }
585
+ else
586
+ {
587
+ // Native class name -- search loaded classes.
588
+ TargetClass = FindObject<UClass>(ANY_PACKAGE, *TargetClassName);
589
+ if (!TargetClass)
590
+ {
591
+ // Try with the Actor suffix convention common in UE.
592
+ FString WithPrefix = FString::Printf(TEXT("/Script/Engine.%s"), *TargetClassName);
593
+ TargetClass = StaticLoadClass(AActor::StaticClass(), nullptr, *WithPrefix);
594
+ }
595
+ }
596
+
597
+ if (!TargetClass)
598
+ {
599
+ SendResponse(BuildSelErrorResponse(CorrId, TEXT("target_class_not_found")) + TEXT("\n"));
600
+ return;
601
+ }
602
+
603
+ // Validate target class is an actor class.
604
+ if (!TargetClass->IsChildOf(AActor::StaticClass()))
605
+ {
606
+ SendResponse(BuildSelErrorResponse(CorrId, TEXT("target_class_not_actor_subclass")) + TEXT("\n"));
607
+ return;
608
+ }
609
+
610
+ // Capture transform before conversion.
611
+ FTransform ActorTransform = FoundActor->GetActorTransform();
612
+ FVector Loc = ActorTransform.GetLocation();
613
+ FRotator Rot = ActorTransform.GetRotation().Rotator();
614
+ FVector Scale = ActorTransform.GetScale3D();
615
+
616
+ // Modify() before conversion write.
617
+ FoundActor->Modify();
618
+
619
+ // Use GEditor->ConvertActors to perform the conversion.
620
+ TArray<AActor*> ActorsToConvert;
621
+ ActorsToConvert.Add(FoundActor);
622
+
623
+ // ConvertActors signature: ConvertActors(const TArray<AActor*>&, UClass*, const TSet<FString>&, bool)
624
+ // The third parameter is a set of property names to preserve; pass empty set for default behavior.
625
+ TSet<FString> PropertyNamesToPreserve;
626
+ GEditor->ConvertActors(ActorsToConvert, TargetClass, PropertyNamesToPreserve, /*bUseStandardTransfer=*/true);
627
+
628
+ // Find the newly converted actor at the same location.
629
+ // ConvertActors destroys the original and spawns the new actor at the same transform.
630
+ // We locate it by searching for the target class at the original location.
631
+ AActor* NewActor = nullptr;
632
+ float BestDist = FLT_MAX;
633
+ for (TActorIterator<AActor> It(World); It; ++It)
634
+ {
635
+ AActor* Candidate = *It;
636
+ if (!Candidate || !Candidate->IsA(TargetClass))
637
+ {
638
+ continue;
639
+ }
640
+ float Dist = static_cast<float>(FVector::Dist(Candidate->GetActorLocation(), Loc));
641
+ if (Dist < BestDist)
642
+ {
643
+ BestDist = Dist;
644
+ NewActor = Candidate;
645
+ }
646
+ }
647
+
648
+ TSharedPtr<FJsonObject> LocObj = MakeShared<FJsonObject>();
649
+ LocObj->SetNumberField(TEXT("x"), static_cast<double>(Loc.X));
650
+ LocObj->SetNumberField(TEXT("y"), static_cast<double>(Loc.Y));
651
+ LocObj->SetNumberField(TEXT("z"), static_cast<double>(Loc.Z));
652
+
653
+ TSharedPtr<FJsonObject> RotObj = MakeShared<FJsonObject>();
654
+ RotObj->SetNumberField(TEXT("pitch"), static_cast<double>(Rot.Pitch));
655
+ RotObj->SetNumberField(TEXT("yaw"), static_cast<double>(Rot.Yaw));
656
+ RotObj->SetNumberField(TEXT("roll"), static_cast<double>(Rot.Roll));
657
+
658
+ TSharedPtr<FJsonObject> ScaleObj = MakeShared<FJsonObject>();
659
+ ScaleObj->SetNumberField(TEXT("x"), static_cast<double>(Scale.X));
660
+ ScaleObj->SetNumberField(TEXT("y"), static_cast<double>(Scale.Y));
661
+ ScaleObj->SetNumberField(TEXT("z"), static_cast<double>(Scale.Z));
662
+
663
+ TSharedPtr<FJsonObject> TransformObj = MakeShared<FJsonObject>();
664
+ TransformObj->SetObjectField(TEXT("location"), LocObj);
665
+ TransformObj->SetObjectField(TEXT("rotation"), RotObj);
666
+ TransformObj->SetObjectField(TEXT("scale"), ScaleObj);
667
+
668
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
669
+ Data->SetStringField(TEXT("original_label"), ActorLabel);
670
+ Data->SetStringField(TEXT("new_label"), NewActor ? NewActor->GetActorLabel() : ActorLabel);
671
+ Data->SetStringField(TEXT("new_class"), TargetClass->GetName());
672
+ Data->SetObjectField(TEXT("transform"), TransformObj);
673
+
674
+ SendResponse(BuildSelSuccessResponse(CorrId, Data) + TEXT("\n"));
675
+ });
676
+ });
677
+ }
@@ -0,0 +1,22 @@
1
+ // MCPSelectionCommands.h (Plan 19-01)
2
+ // Declares the registration function for all actor selection MCP command handlers.
3
+ // Handlers: selection.select, selection.get, selection.duplicate, selection.convert
4
+ //
5
+ // Call RegisterSelectionCommands(*Router) in MCPBridgeSubsystem::Initialize()
6
+ // BEFORE the TCP server starts accepting connections.
7
+
8
+ #pragma once
9
+
10
+ #include "MCPCommandRouter.h"
11
+
12
+ /**
13
+ * Register all four selection command handlers into the given router.
14
+ * Must be called on the game thread before connections arrive.
15
+ *
16
+ * Registered commands:
17
+ * selection.select -- select/deselect actors by name, label, or class filter (SEL-01)
18
+ * selection.get -- get current selection or apply all/none/invert bulk operation (SEL-02)
19
+ * selection.duplicate -- duplicate selected actors with optional position offset (SEL-03)
20
+ * selection.convert -- convert an actor to a different target class (SEL-04)
21
+ */
22
+ void RegisterSelectionCommands(FMCPCommandRouter& Router);