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.
- package/dist/context-cli.js +16 -16
- package/dist/engine-observer.js +68 -68
- package/dist/flow/guard.d.ts +26 -37
- package/dist/flow/guard.js +40 -25
- package/dist/flow/guard.js.map +1 -1
- package/dist/flow/guarded-bridge.d.ts +0 -10
- package/dist/flow/guarded-bridge.js +18 -23
- package/dist/flow/guarded-bridge.js.map +1 -1
- package/dist/flow/schema.d.ts +276 -16
- package/dist/flow/task-guards.d.ts +1 -1
- package/dist/flow/task-guards.js +44 -68
- package/dist/flow/task-guards.js.map +1 -1
- package/dist/instructions.js +130 -130
- package/dist/plugin-cli.js +299 -299
- package/dist/tool-counts.json +2 -2
- package/package.json +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/EngineStatusHooks.cpp +76 -76
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/EngineStatusHooks.h +27 -27
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerRegistry.cpp +128 -128
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerRegistry.h +63 -63
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRig.cpp +230 -230
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +323 -323
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Internal.h +20 -20
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/ChooserHandlers_Nested.cpp +537 -537
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DemoHandlers.h +77 -77
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +353 -353
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.h +18 -18
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_InputRuntime.cpp +246 -246
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +597 -597
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +51 -51
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +878 -878
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +449 -449
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Function.cpp +275 -275
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Graph.cpp +551 -551
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NetworkingHandlers.cpp +531 -531
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NetworkingHandlers.h +28 -28
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +677 -677
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +822 -822
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.h +22 -22
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +218 -218
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.h +17 -17
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/VolumeHelpers_Internal.cpp +46 -46
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/VolumeHelpers_Internal.h +23 -23
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/MCPExternalRegistry.cpp +83 -83
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/UE_MCP_BridgeModule.h +14 -14
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/UE_MCP_BridgeStatusModule.cpp +37 -37
- package/dist/manifest-signature.d.ts +0 -1
- package/dist/manifest-signature.js +0 -53
- package/dist/manifest-signature.js.map +0 -1
- package/dist/pie/schema.d.ts +0 -791
- package/dist/pie/schema.js +0 -151
- package/dist/pie/schema.js.map +0 -1
- package/dist/watch-cli.js.map +0 -1
- /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
|
+
}
|