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,721 @@
1
+ // MCPSequencerCommands.cpp
2
+ // Implements five sequencer command handlers for the MCP bridge:
3
+ // sequencer.create -- create a ULevelSequence asset at the given path (SEQ-01)
4
+ // sequencer.tracks -- list all tracks (name, type, binding, section/key counts) (SEQ-02)
5
+ // sequencer.addTrack -- add a transform or float track bound to a named actor (SEQ-03)
6
+ // sequencer.addKey -- add a keyframe at a specific frame on a named track (SEQ-04)
7
+ // sequencer.playback -- play/pause/stop/scrub via ULevelSequenceEditorSubsystem (SEQ-05)
8
+ //
9
+ // All handlers run on the game thread via FMCPCommandRouter::Dispatch.
10
+ // Call Modify() on ULevelSequence before any mutation (Pitfall 5).
11
+ // asset_path is validated to start with "/Game/" or "/Engine/" before any
12
+ // StaticLoadObject call to prevent path traversal (T-13-01).
13
+
14
+ #include "MCPSequencerCommands.h"
15
+
16
+ #include "Editor.h"
17
+ #include "Engine/World.h"
18
+ #include "GameFramework/Actor.h"
19
+ #include "EngineUtils.h"
20
+
21
+ // LevelSequence / MovieScene headers
22
+ #include "LevelSequence.h"
23
+ #include "MovieScene.h"
24
+ #include "MovieSceneTrack.h"
25
+ #include "Tracks/MovieScene3DTransformTrack.h"
26
+ #include "Tracks/MovieSceneFloatTrack.h"
27
+ #include "Sections/MovieScene3DTransformSection.h"
28
+ #include "Sections/MovieSceneFloatSection.h"
29
+ #include "Channels/MovieSceneFloatChannel.h"
30
+ #include "Channels/MovieSceneDoubleChannel.h"
31
+
32
+ // Editor subsystem for playback control
33
+ #include "LevelSequenceEditorSubsystem.h"
34
+
35
+ // Asset creation
36
+ #include "AssetToolsModule.h"
37
+ #include "IAssetTools.h"
38
+ #include "Factories/LevelSequenceFactoryNew.h"
39
+ #include "PackageName.h"
40
+
41
+ // JSON
42
+ #include "Serialization/JsonSerializer.h"
43
+ #include "Serialization/JsonWriter.h"
44
+ #include "Dom/JsonObject.h"
45
+ #include "Dom/JsonValue.h"
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Internal helpers
49
+ // ---------------------------------------------------------------------------
50
+
51
+ /** Returns a JSON success response string (without trailing newline). */
52
+ static FString BuildSeqSuccessResponse(const FString& CorrId, TSharedPtr<FJsonObject> Data)
53
+ {
54
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
55
+ Obj->SetBoolField(TEXT("success"), true);
56
+ Obj->SetStringField(TEXT("correlationId"), CorrId);
57
+ if (Data.IsValid())
58
+ {
59
+ Obj->SetObjectField(TEXT("data"), Data);
60
+ }
61
+
62
+ FString Output;
63
+ TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
64
+ FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
65
+ return Output;
66
+ }
67
+
68
+ /** Returns a JSON error response string (without trailing newline). */
69
+ static FString BuildSeqErrorResponse(const FString& CorrId, const FString& Error)
70
+ {
71
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
72
+ Obj->SetBoolField(TEXT("success"), false);
73
+ Obj->SetStringField(TEXT("correlationId"), CorrId);
74
+ Obj->SetStringField(TEXT("error"), Error);
75
+
76
+ FString Output;
77
+ TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
78
+ FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
79
+ return Output;
80
+ }
81
+
82
+ /**
83
+ * Validate that asset_path starts with "/Game/" or "/Engine/" to prevent
84
+ * path traversal attacks (T-13-01).
85
+ */
86
+ static bool IsValidAssetPath(const FString& AssetPath)
87
+ {
88
+ return AssetPath.StartsWith(TEXT("/Game/")) || AssetPath.StartsWith(TEXT("/Engine/"));
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // RegisterSequencerCommands
93
+ // ---------------------------------------------------------------------------
94
+
95
+ void RegisterSequencerCommands(FMCPCommandRouter& Router)
96
+ {
97
+ // -----------------------------------------------------------------------
98
+ // sequencer.create (SEQ-01)
99
+ // Creates a new ULevelSequence asset at the specified content-browser path.
100
+ // Returns the asset path and a "created" boolean.
101
+ // -----------------------------------------------------------------------
102
+ Router.RegisterHandler(TEXT("sequencer.create"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
103
+ {
104
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
105
+
106
+ // Extract payload.
107
+ TSharedPtr<FJsonObject> Payload;
108
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
109
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
110
+ {
111
+ Payload = (*PayloadVal)->AsObject();
112
+ }
113
+
114
+ FString AssetPath;
115
+ if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
116
+ {
117
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("missing_asset_path")) + TEXT("\n"));
118
+ return;
119
+ }
120
+
121
+ // Validate path prefix (T-13-01).
122
+ if (!IsValidAssetPath(AssetPath))
123
+ {
124
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("invalid_asset_path")) + TEXT("\n"));
125
+ return;
126
+ }
127
+
128
+ // Split asset_path into package path + asset name.
129
+ const FString PackagePath = FPackageName::GetLongPackagePath(AssetPath);
130
+ const FString AssetName = FPackageName::GetLongPackageAssetName(AssetPath);
131
+
132
+ if (AssetName.IsEmpty())
133
+ {
134
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("invalid_asset_path")) + TEXT("\n"));
135
+ return;
136
+ }
137
+
138
+ // Get AssetTools module.
139
+ IAssetTools& AT = FModuleManager::LoadModuleChecked<FAssetToolsModule>(TEXT("AssetTools")).Get();
140
+
141
+ // Create a ULevelSequenceFactoryNew and make the asset.
142
+ ULevelSequenceFactoryNew* Factory = NewObject<ULevelSequenceFactoryNew>();
143
+ UObject* Asset = AT.CreateAsset(AssetName, PackagePath, ULevelSequence::StaticClass(), Factory);
144
+
145
+ if (!Asset)
146
+ {
147
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("create_failed")) + TEXT("\n"));
148
+ return;
149
+ }
150
+
151
+ ULevelSequence* Seq = Cast<ULevelSequence>(Asset);
152
+ if (!Seq)
153
+ {
154
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("create_failed")) + TEXT("\n"));
155
+ return;
156
+ }
157
+
158
+ // Optionally open the sequence in the editor.
159
+ if (GEditor)
160
+ {
161
+ ULevelSequenceEditorSubsystem* EdSub = GEditor->GetEditorSubsystem<ULevelSequenceEditorSubsystem>();
162
+ if (EdSub)
163
+ {
164
+ EdSub->OpenLevelSequence(Seq);
165
+ }
166
+ else
167
+ {
168
+ UE_LOG(LogTemp, Warning, TEXT("[MCPSequencer] ULevelSequenceEditorSubsystem not available; sequence created but not opened in editor."));
169
+ }
170
+ }
171
+
172
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
173
+ Data->SetStringField(TEXT("asset_path"), AssetPath);
174
+ Data->SetBoolField(TEXT("created"), true);
175
+
176
+ SendResponse(BuildSeqSuccessResponse(CorrId, Data) + TEXT("\n"));
177
+ });
178
+
179
+ // -----------------------------------------------------------------------
180
+ // sequencer.tracks (SEQ-02)
181
+ // Returns every track in the sequence: name, track type, binding name,
182
+ // section count, and key count.
183
+ // -----------------------------------------------------------------------
184
+ Router.RegisterHandler(TEXT("sequencer.tracks"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
185
+ {
186
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
187
+
188
+ // Extract payload.
189
+ TSharedPtr<FJsonObject> Payload;
190
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
191
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
192
+ {
193
+ Payload = (*PayloadVal)->AsObject();
194
+ }
195
+
196
+ FString AssetPath;
197
+ if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
198
+ {
199
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("missing_asset_path")) + TEXT("\n"));
200
+ return;
201
+ }
202
+
203
+ // Validate path prefix (T-13-01).
204
+ if (!IsValidAssetPath(AssetPath))
205
+ {
206
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("invalid_asset_path")) + TEXT("\n"));
207
+ return;
208
+ }
209
+
210
+ // Load the sequence asset.
211
+ ULevelSequence* Seq = Cast<ULevelSequence>(StaticLoadObject(ULevelSequence::StaticClass(), nullptr, *AssetPath));
212
+ if (!Seq)
213
+ {
214
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("sequence_not_found")) + TEXT("\n"));
215
+ return;
216
+ }
217
+
218
+ UMovieScene* Scene = Seq->GetMovieScene();
219
+ if (!Scene)
220
+ {
221
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("sequence_has_no_scene")) + TEXT("\n"));
222
+ return;
223
+ }
224
+
225
+ // Build a map from UMovieSceneTrack* -> binding name by iterating bindings first.
226
+ TMap<UMovieSceneTrack*, FString> TrackBindingNames;
227
+ for (const FMovieSceneBinding& Binding : Scene->GetBindings())
228
+ {
229
+ const FString BindingName = Binding.GetName();
230
+ for (UMovieSceneTrack* Track : Binding.GetTracks())
231
+ {
232
+ if (Track)
233
+ {
234
+ TrackBindingNames.Add(Track, BindingName);
235
+ }
236
+ }
237
+ }
238
+
239
+ // Iterate all tracks and build the JSON array.
240
+ TArray<TSharedPtr<FJsonValue>> TracksArray;
241
+ for (UMovieSceneTrack* Track : Scene->GetAllTracks())
242
+ {
243
+ if (!Track)
244
+ {
245
+ continue;
246
+ }
247
+
248
+ const FString TrackName = Track->GetDisplayName().ToString();
249
+ const FString TrackType = Track->GetClass()->GetName();
250
+ const int32 SectionCount = Track->GetAllSections().Num();
251
+
252
+ // Sum key counts across all sections using GetChannelProxy.
253
+ int32 KeyCount = 0;
254
+ for (UMovieSceneSection* Section : Track->GetAllSections())
255
+ {
256
+ if (Section)
257
+ {
258
+ FMovieSceneChannelProxy& ChannelProxy = Section->GetChannelProxy();
259
+ // Iterate over all channel types we know about.
260
+ // Float channels.
261
+ for (const FMovieSceneFloatChannel* Ch : ChannelProxy.GetChannels<FMovieSceneFloatChannel>())
262
+ {
263
+ if (Ch)
264
+ {
265
+ KeyCount += Ch->GetNumKeys();
266
+ }
267
+ }
268
+ // Double channels (used by 3D transform tracks).
269
+ for (const FMovieSceneDoubleChannel* Ch : ChannelProxy.GetChannels<FMovieSceneDoubleChannel>())
270
+ {
271
+ if (Ch)
272
+ {
273
+ KeyCount += Ch->GetNumKeys();
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+ // Resolve binding name (or "unbound" for master tracks).
280
+ FString BindingName = TEXT("unbound");
281
+ if (const FString* Found = TrackBindingNames.Find(Track))
282
+ {
283
+ BindingName = *Found;
284
+ }
285
+
286
+ TSharedPtr<FJsonObject> TrackObj = MakeShared<FJsonObject>();
287
+ TrackObj->SetStringField(TEXT("name"), TrackName);
288
+ TrackObj->SetStringField(TEXT("track_type"), TrackType);
289
+ TrackObj->SetStringField(TEXT("binding"), BindingName);
290
+ TrackObj->SetNumberField(TEXT("section_count"), static_cast<double>(SectionCount));
291
+ TrackObj->SetNumberField(TEXT("key_count"), static_cast<double>(KeyCount));
292
+
293
+ TracksArray.Add(MakeShared<FJsonValueObject>(TrackObj));
294
+ }
295
+
296
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
297
+ Data->SetArrayField(TEXT("tracks"), TracksArray);
298
+ Data->SetNumberField(TEXT("count"), static_cast<double>(TracksArray.Num()));
299
+ Data->SetStringField(TEXT("asset_path"), AssetPath);
300
+
301
+ SendResponse(BuildSeqSuccessResponse(CorrId, Data) + TEXT("\n"));
302
+ });
303
+
304
+ // -----------------------------------------------------------------------
305
+ // sequencer.addTrack (SEQ-03)
306
+ // Adds a transform or float property track bound to a named actor.
307
+ // Returns the track type and the new binding GUID.
308
+ // -----------------------------------------------------------------------
309
+ Router.RegisterHandler(TEXT("sequencer.addTrack"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
310
+ {
311
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
312
+
313
+ // Extract payload.
314
+ TSharedPtr<FJsonObject> Payload;
315
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
316
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
317
+ {
318
+ Payload = (*PayloadVal)->AsObject();
319
+ }
320
+
321
+ FString AssetPath;
322
+ if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
323
+ {
324
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("missing_asset_path")) + TEXT("\n"));
325
+ return;
326
+ }
327
+
328
+ FString ActorLabel;
329
+ if (!Payload->TryGetStringField(TEXT("actor_label"), ActorLabel) || ActorLabel.IsEmpty())
330
+ {
331
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("missing_actor_label")) + TEXT("\n"));
332
+ return;
333
+ }
334
+
335
+ FString TrackType;
336
+ if (!Payload->TryGetStringField(TEXT("track_type"), TrackType) || TrackType.IsEmpty())
337
+ {
338
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("missing_track_type")) + TEXT("\n"));
339
+ return;
340
+ }
341
+
342
+ // Validate track type.
343
+ if (TrackType != TEXT("transform") && TrackType != TEXT("float"))
344
+ {
345
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("invalid_track_type")) + TEXT("\n"));
346
+ return;
347
+ }
348
+
349
+ // Validate path prefix (T-13-01).
350
+ if (!IsValidAssetPath(AssetPath))
351
+ {
352
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("invalid_asset_path")) + TEXT("\n"));
353
+ return;
354
+ }
355
+
356
+ // Load the sequence asset.
357
+ ULevelSequence* Seq = Cast<ULevelSequence>(StaticLoadObject(ULevelSequence::StaticClass(), nullptr, *AssetPath));
358
+ if (!Seq)
359
+ {
360
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("sequence_not_found")) + TEXT("\n"));
361
+ return;
362
+ }
363
+
364
+ UMovieScene* Scene = Seq->GetMovieScene();
365
+ if (!Scene)
366
+ {
367
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("sequence_has_no_scene")) + TEXT("\n"));
368
+ return;
369
+ }
370
+
371
+ // Find actor by label in the editor world.
372
+ UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
373
+ if (!World)
374
+ {
375
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
376
+ return;
377
+ }
378
+
379
+ AActor* TargetActor = nullptr;
380
+ for (TActorIterator<AActor> It(World); It; ++It)
381
+ {
382
+ if ((*It)->GetActorLabel() == ActorLabel)
383
+ {
384
+ TargetActor = *It;
385
+ break;
386
+ }
387
+ }
388
+
389
+ if (!TargetActor)
390
+ {
391
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("actor_not_found")) + TEXT("\n"));
392
+ return;
393
+ }
394
+
395
+ // Mark the sequence for modification before any mutation (Pitfall 5).
396
+ Seq->Modify();
397
+
398
+ // Create a possessable binding for the actor.
399
+ FGuid BindingGuid = Scene->AddPossessable(TargetActor->GetActorLabel(), TargetActor->GetClass());
400
+ Seq->BindPossessableObject(BindingGuid, *TargetActor, TargetActor->GetWorld());
401
+
402
+ // Add the requested track type.
403
+ if (TrackType == TEXT("transform"))
404
+ {
405
+ Scene->AddTrack<UMovieScene3DTransformTrack>(BindingGuid);
406
+ }
407
+ else // "float"
408
+ {
409
+ Scene->AddTrack<UMovieSceneFloatTrack>(BindingGuid);
410
+ }
411
+
412
+ // Mark the asset package dirty so UE knows it needs saving.
413
+ Seq->MarkPackageDirty();
414
+
415
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
416
+ Data->SetStringField(TEXT("asset_path"), AssetPath);
417
+ Data->SetStringField(TEXT("actor_label"), ActorLabel);
418
+ Data->SetStringField(TEXT("track_type"), TrackType);
419
+ Data->SetStringField(TEXT("binding_guid"), BindingGuid.ToString());
420
+
421
+ SendResponse(BuildSeqSuccessResponse(CorrId, Data) + TEXT("\n"));
422
+ });
423
+
424
+ // -----------------------------------------------------------------------
425
+ // sequencer.addKey (SEQ-04)
426
+ // Adds a keyframe at a specific frame time on a named track type.
427
+ // For float tracks: sets the specified value at the frame.
428
+ // For transform tracks: sets an identity key (pos/rot/scale all zero).
429
+ // Guards against out-of-range channel index dereferences (T-13-04).
430
+ // -----------------------------------------------------------------------
431
+ Router.RegisterHandler(TEXT("sequencer.addKey"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
432
+ {
433
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
434
+
435
+ // Extract payload.
436
+ TSharedPtr<FJsonObject> Payload;
437
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
438
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
439
+ {
440
+ Payload = (*PayloadVal)->AsObject();
441
+ }
442
+
443
+ FString AssetPath;
444
+ if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
445
+ {
446
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("missing_asset_path")) + TEXT("\n"));
447
+ return;
448
+ }
449
+
450
+ FString TrackType;
451
+ if (!Payload->TryGetStringField(TEXT("track_type"), TrackType) || TrackType.IsEmpty())
452
+ {
453
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("missing_track_type")) + TEXT("\n"));
454
+ return;
455
+ }
456
+
457
+ // frame is required.
458
+ double FrameDouble = 0.0;
459
+ if (!Payload->TryGetNumberField(TEXT("frame"), FrameDouble))
460
+ {
461
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("missing_frame")) + TEXT("\n"));
462
+ return;
463
+ }
464
+ const int32 FrameNum = static_cast<int32>(FrameDouble);
465
+
466
+ // value is optional, default 0.0.
467
+ double Value = 0.0;
468
+ Payload->TryGetNumberField(TEXT("value"), Value);
469
+
470
+ // Validate path prefix (T-13-01).
471
+ if (!IsValidAssetPath(AssetPath))
472
+ {
473
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("invalid_asset_path")) + TEXT("\n"));
474
+ return;
475
+ }
476
+
477
+ // Load the sequence asset.
478
+ ULevelSequence* Seq = Cast<ULevelSequence>(StaticLoadObject(ULevelSequence::StaticClass(), nullptr, *AssetPath));
479
+ if (!Seq)
480
+ {
481
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("sequence_not_found")) + TEXT("\n"));
482
+ return;
483
+ }
484
+
485
+ UMovieScene* Scene = Seq->GetMovieScene();
486
+ if (!Scene)
487
+ {
488
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("sequence_has_no_scene")) + TEXT("\n"));
489
+ return;
490
+ }
491
+
492
+ // Find the first track matching the requested track_type class name.
493
+ // "transform" -> UMovieScene3DTransformTrack, "float" -> UMovieSceneFloatTrack.
494
+ UMovieSceneTrack* TargetTrack = nullptr;
495
+ for (UMovieSceneTrack* Track : Scene->GetAllTracks())
496
+ {
497
+ if (!Track)
498
+ {
499
+ continue;
500
+ }
501
+ const FString ClassName = Track->GetClass()->GetName();
502
+ if (TrackType == TEXT("transform") && ClassName == TEXT("MovieScene3DTransformTrack"))
503
+ {
504
+ TargetTrack = Track;
505
+ break;
506
+ }
507
+ if (TrackType == TEXT("float") && ClassName == TEXT("MovieSceneFloatTrack"))
508
+ {
509
+ TargetTrack = Track;
510
+ break;
511
+ }
512
+ }
513
+
514
+ if (!TargetTrack)
515
+ {
516
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("track_not_found")) + TEXT("\n"));
517
+ return;
518
+ }
519
+
520
+ // Mark the sequence for modification before any mutation (Pitfall 5).
521
+ Seq->Modify();
522
+
523
+ // Get or create the first section (T-13-04: guard Num() > 0).
524
+ UMovieSceneSection* Section = nullptr;
525
+ if (TargetTrack->GetAllSections().Num() > 0)
526
+ {
527
+ Section = TargetTrack->GetAllSections()[0];
528
+ }
529
+ else
530
+ {
531
+ Section = TargetTrack->CreateNewSection();
532
+ if (Section)
533
+ {
534
+ Section->SetRange(TRange<FFrameNumber>::All());
535
+ TargetTrack->AddSection(*Section);
536
+ }
537
+ }
538
+
539
+ if (!Section)
540
+ {
541
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("section_creation_failed")) + TEXT("\n"));
542
+ return;
543
+ }
544
+
545
+ const FFrameNumber KeyFrame(FrameNum);
546
+
547
+ if (TrackType == TEXT("float"))
548
+ {
549
+ UMovieSceneFloatSection* FloatSection = Cast<UMovieSceneFloatSection>(Section);
550
+ if (!FloatSection)
551
+ {
552
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("section_type_mismatch")) + TEXT("\n"));
553
+ return;
554
+ }
555
+
556
+ // Guard channel index dereference (T-13-04).
557
+ TArrayView<FMovieSceneFloatChannel*> Channels = FloatSection->GetChannelProxy().GetChannels<FMovieSceneFloatChannel>();
558
+ if (Channels.Num() > 0 && Channels[0])
559
+ {
560
+ Channels[0]->AddLinearKey(KeyFrame, static_cast<float>(Value));
561
+ }
562
+ else
563
+ {
564
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("no_float_channel")) + TEXT("\n"));
565
+ return;
566
+ }
567
+ }
568
+ else // "transform"
569
+ {
570
+ UMovieScene3DTransformSection* TransformSection = Cast<UMovieScene3DTransformSection>(Section);
571
+ if (!TransformSection)
572
+ {
573
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("section_type_mismatch")) + TEXT("\n"));
574
+ return;
575
+ }
576
+
577
+ // UMovieScene3DTransformSection has 9 FMovieSceneDoubleChannel:
578
+ // [0-2] translation XYZ, [3-5] rotation XYZ, [6-8] scale XYZ.
579
+ // Use identity values: translation=0, rotation=0, scale=0
580
+ // (caller can supply meaningful value via 'value' field for uniform application).
581
+ TArrayView<FMovieSceneDoubleChannel*> Channels = TransformSection->GetChannelProxy().GetChannels<FMovieSceneDoubleChannel>();
582
+
583
+ // Guard channel count (T-13-04): expect 9, but be safe.
584
+ const int32 NumChannels = Channels.Num();
585
+ for (int32 i = 0; i < NumChannels && i < 9; ++i)
586
+ {
587
+ if (Channels[i])
588
+ {
589
+ // Scale channels (indices 6-8) default to 1.0 (identity scale),
590
+ // translation and rotation channels default to 0.0 (or caller value).
591
+ const double ChValue = (i >= 6) ? 1.0 : (i < 3 ? Value : 0.0);
592
+ Channels[i]->AddLinearKey(KeyFrame, ChValue);
593
+ }
594
+ }
595
+ }
596
+
597
+ // Mark the asset package dirty.
598
+ Seq->MarkPackageDirty();
599
+
600
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
601
+ Data->SetStringField(TEXT("asset_path"), AssetPath);
602
+ Data->SetNumberField(TEXT("frame"), static_cast<double>(FrameNum));
603
+ Data->SetStringField(TEXT("track_type"), TrackType);
604
+ Data->SetBoolField(TEXT("added"), true);
605
+
606
+ SendResponse(BuildSeqSuccessResponse(CorrId, Data) + TEXT("\n"));
607
+ });
608
+
609
+ // -----------------------------------------------------------------------
610
+ // sequencer.playback (SEQ-05)
611
+ // Routes play/pause/stop/scrub to ULevelSequenceEditorSubsystem.
612
+ // Returns the current playback frame after the action.
613
+ // -----------------------------------------------------------------------
614
+ Router.RegisterHandler(TEXT("sequencer.playback"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
615
+ {
616
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
617
+
618
+ // Extract payload.
619
+ TSharedPtr<FJsonObject> Payload;
620
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
621
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
622
+ {
623
+ Payload = (*PayloadVal)->AsObject();
624
+ }
625
+
626
+ FString AssetPath;
627
+ if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
628
+ {
629
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("missing_asset_path")) + TEXT("\n"));
630
+ return;
631
+ }
632
+
633
+ FString Action;
634
+ if (!Payload->TryGetStringField(TEXT("action"), Action) || Action.IsEmpty())
635
+ {
636
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("missing_action")) + TEXT("\n"));
637
+ return;
638
+ }
639
+
640
+ // Validate action.
641
+ if (Action != TEXT("play") && Action != TEXT("pause") && Action != TEXT("stop") && Action != TEXT("scrub"))
642
+ {
643
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("invalid_action")) + TEXT("\n"));
644
+ return;
645
+ }
646
+
647
+ // For "scrub", extract target frame (default 0).
648
+ double FrameDouble = 0.0;
649
+ if (Action == TEXT("scrub"))
650
+ {
651
+ Payload->TryGetNumberField(TEXT("frame"), FrameDouble);
652
+ }
653
+ const int32 FrameNum = static_cast<int32>(FrameDouble);
654
+
655
+ // Validate path prefix (T-13-01).
656
+ if (!IsValidAssetPath(AssetPath))
657
+ {
658
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("invalid_asset_path")) + TEXT("\n"));
659
+ return;
660
+ }
661
+
662
+ // Get the editor subsystem.
663
+ if (!GEditor)
664
+ {
665
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("subsystem_unavailable")) + TEXT("\n"));
666
+ return;
667
+ }
668
+
669
+ ULevelSequenceEditorSubsystem* EdSub = GEditor->GetEditorSubsystem<ULevelSequenceEditorSubsystem>();
670
+ if (!EdSub)
671
+ {
672
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("subsystem_unavailable")) + TEXT("\n"));
673
+ return;
674
+ }
675
+
676
+ // Load the sequence asset.
677
+ ULevelSequence* Seq = Cast<ULevelSequence>(StaticLoadObject(ULevelSequence::StaticClass(), nullptr, *AssetPath));
678
+ if (!Seq)
679
+ {
680
+ SendResponse(BuildSeqErrorResponse(CorrId, TEXT("sequence_not_found")) + TEXT("\n"));
681
+ return;
682
+ }
683
+
684
+ // Ensure the sequence is open in the editor before controlling playback.
685
+ EdSub->OpenLevelSequence(Seq);
686
+
687
+ // Execute the requested action.
688
+ if (Action == TEXT("play"))
689
+ {
690
+ EdSub->Play();
691
+ }
692
+ else if (Action == TEXT("pause"))
693
+ {
694
+ EdSub->Pause();
695
+ }
696
+ else if (Action == TEXT("stop"))
697
+ {
698
+ EdSub->Stop();
699
+ }
700
+ else if (Action == TEXT("scrub"))
701
+ {
702
+ EdSub->SetCurrentTime(FFrameTime(FFrameNumber(FrameNum)));
703
+ }
704
+
705
+ // Get current frame from the sequence player.
706
+ int32 CurrentFrame = FrameNum; // default to requested frame for scrub
707
+ IMovieScenePlayer* Player = EdSub->GetSequencePlayer();
708
+ if (Player)
709
+ {
710
+ FFrameTime LocalTime = Player->GetCurrentLocalTime(*Seq).Time;
711
+ CurrentFrame = LocalTime.GetFrame().Value;
712
+ }
713
+
714
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
715
+ Data->SetStringField(TEXT("action"), Action);
716
+ Data->SetStringField(TEXT("asset_path"), AssetPath);
717
+ Data->SetNumberField(TEXT("current_frame"), static_cast<double>(CurrentFrame));
718
+
719
+ SendResponse(BuildSeqSuccessResponse(CorrId, Data) + TEXT("\n"));
720
+ });
721
+ }