ue-mcp 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/context-cli.js +16 -16
  2. package/dist/engine-observer.js +68 -68
  3. package/dist/flow/guard.d.ts +26 -37
  4. package/dist/flow/guard.js +40 -25
  5. package/dist/flow/guard.js.map +1 -1
  6. package/dist/flow/guarded-bridge.d.ts +0 -10
  7. package/dist/flow/guarded-bridge.js +18 -23
  8. package/dist/flow/guarded-bridge.js.map +1 -1
  9. package/dist/flow/schema.d.ts +276 -16
  10. package/dist/flow/task-guards.d.ts +1 -1
  11. package/dist/flow/task-guards.js +44 -68
  12. package/dist/flow/task-guards.js.map +1 -1
  13. package/dist/instructions.js +130 -130
  14. package/dist/plugin-cli.js +299 -299
  15. package/dist/tool-counts.json +2 -2
  16. package/package.json +2 -2
  17. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/EngineStatusHooks.cpp +76 -76
  18. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/EngineStatusHooks.h +27 -27
  19. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerRegistry.cpp +128 -128
  20. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerRegistry.h +63 -63
  21. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRig.cpp +230 -230
  22. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +323 -323
  23. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Internal.h +20 -20
  24. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/ChooserHandlers_Nested.cpp +537 -537
  25. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DemoHandlers.h +77 -77
  26. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +353 -353
  27. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.h +18 -18
  28. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_InputRuntime.cpp +246 -246
  29. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +597 -597
  30. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +51 -51
  31. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +878 -878
  32. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +449 -449
  33. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Function.cpp +275 -275
  34. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Graph.cpp +551 -551
  35. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NetworkingHandlers.cpp +531 -531
  36. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NetworkingHandlers.h +28 -28
  37. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +677 -677
  38. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +822 -822
  39. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.h +22 -22
  40. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +218 -218
  41. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.h +17 -17
  42. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/VolumeHelpers_Internal.cpp +46 -46
  43. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/VolumeHelpers_Internal.h +23 -23
  44. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/MCPExternalRegistry.cpp +83 -83
  45. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/UE_MCP_BridgeModule.h +14 -14
  46. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/UE_MCP_BridgeStatusModule.cpp +37 -37
  47. package/dist/manifest-signature.d.ts +0 -1
  48. package/dist/manifest-signature.js +0 -53
  49. package/dist/manifest-signature.js.map +0 -1
  50. package/dist/pie/schema.d.ts +0 -791
  51. package/dist/pie/schema.js +0 -151
  52. package/dist/pie/schema.js.map +0 -1
  53. package/dist/watch-cli.js.map +0 -1
  54. /package/plugin/ue_mcp_bridge/{ue_mcp_bridge.uplugin → UE_MCP_Bridge.uplugin} +0 -0
@@ -1,822 +1,822 @@
1
- #include "SequencerHandlers.h"
2
- #include "HandlerRegistry.h"
3
- #include "HandlerUtils.h"
4
-
5
- #include "LevelSequenceEditorBlueprintLibrary.h"
6
- #include "MovieSceneSequencePlayer.h"
7
-
8
- #include "Subsystems/AssetEditorSubsystem.h"
9
- #include "HandlerAssetCreate.h"
10
- #include "LevelSequence.h"
11
- #include "LevelSequenceActor.h"
12
- // LevelSequenceFactoryNew may not be available; use AssetTools directly
13
- #include "MovieScene.h"
14
- #include "MovieSceneTrack.h"
15
- #include "MovieSceneSection.h"
16
- #include "Tracks/MovieSceneFloatTrack.h"
17
- #include "Tracks/MovieScene3DTransformTrack.h"
18
- #include "Tracks/MovieSceneSkeletalAnimationTrack.h"
19
- #include "Tracks/MovieSceneCameraCutTrack.h"
20
- #include "Tracks/MovieScene3DAttachTrack.h"
21
- #include "Sections/MovieScene3DAttachSection.h"
22
- #include "Sections/MovieScene3DTransformSection.h"
23
- #include "Sections/MovieSceneCameraCutSection.h"
24
- #include "Channels/MovieSceneDoubleChannel.h"
25
- #include "Channels/MovieSceneFloatChannel.h"
26
- #include "Channels/MovieSceneChannelProxy.h"
27
- #include "Tracks/MovieSceneAudioTrack.h"
28
- #include "Tracks/MovieSceneEventTrack.h"
29
- #include "Tracks/MovieSceneFadeTrack.h"
30
- #include "AssetRegistry/AssetRegistryModule.h"
31
- #include "EditorScriptingUtilities/Public/EditorAssetLibrary.h"
32
- #include "UObject/Package.h"
33
- #include "Editor.h"
34
- #include "Engine/World.h"
35
- #include "EngineUtils.h"
36
- #include "GameFramework/Actor.h"
37
- #include "Dom/JsonObject.h"
38
- #include "Dom/JsonValue.h"
39
-
40
- void FSequencerHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
41
- {
42
- Registry.RegisterHandler(TEXT("create_level_sequence"), &CreateLevelSequence);
43
- Registry.RegisterHandler(TEXT("get_sequence_info"), &ReadSequenceInfo);
44
- Registry.RegisterHandler(TEXT("add_sequence_track"), &AddTrack);
45
- Registry.RegisterHandler(TEXT("play_sequence"), &SequenceControl);
46
- Registry.RegisterHandler(TEXT("set_sequence_playback_range"), &SetPlaybackRange);
47
- Registry.RegisterHandler(TEXT("add_sequence_section"), &AddSection);
48
- Registry.RegisterHandler(TEXT("set_sequence_keyframes"), &SetKeyframes);
49
- }
50
-
51
- // ─── #548 sequencer authoring helpers ────────────────────────────────
52
- namespace
53
- {
54
- ULevelSequence* LoadSequence(const TSharedPtr<FJsonObject>& Params, FString& OutPath, FString& OutError)
55
- {
56
- if (!Params->TryGetStringField(TEXT("sequencePath"), OutPath))
57
- {
58
- if (!Params->TryGetStringField(TEXT("assetPath"), OutPath) && !Params->TryGetStringField(TEXT("path"), OutPath))
59
- {
60
- OutError = TEXT("Missing 'sequencePath' parameter");
61
- return nullptr;
62
- }
63
- }
64
- ULevelSequence* Seq = Cast<ULevelSequence>(UEditorAssetLibrary::LoadAsset(OutPath));
65
- if (!Seq) { OutError = FString::Printf(TEXT("LevelSequence not found: %s"), *OutPath); return nullptr; }
66
- return Seq;
67
- }
68
-
69
- // Resolve (creating if needed) the actor's possessable binding GUID.
70
- bool ResolveActorBinding(ULevelSequence* Sequence, UMovieScene* MovieScene, const FString& ActorLabel, FGuid& OutGuid, FString& OutError)
71
- {
72
- UWorld* World = GetEditorWorld();
73
- if (!World) { OutError = TEXT("No editor world available"); return false; }
74
- AActor* TargetActor = FindActorByLabel(World, ActorLabel);
75
- if (!TargetActor) { OutError = FString::Printf(TEXT("Actor not found: %s"), *ActorLabel); return false; }
76
-
77
- for (int32 i = 0; i < MovieScene->GetPossessableCount(); ++i)
78
- {
79
- const FMovieScenePossessable& P = MovieScene->GetPossessable(i);
80
- if (P.GetName() == ActorLabel || P.GetName() == TargetActor->GetName()) { OutGuid = P.GetGuid(); return true; }
81
- }
82
- OutGuid = MovieScene->AddPossessable(ActorLabel, TargetActor->GetClass());
83
- Sequence->BindPossessableObject(OutGuid, *TargetActor, World);
84
- return true;
85
- }
86
-
87
- UClass* ResolveTrackClass(const FString& TrackType)
88
- {
89
- if (TrackType.Equals(TEXT("Transform"), ESearchCase::IgnoreCase)) return UMovieScene3DTransformTrack::StaticClass();
90
- if (TrackType.Equals(TEXT("Float"), ESearchCase::IgnoreCase)) return UMovieSceneFloatTrack::StaticClass();
91
- if (TrackType.Equals(TEXT("SkeletalAnimation"), ESearchCase::IgnoreCase)) return UMovieSceneSkeletalAnimationTrack::StaticClass();
92
- if (TrackType.Equals(TEXT("CameraCut"), ESearchCase::IgnoreCase)) return UMovieSceneCameraCutTrack::StaticClass();
93
- if (TrackType.Equals(TEXT("Audio"), ESearchCase::IgnoreCase)) return UMovieSceneAudioTrack::StaticClass();
94
- if (TrackType.Equals(TEXT("Event"), ESearchCase::IgnoreCase)) return UMovieSceneEventTrack::StaticClass();
95
- if (TrackType.Equals(TEXT("Fade"), ESearchCase::IgnoreCase)) return UMovieSceneFadeTrack::StaticClass();
96
- return nullptr;
97
- }
98
-
99
- // Map a friendly channel name to the canonical transform channel name.
100
- FString CanonicalTransformChannel(const FString& In)
101
- {
102
- const FString L = In.ToLower();
103
- if (L == TEXT("x") || L == TEXT("location.x")) return TEXT("Location.X");
104
- if (L == TEXT("y") || L == TEXT("location.y")) return TEXT("Location.Y");
105
- if (L == TEXT("z") || L == TEXT("location.z")) return TEXT("Location.Z");
106
- if (L == TEXT("roll") || L == TEXT("rotation.x")) return TEXT("Rotation.X");
107
- if (L == TEXT("pitch") || L == TEXT("rotation.y")) return TEXT("Rotation.Y");
108
- if (L == TEXT("yaw") || L == TEXT("rotation.z")) return TEXT("Rotation.Z");
109
- if (L == TEXT("scale.x")) return TEXT("Scale.X");
110
- if (L == TEXT("scale.y")) return TEXT("Scale.Y");
111
- if (L == TEXT("scale.z")) return TEXT("Scale.Z");
112
- return In;
113
- }
114
- }
115
-
116
- TSharedPtr<FJsonValue> FSequencerHandlers::CreateLevelSequence(const TSharedPtr<FJsonObject>& Params)
117
- {
118
- FString Name;
119
- if (auto Err = RequireString(Params, TEXT("name"), Name)) return Err;
120
-
121
- FString PackagePath = OptionalString(Params, TEXT("packagePath"), TEXT("/Game/Cinematics"));
122
- const FString OnConflict = OptionalString(Params, TEXT("onConflict"), TEXT("skip"));
123
-
124
- auto Created = MCPCreateAssetIdempotentNewObject<ULevelSequence>(Name, PackagePath, OnConflict, TEXT("LevelSequence"));
125
- if (Created.EarlyReturn) return Created.EarlyReturn;
126
- ULevelSequence* NewSequence = Created.Asset;
127
- NewSequence->Initialize();
128
-
129
- auto Result = MCPSuccess();
130
- MCPSetCreated(Result);
131
- Result->SetStringField(TEXT("name"), Name);
132
- Result->SetStringField(TEXT("path"), NewSequence->GetPathName());
133
- Result->SetStringField(TEXT("packagePath"), PackagePath + TEXT("/") + Name);
134
- MCPSetDeleteAssetRollback(Result, NewSequence->GetPathName());
135
-
136
- return MCPResult(Result);
137
- }
138
-
139
- TSharedPtr<FJsonValue> FSequencerHandlers::ReadSequenceInfo(const TSharedPtr<FJsonObject>& Params)
140
- {
141
- FString AssetPath;
142
- if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
143
-
144
- UObject* LoadedAsset = UEditorAssetLibrary::LoadAsset(AssetPath);
145
- ULevelSequence* Sequence = Cast<ULevelSequence>(LoadedAsset);
146
- if (!Sequence)
147
- {
148
- return MCPError(FString::Printf(TEXT("Failed to load LevelSequence at '%s'"), *AssetPath));
149
- }
150
-
151
- UMovieScene* MovieScene = Sequence->GetMovieScene();
152
- if (!MovieScene)
153
- {
154
- return MCPError(TEXT("LevelSequence has no MovieScene"));
155
- }
156
-
157
- auto Result = MCPSuccess();
158
- Result->SetStringField(TEXT("name"), Sequence->GetName());
159
- Result->SetStringField(TEXT("path"), Sequence->GetPathName());
160
-
161
- // Display rate
162
- FFrameRate DisplayRate = MovieScene->GetDisplayRate();
163
- TSharedPtr<FJsonObject> DisplayRateObj = MakeShared<FJsonObject>();
164
- DisplayRateObj->SetNumberField(TEXT("numerator"), DisplayRate.Numerator);
165
- DisplayRateObj->SetNumberField(TEXT("denominator"), DisplayRate.Denominator);
166
- Result->SetObjectField(TEXT("displayRate"), DisplayRateObj);
167
-
168
- // Playback range
169
- TRange<FFrameNumber> PlaybackRange = MovieScene->GetPlaybackRange();
170
- TSharedPtr<FJsonObject> RangeObj = MakeShared<FJsonObject>();
171
- if (PlaybackRange.HasLowerBound())
172
- {
173
- RangeObj->SetNumberField(TEXT("startFrame"), PlaybackRange.GetLowerBoundValue().Value);
174
- }
175
- if (PlaybackRange.HasUpperBound())
176
- {
177
- RangeObj->SetNumberField(TEXT("endFrame"), PlaybackRange.GetUpperBoundValue().Value);
178
- }
179
- Result->SetObjectField(TEXT("playbackRange"), RangeObj);
180
-
181
- // #52: optional section-level detail (attach sockets, first transform key values)
182
- const bool bIncludeDetails = OptionalBool(Params, TEXT("includeSectionDetails"));
183
-
184
- auto ExtractSectionDetails = [&](UMovieSceneTrack* Track, TSharedPtr<FJsonObject>& TrackObj)
185
- {
186
- if (!bIncludeDetails || !Track) return;
187
- TArray<TSharedPtr<FJsonValue>> SectionsArr;
188
- for (UMovieSceneSection* Section : Track->GetAllSections())
189
- {
190
- if (!Section) continue;
191
- TSharedPtr<FJsonObject> SObj = MakeShared<FJsonObject>();
192
- if (UMovieScene3DAttachSection* Attach = Cast<UMovieScene3DAttachSection>(Section))
193
- {
194
- SObj->SetStringField(TEXT("attachSocket"), Attach->AttachSocketName.ToString());
195
- SObj->SetStringField(TEXT("attachComponent"), Attach->AttachComponentName.ToString());
196
- }
197
- if (UMovieScene3DTransformSection* Xf = Cast<UMovieScene3DTransformSection>(Section))
198
- {
199
- FMovieSceneChannelProxy& Proxy = Xf->GetChannelProxy();
200
- TArray<FName> ChannelNames = {
201
- TEXT("Location.X"), TEXT("Location.Y"), TEXT("Location.Z"),
202
- TEXT("Rotation.X"), TEXT("Rotation.Y"), TEXT("Rotation.Z"),
203
- TEXT("Scale.X"), TEXT("Scale.Y"), TEXT("Scale.Z"),
204
- };
205
- TSharedPtr<FJsonObject> FirstKeys = MakeShared<FJsonObject>();
206
- for (FName ChName : ChannelNames)
207
- {
208
- // UE 5.7: GetChannel<T>(FName) overload was removed. Use
209
- // GetChannelByName<T>(FName) which returns a typed handle,
210
- // and GetData().GetValues() now yields a TArrayView.
211
- if (FMovieSceneDoubleChannel* Ch =
212
- Proxy.GetChannelByName<FMovieSceneDoubleChannel>(ChName).Get())
213
- {
214
- TArrayView<const FMovieSceneDoubleValue> Values = Ch->GetData().GetValues();
215
- if (Values.Num() > 0)
216
- {
217
- FirstKeys->SetNumberField(ChName.ToString(), Values[0].Value);
218
- }
219
- }
220
- else if (FMovieSceneFloatChannel* FCh =
221
- Proxy.GetChannelByName<FMovieSceneFloatChannel>(ChName).Get())
222
- {
223
- TArrayView<const FMovieSceneFloatValue> FVs = FCh->GetData().GetValues();
224
- if (FVs.Num() > 0)
225
- {
226
- FirstKeys->SetNumberField(ChName.ToString(), FVs[0].Value);
227
- }
228
- }
229
- }
230
- SObj->SetObjectField(TEXT("firstKeyValues"), FirstKeys);
231
- }
232
- SObj->SetStringField(TEXT("class"), Section->GetClass()->GetName());
233
- SectionsArr.Add(MakeShared<FJsonValueObject>(SObj));
234
- }
235
- TrackObj->SetArrayField(TEXT("sections"), SectionsArr);
236
- };
237
-
238
- // #556: collect the Sequencer binding tags (group labels) that reference a
239
- // given binding guid, from the MovieScene's tagged-binding map.
240
- auto TagsForGuid = [MovieScene](const FGuid& Guid) -> TArray<TSharedPtr<FJsonValue>>
241
- {
242
- TArray<TSharedPtr<FJsonValue>> Out;
243
- for (const TPair<FName, FMovieSceneObjectBindingIDs>& Pair : MovieScene->AllTaggedBindings())
244
- {
245
- for (const FMovieSceneObjectBindingID& ID : Pair.Value.IDs)
246
- {
247
- if (ID.GetGuid() == Guid)
248
- {
249
- Out.Add(MakeShared<FJsonValueString>(Pair.Key.ToString()));
250
- break;
251
- }
252
- }
253
- }
254
- return Out;
255
- };
256
-
257
- // Bindings (possessables and spawnables)
258
- TArray<TSharedPtr<FJsonValue>> BindingsArray;
259
- for (int32 i = 0; i < MovieScene->GetPossessableCount(); ++i)
260
- {
261
- const FMovieScenePossessable& Possessable = MovieScene->GetPossessable(i);
262
- TSharedPtr<FJsonObject> BindingObj = MakeShared<FJsonObject>();
263
- BindingObj->SetStringField(TEXT("name"), Possessable.GetName());
264
- BindingObj->SetStringField(TEXT("guid"), Possessable.GetGuid().ToString());
265
- BindingObj->SetStringField(TEXT("type"), TEXT("possessable"));
266
- BindingObj->SetArrayField(TEXT("tags"), TagsForGuid(Possessable.GetGuid()));
267
-
268
- // List tracks for this binding (with optional section detail)
269
- TArray<TSharedPtr<FJsonValue>> TrackArr;
270
- const FMovieSceneBinding* Binding = MovieScene->FindBinding(Possessable.GetGuid());
271
- if (Binding)
272
- {
273
- for (UMovieSceneTrack* Track : Binding->GetTracks())
274
- {
275
- if (!Track) continue;
276
- TSharedPtr<FJsonObject> TObj = MakeShared<FJsonObject>();
277
- TObj->SetStringField(TEXT("class"), Track->GetClass()->GetName());
278
- TObj->SetStringField(TEXT("name"), Track->GetTrackName().ToString());
279
- ExtractSectionDetails(Track, TObj);
280
- TrackArr.Add(MakeShared<FJsonValueObject>(TObj));
281
- }
282
- }
283
- BindingObj->SetArrayField(TEXT("tracks"), TrackArr);
284
-
285
- BindingsArray.Add(MakeShared<FJsonValueObject>(BindingObj));
286
- }
287
-
288
- for (int32 i = 0; i < MovieScene->GetSpawnableCount(); ++i)
289
- {
290
- const FMovieSceneSpawnable& Spawnable = MovieScene->GetSpawnable(i);
291
- TSharedPtr<FJsonObject> BindingObj = MakeShared<FJsonObject>();
292
- BindingObj->SetStringField(TEXT("name"), Spawnable.GetName());
293
- BindingObj->SetStringField(TEXT("guid"), Spawnable.GetGuid().ToString());
294
- BindingObj->SetStringField(TEXT("type"), TEXT("spawnable"));
295
- BindingObj->SetArrayField(TEXT("tags"), TagsForGuid(Spawnable.GetGuid()));
296
-
297
- TArray<TSharedPtr<FJsonValue>> TrackArr;
298
- const FMovieSceneBinding* Binding = MovieScene->FindBinding(Spawnable.GetGuid());
299
- if (Binding)
300
- {
301
- for (UMovieSceneTrack* Track : Binding->GetTracks())
302
- {
303
- if (!Track) continue;
304
- TSharedPtr<FJsonObject> TObj = MakeShared<FJsonObject>();
305
- TObj->SetStringField(TEXT("class"), Track->GetClass()->GetName());
306
- TObj->SetStringField(TEXT("name"), Track->GetTrackName().ToString());
307
- ExtractSectionDetails(Track, TObj);
308
- TrackArr.Add(MakeShared<FJsonValueObject>(TObj));
309
- }
310
- }
311
- BindingObj->SetArrayField(TEXT("tracks"), TrackArr);
312
-
313
- BindingsArray.Add(MakeShared<FJsonValueObject>(BindingObj));
314
- }
315
- Result->SetArrayField(TEXT("bindings"), BindingsArray);
316
- Result->SetNumberField(TEXT("bindingCount"), BindingsArray.Num());
317
-
318
- // Master tracks
319
- TArray<TSharedPtr<FJsonValue>> MasterTracksArray;
320
- for (UMovieSceneTrack* Track : MovieScene->GetTracks())
321
- {
322
- if (!Track) continue;
323
- TSharedPtr<FJsonObject> TrackObj = MakeShared<FJsonObject>();
324
- TrackObj->SetStringField(TEXT("name"), Track->GetTrackName().ToString());
325
- TrackObj->SetStringField(TEXT("class"), Track->GetClass()->GetName());
326
- TrackObj->SetNumberField(TEXT("sectionCount"), Track->GetAllSections().Num());
327
- MasterTracksArray.Add(MakeShared<FJsonValueObject>(TrackObj));
328
- }
329
- Result->SetArrayField(TEXT("masterTracks"), MasterTracksArray);
330
- Result->SetNumberField(TEXT("masterTrackCount"), MasterTracksArray.Num());
331
-
332
- return MCPResult(Result);
333
- }
334
-
335
- TSharedPtr<FJsonValue> FSequencerHandlers::AddTrack(const TSharedPtr<FJsonObject>& Params)
336
- {
337
- FString AssetPath;
338
- if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
339
-
340
- FString TrackType;
341
- if (auto Err = RequireString(Params, TEXT("trackType"), TrackType)) return Err;
342
-
343
- UObject* LoadedAsset = UEditorAssetLibrary::LoadAsset(AssetPath);
344
- ULevelSequence* Sequence = Cast<ULevelSequence>(LoadedAsset);
345
- if (!Sequence)
346
- {
347
- return MCPError(FString::Printf(TEXT("Failed to load LevelSequence at '%s'"), *AssetPath));
348
- }
349
-
350
- UMovieScene* MovieScene = Sequence->GetMovieScene();
351
- if (!MovieScene)
352
- {
353
- return MCPError(TEXT("LevelSequence has no MovieScene"));
354
- }
355
-
356
- // Determine track class from type name
357
- UClass* TrackClass = nullptr;
358
- if (TrackType.Equals(TEXT("Transform"), ESearchCase::IgnoreCase))
359
- {
360
- TrackClass = UMovieScene3DTransformTrack::StaticClass();
361
- }
362
- else if (TrackType.Equals(TEXT("Float"), ESearchCase::IgnoreCase))
363
- {
364
- TrackClass = UMovieSceneFloatTrack::StaticClass();
365
- }
366
- else if (TrackType.Equals(TEXT("SkeletalAnimation"), ESearchCase::IgnoreCase))
367
- {
368
- TrackClass = UMovieSceneSkeletalAnimationTrack::StaticClass();
369
- }
370
- else if (TrackType.Equals(TEXT("CameraCut"), ESearchCase::IgnoreCase))
371
- {
372
- TrackClass = UMovieSceneCameraCutTrack::StaticClass();
373
- }
374
- else if (TrackType.Equals(TEXT("Audio"), ESearchCase::IgnoreCase))
375
- {
376
- TrackClass = UMovieSceneAudioTrack::StaticClass();
377
- }
378
- else if (TrackType.Equals(TEXT("Event"), ESearchCase::IgnoreCase))
379
- {
380
- TrackClass = UMovieSceneEventTrack::StaticClass();
381
- }
382
- else if (TrackType.Equals(TEXT("Fade"), ESearchCase::IgnoreCase))
383
- {
384
- TrackClass = UMovieSceneFadeTrack::StaticClass();
385
- }
386
- else
387
- {
388
- return MCPError(FString::Printf(TEXT("Unknown track type: '%s'. Use Transform, Float, SkeletalAnimation, CameraCut, Audio, Event, or Fade."), *TrackType));
389
- }
390
-
391
- // Check if we should add to an actor binding or as a master track
392
- FString ActorLabel = OptionalString(Params, TEXT("actorLabel"));
393
- auto Result = MCPSuccess();
394
-
395
- if (!ActorLabel.IsEmpty())
396
- {
397
- // Find the binding for this actor
398
- REQUIRE_EDITOR_WORLD(World);
399
-
400
- AActor* TargetActor = FindActorByLabel(World, ActorLabel);
401
- if (!TargetActor)
402
- {
403
- return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
404
- }
405
-
406
- // Find or create a binding for this actor
407
- FGuid BindingGuid;
408
- bool bFoundBinding = false;
409
-
410
- // Search existing possessables
411
- for (int32 i = 0; i < MovieScene->GetPossessableCount(); ++i)
412
- {
413
- const FMovieScenePossessable& Possessable = MovieScene->GetPossessable(i);
414
- if (Possessable.GetName() == ActorLabel || Possessable.GetName() == TargetActor->GetName())
415
- {
416
- BindingGuid = Possessable.GetGuid();
417
- bFoundBinding = true;
418
- break;
419
- }
420
- }
421
-
422
- if (!bFoundBinding)
423
- {
424
- // Create a new possessable binding for the actor
425
- BindingGuid = MovieScene->AddPossessable(ActorLabel, TargetActor->GetClass());
426
- Sequence->BindPossessableObject(BindingGuid, *TargetActor, World);
427
- }
428
-
429
- // Idempotency: existing track of this class on binding?
430
- if (UMovieSceneTrack* ExistingTrack = MovieScene->FindTrack(TrackClass, BindingGuid))
431
- {
432
- MCPSetExisted(Result);
433
- Result->SetStringField(TEXT("actorLabel"), ActorLabel);
434
- Result->SetStringField(TEXT("bindingGuid"), BindingGuid.ToString());
435
- Result->SetStringField(TEXT("trackType"), TrackType);
436
- Result->SetStringField(TEXT("trackClass"), ExistingTrack->GetClass()->GetName());
437
- Result->SetStringField(TEXT("scope"), TEXT("binding"));
438
- return MCPResult(Result);
439
- }
440
-
441
- // Add track to binding
442
- UMovieSceneTrack* NewTrack = MovieScene->AddTrack(TrackClass, BindingGuid);
443
- if (!NewTrack)
444
- {
445
- return MCPError(FString::Printf(TEXT("Failed to add %s track to actor '%s'"), *TrackType, *ActorLabel));
446
- }
447
-
448
- MCPSetCreated(Result);
449
- Result->SetStringField(TEXT("actorLabel"), ActorLabel);
450
- Result->SetStringField(TEXT("bindingGuid"), BindingGuid.ToString());
451
- Result->SetStringField(TEXT("trackType"), TrackType);
452
- Result->SetStringField(TEXT("trackClass"), NewTrack->GetClass()->GetName());
453
- Result->SetStringField(TEXT("scope"), TEXT("binding"));
454
- }
455
- else
456
- {
457
- // Idempotency: any existing master track of this class?
458
- TArray<UMovieSceneTrack*> MasterTracks = MovieScene->GetTracks();
459
- for (UMovieSceneTrack* T : MasterTracks)
460
- {
461
- if (T && T->IsA(TrackClass))
462
- {
463
- MCPSetExisted(Result);
464
- Result->SetStringField(TEXT("trackType"), TrackType);
465
- Result->SetStringField(TEXT("trackClass"), T->GetClass()->GetName());
466
- Result->SetStringField(TEXT("scope"), TEXT("master"));
467
- return MCPResult(Result);
468
- }
469
- }
470
-
471
- // Add as master track
472
- UMovieSceneTrack* NewTrack = MovieScene->AddTrack(TrackClass);
473
- if (!NewTrack)
474
- {
475
- return MCPError(FString::Printf(TEXT("Failed to add master %s track"), *TrackType));
476
- }
477
-
478
- MCPSetCreated(Result);
479
- Result->SetStringField(TEXT("trackType"), TrackType);
480
- Result->SetStringField(TEXT("trackClass"), NewTrack->GetClass()->GetName());
481
- Result->SetStringField(TEXT("scope"), TEXT("master"));
482
- }
483
-
484
- // Mark the sequence package dirty
485
- Sequence->GetOutermost()->MarkPackageDirty();
486
-
487
- return MCPResult(Result);
488
- }
489
-
490
- // play_sequence - drive the Sequencer editor's transport.
491
- //
492
- // This used to issue "Sequencer.Play"/"Sequencer.Pause"/"Sequencer.Stop"
493
- // through GEditor->Exec. No such console commands exist in the engine: the
494
- // Sequencer module registers neither an exec handler nor a console command for
495
- // them, so Exec returned false and the handler reported success for a call that
496
- // did nothing at all. Every use of this action since it shipped was a no-op.
497
- //
498
- // ULevelSequenceEditorBlueprintLibrary is the real scripting surface - the same
499
- // one the editor's own Python/Blueprint automation uses.
500
- TSharedPtr<FJsonValue> FSequencerHandlers::SequenceControl(const TSharedPtr<FJsonObject>& Params)
501
- {
502
- FString Action;
503
- if (auto Err = RequireString(Params, TEXT("action"), Action)) return Err;
504
-
505
- const bool bPlay = Action.Equals(TEXT("play"), ESearchCase::IgnoreCase);
506
- const bool bPause = Action.Equals(TEXT("pause"), ESearchCase::IgnoreCase);
507
- const bool bStop = Action.Equals(TEXT("stop"), ESearchCase::IgnoreCase);
508
- if (!bPlay && !bPause && !bStop)
509
- {
510
- return MCPError(FString::Printf(TEXT("Unknown action: '%s'. Use play, pause, or stop."), *Action));
511
- }
512
-
513
- // Open the requested sequence first: the transport acts on whatever
514
- // Sequencer currently has open, so naming one and not opening it would
515
- // drive a different sequence.
516
- FString RequestedPath = OptionalString(Params, TEXT("sequencePath"), OptionalString(Params, TEXT("assetPath")));
517
- if (!RequestedPath.IsEmpty())
518
- {
519
- ULevelSequence* Sequence = LoadAssetByPath<ULevelSequence>(RequestedPath);
520
- if (!Sequence)
521
- {
522
- return MCPError(FString::Printf(TEXT("Level Sequence not found: %s"), *RequestedPath));
523
- }
524
- if (!ULevelSequenceEditorBlueprintLibrary::OpenLevelSequence(Sequence))
525
- {
526
- return MCPError(FString::Printf(
527
- TEXT("Failed to open '%s' in Sequencer."), *Sequence->GetPathName()));
528
- }
529
- }
530
-
531
- ULevelSequence* Current = ULevelSequenceEditorBlueprintLibrary::GetCurrentLevelSequence();
532
- if (!Current)
533
- {
534
- return MCPError(TEXT("No Level Sequence is open in Sequencer. Pass sequencePath to open one, or open it in the editor first."));
535
- }
536
-
537
- if (bPlay)
538
- {
539
- ULevelSequenceEditorBlueprintLibrary::Play();
540
- }
541
- else if (bPause)
542
- {
543
- ULevelSequenceEditorBlueprintLibrary::Pause();
544
- }
545
- else
546
- {
547
- // There is no Stop(): pause, then rewind to the start of the playback
548
- // range, which is what stopping means to a caller.
549
- ULevelSequenceEditorBlueprintLibrary::Pause();
550
- const FMovieSceneSequencePlaybackParams Rewind(FFrameTime(0), EUpdatePositionMethod::Scrub);
551
- ULevelSequenceEditorBlueprintLibrary::SetGlobalPosition(Rewind);
552
- }
553
-
554
- auto Result = MCPSuccess();
555
- Result->SetStringField(TEXT("action"), Action);
556
- Result->SetStringField(TEXT("sequencePath"), Current->GetPathName());
557
- // Read the transport back rather than reporting what was asked for.
558
- Result->SetBoolField(TEXT("playing"), ULevelSequenceEditorBlueprintLibrary::IsPlaying());
559
- return MCPResult(Result);
560
- }
561
-
562
- // set_sequence_playback_range -- set a Level Sequence's playback range in
563
- // seconds. (#548) Params: sequencePath, startSeconds, endSeconds.
564
- TSharedPtr<FJsonValue> FSequencerHandlers::SetPlaybackRange(const TSharedPtr<FJsonObject>& Params)
565
- {
566
- FString Path, Err;
567
- ULevelSequence* Sequence = LoadSequence(Params, Path, Err);
568
- if (!Sequence) return MCPError(Err);
569
- UMovieScene* MovieScene = Sequence->GetMovieScene();
570
- if (!MovieScene) return MCPError(TEXT("Sequence has no MovieScene"));
571
-
572
- double StartSeconds = 0.0, EndSeconds = 0.0;
573
- if (!Params->TryGetNumberField(TEXT("startSeconds"), StartSeconds) ||
574
- !Params->TryGetNumberField(TEXT("endSeconds"), EndSeconds))
575
- {
576
- return MCPError(TEXT("Missing 'startSeconds' and/or 'endSeconds'"));
577
- }
578
-
579
- const FFrameRate Tick = MovieScene->GetTickResolution();
580
- const FFrameNumber Start = Tick.AsFrameNumber(StartSeconds);
581
- const FFrameNumber End = Tick.AsFrameNumber(EndSeconds);
582
- MovieScene->SetPlaybackRange(TRange<FFrameNumber>(Start, End));
583
- Sequence->GetOutermost()->MarkPackageDirty();
584
-
585
- auto Result = MCPSuccess();
586
- MCPSetUpdated(Result);
587
- Result->SetStringField(TEXT("sequencePath"), Path);
588
- Result->SetNumberField(TEXT("startSeconds"), StartSeconds);
589
- Result->SetNumberField(TEXT("endSeconds"), EndSeconds);
590
- return MCPResult(Result);
591
- }
592
-
593
- // add_sequence_section -- add a section to a track (creating the track if
594
- // needed), set its start/end in seconds, and for a CameraCut track bind it to
595
- // a camera actor. Returns the section index and its channel names. (#548)
596
- // Params: sequencePath, trackType, actorLabel? (binding scope), startSeconds?,
597
- // endSeconds?, cameraActorLabel? (CameraCut).
598
- TSharedPtr<FJsonValue> FSequencerHandlers::AddSection(const TSharedPtr<FJsonObject>& Params)
599
- {
600
- FString Path, Err;
601
- ULevelSequence* Sequence = LoadSequence(Params, Path, Err);
602
- if (!Sequence) return MCPError(Err);
603
- UMovieScene* MovieScene = Sequence->GetMovieScene();
604
- if (!MovieScene) return MCPError(TEXT("Sequence has no MovieScene"));
605
-
606
- FString TrackType;
607
- if (auto E = RequireString(Params, TEXT("trackType"), TrackType)) return E;
608
- UClass* TrackClass = ResolveTrackClass(TrackType);
609
- if (!TrackClass) return MCPError(FString::Printf(TEXT("Unknown track type: '%s'"), *TrackType));
610
-
611
- const FString ActorLabel = OptionalString(Params, TEXT("actorLabel"));
612
-
613
- UMovieSceneTrack* Track = nullptr;
614
- FGuid BindingGuid;
615
- if (!ActorLabel.IsEmpty())
616
- {
617
- if (!ResolveActorBinding(Sequence, MovieScene, ActorLabel, BindingGuid, Err)) return MCPError(Err);
618
- Track = MovieScene->FindTrack(TrackClass, BindingGuid);
619
- if (!Track) Track = MovieScene->AddTrack(TrackClass, BindingGuid);
620
- }
621
- else
622
- {
623
- for (UMovieSceneTrack* T : MovieScene->GetTracks())
624
- {
625
- if (T && T->IsA(TrackClass)) { Track = T; break; }
626
- }
627
- if (!Track) Track = MovieScene->AddTrack(TrackClass);
628
- }
629
- if (!Track) return MCPError(FString::Printf(TEXT("Failed to resolve/add %s track"), *TrackType));
630
-
631
- // Resolve the camera binding up front so a bad cameraActorLabel fails before
632
- // we create an orphan section.
633
- const FString CameraActorLabel = OptionalString(Params, TEXT("cameraActorLabel"));
634
- FGuid CamGuid;
635
- if (!CameraActorLabel.IsEmpty())
636
- {
637
- if (!ResolveActorBinding(Sequence, MovieScene, CameraActorLabel, CamGuid, Err)) return MCPError(Err);
638
- }
639
-
640
- UMovieSceneSection* Section = Track->CreateNewSection();
641
- if (!Section) return MCPError(TEXT("Failed to create section"));
642
- Track->AddSection(*Section);
643
-
644
- const FFrameRate Tick = MovieScene->GetTickResolution();
645
- double StartSeconds = 0.0, EndSeconds = 0.0;
646
- const bool bHasStart = Params->TryGetNumberField(TEXT("startSeconds"), StartSeconds);
647
- const bool bHasEnd = Params->TryGetNumberField(TEXT("endSeconds"), EndSeconds);
648
- if (bHasStart || bHasEnd)
649
- {
650
- const FFrameNumber Start = Tick.AsFrameNumber(StartSeconds);
651
- const FFrameNumber End = Tick.AsFrameNumber(bHasEnd ? EndSeconds : StartSeconds + 1.0);
652
- Section->SetRange(TRange<FFrameNumber>(Start, End));
653
- }
654
-
655
- // CameraCut: bind to the camera actor's possessable resolved above.
656
- if (!CameraActorLabel.IsEmpty())
657
- {
658
- if (UMovieSceneCameraCutSection* CutSection = Cast<UMovieSceneCameraCutSection>(Section))
659
- {
660
- CutSection->SetCameraGuid(CamGuid);
661
- }
662
- }
663
-
664
- const int32 SectionIndex = Track->GetAllSections().IndexOfByKey(Section);
665
-
666
- // Enumerate channel names so the caller knows what to key.
667
- TArray<TSharedPtr<FJsonValue>> ChannelNames;
668
- FMovieSceneChannelProxy& Proxy = Section->GetChannelProxy();
669
- for (const FMovieSceneChannelEntry& Entry : Proxy.GetAllEntries())
670
- {
671
- TArrayView<const FMovieSceneChannelMetaData> AllMeta = Entry.GetMetaData();
672
- for (const FMovieSceneChannelMetaData& Meta : AllMeta)
673
- {
674
- ChannelNames.Add(MakeShared<FJsonValueString>(Meta.Name.ToString()));
675
- }
676
- }
677
-
678
- Sequence->GetOutermost()->MarkPackageDirty();
679
-
680
- auto Result = MCPSuccess();
681
- MCPSetCreated(Result);
682
- Result->SetStringField(TEXT("sequencePath"), Path);
683
- Result->SetStringField(TEXT("trackType"), TrackType);
684
- if (!ActorLabel.IsEmpty()) Result->SetStringField(TEXT("bindingGuid"), BindingGuid.ToString());
685
- Result->SetNumberField(TEXT("sectionIndex"), SectionIndex);
686
- Result->SetArrayField(TEXT("channels"), ChannelNames);
687
- return MCPResult(Result);
688
- }
689
-
690
- // set_sequence_keyframes -- add keyframes to a named channel of a section.
691
- // Supports transform double channels (Location.X..Scale.Z, plus friendly
692
- // x/y/z/yaw/pitch/roll) and float channels (Fade/Float). (#548)
693
- // Params: sequencePath, trackType, actorLabel? (binding scope), sectionIndex?
694
- // (default 0), channel, keyframes ([{seconds, value}]), interpolation? (cubic|linear).
695
- TSharedPtr<FJsonValue> FSequencerHandlers::SetKeyframes(const TSharedPtr<FJsonObject>& Params)
696
- {
697
- FString Path, Err;
698
- ULevelSequence* Sequence = LoadSequence(Params, Path, Err);
699
- if (!Sequence) return MCPError(Err);
700
- UMovieScene* MovieScene = Sequence->GetMovieScene();
701
- if (!MovieScene) return MCPError(TEXT("Sequence has no MovieScene"));
702
-
703
- FString TrackType;
704
- if (auto E = RequireString(Params, TEXT("trackType"), TrackType)) return E;
705
- UClass* TrackClass = ResolveTrackClass(TrackType);
706
- if (!TrackClass) return MCPError(FString::Printf(TEXT("Unknown track type: '%s'"), *TrackType));
707
-
708
- FString ChannelName;
709
- if (auto E = RequireString(Params, TEXT("channel"), ChannelName)) return E;
710
-
711
- const TArray<TSharedPtr<FJsonValue>>* Keyframes = nullptr;
712
- if (!Params->TryGetArrayField(TEXT("keyframes"), Keyframes) || !Keyframes)
713
- {
714
- return MCPError(TEXT("Missing 'keyframes' array ([{seconds, value}, ...])"));
715
- }
716
-
717
- const FString ActorLabel = OptionalString(Params, TEXT("actorLabel"));
718
- UMovieSceneTrack* Track = nullptr;
719
- if (!ActorLabel.IsEmpty())
720
- {
721
- FGuid BindingGuid;
722
- if (!ResolveActorBinding(Sequence, MovieScene, ActorLabel, BindingGuid, Err)) return MCPError(Err);
723
- Track = MovieScene->FindTrack(TrackClass, BindingGuid);
724
- }
725
- else
726
- {
727
- for (UMovieSceneTrack* T : MovieScene->GetTracks())
728
- {
729
- if (T && T->IsA(TrackClass)) { Track = T; break; }
730
- }
731
- }
732
- if (!Track) return MCPError(FString::Printf(TEXT("No %s track found (add a section first)"), *TrackType));
733
-
734
- const TArray<UMovieSceneSection*>& Sections = Track->GetAllSections();
735
- if (Sections.Num() == 0) return MCPError(TEXT("Track has no sections (call add_sequence_section first)"));
736
- int32 SectionIndex = 0;
737
- Params->TryGetNumberField(TEXT("sectionIndex"), SectionIndex);
738
- if (SectionIndex < 0 || SectionIndex >= Sections.Num())
739
- {
740
- return MCPError(FString::Printf(TEXT("sectionIndex %d out of range (sections=%d)"), SectionIndex, Sections.Num()));
741
- }
742
- UMovieSceneSection* Section = Sections[SectionIndex];
743
-
744
- const FString Canonical = CanonicalTransformChannel(ChannelName);
745
- const bool bLinear = OptionalString(Params, TEXT("interpolation"), TEXT("cubic")).Equals(TEXT("linear"), ESearchCase::IgnoreCase);
746
- const FFrameRate Tick = MovieScene->GetTickResolution();
747
- Section->Modify();
748
-
749
- int32 KeysAdded = 0;
750
- FMovieSceneChannelProxy& Proxy = Section->GetChannelProxy();
751
-
752
- // Try double channels (transform) by metadata name.
753
- TArrayView<FMovieSceneDoubleChannel*> DoubleChannels = Proxy.GetChannels<FMovieSceneDoubleChannel>();
754
- TArrayView<const FMovieSceneChannelMetaData> DoubleMeta = Proxy.GetMetaData<FMovieSceneDoubleChannel>();
755
- bool bMatched = false;
756
- for (int32 i = 0; i < DoubleChannels.Num(); ++i)
757
- {
758
- if (DoubleMeta.IsValidIndex(i) && DoubleMeta[i].Name.ToString().Equals(Canonical, ESearchCase::IgnoreCase))
759
- {
760
- for (const TSharedPtr<FJsonValue>& KfVal : *Keyframes)
761
- {
762
- const TSharedPtr<FJsonObject>* Kf = nullptr;
763
- if (!KfVal->TryGetObject(Kf) || !Kf) continue;
764
- double Sec = 0.0, Val = 0.0;
765
- (*Kf)->TryGetNumberField(TEXT("seconds"), Sec);
766
- (*Kf)->TryGetNumberField(TEXT("value"), Val);
767
- const FFrameNumber Frame = Tick.AsFrameNumber(Sec);
768
- if (bLinear) DoubleChannels[i]->AddLinearKey(Frame, Val);
769
- else DoubleChannels[i]->AddCubicKey(Frame, Val);
770
- ++KeysAdded;
771
- }
772
- bMatched = true;
773
- break;
774
- }
775
- }
776
-
777
- // Otherwise try float channels (fade/float track). Match by name, or take
778
- // the only channel when the name doesn't disambiguate.
779
- if (!bMatched)
780
- {
781
- TArrayView<FMovieSceneFloatChannel*> FloatChannels = Proxy.GetChannels<FMovieSceneFloatChannel>();
782
- TArrayView<const FMovieSceneChannelMetaData> FloatMeta = Proxy.GetMetaData<FMovieSceneFloatChannel>();
783
- int32 ChosenIdx = INDEX_NONE;
784
- for (int32 i = 0; i < FloatChannels.Num(); ++i)
785
- {
786
- if (FloatMeta.IsValidIndex(i) && FloatMeta[i].Name.ToString().Equals(ChannelName, ESearchCase::IgnoreCase)) { ChosenIdx = i; break; }
787
- }
788
- if (ChosenIdx == INDEX_NONE && FloatChannels.Num() == 1) ChosenIdx = 0;
789
- if (ChosenIdx != INDEX_NONE)
790
- {
791
- for (const TSharedPtr<FJsonValue>& KfVal : *Keyframes)
792
- {
793
- const TSharedPtr<FJsonObject>* Kf = nullptr;
794
- if (!KfVal->TryGetObject(Kf) || !Kf) continue;
795
- double Sec = 0.0, Val = 0.0;
796
- (*Kf)->TryGetNumberField(TEXT("seconds"), Sec);
797
- (*Kf)->TryGetNumberField(TEXT("value"), Val);
798
- const FFrameNumber Frame = Tick.AsFrameNumber(Sec);
799
- if (bLinear) FloatChannels[ChosenIdx]->AddLinearKey(Frame, (float)Val);
800
- else FloatChannels[ChosenIdx]->AddCubicKey(Frame, (float)Val);
801
- ++KeysAdded;
802
- }
803
- bMatched = true;
804
- }
805
- }
806
-
807
- if (!bMatched)
808
- {
809
- return MCPError(FString::Printf(TEXT("Channel '%s' not found on the section. For Transform use Location.X/Rotation.Z/etc or x/yaw; for Fade use the float channel."), *ChannelName));
810
- }
811
-
812
- Sequence->GetOutermost()->MarkPackageDirty();
813
-
814
- auto Result = MCPSuccess();
815
- MCPSetUpdated(Result);
816
- Result->SetStringField(TEXT("sequencePath"), Path);
817
- Result->SetStringField(TEXT("trackType"), TrackType);
818
- Result->SetStringField(TEXT("channel"), ChannelName);
819
- Result->SetNumberField(TEXT("sectionIndex"), SectionIndex);
820
- Result->SetNumberField(TEXT("keysAdded"), KeysAdded);
821
- return MCPResult(Result);
822
- }
1
+ #include "SequencerHandlers.h"
2
+ #include "HandlerRegistry.h"
3
+ #include "HandlerUtils.h"
4
+
5
+ #include "LevelSequenceEditorBlueprintLibrary.h"
6
+ #include "MovieSceneSequencePlayer.h"
7
+
8
+ #include "Subsystems/AssetEditorSubsystem.h"
9
+ #include "HandlerAssetCreate.h"
10
+ #include "LevelSequence.h"
11
+ #include "LevelSequenceActor.h"
12
+ // LevelSequenceFactoryNew may not be available; use AssetTools directly
13
+ #include "MovieScene.h"
14
+ #include "MovieSceneTrack.h"
15
+ #include "MovieSceneSection.h"
16
+ #include "Tracks/MovieSceneFloatTrack.h"
17
+ #include "Tracks/MovieScene3DTransformTrack.h"
18
+ #include "Tracks/MovieSceneSkeletalAnimationTrack.h"
19
+ #include "Tracks/MovieSceneCameraCutTrack.h"
20
+ #include "Tracks/MovieScene3DAttachTrack.h"
21
+ #include "Sections/MovieScene3DAttachSection.h"
22
+ #include "Sections/MovieScene3DTransformSection.h"
23
+ #include "Sections/MovieSceneCameraCutSection.h"
24
+ #include "Channels/MovieSceneDoubleChannel.h"
25
+ #include "Channels/MovieSceneFloatChannel.h"
26
+ #include "Channels/MovieSceneChannelProxy.h"
27
+ #include "Tracks/MovieSceneAudioTrack.h"
28
+ #include "Tracks/MovieSceneEventTrack.h"
29
+ #include "Tracks/MovieSceneFadeTrack.h"
30
+ #include "AssetRegistry/AssetRegistryModule.h"
31
+ #include "EditorScriptingUtilities/Public/EditorAssetLibrary.h"
32
+ #include "UObject/Package.h"
33
+ #include "Editor.h"
34
+ #include "Engine/World.h"
35
+ #include "EngineUtils.h"
36
+ #include "GameFramework/Actor.h"
37
+ #include "Dom/JsonObject.h"
38
+ #include "Dom/JsonValue.h"
39
+
40
+ void FSequencerHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
41
+ {
42
+ Registry.RegisterHandler(TEXT("create_level_sequence"), &CreateLevelSequence);
43
+ Registry.RegisterHandler(TEXT("get_sequence_info"), &ReadSequenceInfo);
44
+ Registry.RegisterHandler(TEXT("add_sequence_track"), &AddTrack);
45
+ Registry.RegisterHandler(TEXT("play_sequence"), &SequenceControl);
46
+ Registry.RegisterHandler(TEXT("set_sequence_playback_range"), &SetPlaybackRange);
47
+ Registry.RegisterHandler(TEXT("add_sequence_section"), &AddSection);
48
+ Registry.RegisterHandler(TEXT("set_sequence_keyframes"), &SetKeyframes);
49
+ }
50
+
51
+ // ─── #548 sequencer authoring helpers ────────────────────────────────
52
+ namespace
53
+ {
54
+ ULevelSequence* LoadSequence(const TSharedPtr<FJsonObject>& Params, FString& OutPath, FString& OutError)
55
+ {
56
+ if (!Params->TryGetStringField(TEXT("sequencePath"), OutPath))
57
+ {
58
+ if (!Params->TryGetStringField(TEXT("assetPath"), OutPath) && !Params->TryGetStringField(TEXT("path"), OutPath))
59
+ {
60
+ OutError = TEXT("Missing 'sequencePath' parameter");
61
+ return nullptr;
62
+ }
63
+ }
64
+ ULevelSequence* Seq = Cast<ULevelSequence>(UEditorAssetLibrary::LoadAsset(OutPath));
65
+ if (!Seq) { OutError = FString::Printf(TEXT("LevelSequence not found: %s"), *OutPath); return nullptr; }
66
+ return Seq;
67
+ }
68
+
69
+ // Resolve (creating if needed) the actor's possessable binding GUID.
70
+ bool ResolveActorBinding(ULevelSequence* Sequence, UMovieScene* MovieScene, const FString& ActorLabel, FGuid& OutGuid, FString& OutError)
71
+ {
72
+ UWorld* World = GetEditorWorld();
73
+ if (!World) { OutError = TEXT("No editor world available"); return false; }
74
+ AActor* TargetActor = FindActorByLabel(World, ActorLabel);
75
+ if (!TargetActor) { OutError = FString::Printf(TEXT("Actor not found: %s"), *ActorLabel); return false; }
76
+
77
+ for (int32 i = 0; i < MovieScene->GetPossessableCount(); ++i)
78
+ {
79
+ const FMovieScenePossessable& P = MovieScene->GetPossessable(i);
80
+ if (P.GetName() == ActorLabel || P.GetName() == TargetActor->GetName()) { OutGuid = P.GetGuid(); return true; }
81
+ }
82
+ OutGuid = MovieScene->AddPossessable(ActorLabel, TargetActor->GetClass());
83
+ Sequence->BindPossessableObject(OutGuid, *TargetActor, World);
84
+ return true;
85
+ }
86
+
87
+ UClass* ResolveTrackClass(const FString& TrackType)
88
+ {
89
+ if (TrackType.Equals(TEXT("Transform"), ESearchCase::IgnoreCase)) return UMovieScene3DTransformTrack::StaticClass();
90
+ if (TrackType.Equals(TEXT("Float"), ESearchCase::IgnoreCase)) return UMovieSceneFloatTrack::StaticClass();
91
+ if (TrackType.Equals(TEXT("SkeletalAnimation"), ESearchCase::IgnoreCase)) return UMovieSceneSkeletalAnimationTrack::StaticClass();
92
+ if (TrackType.Equals(TEXT("CameraCut"), ESearchCase::IgnoreCase)) return UMovieSceneCameraCutTrack::StaticClass();
93
+ if (TrackType.Equals(TEXT("Audio"), ESearchCase::IgnoreCase)) return UMovieSceneAudioTrack::StaticClass();
94
+ if (TrackType.Equals(TEXT("Event"), ESearchCase::IgnoreCase)) return UMovieSceneEventTrack::StaticClass();
95
+ if (TrackType.Equals(TEXT("Fade"), ESearchCase::IgnoreCase)) return UMovieSceneFadeTrack::StaticClass();
96
+ return nullptr;
97
+ }
98
+
99
+ // Map a friendly channel name to the canonical transform channel name.
100
+ FString CanonicalTransformChannel(const FString& In)
101
+ {
102
+ const FString L = In.ToLower();
103
+ if (L == TEXT("x") || L == TEXT("location.x")) return TEXT("Location.X");
104
+ if (L == TEXT("y") || L == TEXT("location.y")) return TEXT("Location.Y");
105
+ if (L == TEXT("z") || L == TEXT("location.z")) return TEXT("Location.Z");
106
+ if (L == TEXT("roll") || L == TEXT("rotation.x")) return TEXT("Rotation.X");
107
+ if (L == TEXT("pitch") || L == TEXT("rotation.y")) return TEXT("Rotation.Y");
108
+ if (L == TEXT("yaw") || L == TEXT("rotation.z")) return TEXT("Rotation.Z");
109
+ if (L == TEXT("scale.x")) return TEXT("Scale.X");
110
+ if (L == TEXT("scale.y")) return TEXT("Scale.Y");
111
+ if (L == TEXT("scale.z")) return TEXT("Scale.Z");
112
+ return In;
113
+ }
114
+ }
115
+
116
+ TSharedPtr<FJsonValue> FSequencerHandlers::CreateLevelSequence(const TSharedPtr<FJsonObject>& Params)
117
+ {
118
+ FString Name;
119
+ if (auto Err = RequireString(Params, TEXT("name"), Name)) return Err;
120
+
121
+ FString PackagePath = OptionalString(Params, TEXT("packagePath"), TEXT("/Game/Cinematics"));
122
+ const FString OnConflict = OptionalString(Params, TEXT("onConflict"), TEXT("skip"));
123
+
124
+ auto Created = MCPCreateAssetIdempotentNewObject<ULevelSequence>(Name, PackagePath, OnConflict, TEXT("LevelSequence"));
125
+ if (Created.EarlyReturn) return Created.EarlyReturn;
126
+ ULevelSequence* NewSequence = Created.Asset;
127
+ NewSequence->Initialize();
128
+
129
+ auto Result = MCPSuccess();
130
+ MCPSetCreated(Result);
131
+ Result->SetStringField(TEXT("name"), Name);
132
+ Result->SetStringField(TEXT("path"), NewSequence->GetPathName());
133
+ Result->SetStringField(TEXT("packagePath"), PackagePath + TEXT("/") + Name);
134
+ MCPSetDeleteAssetRollback(Result, NewSequence->GetPathName());
135
+
136
+ return MCPResult(Result);
137
+ }
138
+
139
+ TSharedPtr<FJsonValue> FSequencerHandlers::ReadSequenceInfo(const TSharedPtr<FJsonObject>& Params)
140
+ {
141
+ FString AssetPath;
142
+ if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
143
+
144
+ UObject* LoadedAsset = UEditorAssetLibrary::LoadAsset(AssetPath);
145
+ ULevelSequence* Sequence = Cast<ULevelSequence>(LoadedAsset);
146
+ if (!Sequence)
147
+ {
148
+ return MCPError(FString::Printf(TEXT("Failed to load LevelSequence at '%s'"), *AssetPath));
149
+ }
150
+
151
+ UMovieScene* MovieScene = Sequence->GetMovieScene();
152
+ if (!MovieScene)
153
+ {
154
+ return MCPError(TEXT("LevelSequence has no MovieScene"));
155
+ }
156
+
157
+ auto Result = MCPSuccess();
158
+ Result->SetStringField(TEXT("name"), Sequence->GetName());
159
+ Result->SetStringField(TEXT("path"), Sequence->GetPathName());
160
+
161
+ // Display rate
162
+ FFrameRate DisplayRate = MovieScene->GetDisplayRate();
163
+ TSharedPtr<FJsonObject> DisplayRateObj = MakeShared<FJsonObject>();
164
+ DisplayRateObj->SetNumberField(TEXT("numerator"), DisplayRate.Numerator);
165
+ DisplayRateObj->SetNumberField(TEXT("denominator"), DisplayRate.Denominator);
166
+ Result->SetObjectField(TEXT("displayRate"), DisplayRateObj);
167
+
168
+ // Playback range
169
+ TRange<FFrameNumber> PlaybackRange = MovieScene->GetPlaybackRange();
170
+ TSharedPtr<FJsonObject> RangeObj = MakeShared<FJsonObject>();
171
+ if (PlaybackRange.HasLowerBound())
172
+ {
173
+ RangeObj->SetNumberField(TEXT("startFrame"), PlaybackRange.GetLowerBoundValue().Value);
174
+ }
175
+ if (PlaybackRange.HasUpperBound())
176
+ {
177
+ RangeObj->SetNumberField(TEXT("endFrame"), PlaybackRange.GetUpperBoundValue().Value);
178
+ }
179
+ Result->SetObjectField(TEXT("playbackRange"), RangeObj);
180
+
181
+ // #52: optional section-level detail (attach sockets, first transform key values)
182
+ const bool bIncludeDetails = OptionalBool(Params, TEXT("includeSectionDetails"));
183
+
184
+ auto ExtractSectionDetails = [&](UMovieSceneTrack* Track, TSharedPtr<FJsonObject>& TrackObj)
185
+ {
186
+ if (!bIncludeDetails || !Track) return;
187
+ TArray<TSharedPtr<FJsonValue>> SectionsArr;
188
+ for (UMovieSceneSection* Section : Track->GetAllSections())
189
+ {
190
+ if (!Section) continue;
191
+ TSharedPtr<FJsonObject> SObj = MakeShared<FJsonObject>();
192
+ if (UMovieScene3DAttachSection* Attach = Cast<UMovieScene3DAttachSection>(Section))
193
+ {
194
+ SObj->SetStringField(TEXT("attachSocket"), Attach->AttachSocketName.ToString());
195
+ SObj->SetStringField(TEXT("attachComponent"), Attach->AttachComponentName.ToString());
196
+ }
197
+ if (UMovieScene3DTransformSection* Xf = Cast<UMovieScene3DTransformSection>(Section))
198
+ {
199
+ FMovieSceneChannelProxy& Proxy = Xf->GetChannelProxy();
200
+ TArray<FName> ChannelNames = {
201
+ TEXT("Location.X"), TEXT("Location.Y"), TEXT("Location.Z"),
202
+ TEXT("Rotation.X"), TEXT("Rotation.Y"), TEXT("Rotation.Z"),
203
+ TEXT("Scale.X"), TEXT("Scale.Y"), TEXT("Scale.Z"),
204
+ };
205
+ TSharedPtr<FJsonObject> FirstKeys = MakeShared<FJsonObject>();
206
+ for (FName ChName : ChannelNames)
207
+ {
208
+ // UE 5.7: GetChannel<T>(FName) overload was removed. Use
209
+ // GetChannelByName<T>(FName) which returns a typed handle,
210
+ // and GetData().GetValues() now yields a TArrayView.
211
+ if (FMovieSceneDoubleChannel* Ch =
212
+ Proxy.GetChannelByName<FMovieSceneDoubleChannel>(ChName).Get())
213
+ {
214
+ TArrayView<const FMovieSceneDoubleValue> Values = Ch->GetData().GetValues();
215
+ if (Values.Num() > 0)
216
+ {
217
+ FirstKeys->SetNumberField(ChName.ToString(), Values[0].Value);
218
+ }
219
+ }
220
+ else if (FMovieSceneFloatChannel* FCh =
221
+ Proxy.GetChannelByName<FMovieSceneFloatChannel>(ChName).Get())
222
+ {
223
+ TArrayView<const FMovieSceneFloatValue> FVs = FCh->GetData().GetValues();
224
+ if (FVs.Num() > 0)
225
+ {
226
+ FirstKeys->SetNumberField(ChName.ToString(), FVs[0].Value);
227
+ }
228
+ }
229
+ }
230
+ SObj->SetObjectField(TEXT("firstKeyValues"), FirstKeys);
231
+ }
232
+ SObj->SetStringField(TEXT("class"), Section->GetClass()->GetName());
233
+ SectionsArr.Add(MakeShared<FJsonValueObject>(SObj));
234
+ }
235
+ TrackObj->SetArrayField(TEXT("sections"), SectionsArr);
236
+ };
237
+
238
+ // #556: collect the Sequencer binding tags (group labels) that reference a
239
+ // given binding guid, from the MovieScene's tagged-binding map.
240
+ auto TagsForGuid = [MovieScene](const FGuid& Guid) -> TArray<TSharedPtr<FJsonValue>>
241
+ {
242
+ TArray<TSharedPtr<FJsonValue>> Out;
243
+ for (const TPair<FName, FMovieSceneObjectBindingIDs>& Pair : MovieScene->AllTaggedBindings())
244
+ {
245
+ for (const FMovieSceneObjectBindingID& ID : Pair.Value.IDs)
246
+ {
247
+ if (ID.GetGuid() == Guid)
248
+ {
249
+ Out.Add(MakeShared<FJsonValueString>(Pair.Key.ToString()));
250
+ break;
251
+ }
252
+ }
253
+ }
254
+ return Out;
255
+ };
256
+
257
+ // Bindings (possessables and spawnables)
258
+ TArray<TSharedPtr<FJsonValue>> BindingsArray;
259
+ for (int32 i = 0; i < MovieScene->GetPossessableCount(); ++i)
260
+ {
261
+ const FMovieScenePossessable& Possessable = MovieScene->GetPossessable(i);
262
+ TSharedPtr<FJsonObject> BindingObj = MakeShared<FJsonObject>();
263
+ BindingObj->SetStringField(TEXT("name"), Possessable.GetName());
264
+ BindingObj->SetStringField(TEXT("guid"), Possessable.GetGuid().ToString());
265
+ BindingObj->SetStringField(TEXT("type"), TEXT("possessable"));
266
+ BindingObj->SetArrayField(TEXT("tags"), TagsForGuid(Possessable.GetGuid()));
267
+
268
+ // List tracks for this binding (with optional section detail)
269
+ TArray<TSharedPtr<FJsonValue>> TrackArr;
270
+ const FMovieSceneBinding* Binding = MovieScene->FindBinding(Possessable.GetGuid());
271
+ if (Binding)
272
+ {
273
+ for (UMovieSceneTrack* Track : Binding->GetTracks())
274
+ {
275
+ if (!Track) continue;
276
+ TSharedPtr<FJsonObject> TObj = MakeShared<FJsonObject>();
277
+ TObj->SetStringField(TEXT("class"), Track->GetClass()->GetName());
278
+ TObj->SetStringField(TEXT("name"), Track->GetTrackName().ToString());
279
+ ExtractSectionDetails(Track, TObj);
280
+ TrackArr.Add(MakeShared<FJsonValueObject>(TObj));
281
+ }
282
+ }
283
+ BindingObj->SetArrayField(TEXT("tracks"), TrackArr);
284
+
285
+ BindingsArray.Add(MakeShared<FJsonValueObject>(BindingObj));
286
+ }
287
+
288
+ for (int32 i = 0; i < MovieScene->GetSpawnableCount(); ++i)
289
+ {
290
+ const FMovieSceneSpawnable& Spawnable = MovieScene->GetSpawnable(i);
291
+ TSharedPtr<FJsonObject> BindingObj = MakeShared<FJsonObject>();
292
+ BindingObj->SetStringField(TEXT("name"), Spawnable.GetName());
293
+ BindingObj->SetStringField(TEXT("guid"), Spawnable.GetGuid().ToString());
294
+ BindingObj->SetStringField(TEXT("type"), TEXT("spawnable"));
295
+ BindingObj->SetArrayField(TEXT("tags"), TagsForGuid(Spawnable.GetGuid()));
296
+
297
+ TArray<TSharedPtr<FJsonValue>> TrackArr;
298
+ const FMovieSceneBinding* Binding = MovieScene->FindBinding(Spawnable.GetGuid());
299
+ if (Binding)
300
+ {
301
+ for (UMovieSceneTrack* Track : Binding->GetTracks())
302
+ {
303
+ if (!Track) continue;
304
+ TSharedPtr<FJsonObject> TObj = MakeShared<FJsonObject>();
305
+ TObj->SetStringField(TEXT("class"), Track->GetClass()->GetName());
306
+ TObj->SetStringField(TEXT("name"), Track->GetTrackName().ToString());
307
+ ExtractSectionDetails(Track, TObj);
308
+ TrackArr.Add(MakeShared<FJsonValueObject>(TObj));
309
+ }
310
+ }
311
+ BindingObj->SetArrayField(TEXT("tracks"), TrackArr);
312
+
313
+ BindingsArray.Add(MakeShared<FJsonValueObject>(BindingObj));
314
+ }
315
+ Result->SetArrayField(TEXT("bindings"), BindingsArray);
316
+ Result->SetNumberField(TEXT("bindingCount"), BindingsArray.Num());
317
+
318
+ // Master tracks
319
+ TArray<TSharedPtr<FJsonValue>> MasterTracksArray;
320
+ for (UMovieSceneTrack* Track : MovieScene->GetTracks())
321
+ {
322
+ if (!Track) continue;
323
+ TSharedPtr<FJsonObject> TrackObj = MakeShared<FJsonObject>();
324
+ TrackObj->SetStringField(TEXT("name"), Track->GetTrackName().ToString());
325
+ TrackObj->SetStringField(TEXT("class"), Track->GetClass()->GetName());
326
+ TrackObj->SetNumberField(TEXT("sectionCount"), Track->GetAllSections().Num());
327
+ MasterTracksArray.Add(MakeShared<FJsonValueObject>(TrackObj));
328
+ }
329
+ Result->SetArrayField(TEXT("masterTracks"), MasterTracksArray);
330
+ Result->SetNumberField(TEXT("masterTrackCount"), MasterTracksArray.Num());
331
+
332
+ return MCPResult(Result);
333
+ }
334
+
335
+ TSharedPtr<FJsonValue> FSequencerHandlers::AddTrack(const TSharedPtr<FJsonObject>& Params)
336
+ {
337
+ FString AssetPath;
338
+ if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
339
+
340
+ FString TrackType;
341
+ if (auto Err = RequireString(Params, TEXT("trackType"), TrackType)) return Err;
342
+
343
+ UObject* LoadedAsset = UEditorAssetLibrary::LoadAsset(AssetPath);
344
+ ULevelSequence* Sequence = Cast<ULevelSequence>(LoadedAsset);
345
+ if (!Sequence)
346
+ {
347
+ return MCPError(FString::Printf(TEXT("Failed to load LevelSequence at '%s'"), *AssetPath));
348
+ }
349
+
350
+ UMovieScene* MovieScene = Sequence->GetMovieScene();
351
+ if (!MovieScene)
352
+ {
353
+ return MCPError(TEXT("LevelSequence has no MovieScene"));
354
+ }
355
+
356
+ // Determine track class from type name
357
+ UClass* TrackClass = nullptr;
358
+ if (TrackType.Equals(TEXT("Transform"), ESearchCase::IgnoreCase))
359
+ {
360
+ TrackClass = UMovieScene3DTransformTrack::StaticClass();
361
+ }
362
+ else if (TrackType.Equals(TEXT("Float"), ESearchCase::IgnoreCase))
363
+ {
364
+ TrackClass = UMovieSceneFloatTrack::StaticClass();
365
+ }
366
+ else if (TrackType.Equals(TEXT("SkeletalAnimation"), ESearchCase::IgnoreCase))
367
+ {
368
+ TrackClass = UMovieSceneSkeletalAnimationTrack::StaticClass();
369
+ }
370
+ else if (TrackType.Equals(TEXT("CameraCut"), ESearchCase::IgnoreCase))
371
+ {
372
+ TrackClass = UMovieSceneCameraCutTrack::StaticClass();
373
+ }
374
+ else if (TrackType.Equals(TEXT("Audio"), ESearchCase::IgnoreCase))
375
+ {
376
+ TrackClass = UMovieSceneAudioTrack::StaticClass();
377
+ }
378
+ else if (TrackType.Equals(TEXT("Event"), ESearchCase::IgnoreCase))
379
+ {
380
+ TrackClass = UMovieSceneEventTrack::StaticClass();
381
+ }
382
+ else if (TrackType.Equals(TEXT("Fade"), ESearchCase::IgnoreCase))
383
+ {
384
+ TrackClass = UMovieSceneFadeTrack::StaticClass();
385
+ }
386
+ else
387
+ {
388
+ return MCPError(FString::Printf(TEXT("Unknown track type: '%s'. Use Transform, Float, SkeletalAnimation, CameraCut, Audio, Event, or Fade."), *TrackType));
389
+ }
390
+
391
+ // Check if we should add to an actor binding or as a master track
392
+ FString ActorLabel = OptionalString(Params, TEXT("actorLabel"));
393
+ auto Result = MCPSuccess();
394
+
395
+ if (!ActorLabel.IsEmpty())
396
+ {
397
+ // Find the binding for this actor
398
+ REQUIRE_EDITOR_WORLD(World);
399
+
400
+ AActor* TargetActor = FindActorByLabel(World, ActorLabel);
401
+ if (!TargetActor)
402
+ {
403
+ return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
404
+ }
405
+
406
+ // Find or create a binding for this actor
407
+ FGuid BindingGuid;
408
+ bool bFoundBinding = false;
409
+
410
+ // Search existing possessables
411
+ for (int32 i = 0; i < MovieScene->GetPossessableCount(); ++i)
412
+ {
413
+ const FMovieScenePossessable& Possessable = MovieScene->GetPossessable(i);
414
+ if (Possessable.GetName() == ActorLabel || Possessable.GetName() == TargetActor->GetName())
415
+ {
416
+ BindingGuid = Possessable.GetGuid();
417
+ bFoundBinding = true;
418
+ break;
419
+ }
420
+ }
421
+
422
+ if (!bFoundBinding)
423
+ {
424
+ // Create a new possessable binding for the actor
425
+ BindingGuid = MovieScene->AddPossessable(ActorLabel, TargetActor->GetClass());
426
+ Sequence->BindPossessableObject(BindingGuid, *TargetActor, World);
427
+ }
428
+
429
+ // Idempotency: existing track of this class on binding?
430
+ if (UMovieSceneTrack* ExistingTrack = MovieScene->FindTrack(TrackClass, BindingGuid))
431
+ {
432
+ MCPSetExisted(Result);
433
+ Result->SetStringField(TEXT("actorLabel"), ActorLabel);
434
+ Result->SetStringField(TEXT("bindingGuid"), BindingGuid.ToString());
435
+ Result->SetStringField(TEXT("trackType"), TrackType);
436
+ Result->SetStringField(TEXT("trackClass"), ExistingTrack->GetClass()->GetName());
437
+ Result->SetStringField(TEXT("scope"), TEXT("binding"));
438
+ return MCPResult(Result);
439
+ }
440
+
441
+ // Add track to binding
442
+ UMovieSceneTrack* NewTrack = MovieScene->AddTrack(TrackClass, BindingGuid);
443
+ if (!NewTrack)
444
+ {
445
+ return MCPError(FString::Printf(TEXT("Failed to add %s track to actor '%s'"), *TrackType, *ActorLabel));
446
+ }
447
+
448
+ MCPSetCreated(Result);
449
+ Result->SetStringField(TEXT("actorLabel"), ActorLabel);
450
+ Result->SetStringField(TEXT("bindingGuid"), BindingGuid.ToString());
451
+ Result->SetStringField(TEXT("trackType"), TrackType);
452
+ Result->SetStringField(TEXT("trackClass"), NewTrack->GetClass()->GetName());
453
+ Result->SetStringField(TEXT("scope"), TEXT("binding"));
454
+ }
455
+ else
456
+ {
457
+ // Idempotency: any existing master track of this class?
458
+ TArray<UMovieSceneTrack*> MasterTracks = MovieScene->GetTracks();
459
+ for (UMovieSceneTrack* T : MasterTracks)
460
+ {
461
+ if (T && T->IsA(TrackClass))
462
+ {
463
+ MCPSetExisted(Result);
464
+ Result->SetStringField(TEXT("trackType"), TrackType);
465
+ Result->SetStringField(TEXT("trackClass"), T->GetClass()->GetName());
466
+ Result->SetStringField(TEXT("scope"), TEXT("master"));
467
+ return MCPResult(Result);
468
+ }
469
+ }
470
+
471
+ // Add as master track
472
+ UMovieSceneTrack* NewTrack = MovieScene->AddTrack(TrackClass);
473
+ if (!NewTrack)
474
+ {
475
+ return MCPError(FString::Printf(TEXT("Failed to add master %s track"), *TrackType));
476
+ }
477
+
478
+ MCPSetCreated(Result);
479
+ Result->SetStringField(TEXT("trackType"), TrackType);
480
+ Result->SetStringField(TEXT("trackClass"), NewTrack->GetClass()->GetName());
481
+ Result->SetStringField(TEXT("scope"), TEXT("master"));
482
+ }
483
+
484
+ // Mark the sequence package dirty
485
+ Sequence->GetOutermost()->MarkPackageDirty();
486
+
487
+ return MCPResult(Result);
488
+ }
489
+
490
+ // play_sequence - drive the Sequencer editor's transport.
491
+ //
492
+ // This used to issue "Sequencer.Play"/"Sequencer.Pause"/"Sequencer.Stop"
493
+ // through GEditor->Exec. No such console commands exist in the engine: the
494
+ // Sequencer module registers neither an exec handler nor a console command for
495
+ // them, so Exec returned false and the handler reported success for a call that
496
+ // did nothing at all. Every use of this action since it shipped was a no-op.
497
+ //
498
+ // ULevelSequenceEditorBlueprintLibrary is the real scripting surface - the same
499
+ // one the editor's own Python/Blueprint automation uses.
500
+ TSharedPtr<FJsonValue> FSequencerHandlers::SequenceControl(const TSharedPtr<FJsonObject>& Params)
501
+ {
502
+ FString Action;
503
+ if (auto Err = RequireString(Params, TEXT("action"), Action)) return Err;
504
+
505
+ const bool bPlay = Action.Equals(TEXT("play"), ESearchCase::IgnoreCase);
506
+ const bool bPause = Action.Equals(TEXT("pause"), ESearchCase::IgnoreCase);
507
+ const bool bStop = Action.Equals(TEXT("stop"), ESearchCase::IgnoreCase);
508
+ if (!bPlay && !bPause && !bStop)
509
+ {
510
+ return MCPError(FString::Printf(TEXT("Unknown action: '%s'. Use play, pause, or stop."), *Action));
511
+ }
512
+
513
+ // Open the requested sequence first: the transport acts on whatever
514
+ // Sequencer currently has open, so naming one and not opening it would
515
+ // drive a different sequence.
516
+ FString RequestedPath = OptionalString(Params, TEXT("sequencePath"), OptionalString(Params, TEXT("assetPath")));
517
+ if (!RequestedPath.IsEmpty())
518
+ {
519
+ ULevelSequence* Sequence = LoadAssetByPath<ULevelSequence>(RequestedPath);
520
+ if (!Sequence)
521
+ {
522
+ return MCPError(FString::Printf(TEXT("Level Sequence not found: %s"), *RequestedPath));
523
+ }
524
+ if (!ULevelSequenceEditorBlueprintLibrary::OpenLevelSequence(Sequence))
525
+ {
526
+ return MCPError(FString::Printf(
527
+ TEXT("Failed to open '%s' in Sequencer."), *Sequence->GetPathName()));
528
+ }
529
+ }
530
+
531
+ ULevelSequence* Current = ULevelSequenceEditorBlueprintLibrary::GetCurrentLevelSequence();
532
+ if (!Current)
533
+ {
534
+ return MCPError(TEXT("No Level Sequence is open in Sequencer. Pass sequencePath to open one, or open it in the editor first."));
535
+ }
536
+
537
+ if (bPlay)
538
+ {
539
+ ULevelSequenceEditorBlueprintLibrary::Play();
540
+ }
541
+ else if (bPause)
542
+ {
543
+ ULevelSequenceEditorBlueprintLibrary::Pause();
544
+ }
545
+ else
546
+ {
547
+ // There is no Stop(): pause, then rewind to the start of the playback
548
+ // range, which is what stopping means to a caller.
549
+ ULevelSequenceEditorBlueprintLibrary::Pause();
550
+ const FMovieSceneSequencePlaybackParams Rewind(FFrameTime(0), EUpdatePositionMethod::Scrub);
551
+ ULevelSequenceEditorBlueprintLibrary::SetGlobalPosition(Rewind);
552
+ }
553
+
554
+ auto Result = MCPSuccess();
555
+ Result->SetStringField(TEXT("action"), Action);
556
+ Result->SetStringField(TEXT("sequencePath"), Current->GetPathName());
557
+ // Read the transport back rather than reporting what was asked for.
558
+ Result->SetBoolField(TEXT("playing"), ULevelSequenceEditorBlueprintLibrary::IsPlaying());
559
+ return MCPResult(Result);
560
+ }
561
+
562
+ // set_sequence_playback_range -- set a Level Sequence's playback range in
563
+ // seconds. (#548) Params: sequencePath, startSeconds, endSeconds.
564
+ TSharedPtr<FJsonValue> FSequencerHandlers::SetPlaybackRange(const TSharedPtr<FJsonObject>& Params)
565
+ {
566
+ FString Path, Err;
567
+ ULevelSequence* Sequence = LoadSequence(Params, Path, Err);
568
+ if (!Sequence) return MCPError(Err);
569
+ UMovieScene* MovieScene = Sequence->GetMovieScene();
570
+ if (!MovieScene) return MCPError(TEXT("Sequence has no MovieScene"));
571
+
572
+ double StartSeconds = 0.0, EndSeconds = 0.0;
573
+ if (!Params->TryGetNumberField(TEXT("startSeconds"), StartSeconds) ||
574
+ !Params->TryGetNumberField(TEXT("endSeconds"), EndSeconds))
575
+ {
576
+ return MCPError(TEXT("Missing 'startSeconds' and/or 'endSeconds'"));
577
+ }
578
+
579
+ const FFrameRate Tick = MovieScene->GetTickResolution();
580
+ const FFrameNumber Start = Tick.AsFrameNumber(StartSeconds);
581
+ const FFrameNumber End = Tick.AsFrameNumber(EndSeconds);
582
+ MovieScene->SetPlaybackRange(TRange<FFrameNumber>(Start, End));
583
+ Sequence->GetOutermost()->MarkPackageDirty();
584
+
585
+ auto Result = MCPSuccess();
586
+ MCPSetUpdated(Result);
587
+ Result->SetStringField(TEXT("sequencePath"), Path);
588
+ Result->SetNumberField(TEXT("startSeconds"), StartSeconds);
589
+ Result->SetNumberField(TEXT("endSeconds"), EndSeconds);
590
+ return MCPResult(Result);
591
+ }
592
+
593
+ // add_sequence_section -- add a section to a track (creating the track if
594
+ // needed), set its start/end in seconds, and for a CameraCut track bind it to
595
+ // a camera actor. Returns the section index and its channel names. (#548)
596
+ // Params: sequencePath, trackType, actorLabel? (binding scope), startSeconds?,
597
+ // endSeconds?, cameraActorLabel? (CameraCut).
598
+ TSharedPtr<FJsonValue> FSequencerHandlers::AddSection(const TSharedPtr<FJsonObject>& Params)
599
+ {
600
+ FString Path, Err;
601
+ ULevelSequence* Sequence = LoadSequence(Params, Path, Err);
602
+ if (!Sequence) return MCPError(Err);
603
+ UMovieScene* MovieScene = Sequence->GetMovieScene();
604
+ if (!MovieScene) return MCPError(TEXT("Sequence has no MovieScene"));
605
+
606
+ FString TrackType;
607
+ if (auto E = RequireString(Params, TEXT("trackType"), TrackType)) return E;
608
+ UClass* TrackClass = ResolveTrackClass(TrackType);
609
+ if (!TrackClass) return MCPError(FString::Printf(TEXT("Unknown track type: '%s'"), *TrackType));
610
+
611
+ const FString ActorLabel = OptionalString(Params, TEXT("actorLabel"));
612
+
613
+ UMovieSceneTrack* Track = nullptr;
614
+ FGuid BindingGuid;
615
+ if (!ActorLabel.IsEmpty())
616
+ {
617
+ if (!ResolveActorBinding(Sequence, MovieScene, ActorLabel, BindingGuid, Err)) return MCPError(Err);
618
+ Track = MovieScene->FindTrack(TrackClass, BindingGuid);
619
+ if (!Track) Track = MovieScene->AddTrack(TrackClass, BindingGuid);
620
+ }
621
+ else
622
+ {
623
+ for (UMovieSceneTrack* T : MovieScene->GetTracks())
624
+ {
625
+ if (T && T->IsA(TrackClass)) { Track = T; break; }
626
+ }
627
+ if (!Track) Track = MovieScene->AddTrack(TrackClass);
628
+ }
629
+ if (!Track) return MCPError(FString::Printf(TEXT("Failed to resolve/add %s track"), *TrackType));
630
+
631
+ // Resolve the camera binding up front so a bad cameraActorLabel fails before
632
+ // we create an orphan section.
633
+ const FString CameraActorLabel = OptionalString(Params, TEXT("cameraActorLabel"));
634
+ FGuid CamGuid;
635
+ if (!CameraActorLabel.IsEmpty())
636
+ {
637
+ if (!ResolveActorBinding(Sequence, MovieScene, CameraActorLabel, CamGuid, Err)) return MCPError(Err);
638
+ }
639
+
640
+ UMovieSceneSection* Section = Track->CreateNewSection();
641
+ if (!Section) return MCPError(TEXT("Failed to create section"));
642
+ Track->AddSection(*Section);
643
+
644
+ const FFrameRate Tick = MovieScene->GetTickResolution();
645
+ double StartSeconds = 0.0, EndSeconds = 0.0;
646
+ const bool bHasStart = Params->TryGetNumberField(TEXT("startSeconds"), StartSeconds);
647
+ const bool bHasEnd = Params->TryGetNumberField(TEXT("endSeconds"), EndSeconds);
648
+ if (bHasStart || bHasEnd)
649
+ {
650
+ const FFrameNumber Start = Tick.AsFrameNumber(StartSeconds);
651
+ const FFrameNumber End = Tick.AsFrameNumber(bHasEnd ? EndSeconds : StartSeconds + 1.0);
652
+ Section->SetRange(TRange<FFrameNumber>(Start, End));
653
+ }
654
+
655
+ // CameraCut: bind to the camera actor's possessable resolved above.
656
+ if (!CameraActorLabel.IsEmpty())
657
+ {
658
+ if (UMovieSceneCameraCutSection* CutSection = Cast<UMovieSceneCameraCutSection>(Section))
659
+ {
660
+ CutSection->SetCameraGuid(CamGuid);
661
+ }
662
+ }
663
+
664
+ const int32 SectionIndex = Track->GetAllSections().IndexOfByKey(Section);
665
+
666
+ // Enumerate channel names so the caller knows what to key.
667
+ TArray<TSharedPtr<FJsonValue>> ChannelNames;
668
+ FMovieSceneChannelProxy& Proxy = Section->GetChannelProxy();
669
+ for (const FMovieSceneChannelEntry& Entry : Proxy.GetAllEntries())
670
+ {
671
+ TArrayView<const FMovieSceneChannelMetaData> AllMeta = Entry.GetMetaData();
672
+ for (const FMovieSceneChannelMetaData& Meta : AllMeta)
673
+ {
674
+ ChannelNames.Add(MakeShared<FJsonValueString>(Meta.Name.ToString()));
675
+ }
676
+ }
677
+
678
+ Sequence->GetOutermost()->MarkPackageDirty();
679
+
680
+ auto Result = MCPSuccess();
681
+ MCPSetCreated(Result);
682
+ Result->SetStringField(TEXT("sequencePath"), Path);
683
+ Result->SetStringField(TEXT("trackType"), TrackType);
684
+ if (!ActorLabel.IsEmpty()) Result->SetStringField(TEXT("bindingGuid"), BindingGuid.ToString());
685
+ Result->SetNumberField(TEXT("sectionIndex"), SectionIndex);
686
+ Result->SetArrayField(TEXT("channels"), ChannelNames);
687
+ return MCPResult(Result);
688
+ }
689
+
690
+ // set_sequence_keyframes -- add keyframes to a named channel of a section.
691
+ // Supports transform double channels (Location.X..Scale.Z, plus friendly
692
+ // x/y/z/yaw/pitch/roll) and float channels (Fade/Float). (#548)
693
+ // Params: sequencePath, trackType, actorLabel? (binding scope), sectionIndex?
694
+ // (default 0), channel, keyframes ([{seconds, value}]), interpolation? (cubic|linear).
695
+ TSharedPtr<FJsonValue> FSequencerHandlers::SetKeyframes(const TSharedPtr<FJsonObject>& Params)
696
+ {
697
+ FString Path, Err;
698
+ ULevelSequence* Sequence = LoadSequence(Params, Path, Err);
699
+ if (!Sequence) return MCPError(Err);
700
+ UMovieScene* MovieScene = Sequence->GetMovieScene();
701
+ if (!MovieScene) return MCPError(TEXT("Sequence has no MovieScene"));
702
+
703
+ FString TrackType;
704
+ if (auto E = RequireString(Params, TEXT("trackType"), TrackType)) return E;
705
+ UClass* TrackClass = ResolveTrackClass(TrackType);
706
+ if (!TrackClass) return MCPError(FString::Printf(TEXT("Unknown track type: '%s'"), *TrackType));
707
+
708
+ FString ChannelName;
709
+ if (auto E = RequireString(Params, TEXT("channel"), ChannelName)) return E;
710
+
711
+ const TArray<TSharedPtr<FJsonValue>>* Keyframes = nullptr;
712
+ if (!Params->TryGetArrayField(TEXT("keyframes"), Keyframes) || !Keyframes)
713
+ {
714
+ return MCPError(TEXT("Missing 'keyframes' array ([{seconds, value}, ...])"));
715
+ }
716
+
717
+ const FString ActorLabel = OptionalString(Params, TEXT("actorLabel"));
718
+ UMovieSceneTrack* Track = nullptr;
719
+ if (!ActorLabel.IsEmpty())
720
+ {
721
+ FGuid BindingGuid;
722
+ if (!ResolveActorBinding(Sequence, MovieScene, ActorLabel, BindingGuid, Err)) return MCPError(Err);
723
+ Track = MovieScene->FindTrack(TrackClass, BindingGuid);
724
+ }
725
+ else
726
+ {
727
+ for (UMovieSceneTrack* T : MovieScene->GetTracks())
728
+ {
729
+ if (T && T->IsA(TrackClass)) { Track = T; break; }
730
+ }
731
+ }
732
+ if (!Track) return MCPError(FString::Printf(TEXT("No %s track found (add a section first)"), *TrackType));
733
+
734
+ const TArray<UMovieSceneSection*>& Sections = Track->GetAllSections();
735
+ if (Sections.Num() == 0) return MCPError(TEXT("Track has no sections (call add_sequence_section first)"));
736
+ int32 SectionIndex = 0;
737
+ Params->TryGetNumberField(TEXT("sectionIndex"), SectionIndex);
738
+ if (SectionIndex < 0 || SectionIndex >= Sections.Num())
739
+ {
740
+ return MCPError(FString::Printf(TEXT("sectionIndex %d out of range (sections=%d)"), SectionIndex, Sections.Num()));
741
+ }
742
+ UMovieSceneSection* Section = Sections[SectionIndex];
743
+
744
+ const FString Canonical = CanonicalTransformChannel(ChannelName);
745
+ const bool bLinear = OptionalString(Params, TEXT("interpolation"), TEXT("cubic")).Equals(TEXT("linear"), ESearchCase::IgnoreCase);
746
+ const FFrameRate Tick = MovieScene->GetTickResolution();
747
+ Section->Modify();
748
+
749
+ int32 KeysAdded = 0;
750
+ FMovieSceneChannelProxy& Proxy = Section->GetChannelProxy();
751
+
752
+ // Try double channels (transform) by metadata name.
753
+ TArrayView<FMovieSceneDoubleChannel*> DoubleChannels = Proxy.GetChannels<FMovieSceneDoubleChannel>();
754
+ TArrayView<const FMovieSceneChannelMetaData> DoubleMeta = Proxy.GetMetaData<FMovieSceneDoubleChannel>();
755
+ bool bMatched = false;
756
+ for (int32 i = 0; i < DoubleChannels.Num(); ++i)
757
+ {
758
+ if (DoubleMeta.IsValidIndex(i) && DoubleMeta[i].Name.ToString().Equals(Canonical, ESearchCase::IgnoreCase))
759
+ {
760
+ for (const TSharedPtr<FJsonValue>& KfVal : *Keyframes)
761
+ {
762
+ const TSharedPtr<FJsonObject>* Kf = nullptr;
763
+ if (!KfVal->TryGetObject(Kf) || !Kf) continue;
764
+ double Sec = 0.0, Val = 0.0;
765
+ (*Kf)->TryGetNumberField(TEXT("seconds"), Sec);
766
+ (*Kf)->TryGetNumberField(TEXT("value"), Val);
767
+ const FFrameNumber Frame = Tick.AsFrameNumber(Sec);
768
+ if (bLinear) DoubleChannels[i]->AddLinearKey(Frame, Val);
769
+ else DoubleChannels[i]->AddCubicKey(Frame, Val);
770
+ ++KeysAdded;
771
+ }
772
+ bMatched = true;
773
+ break;
774
+ }
775
+ }
776
+
777
+ // Otherwise try float channels (fade/float track). Match by name, or take
778
+ // the only channel when the name doesn't disambiguate.
779
+ if (!bMatched)
780
+ {
781
+ TArrayView<FMovieSceneFloatChannel*> FloatChannels = Proxy.GetChannels<FMovieSceneFloatChannel>();
782
+ TArrayView<const FMovieSceneChannelMetaData> FloatMeta = Proxy.GetMetaData<FMovieSceneFloatChannel>();
783
+ int32 ChosenIdx = INDEX_NONE;
784
+ for (int32 i = 0; i < FloatChannels.Num(); ++i)
785
+ {
786
+ if (FloatMeta.IsValidIndex(i) && FloatMeta[i].Name.ToString().Equals(ChannelName, ESearchCase::IgnoreCase)) { ChosenIdx = i; break; }
787
+ }
788
+ if (ChosenIdx == INDEX_NONE && FloatChannels.Num() == 1) ChosenIdx = 0;
789
+ if (ChosenIdx != INDEX_NONE)
790
+ {
791
+ for (const TSharedPtr<FJsonValue>& KfVal : *Keyframes)
792
+ {
793
+ const TSharedPtr<FJsonObject>* Kf = nullptr;
794
+ if (!KfVal->TryGetObject(Kf) || !Kf) continue;
795
+ double Sec = 0.0, Val = 0.0;
796
+ (*Kf)->TryGetNumberField(TEXT("seconds"), Sec);
797
+ (*Kf)->TryGetNumberField(TEXT("value"), Val);
798
+ const FFrameNumber Frame = Tick.AsFrameNumber(Sec);
799
+ if (bLinear) FloatChannels[ChosenIdx]->AddLinearKey(Frame, (float)Val);
800
+ else FloatChannels[ChosenIdx]->AddCubicKey(Frame, (float)Val);
801
+ ++KeysAdded;
802
+ }
803
+ bMatched = true;
804
+ }
805
+ }
806
+
807
+ if (!bMatched)
808
+ {
809
+ return MCPError(FString::Printf(TEXT("Channel '%s' not found on the section. For Transform use Location.X/Rotation.Z/etc or x/yaw; for Fade use the float channel."), *ChannelName));
810
+ }
811
+
812
+ Sequence->GetOutermost()->MarkPackageDirty();
813
+
814
+ auto Result = MCPSuccess();
815
+ MCPSetUpdated(Result);
816
+ Result->SetStringField(TEXT("sequencePath"), Path);
817
+ Result->SetStringField(TEXT("trackType"), TrackType);
818
+ Result->SetStringField(TEXT("channel"), ChannelName);
819
+ Result->SetNumberField(TEXT("sectionIndex"), SectionIndex);
820
+ Result->SetNumberField(TEXT("keysAdded"), KeysAdded);
821
+ return MCPResult(Result);
822
+ }