ue-mcp 1.3.8-beta.13 → 1.3.8-beta.14

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 (56) hide show
  1. package/dist/tool-counts.json +2 -2
  2. package/package.json +1 -1
  3. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerJsonProperty.h +1 -1
  4. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerPoseSearchSchema.h +29 -1
  5. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Depth.cpp +10 -0
  6. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRigAuthoring.cpp +1 -1
  7. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_MotionMatching.cpp +174 -36
  8. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Sequence.cpp +32 -0
  9. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_SkeletalLive.cpp +13 -0
  10. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Skeleton.cpp +66 -5
  11. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_StateMachine.cpp +34 -8
  12. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_GeometryScript.cpp +4 -4
  13. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Import.cpp +30 -4
  14. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.cpp +4 -4
  15. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +1 -1
  16. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers_Depth.cpp +85 -2
  17. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers_MetaSound.cpp +126 -0
  18. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers_MetaSoundRead.cpp +78 -0
  19. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.cpp +11 -2
  20. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_UserTypes.cpp +20 -1
  21. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/ChooserHandlers.cpp +93 -8
  22. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/ChooserHandlers_Nested.cpp +4 -3
  23. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.cpp +84 -12
  24. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +10 -0
  25. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Profiling.cpp +55 -5
  26. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.cpp +11 -0
  27. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BTAuthoring.cpp +15 -2
  28. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +15 -0
  29. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers_Runtime.cpp +1 -1
  30. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers_Snapshot.cpp +1 -1
  31. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +20 -1
  32. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +30 -9
  33. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.cpp +5 -5
  34. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Query.cpp +2 -2
  35. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Save.cpp +3 -1
  36. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartitionSettings.cpp +8 -0
  37. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Build.cpp +6 -0
  38. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_RVT.cpp +45 -8
  39. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NiagaraHandlers.cpp +80 -0
  40. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NiagaraHandlers_Advanced.cpp +10 -0
  41. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/ProjectHandlers_Plugins.cpp +15 -0
  42. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +27 -0
  43. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.cpp +7 -0
  44. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/StateTreeHandlers.cpp +1 -1
  45. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/StateTreeHandlers_Depth.cpp +1 -3
  46. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetDeleteGuardTests.cpp +1 -1
  47. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPropertyPersistTests.cpp +1 -1
  48. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetSubobjectTests.cpp +1 -1
  49. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/DataTableRowWriteTests.cpp +9 -1
  50. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTestTypes.h +1 -1
  51. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageWriteGuardTests.cpp +1 -1
  52. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerPagination.h +15 -4
  53. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerUtils.h +66 -0
  54. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/MCPEngineCompat.h +38 -0
  55. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/UE_MCP_Bridge.Build.cs +18 -2
  56. package/plugin/ue_mcp_bridge/UE_MCP_Bridge.uplugin +1 -1
@@ -33,6 +33,6 @@
33
33
  },
34
34
  "nativeToolsets": 0,
35
35
  "nativeToolActions": 0,
36
- "generatedAt": "2026-09-10T04:36:36.698Z",
37
- "version": "1.3.8-beta.13"
36
+ "generatedAt": "2026-09-10T16:45:03.586Z",
37
+ "version": "1.3.8-beta.14"
38
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ue-mcp",
3
- "version": "1.3.8-beta.13",
3
+ "version": "1.3.8-beta.14",
4
4
  "description": "Unreal Engine MCP server - 26 tools, 1930+ actions for AI-driven editor control",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -10,7 +10,7 @@
10
10
  #include "UObject/UnrealType.h"
11
11
  #include "UObject/PropertyPortFlags.h"
12
12
  #include "UObject/SoftObjectPtr.h"
13
- #include "StructUtils/InstancedStruct.h"
13
+ #include "MCPEngineCompat.h"
14
14
  #include "GameplayTagContainer.h"
15
15
  #include "GameplayTagsManager.h"
16
16
  #include "Engine/Blueprint.h"
@@ -19,9 +19,37 @@
19
19
  #include "CoreMinimal.h"
20
20
  #include "UObject/UnrealType.h"
21
21
  #include "PoseSearch/PoseSearchSchema.h"
22
+ #include "HandlerUtils.h"
22
23
 
23
24
  namespace MCPPoseSearch
24
25
  {
26
+ /** The schema's per-role skeletons.
27
+ *
28
+ * 5.5 added GetRoledSkeletons(). On 5.4 the Skeletons array is private
29
+ * with no accessor at all, so it is read through the property system -
30
+ * FPoseSearchRoledSkeleton itself is public there, only the member is not.
31
+ * Returned by value so both engines present the same signature; the
32
+ * callers are read paths over a handful of roles. */
33
+ inline TArray<FPoseSearchRoledSkeleton> RoledSkeletons(const UPoseSearchSchema* Schema)
34
+ {
35
+ TArray<FPoseSearchRoledSkeleton> Out;
36
+ if (!Schema) return Out;
37
+ #if UE_MCP_HAS_5_5_API
38
+ Out = Schema->GetRoledSkeletons();
39
+ #else
40
+ if (const FArrayProperty* Prop = FindFProperty<FArrayProperty>(UPoseSearchSchema::StaticClass(), TEXT("Skeletons")))
41
+ {
42
+ FScriptArrayHelper Helper(Prop, Prop->ContainerPtrToValuePtr<void>(Schema));
43
+ Out.Reserve(Helper.Num());
44
+ for (int32 Index = 0; Index < Helper.Num(); ++Index)
45
+ {
46
+ Out.Add(*reinterpret_cast<const FPoseSearchRoledSkeleton*>(Helper.GetRawPtr(Index)));
47
+ }
48
+ }
49
+ #endif
50
+ return Out;
51
+ }
52
+
25
53
  /** Recompute FinalizedChannels and SchemaCardinality after a channel or
26
54
  * skeleton edit. Finalize() itself is private; PostEditChangeProperty is
27
55
  * the public door onto it. */
@@ -44,7 +72,7 @@ namespace MCPPoseSearch
44
72
  TArray<FString> Problems;
45
73
 
46
74
  bool bHasSkeleton = false;
47
- for (const FPoseSearchRoledSkeleton& Roled : Schema->GetRoledSkeletons())
75
+ for (const FPoseSearchRoledSkeleton& Roled : RoledSkeletons(Schema))
48
76
  {
49
77
  if (Roled.Skeleton)
50
78
  {
@@ -282,7 +282,17 @@ TSharedPtr<FJsonValue> FAnimationHandlers::SetStateMachineEntry(const TSharedPtr
282
282
  TEXT("Recreate it with animation(create_state_machine)."), *SMName));
283
283
  }
284
284
 
285
+ #if UE_MCP_HAS_5_5_API
285
286
  UEdGraphPin* EntryPin = Entry->GetOutputPin();
287
+ #else
288
+ // 5.4's UAnimStateEntryNode has no GetOutputPin(); the entry node allocates
289
+ // exactly one output pin, which is the same pin that accessor returns.
290
+ UEdGraphPin* EntryPin = nullptr;
291
+ for (UEdGraphPin* Pin : Entry->Pins)
292
+ {
293
+ if (Pin && Pin->Direction == EGPD_Output) { EntryPin = Pin; break; }
294
+ }
295
+ #endif
286
296
  if (!EntryPin)
287
297
  {
288
298
  return MCPError(TEXT("The state machine's entry node has no output pin."));
@@ -14,7 +14,7 @@
14
14
  #include "RigEditor/IKRigAutoCharacterizer.h"
15
15
  #include "RigEditor/IKRigController.h"
16
16
  #include "ScopedTransaction.h"
17
- #include "StructUtils/InstancedStruct.h"
17
+ #include "MCPEngineCompat.h"
18
18
  #endif
19
19
 
20
20
  namespace UE_MCP_IKRigAuthoring
@@ -15,8 +15,17 @@
15
15
  #include "PoseSearch/PoseSearchSchema.h"
16
16
  #include "HandlerPoseSearchSchema.h"
17
17
  #include "PoseSearch/PoseSearchFeatureChannel.h"
18
+ #if UE_MCP_HAS_5_5_API
18
19
  #include "PoseSearch/PoseSearchFeatureChannel_Pose.h"
19
20
  #include "PoseSearch/PoseSearchFeatureChannel_Trajectory.h"
21
+ #else
22
+ // UE 5.4 keeps both channel headers (and the FPoseSearchBone /
23
+ // FPoseSearchTrajectorySample structs inside them) private to the PoseSearch
24
+ // module, so nothing outside it can name the types. The classes are still
25
+ // UCLASSes with reflected properties, so the two channel actions author them
26
+ // through the property system instead - same asset, same fields, no header.
27
+ #include "HandlerJsonProperty.h"
28
+ #endif
20
29
  #include "PoseSearch/PoseSearchDatabase.h"
21
30
  #include "PoseSearch/PoseSearchNormalizationSet.h"
22
31
  #include "Animation/MirrorDataTable.h"
@@ -63,13 +72,47 @@ static int32 ParseFlagArray(const TSharedPtr<FJsonObject>& Obj, const TCHAR* Fie
63
72
  return Flags == 0 ? Default : Flags;
64
73
  }
65
74
 
75
+ // Bit values of EPoseSearchBoneFlags / EPoseSearchTrajectoryFlags. They are
76
+ // spelled out rather than taken from the enums because on 5.4 the enums are
77
+ // declared in the private channel headers; these are the values the asset
78
+ // serializes and they are the same from 5.4 through 5.8.
79
+ static constexpr int32 MCPBoneFlagVelocity = 1 << 0;
80
+ static constexpr int32 MCPBoneFlagPosition = 1 << 1;
81
+ static constexpr int32 MCPBoneFlagRotation = 1 << 2;
82
+ static constexpr int32 MCPBoneFlagPhase = 1 << 3;
83
+
84
+ static constexpr int32 MCPTrajFlagVelocity = 1 << 0;
85
+ static constexpr int32 MCPTrajFlagPosition = 1 << 1;
86
+ static constexpr int32 MCPTrajFlagVelocityDirection = 1 << 2;
87
+ static constexpr int32 MCPTrajFlagFacingDirection = 1 << 3;
88
+ static constexpr int32 MCPTrajFlagVelocityXY = 1 << 4;
89
+ static constexpr int32 MCPTrajFlagPositionXY = 1 << 5;
90
+ static constexpr int32 MCPTrajFlagVelocityDirectionXY = 1 << 6;
91
+ static constexpr int32 MCPTrajFlagFacingDirectionXY = 1 << 7;
92
+
93
+ #if UE_MCP_HAS_5_5_API
94
+ // Where the enums are reachable, hold the spelled-out values to them.
95
+ static_assert(MCPBoneFlagVelocity == int32(EPoseSearchBoneFlags::Velocity), "EPoseSearchBoneFlags::Velocity moved");
96
+ static_assert(MCPBoneFlagPosition == int32(EPoseSearchBoneFlags::Position), "EPoseSearchBoneFlags::Position moved");
97
+ static_assert(MCPBoneFlagRotation == int32(EPoseSearchBoneFlags::Rotation), "EPoseSearchBoneFlags::Rotation moved");
98
+ static_assert(MCPBoneFlagPhase == int32(EPoseSearchBoneFlags::Phase), "EPoseSearchBoneFlags::Phase moved");
99
+ static_assert(MCPTrajFlagVelocity == int32(EPoseSearchTrajectoryFlags::Velocity), "EPoseSearchTrajectoryFlags::Velocity moved");
100
+ static_assert(MCPTrajFlagPosition == int32(EPoseSearchTrajectoryFlags::Position), "EPoseSearchTrajectoryFlags::Position moved");
101
+ static_assert(MCPTrajFlagVelocityDirection == int32(EPoseSearchTrajectoryFlags::VelocityDirection), "EPoseSearchTrajectoryFlags::VelocityDirection moved");
102
+ static_assert(MCPTrajFlagFacingDirection == int32(EPoseSearchTrajectoryFlags::FacingDirection), "EPoseSearchTrajectoryFlags::FacingDirection moved");
103
+ static_assert(MCPTrajFlagVelocityXY == int32(EPoseSearchTrajectoryFlags::VelocityXY), "EPoseSearchTrajectoryFlags::VelocityXY moved");
104
+ static_assert(MCPTrajFlagPositionXY == int32(EPoseSearchTrajectoryFlags::PositionXY), "EPoseSearchTrajectoryFlags::PositionXY moved");
105
+ static_assert(MCPTrajFlagVelocityDirectionXY == int32(EPoseSearchTrajectoryFlags::VelocityDirectionXY), "EPoseSearchTrajectoryFlags::VelocityDirectionXY moved");
106
+ static_assert(MCPTrajFlagFacingDirectionXY == int32(EPoseSearchTrajectoryFlags::FacingDirectionXY), "EPoseSearchTrajectoryFlags::FacingDirectionXY moved");
107
+ #endif
108
+
66
109
  static const TMap<FString, int32>& BoneFlagTable()
67
110
  {
68
111
  static const TMap<FString, int32> Table = {
69
- { TEXT("velocity"), int32(EPoseSearchBoneFlags::Velocity) },
70
- { TEXT("position"), int32(EPoseSearchBoneFlags::Position) },
71
- { TEXT("rotation"), int32(EPoseSearchBoneFlags::Rotation) },
72
- { TEXT("phase"), int32(EPoseSearchBoneFlags::Phase) },
112
+ { TEXT("velocity"), MCPBoneFlagVelocity },
113
+ { TEXT("position"), MCPBoneFlagPosition },
114
+ { TEXT("rotation"), MCPBoneFlagRotation },
115
+ { TEXT("phase"), MCPBoneFlagPhase },
73
116
  };
74
117
  return Table;
75
118
  }
@@ -77,18 +120,36 @@ static const TMap<FString, int32>& BoneFlagTable()
77
120
  static const TMap<FString, int32>& TrajectoryFlagTable()
78
121
  {
79
122
  static const TMap<FString, int32> Table = {
80
- { TEXT("velocity"), int32(EPoseSearchTrajectoryFlags::Velocity) },
81
- { TEXT("position"), int32(EPoseSearchTrajectoryFlags::Position) },
82
- { TEXT("velocitydirection"), int32(EPoseSearchTrajectoryFlags::VelocityDirection) },
83
- { TEXT("facingdirection"), int32(EPoseSearchTrajectoryFlags::FacingDirection) },
84
- { TEXT("velocityxy"), int32(EPoseSearchTrajectoryFlags::VelocityXY) },
85
- { TEXT("positionxy"), int32(EPoseSearchTrajectoryFlags::PositionXY) },
86
- { TEXT("velocitydirectionxy"), int32(EPoseSearchTrajectoryFlags::VelocityDirectionXY) },
87
- { TEXT("facingdirectionxy"), int32(EPoseSearchTrajectoryFlags::FacingDirectionXY) },
123
+ { TEXT("velocity"), MCPTrajFlagVelocity },
124
+ { TEXT("position"), MCPTrajFlagPosition },
125
+ { TEXT("velocitydirection"), MCPTrajFlagVelocityDirection },
126
+ { TEXT("facingdirection"), MCPTrajFlagFacingDirection },
127
+ { TEXT("velocityxy"), MCPTrajFlagVelocityXY },
128
+ { TEXT("positionxy"), MCPTrajFlagPositionXY },
129
+ { TEXT("velocitydirectionxy"), MCPTrajFlagVelocityDirectionXY },
130
+ { TEXT("facingdirectionxy"), MCPTrajFlagFacingDirectionXY },
88
131
  };
89
132
  return Table;
90
133
  }
91
134
 
135
+ #if !UE_MCP_HAS_5_5_API
136
+ /** Construct one of the PoseSearch channel classes by its script path. On 5.4
137
+ * the concrete classes are private to the PoseSearch module, but the UClass is
138
+ * registered like any other, so the object is created through it. */
139
+ static UPoseSearchFeatureChannel* MCPNewPoseSearchChannel(UPoseSearchSchema* Schema, const TCHAR* ClassPath)
140
+ {
141
+ UClass* ChannelClass = FindObject<UClass>(nullptr, ClassPath);
142
+ if (!ChannelClass) return nullptr;
143
+ return NewObject<UPoseSearchFeatureChannel>(Schema, ChannelClass, NAME_None, RF_Transactional);
144
+ }
145
+
146
+ /** Write one reflected property of a channel from JSON. */
147
+ static bool MCPSetChannelProperty(UPoseSearchFeatureChannel* Channel, const TCHAR* PropertyName, const TSharedPtr<FJsonValue>& Value, FString& OutError)
148
+ {
149
+ return MCPJsonProperty::SetDottedPropertyFromJson(Channel, PropertyName, Value, OutError);
150
+ }
151
+ #endif
152
+
92
153
  // ─── AnimGraph node authoring helpers ─────────────────────────────────────
93
154
 
94
155
  // Anim graph nodes keep their settings in a private `Node` USTRUCT member. Reach
@@ -228,23 +289,22 @@ TSharedPtr<FJsonValue> FAnimationHandlers::AddPoseSearchSchemaPoseChannel(const
228
289
  return MCPError(TEXT("Missing 'bones' (array of {bone, flags?:[velocity,position,rotation,phase], weight?})"));
229
290
  }
230
291
 
231
- Schema->Modify();
232
- UPoseSearchFeatureChannel_Pose* Channel = NewObject<UPoseSearchFeatureChannel_Pose>(Schema, NAME_None, RF_Transactional);
233
- double ChannelWeight = 0.0;
234
- if (Params->TryGetNumberField(TEXT("weight"), ChannelWeight)) Channel->Weight = (float)ChannelWeight;
235
-
292
+ // Parse first, engine-independently, then write the parsed bones into the
293
+ // channel the way the running engine allows.
294
+ struct FParsedBone { FString Name; int32 Flags; float Weight; };
295
+ TArray<FParsedBone> ParsedBones;
236
296
  TArray<TSharedPtr<FJsonValue>> Added;
237
297
  for (const TSharedPtr<FJsonValue>& V : *Bones)
238
298
  {
239
299
  const TSharedPtr<FJsonObject>* BoneObj = nullptr;
240
300
  FString BoneName;
241
- int32 Flags = int32(EPoseSearchBoneFlags::Position);
301
+ int32 Flags = MCPBoneFlagPosition;
242
302
  float Weight = 1.f;
243
303
  if (V->TryGetObject(BoneObj) && BoneObj && (*BoneObj).IsValid())
244
304
  {
245
305
  if (!(*BoneObj)->TryGetStringField(TEXT("bone"), BoneName))
246
306
  return MCPError(TEXT("Each bone entry needs a 'bone' name"));
247
- Flags = ParseFlagArray(*BoneObj, TEXT("flags"), BoneFlagTable(), int32(EPoseSearchBoneFlags::Position));
307
+ Flags = ParseFlagArray(*BoneObj, TEXT("flags"), BoneFlagTable(), MCPBoneFlagPosition);
248
308
  double W = 0.0;
249
309
  if ((*BoneObj)->TryGetNumberField(TEXT("weight"), W)) Weight = (float)W;
250
310
  }
@@ -253,13 +313,52 @@ TSharedPtr<FJsonValue> FAnimationHandlers::AddPoseSearchSchemaPoseChannel(const
253
313
  return MCPError(TEXT("Each bone entry must be an object or a bone-name string"));
254
314
  }
255
315
 
316
+ ParsedBones.Add({ BoneName, Flags, Weight });
317
+ Added.Add(MakeShared<FJsonValueString>(BoneName));
318
+ }
319
+
320
+ Schema->Modify();
321
+ double ChannelWeight = 0.0;
322
+ const bool bHasChannelWeight = Params->TryGetNumberField(TEXT("weight"), ChannelWeight);
323
+
324
+ #if UE_MCP_HAS_5_5_API
325
+ UPoseSearchFeatureChannel_Pose* Channel = NewObject<UPoseSearchFeatureChannel_Pose>(Schema, NAME_None, RF_Transactional);
326
+ if (bHasChannelWeight) Channel->Weight = (float)ChannelWeight;
327
+ for (const FParsedBone& Parsed : ParsedBones)
328
+ {
256
329
  FPoseSearchBone Bone;
257
- Bone.Reference.BoneName = FName(*BoneName);
258
- Bone.Flags = Flags;
259
- Bone.Weight = Weight;
330
+ Bone.Reference.BoneName = FName(*Parsed.Name);
331
+ Bone.Flags = Parsed.Flags;
332
+ Bone.Weight = Parsed.Weight;
260
333
  Channel->SampledBones.Add(Bone);
261
- Added.Add(MakeShared<FJsonValueString>(BoneName));
262
334
  }
335
+ #else
336
+ UPoseSearchFeatureChannel* Channel = MCPNewPoseSearchChannel(Schema, TEXT("/Script/PoseSearch.PoseSearchFeatureChannel_Pose"));
337
+ if (!Channel) return MCPError(TEXT("PoseSearchFeatureChannel_Pose class is not loaded - is the PoseSearch plugin enabled?"));
338
+
339
+ FString PropertyError;
340
+ if (bHasChannelWeight && !MCPSetChannelProperty(Channel, TEXT("Weight"), MakeShared<FJsonValueNumber>(ChannelWeight), PropertyError))
341
+ {
342
+ return MCPError(FString::Printf(TEXT("Could not set channel weight: %s"), *PropertyError));
343
+ }
344
+
345
+ TArray<TSharedPtr<FJsonValue>> BoneStructs;
346
+ for (const FParsedBone& Parsed : ParsedBones)
347
+ {
348
+ TSharedPtr<FJsonObject> Reference = MakeShared<FJsonObject>();
349
+ Reference->SetStringField(TEXT("BoneName"), Parsed.Name);
350
+
351
+ TSharedPtr<FJsonObject> Bone = MakeShared<FJsonObject>();
352
+ Bone->SetObjectField(TEXT("Reference"), Reference);
353
+ Bone->SetNumberField(TEXT("Flags"), Parsed.Flags);
354
+ Bone->SetNumberField(TEXT("Weight"), Parsed.Weight);
355
+ BoneStructs.Add(MakeShared<FJsonValueObject>(Bone));
356
+ }
357
+ if (!MCPSetChannelProperty(Channel, TEXT("SampledBones"), MakeShared<FJsonValueArray>(BoneStructs), PropertyError))
358
+ {
359
+ return MCPError(FString::Printf(TEXT("Could not set SampledBones: %s"), *PropertyError));
360
+ }
361
+ #endif
263
362
 
264
363
  Schema->AddChannel(Channel);
265
364
  MCPPoseSearch::Finalize(Schema);
@@ -292,28 +391,62 @@ TSharedPtr<FJsonValue> FAnimationHandlers::AddPoseSearchSchemaTrajectoryChannel(
292
391
  return MCPError(TEXT("Missing 'samples' (array of {offset, flags?:[position,velocity,facingDirection,...], weight?}). Negative offsets are history, positive are prediction."));
293
392
  }
294
393
 
295
- Schema->Modify();
296
- UPoseSearchFeatureChannel_Trajectory* Channel = NewObject<UPoseSearchFeatureChannel_Trajectory>(Schema, NAME_None, RF_Transactional);
297
- double ChannelWeight = 0.0;
298
- if (Params->TryGetNumberField(TEXT("weight"), ChannelWeight)) Channel->Weight = (float)ChannelWeight;
299
-
300
- int32 Count = 0;
394
+ struct FParsedSample { float Offset; int32 Flags; float Weight; };
395
+ TArray<FParsedSample> ParsedSamples;
301
396
  for (const TSharedPtr<FJsonValue>& V : *Samples)
302
397
  {
303
398
  const TSharedPtr<FJsonObject>* SampleObj = nullptr;
304
399
  if (!V->TryGetObject(SampleObj) || !SampleObj || !(*SampleObj).IsValid())
305
400
  return MCPError(TEXT("Each sample must be an object {offset, flags?, weight?}"));
306
401
 
307
- FPoseSearchTrajectorySample Sample;
308
402
  double Offset = 0.0;
309
403
  (*SampleObj)->TryGetNumberField(TEXT("offset"), Offset);
310
- Sample.Offset = (float)Offset;
311
- Sample.Flags = ParseFlagArray(*SampleObj, TEXT("flags"), TrajectoryFlagTable(), int32(EPoseSearchTrajectoryFlags::Position));
312
- double W = 0.0;
313
- if ((*SampleObj)->TryGetNumberField(TEXT("weight"), W)) Sample.Weight = (float)W;
404
+ const int32 Flags = ParseFlagArray(*SampleObj, TEXT("flags"), TrajectoryFlagTable(), MCPTrajFlagPosition);
405
+ double W = 1.0;
406
+ (*SampleObj)->TryGetNumberField(TEXT("weight"), W);
407
+ ParsedSamples.Add({ (float)Offset, Flags, (float)W });
408
+ }
409
+ const int32 Count = ParsedSamples.Num();
410
+
411
+ Schema->Modify();
412
+ double ChannelWeight = 0.0;
413
+ const bool bHasChannelWeight = Params->TryGetNumberField(TEXT("weight"), ChannelWeight);
414
+
415
+ #if UE_MCP_HAS_5_5_API
416
+ UPoseSearchFeatureChannel_Trajectory* Channel = NewObject<UPoseSearchFeatureChannel_Trajectory>(Schema, NAME_None, RF_Transactional);
417
+ if (bHasChannelWeight) Channel->Weight = (float)ChannelWeight;
418
+ for (const FParsedSample& Parsed : ParsedSamples)
419
+ {
420
+ FPoseSearchTrajectorySample Sample;
421
+ Sample.Offset = Parsed.Offset;
422
+ Sample.Flags = Parsed.Flags;
423
+ Sample.Weight = Parsed.Weight;
314
424
  Channel->Samples.Add(Sample);
315
- ++Count;
316
425
  }
426
+ #else
427
+ UPoseSearchFeatureChannel* Channel = MCPNewPoseSearchChannel(Schema, TEXT("/Script/PoseSearch.PoseSearchFeatureChannel_Trajectory"));
428
+ if (!Channel) return MCPError(TEXT("PoseSearchFeatureChannel_Trajectory class is not loaded - is the PoseSearch plugin enabled?"));
429
+
430
+ FString PropertyError;
431
+ if (bHasChannelWeight && !MCPSetChannelProperty(Channel, TEXT("Weight"), MakeShared<FJsonValueNumber>(ChannelWeight), PropertyError))
432
+ {
433
+ return MCPError(FString::Printf(TEXT("Could not set channel weight: %s"), *PropertyError));
434
+ }
435
+
436
+ TArray<TSharedPtr<FJsonValue>> SampleStructs;
437
+ for (const FParsedSample& Parsed : ParsedSamples)
438
+ {
439
+ TSharedPtr<FJsonObject> Sample = MakeShared<FJsonObject>();
440
+ Sample->SetNumberField(TEXT("Offset"), Parsed.Offset);
441
+ Sample->SetNumberField(TEXT("Flags"), Parsed.Flags);
442
+ Sample->SetNumberField(TEXT("Weight"), Parsed.Weight);
443
+ SampleStructs.Add(MakeShared<FJsonValueObject>(Sample));
444
+ }
445
+ if (!MCPSetChannelProperty(Channel, TEXT("Samples"), MakeShared<FJsonValueArray>(SampleStructs), PropertyError))
446
+ {
447
+ return MCPError(FString::Printf(TEXT("Could not set Samples: %s"), *PropertyError));
448
+ }
449
+ #endif
317
450
 
318
451
  Schema->AddChannel(Channel);
319
452
  MCPPoseSearch::Finalize(Schema);
@@ -344,7 +477,7 @@ TSharedPtr<FJsonValue> FAnimationHandlers::ReadPoseSearchSchema(const TSharedPtr
344
477
  Res->SetNumberField(TEXT("sampleRate"), Schema->SampleRate);
345
478
 
346
479
  TArray<TSharedPtr<FJsonValue>> Skeletons;
347
- for (const FPoseSearchRoledSkeleton& Roled : Schema->GetRoledSkeletons())
480
+ for (const FPoseSearchRoledSkeleton& Roled : MCPPoseSearch::RoledSkeletons(Schema))
348
481
  {
349
482
  TSharedPtr<FJsonObject> S = MakeShared<FJsonObject>();
350
483
  S->SetStringField(TEXT("role"), Roled.Role.ToString());
@@ -526,7 +659,10 @@ TSharedPtr<FJsonValue> FAnimationHandlers::SetPoseSearchDatabaseSettings(const T
526
659
  FString PrevPoseSearchModeName = TEXT("BruteForce");
527
660
  if (PrevPoseSearchMode == EPoseSearchMode::PCAKDTree) PrevPoseSearchModeName = TEXT("PCAKDTree");
528
661
  else if (PrevPoseSearchMode == EPoseSearchMode::VPTree) PrevPoseSearchModeName = TEXT("VPTree");
662
+ #if UE_MCP_HAS_5_5_API
663
+ // EPoseSearchMode::EventOnly is a later addition to the enum.
529
664
  else if (PrevPoseSearchMode == EPoseSearchMode::EventOnly) PrevPoseSearchModeName = TEXT("EventOnly");
665
+ #endif
530
666
  #if WITH_EDITORONLY_DATA
531
667
  const int32 PrevNumberOfPrincipalComponents = Database->NumberOfPrincipalComponents;
532
668
  const FString PrevNormalizationSetPath =
@@ -547,7 +683,9 @@ TSharedPtr<FJsonValue> FAnimationHandlers::SetPoseSearchDatabaseSettings(const T
547
683
  if (Mode.Equals(TEXT("bruteforce"), ESearchCase::IgnoreCase)) Database->PoseSearchMode = EPoseSearchMode::BruteForce;
548
684
  else if (Mode.Equals(TEXT("pcakdtree"), ESearchCase::IgnoreCase)) Database->PoseSearchMode = EPoseSearchMode::PCAKDTree;
549
685
  else if (Mode.Equals(TEXT("vptree"), ESearchCase::IgnoreCase)) Database->PoseSearchMode = EPoseSearchMode::VPTree;
686
+ #if UE_MCP_HAS_5_5_API
550
687
  else if (Mode.Equals(TEXT("eventonly"), ESearchCase::IgnoreCase)) Database->PoseSearchMode = EPoseSearchMode::EventOnly;
688
+ #endif
551
689
  }
552
690
 
553
691
  #if WITH_EDITORONLY_DATA
@@ -1187,8 +1187,40 @@ TSharedPtr<FJsonValue> FAnimationHandlers::ApplyAnimationModifier(const TSharedP
1187
1187
  bool bRegistered = false;
1188
1188
  if (!Instance)
1189
1189
  {
1190
+ #if UE_MCP_HAS_5_5_API
1190
1191
  UAnimationModifiersAssetUserData::AddAnimationModifierOfClass(Seq, ModClass);
1191
1192
  bRegistered = true;
1193
+ #else
1194
+ // 5.4 has no static AddAnimationModifierOfClass, and its member
1195
+ // AddAnimationModifier is protected. That member does exactly one
1196
+ // thing - append to the reflected AnimationModifierInstances array -
1197
+ // so the registration is done through the property system here, and
1198
+ // the sequence keeps the same modifier list the editor tab shows.
1199
+ bRegistered = false;
1200
+ {
1201
+ UAnimationModifiersAssetUserData* Data = Seq->GetAssetUserData<UAnimationModifiersAssetUserData>();
1202
+ if (!Data)
1203
+ {
1204
+ Data = NewObject<UAnimationModifiersAssetUserData>(
1205
+ Seq, UAnimationModifiersAssetUserData::StaticClass(), NAME_None, RF_Transactional);
1206
+ Seq->AddAssetUserData(Data);
1207
+ }
1208
+ FArrayProperty* InstancesProp = FindFProperty<FArrayProperty>(
1209
+ UAnimationModifiersAssetUserData::StaticClass(), TEXT("AnimationModifierInstances"));
1210
+ FObjectProperty* InstanceEntryProp = InstancesProp
1211
+ ? CastField<FObjectProperty>(InstancesProp->Inner) : nullptr;
1212
+ if (Data && InstanceEntryProp)
1213
+ {
1214
+ UAnimationModifier* NewModifier = NewObject<UAnimationModifier>(
1215
+ Data, ModClass, NAME_None, RF_Transactional);
1216
+ Data->Modify();
1217
+ FScriptArrayHelper ArrayHelper(InstancesProp, InstancesProp->ContainerPtrToValuePtr<void>(Data));
1218
+ const int32 NewIndex = ArrayHelper.AddValue();
1219
+ InstanceEntryProp->SetObjectPropertyValue(ArrayHelper.GetRawPtr(NewIndex), NewModifier);
1220
+ bRegistered = true;
1221
+ }
1222
+ }
1223
+ #endif
1192
1224
  UserData = Seq->GetAssetUserData<UAnimationModifiersAssetUserData>();
1193
1225
  if (UserData)
1194
1226
  {
@@ -507,6 +507,18 @@ TSharedPtr<FJsonValue> FAnimationHandlers::PreviewAnimation(const TSharedPtr<FJs
507
507
  // or template and disappears when that component is reconstructed.
508
508
  TSharedPtr<FJsonValue> FAnimationHandlers::SetLivePostProcessAnimBlueprint(const TSharedPtr<FJsonObject>& Params)
509
509
  {
510
+ #if !UE_MCP_HAS_5_5_API
511
+ // USkeletalMeshComponent::OverridePostProcessAnimBP, its setter, and
512
+ // GetPostProcessAnimBPClassToBeUsed all arrived in 5.5. On 5.4 the
513
+ // post-process AnimBP belongs to the SkeletalMesh asset and a component
514
+ // carries no override to set or clear, so there is no live override to
515
+ // report on either.
516
+ auto Result = MakeShared<FJsonObject>();
517
+ Result->SetBoolField(TEXT("success"), false);
518
+ Result->SetStringField(TEXT("errorCode"), TEXT("unsupported_engine_version"));
519
+ Result->SetStringField(TEXT("error"), TEXT("set_live_post_process_anim_blueprint requires Unreal Engine 5.5 or newer, because the per-component post-process AnimBP override does not exist in 5.4. Set the post-process AnimBP on the SkeletalMesh asset instead."));
520
+ return MCPResult(Result);
521
+ #else
510
522
  FString ActorLabel;
511
523
  if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return Err;
512
524
  const FString ComponentName = OptionalString(Params, TEXT("componentName"));
@@ -612,6 +624,7 @@ TSharedPtr<FJsonValue> FAnimationHandlers::SetLivePostProcessAnimBlueprint(const
612
624
  }
613
625
  MCPSetRollback(Result, TEXT("set_live_post_process_anim_blueprint"), Rollback);
614
626
  return MCPResult(Result);
627
+ #endif
615
628
  }
616
629
 
617
630
 
@@ -823,6 +823,62 @@ static TArray<FString> SkeletonBoneNames(const USkeleton* Skeleton)
823
823
  return Out;
824
824
  }
825
825
 
826
+ // ── Curve metadata flags ─────────────────────────────────────────────────────
827
+ //
828
+ // 5.5 added the named accessors (GetCurveMetaDataMaterial and friends). On 5.4
829
+ // the same two bits live in FCurveMetaData::Type, reached through the mutable
830
+ // GetCurveMetaData, so the flags read and write the same asset data either way.
831
+
832
+ static bool MCPGetCurveFlagMaterial(const USkeleton* Skeleton, FName CurveName)
833
+ {
834
+ if (!Skeleton) return false;
835
+ #if UE_MCP_HAS_5_5_API
836
+ return Skeleton->GetCurveMetaDataMaterial(CurveName);
837
+ #else
838
+ const FCurveMetaData* MetaData = Skeleton->GetCurveMetaData(CurveName);
839
+ return MetaData ? MetaData->Type.bMaterial : false;
840
+ #endif
841
+ }
842
+
843
+ static bool MCPGetCurveFlagMorphTarget(const USkeleton* Skeleton, FName CurveName)
844
+ {
845
+ if (!Skeleton) return false;
846
+ #if UE_MCP_HAS_5_5_API
847
+ return Skeleton->GetCurveMetaDataMorphTarget(CurveName);
848
+ #else
849
+ const FCurveMetaData* MetaData = Skeleton->GetCurveMetaData(CurveName);
850
+ return MetaData ? MetaData->Type.bMorphtarget : false;
851
+ #endif
852
+ }
853
+
854
+ static void MCPSetCurveFlagMaterial(USkeleton* Skeleton, FName CurveName, bool bValue)
855
+ {
856
+ if (!Skeleton) return;
857
+ #if UE_MCP_HAS_5_5_API
858
+ Skeleton->SetCurveMetaDataMaterial(CurveName, bValue);
859
+ #else
860
+ if (FCurveMetaData* MetaData = Skeleton->GetCurveMetaData(CurveName))
861
+ {
862
+ Skeleton->Modify();
863
+ MetaData->Type.bMaterial = bValue;
864
+ }
865
+ #endif
866
+ }
867
+
868
+ static void MCPSetCurveFlagMorphTarget(USkeleton* Skeleton, FName CurveName, bool bValue)
869
+ {
870
+ if (!Skeleton) return;
871
+ #if UE_MCP_HAS_5_5_API
872
+ Skeleton->SetCurveMetaDataMorphTarget(CurveName, bValue);
873
+ #else
874
+ if (FCurveMetaData* MetaData = Skeleton->GetCurveMetaData(CurveName))
875
+ {
876
+ Skeleton->Modify();
877
+ MetaData->Type.bMorphtarget = bValue;
878
+ }
879
+ #endif
880
+ }
881
+
826
882
  } // namespace UE_MCP_SkeletonEdit
827
883
 
828
884
  // ═════════════════════════════════════════════════════════════════════════════
@@ -2572,7 +2628,12 @@ TSharedPtr<FJsonValue> FAnimationHandlers::EditCurveMetadata(const TSharedPtr<FJ
2572
2628
  Row->SetStringField(TEXT("curve"), Curve);
2573
2629
  // Returns false when an entry already existed, which is the
2574
2630
  // idempotent case rather than a failure.
2575
- const bool bNew = Skeleton->AddCurveMetaData(FName(*Curve), false);
2631
+ #if UE_MCP_HAS_5_5_API
2632
+ const bool bNew = Skeleton->AddCurveMetaData(FName(*Curve), /*bTransact*/ false);
2633
+ #else
2634
+ // 5.4 has no bTransact parameter; the call transacts either way.
2635
+ const bool bNew = Skeleton->AddCurveMetaData(FName(*Curve));
2636
+ #endif
2576
2637
  Row->SetBoolField(TEXT("created"), bNew);
2577
2638
  Row->SetBoolField(TEXT("existed"), !bNew);
2578
2639
  bNew ? ++Added : ++AlreadyPresent;
@@ -2587,23 +2648,23 @@ TSharedPtr<FJsonValue> FAnimationHandlers::EditCurveMetadata(const TSharedPtr<FJ
2587
2648
  bool bChanged = false;
2588
2649
  if (Flag.bMaterial.IsSet())
2589
2650
  {
2590
- const bool Prior = Skeleton->GetCurveMetaDataMaterial(FName(*Flag.Curve));
2651
+ const bool Prior = MCPGetCurveFlagMaterial(Skeleton, FName(*Flag.Curve));
2591
2652
  Row->SetBoolField(TEXT("priorMaterial"), Prior);
2592
2653
  Row->SetBoolField(TEXT("material"), Flag.bMaterial.GetValue());
2593
2654
  if (Prior != Flag.bMaterial.GetValue())
2594
2655
  {
2595
- Skeleton->SetCurveMetaDataMaterial(FName(*Flag.Curve), Flag.bMaterial.GetValue());
2656
+ MCPSetCurveFlagMaterial(Skeleton, FName(*Flag.Curve), Flag.bMaterial.GetValue());
2596
2657
  bChanged = true;
2597
2658
  }
2598
2659
  }
2599
2660
  if (Flag.bMorphTarget.IsSet())
2600
2661
  {
2601
- const bool Prior = Skeleton->GetCurveMetaDataMorphTarget(FName(*Flag.Curve));
2662
+ const bool Prior = MCPGetCurveFlagMorphTarget(Skeleton, FName(*Flag.Curve));
2602
2663
  Row->SetBoolField(TEXT("priorMorphTarget"), Prior);
2603
2664
  Row->SetBoolField(TEXT("morphTarget"), Flag.bMorphTarget.GetValue());
2604
2665
  if (Prior != Flag.bMorphTarget.GetValue())
2605
2666
  {
2606
- Skeleton->SetCurveMetaDataMorphTarget(FName(*Flag.Curve), Flag.bMorphTarget.GetValue());
2667
+ MCPSetCurveFlagMorphTarget(Skeleton, FName(*Flag.Curve), Flag.bMorphTarget.GetValue());
2607
2668
  bChanged = true;
2608
2669
  }
2609
2670
  }