ultimate-unreal-engine-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +729 -0
  2. package/dist/build/error-parser.js +51 -0
  3. package/dist/build/fix-suggester.js +84 -0
  4. package/dist/build/ubt-runner.js +146 -0
  5. package/dist/cli.js +13 -0
  6. package/dist/config.js +8 -0
  7. package/dist/docs/data/ue57-api.js +228 -0
  8. package/dist/docs/doc-index.js +110 -0
  9. package/dist/docs/types.js +4 -0
  10. package/dist/generators/class-generator.js +363 -0
  11. package/dist/generators/file-modifier.js +276 -0
  12. package/dist/generators/uht-validator.js +177 -0
  13. package/dist/index.js +89 -0
  14. package/dist/parsers/cpp-class-index.js +230 -0
  15. package/dist/parsers/cpp-parser.js +369 -0
  16. package/dist/parsers/ini-parser.js +216 -0
  17. package/dist/parsers/uproject-parser.js +130 -0
  18. package/dist/plugin-bridge/client.js +217 -0
  19. package/dist/plugin-bridge/protocol.js +6 -0
  20. package/dist/plugin-bridge/retry.js +23 -0
  21. package/dist/setup.js +209 -0
  22. package/dist/tools/ai-systems/index.js +247 -0
  23. package/dist/tools/ai-systems/types.js +4 -0
  24. package/dist/tools/animation/index.js +241 -0
  25. package/dist/tools/animation/types.js +4 -0
  26. package/dist/tools/audio/index.js +204 -0
  27. package/dist/tools/audio/types.js +4 -0
  28. package/dist/tools/blueprint/index.js +495 -0
  29. package/dist/tools/blueprint/types.js +4 -0
  30. package/dist/tools/build/index.js +163 -0
  31. package/dist/tools/chaos/index.js +230 -0
  32. package/dist/tools/chaos/types.js +4 -0
  33. package/dist/tools/collision-physics/index.js +211 -0
  34. package/dist/tools/config/index.js +288 -0
  35. package/dist/tools/cpp/index.js +305 -0
  36. package/dist/tools/docs/index.js +251 -0
  37. package/dist/tools/editor/index.js +242 -0
  38. package/dist/tools/gas/index.js +222 -0
  39. package/dist/tools/gas/types.js +5 -0
  40. package/dist/tools/import-export/index.js +218 -0
  41. package/dist/tools/input/index.js +146 -0
  42. package/dist/tools/known-issues/index.js +88 -0
  43. package/dist/tools/known-issues/middleware.js +55 -0
  44. package/dist/tools/known-issues/store.js +125 -0
  45. package/dist/tools/livelink/index.js +203 -0
  46. package/dist/tools/livelink/types.js +4 -0
  47. package/dist/tools/material/index.js +190 -0
  48. package/dist/tools/motion-design/index.js +251 -0
  49. package/dist/tools/motion-design/types.js +6 -0
  50. package/dist/tools/movie-render/index.js +220 -0
  51. package/dist/tools/networking/index.js +149 -0
  52. package/dist/tools/pcg/index.js +164 -0
  53. package/dist/tools/selection/index.js +180 -0
  54. package/dist/tools/sequencer/index.js +218 -0
  55. package/dist/tools/validation/index.js +183 -0
  56. package/dist/tools/validation/types.js +4 -0
  57. package/dist/tools/viewport/index.js +310 -0
  58. package/dist/tools/worldpartition/index.js +226 -0
  59. package/dist/tools/worldpartition/types.js +4 -0
  60. package/dist/utils/execFileNoThrow.js +40 -0
  61. package/dist/utils/logger.js +27 -0
  62. package/dist/utils/path-guard.js +26 -0
  63. package/package.json +40 -0
  64. package/unreal-plugin/MCPBridge/MCPBridge.uplugin +29 -0
  65. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/MCPBridgeEditor.Build.cs +68 -0
  66. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.cpp +919 -0
  67. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.h +23 -0
  68. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.cpp +415 -0
  69. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.h +16 -0
  70. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.cpp +653 -0
  71. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.h +24 -0
  72. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.cpp +290 -0
  73. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.h +17 -0
  74. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.cpp +624 -0
  75. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.h +22 -0
  76. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.cpp +616 -0
  77. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.h +25 -0
  78. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.cpp +744 -0
  79. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.h +24 -0
  80. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeEditor.cpp +23 -0
  81. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.cpp +149 -0
  82. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.h +38 -0
  83. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.cpp +771 -0
  84. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.h +22 -0
  85. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.cpp +749 -0
  86. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.h +22 -0
  87. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.cpp +172 -0
  88. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.h +16 -0
  89. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.cpp +715 -0
  90. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.h +22 -0
  91. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.cpp +679 -0
  92. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.h +22 -0
  93. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.cpp +381 -0
  94. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.h +24 -0
  95. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.cpp +504 -0
  96. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.h +22 -0
  97. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.cpp +511 -0
  98. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.h +22 -0
  99. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.cpp +1110 -0
  100. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.h +28 -0
  101. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.cpp +590 -0
  102. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.h +16 -0
  103. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.cpp +482 -0
  104. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.h +16 -0
  105. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.cpp +338 -0
  106. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.h +16 -0
  107. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.cpp +677 -0
  108. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.h +22 -0
  109. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.cpp +721 -0
  110. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.h +16 -0
  111. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.cpp +368 -0
  112. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.h +22 -0
  113. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.cpp +1208 -0
  114. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.h +29 -0
  115. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.cpp +822 -0
  116. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.h +23 -0
  117. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Public/MCPBridgeEditor.h +14 -0
  118. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/MCPBridgeRuntime.Build.cs +28 -0
  119. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPBridgeRuntime.cpp +22 -0
  120. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPCommandRouter.cpp +118 -0
  121. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPTcpServer.cpp +196 -0
  122. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPBridgeRuntime.h +15 -0
  123. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPCommandRouter.h +55 -0
  124. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPTcpServer.h +59 -0
@@ -0,0 +1,1110 @@
1
+ // MCPMotionDesignCommands.cpp (Plan 28-01)
2
+ // Implements four Motion Design command handlers for the MCP bridge:
3
+ // motiondesign.sceneStates -- list Scene State machines with states and categories (MD-01)
4
+ // motiondesign.transition -- trigger Scene State transitions and set property values (MD-02)
5
+ // motiondesign.transitionLogic -- inspect Transition Logic sequences: in/out labels, layer changes (MD-03)
6
+ // motiondesign.remoteControl -- read/modify Remote Control preset properties and trigger events (MD-04)
7
+ //
8
+ // All handlers run on the game thread via FMCPCommandRouter::Dispatch.
9
+ // asset_path and preset_path are validated to start with "/Game/" or "/Engine/" before any
10
+ // StaticLoadObject call to prevent path traversal (T-28-01).
11
+ // Modify() is called before all write operations (T-28-03, PITFALLS.md Pitfall 5).
12
+ // Handlers for sceneStates, transition, and transitionLogic check FModuleManager at
13
+ // runtime to degrade gracefully when the Avalanche plugin is not enabled.
14
+
15
+ #include "MCPMotionDesignCommands.h"
16
+
17
+ // Remote Control headers (always available when RemoteControl module is loaded)
18
+ #include "RemoteControlPreset.h"
19
+ #include "RemoteControlField.h"
20
+
21
+ // Editor world context
22
+ #include "Editor.h"
23
+
24
+ // JSON
25
+ #include "Serialization/JsonSerializer.h"
26
+ #include "Serialization/JsonWriter.h"
27
+ #include "Dom/JsonObject.h"
28
+ #include "Dom/JsonValue.h"
29
+
30
+ // Module manager for runtime Avalanche availability check
31
+ #include "Modules/ModuleManager.h"
32
+
33
+ // UObject reflection for Remote Control property access
34
+ #include "UObject/UnrealType.h"
35
+ #include "UObject/PropertyPortFlags.h"
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Forward declarations for Avalanche types (conditional -- may not be present)
39
+ // We load these headers only when the modules are actually available.
40
+ // If headers are not present in the project's installed UE, conditional
41
+ // compilation will still allow the file to compile by avoiding direct type usage
42
+ // when modules are absent.
43
+ // ---------------------------------------------------------------------------
44
+
45
+ // Avalanche headers -- present only when Motion Design plugin is installed.
46
+ // Guard with a macro so compilation succeeds even without the Avalanche plugin.
47
+ #if defined(AVALANCHERUNDOWN_API)
48
+ #include "AvaRundownSubsystem.h"
49
+ #endif
50
+
51
+ #if defined(AVALANCHETRANSITION_API)
52
+ #include "AvaTransitionTree.h"
53
+ #endif
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Internal helpers
57
+ // ---------------------------------------------------------------------------
58
+
59
+ /** Returns a JSON success response string (without trailing newline). */
60
+ static FString BuildMDSuccessResponse(const FString& CorrId, TSharedPtr<FJsonObject> Data)
61
+ {
62
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
63
+ Obj->SetBoolField(TEXT("success"), true);
64
+ Obj->SetStringField(TEXT("correlationId"), CorrId);
65
+ if (Data.IsValid())
66
+ {
67
+ Obj->SetObjectField(TEXT("data"), Data);
68
+ }
69
+
70
+ FString Output;
71
+ TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
72
+ FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
73
+ return Output;
74
+ }
75
+
76
+ /** Returns a JSON error response string (without trailing newline). */
77
+ static FString BuildMDErrorResponse(const FString& CorrId, const FString& Error)
78
+ {
79
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
80
+ Obj->SetBoolField(TEXT("success"), false);
81
+ Obj->SetStringField(TEXT("correlationId"), CorrId);
82
+ Obj->SetStringField(TEXT("error"), Error);
83
+
84
+ FString Output;
85
+ TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
86
+ FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
87
+ return Output;
88
+ }
89
+
90
+ /**
91
+ * Validate that asset_path starts with "/Game/" or "/Engine/" to prevent
92
+ * path traversal attacks (T-28-01).
93
+ */
94
+ static bool IsValidAssetPath(const FString& AssetPath)
95
+ {
96
+ return AssetPath.StartsWith(TEXT("/Game/")) || AssetPath.StartsWith(TEXT("/Engine/"));
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // RegisterMotionDesignCommands
101
+ // ---------------------------------------------------------------------------
102
+
103
+ void RegisterMotionDesignCommands(FMCPCommandRouter& Router)
104
+ {
105
+ // -----------------------------------------------------------------------
106
+ // motiondesign.sceneStates (MD-01)
107
+ // Lists Scene State machines with their states and categories from the
108
+ // Avalanche Rundown subsystem.
109
+ // Optional payload field: rundown_path (string) -- if omitted, all rundown
110
+ // subsystem instances from the editor world are enumerated.
111
+ // Returns: scene_state_machines[] (machine_name, current_state, states[]).
112
+ // Gracefully degrades if Avalanche plugin is not loaded.
113
+ // -----------------------------------------------------------------------
114
+ Router.RegisterHandler(TEXT("motiondesign.sceneStates"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
115
+ {
116
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
117
+
118
+ // Runtime check: ensure AvalancheRundown module is available.
119
+ if (!FModuleManager::Get().IsModuleLoaded(TEXT("AvalancheRundown")))
120
+ {
121
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("motion_design_plugin_not_enabled")) + TEXT("\n"));
122
+ return;
123
+ }
124
+
125
+ // Extract optional payload.
126
+ TSharedPtr<FJsonObject> Payload;
127
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
128
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
129
+ {
130
+ Payload = (*PayloadVal)->AsObject();
131
+ }
132
+
133
+ FString RundownPath;
134
+ if (Payload.IsValid())
135
+ {
136
+ Payload->TryGetStringField(TEXT("rundown_path"), RundownPath);
137
+ }
138
+
139
+ // Get the editor world for subsystem access.
140
+ UWorld* World = nullptr;
141
+ if (GEditor)
142
+ {
143
+ World = GEditor->GetEditorWorldContext().World();
144
+ }
145
+
146
+ if (!World)
147
+ {
148
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("no_editor_world")) + TEXT("\n"));
149
+ return;
150
+ }
151
+
152
+ // Use reflection to access UAvaRundownSubsystem so compilation succeeds
153
+ // even if the header is not included.
154
+ // We look up the subsystem class by name to maintain header-independence.
155
+ TArray<TSharedPtr<FJsonValue>> MachinesArray;
156
+
157
+ // Try to get UAvaRundownSubsystem via the engine subsystem registry.
158
+ // Since the module is confirmed loaded above, we use the subsystem by class name.
159
+ UClass* RundownSubsystemClass = FindObject<UClass>(nullptr, TEXT("/Script/AvalancheRundown.AvaRundownSubsystem"));
160
+ if (!RundownSubsystemClass)
161
+ {
162
+ // Module loaded but class not found -- return empty list gracefully.
163
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
164
+ Data->SetStringField(TEXT("rundown_path"), RundownPath);
165
+ Data->SetArrayField(TEXT("scene_state_machines"), MachinesArray);
166
+ SendResponse(BuildMDSuccessResponse(CorrId, Data) + TEXT("\n"));
167
+ return;
168
+ }
169
+
170
+ UGameInstanceSubsystem* Subsystem = nullptr;
171
+ if (World->GetGameInstance())
172
+ {
173
+ Subsystem = Cast<UGameInstanceSubsystem>(World->GetGameInstance()->GetSubsystemBase(RundownSubsystemClass));
174
+ }
175
+
176
+ if (!Subsystem)
177
+ {
178
+ // Try world subsystem as a fallback.
179
+ UWorldSubsystem* WorldSub = Cast<UWorldSubsystem>(World->GetSubsystemBase(RundownSubsystemClass));
180
+ if (!WorldSub)
181
+ {
182
+ // Subsystem not active in this world -- return informative empty result.
183
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
184
+ Data->SetStringField(TEXT("rundown_path"), RundownPath);
185
+ Data->SetArrayField(TEXT("scene_state_machines"), MachinesArray);
186
+ Data->SetStringField(TEXT("note"), TEXT("AvaRundownSubsystem not active in current world; open a level that uses Motion Design"));
187
+ SendResponse(BuildMDSuccessResponse(CorrId, Data) + TEXT("\n"));
188
+ return;
189
+ }
190
+ }
191
+
192
+ // Use reflection to enumerate scene state machines from the subsystem.
193
+ // AvaRundownSubsystem exposes state machine data via properties.
194
+ // We iterate FProperty fields to find arrays of state machine descriptors.
195
+ UObject* SubsystemObj = Subsystem ? Cast<UObject>(Subsystem) : nullptr;
196
+ if (!SubsystemObj)
197
+ {
198
+ UWorldSubsystem* WorldSub = Cast<UWorldSubsystem>(World->GetSubsystemBase(RundownSubsystemClass));
199
+ SubsystemObj = WorldSub;
200
+ }
201
+
202
+ if (SubsystemObj)
203
+ {
204
+ // Walk properties to find scene state machine collection.
205
+ for (TFieldIterator<FArrayProperty> PropIt(SubsystemObj->GetClass()); PropIt; ++PropIt)
206
+ {
207
+ FArrayProperty* ArrayProp = *PropIt;
208
+ FStructProperty* ElemProp = CastField<FStructProperty>(ArrayProp->Inner);
209
+ if (!ElemProp)
210
+ {
211
+ continue;
212
+ }
213
+
214
+ // Look for array properties whose element struct contains "State" or "Machine" in name.
215
+ FString StructName = ElemProp->Struct->GetName();
216
+ if (!StructName.Contains(TEXT("State")) && !StructName.Contains(TEXT("Machine")))
217
+ {
218
+ continue;
219
+ }
220
+
221
+ FScriptArrayHelper ArrayHelper(ArrayProp, ArrayProp->ContainerPtrToValuePtr<void>(SubsystemObj));
222
+ for (int32 i = 0; i < ArrayHelper.Num(); ++i)
223
+ {
224
+ void* ElemPtr = ArrayHelper.GetRawPtr(i);
225
+ TSharedPtr<FJsonObject> MachineObj = MakeShared<FJsonObject>();
226
+
227
+ FString MachineName = FString::Printf(TEXT("Machine_%d"), i);
228
+ FString CurrentState = TEXT("");
229
+
230
+ FNameProperty* NameProp = CastField<FNameProperty>(ElemProp->Struct->FindPropertyByName(TEXT("Name")));
231
+ if (!NameProp)
232
+ {
233
+ NameProp = CastField<FNameProperty>(ElemProp->Struct->FindPropertyByName(TEXT("MachineName")));
234
+ }
235
+ if (NameProp)
236
+ {
237
+ MachineName = NameProp->GetPropertyValue_InContainer(ElemPtr).ToString();
238
+ }
239
+ MachineObj->SetStringField(TEXT("machine_name"), MachineName);
240
+
241
+ FNameProperty* CurStateProp = CastField<FNameProperty>(ElemProp->Struct->FindPropertyByName(TEXT("CurrentState")));
242
+ if (!CurStateProp)
243
+ {
244
+ CurStateProp = CastField<FNameProperty>(ElemProp->Struct->FindPropertyByName(TEXT("ActiveState")));
245
+ }
246
+ if (CurStateProp)
247
+ {
248
+ CurrentState = CurStateProp->GetPropertyValue_InContainer(ElemPtr).ToString();
249
+ }
250
+ MachineObj->SetStringField(TEXT("current_state"), CurrentState);
251
+
252
+ // Enumerate available states.
253
+ TArray<TSharedPtr<FJsonValue>> StatesArray;
254
+ FArrayProperty* StatesProp = CastField<FArrayProperty>(ElemProp->Struct->FindPropertyByName(TEXT("States")));
255
+ if (!StatesProp)
256
+ {
257
+ StatesProp = CastField<FArrayProperty>(ElemProp->Struct->FindPropertyByName(TEXT("AvailableStates")));
258
+ }
259
+ if (StatesProp)
260
+ {
261
+ FScriptArrayHelper StatesHelper(StatesProp, StatesProp->ContainerPtrToValuePtr<void>(ElemPtr));
262
+ FStructProperty* StateElemProp = CastField<FStructProperty>(StatesProp->Inner);
263
+ for (int32 j = 0; j < StatesHelper.Num(); ++j)
264
+ {
265
+ void* StatePtr = StatesHelper.GetRawPtr(j);
266
+ TSharedPtr<FJsonObject> StateObj = MakeShared<FJsonObject>();
267
+
268
+ FString StateName = FString::Printf(TEXT("State_%d"), j);
269
+ FString Category = TEXT("");
270
+
271
+ if (StateElemProp)
272
+ {
273
+ FNameProperty* StateNameProp = CastField<FNameProperty>(StateElemProp->Struct->FindPropertyByName(TEXT("Name")));
274
+ if (!StateNameProp)
275
+ {
276
+ StateNameProp = CastField<FNameProperty>(StateElemProp->Struct->FindPropertyByName(TEXT("StateName")));
277
+ }
278
+ if (StateNameProp)
279
+ {
280
+ StateName = StateNameProp->GetPropertyValue_InContainer(StatePtr).ToString();
281
+ }
282
+
283
+ FNameProperty* CatProp = CastField<FNameProperty>(StateElemProp->Struct->FindPropertyByName(TEXT("Category")));
284
+ FStrProperty* CatStrProp = CastField<FStrProperty>(StateElemProp->Struct->FindPropertyByName(TEXT("Category")));
285
+ if (CatProp)
286
+ {
287
+ Category = CatProp->GetPropertyValue_InContainer(StatePtr).ToString();
288
+ }
289
+ else if (CatStrProp)
290
+ {
291
+ Category = CatStrProp->GetPropertyValue_InContainer(StatePtr);
292
+ }
293
+ }
294
+
295
+ StateObj->SetStringField(TEXT("name"), StateName);
296
+ StateObj->SetStringField(TEXT("category"), Category);
297
+ StatesArray.Add(MakeShared<FJsonValueObject>(StateObj));
298
+ }
299
+ }
300
+ MachineObj->SetArrayField(TEXT("states"), StatesArray);
301
+
302
+ MachinesArray.Add(MakeShared<FJsonValueObject>(MachineObj));
303
+ }
304
+ }
305
+ }
306
+
307
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
308
+ Data->SetStringField(TEXT("rundown_path"), RundownPath);
309
+ Data->SetArrayField(TEXT("scene_state_machines"), MachinesArray);
310
+
311
+ SendResponse(BuildMDSuccessResponse(CorrId, Data) + TEXT("\n"));
312
+ });
313
+
314
+ // -----------------------------------------------------------------------
315
+ // motiondesign.transition (MD-02)
316
+ // Triggers a Scene State transition on the Avalanche Rundown subsystem.
317
+ // Required payload fields: rundown_path (string), target_state (string).
318
+ // Optional payload fields: properties (object) -- key/value property overrides.
319
+ // Returns: success, new_state, applied_properties[].
320
+ // Calls Modify() before mutating state (PITFALLS.md Pitfall 5, T-28-03).
321
+ // Gracefully degrades if Avalanche plugin is not loaded.
322
+ // -----------------------------------------------------------------------
323
+ Router.RegisterHandler(TEXT("motiondesign.transition"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
324
+ {
325
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
326
+
327
+ // Runtime check: ensure AvalancheRundown module is available.
328
+ if (!FModuleManager::Get().IsModuleLoaded(TEXT("AvalancheRundown")))
329
+ {
330
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("motion_design_plugin_not_enabled")) + TEXT("\n"));
331
+ return;
332
+ }
333
+
334
+ // Extract payload.
335
+ TSharedPtr<FJsonObject> Payload;
336
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
337
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
338
+ {
339
+ Payload = (*PayloadVal)->AsObject();
340
+ }
341
+
342
+ FString RundownPath;
343
+ FString TargetState;
344
+
345
+ if (!Payload.IsValid())
346
+ {
347
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("missing_payload")) + TEXT("\n"));
348
+ return;
349
+ }
350
+
351
+ Payload->TryGetStringField(TEXT("rundown_path"), RundownPath);
352
+ if (!Payload->TryGetStringField(TEXT("target_state"), TargetState) || TargetState.IsEmpty())
353
+ {
354
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("missing_target_state")) + TEXT("\n"));
355
+ return;
356
+ }
357
+
358
+ // Get the editor world.
359
+ UWorld* World = nullptr;
360
+ if (GEditor)
361
+ {
362
+ World = GEditor->GetEditorWorldContext().World();
363
+ }
364
+
365
+ if (!World)
366
+ {
367
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("no_editor_world")) + TEXT("\n"));
368
+ return;
369
+ }
370
+
371
+ // Access the rundown subsystem by class lookup.
372
+ UClass* RundownSubsystemClass = FindObject<UClass>(nullptr, TEXT("/Script/AvalancheRundown.AvaRundownSubsystem"));
373
+ if (!RundownSubsystemClass)
374
+ {
375
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("rundown_subsystem_class_not_found")) + TEXT("\n"));
376
+ return;
377
+ }
378
+
379
+ UObject* SubsystemObj = nullptr;
380
+ if (World->GetGameInstance())
381
+ {
382
+ UGameInstanceSubsystem* GISub = Cast<UGameInstanceSubsystem>(World->GetGameInstance()->GetSubsystemBase(RundownSubsystemClass));
383
+ SubsystemObj = GISub;
384
+ }
385
+ if (!SubsystemObj)
386
+ {
387
+ UWorldSubsystem* WorldSub = Cast<UWorldSubsystem>(World->GetSubsystemBase(RundownSubsystemClass));
388
+ SubsystemObj = WorldSub;
389
+ }
390
+
391
+ if (!SubsystemObj)
392
+ {
393
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("rundown_subsystem_not_active")) + TEXT("\n"));
394
+ return;
395
+ }
396
+
397
+ // Mark the subsystem as modified before triggering the transition (T-28-03).
398
+ SubsystemObj->Modify();
399
+
400
+ // Use reflection to find and invoke the transition method.
401
+ // Look for a function named "SetState" or "TriggerTransition" or similar.
402
+ UFunction* TransitionFunc = SubsystemObj->GetClass()->FindFunctionByName(TEXT("SetState"));
403
+ if (!TransitionFunc)
404
+ {
405
+ TransitionFunc = SubsystemObj->GetClass()->FindFunctionByName(TEXT("TriggerTransition"));
406
+ }
407
+ if (!TransitionFunc)
408
+ {
409
+ TransitionFunc = SubsystemObj->GetClass()->FindFunctionByName(TEXT("RequestStateTransition"));
410
+ }
411
+
412
+ bool bTransitionTriggered = false;
413
+ if (TransitionFunc)
414
+ {
415
+ // Prepare parameters buffer.
416
+ TArray<uint8> Params;
417
+ Params.SetNumZeroed(TransitionFunc->ParmsSize);
418
+
419
+ // Set the TargetState parameter using reflection.
420
+ for (TFieldIterator<FProperty> ParamIt(TransitionFunc); ParamIt && (ParamIt->PropertyFlags & CPF_Parm); ++ParamIt)
421
+ {
422
+ FProperty* Param = *ParamIt;
423
+ if (Param->GetName().Contains(TEXT("State")) || Param->GetName().Contains(TEXT("Target")))
424
+ {
425
+ FNameProperty* NameParam = CastField<FNameProperty>(Param);
426
+ FStrProperty* StrParam = CastField<FStrProperty>(Param);
427
+ if (NameParam)
428
+ {
429
+ NameParam->SetPropertyValue(Params.GetData() + Param->GetOffset_ForInternal(), FName(*TargetState));
430
+ }
431
+ else if (StrParam)
432
+ {
433
+ StrParam->SetPropertyValue(Params.GetData() + Param->GetOffset_ForInternal(), TargetState);
434
+ }
435
+ }
436
+ }
437
+
438
+ SubsystemObj->ProcessEvent(TransitionFunc, Params.GetData());
439
+ bTransitionTriggered = true;
440
+ }
441
+
442
+ if (!bTransitionTriggered)
443
+ {
444
+ // Fallback: set a "CurrentState" property directly via reflection.
445
+ for (TFieldIterator<FProperty> PropIt(SubsystemObj->GetClass()); PropIt; ++PropIt)
446
+ {
447
+ FProperty* Prop = *PropIt;
448
+ if (Prop->GetName() == TEXT("CurrentState") || Prop->GetName() == TEXT("ActiveState"))
449
+ {
450
+ FNameProperty* NameProp = CastField<FNameProperty>(Prop);
451
+ FStrProperty* StrProp = CastField<FStrProperty>(Prop);
452
+ if (NameProp)
453
+ {
454
+ NameProp->SetPropertyValue_InContainer(SubsystemObj, FName(*TargetState));
455
+ bTransitionTriggered = true;
456
+ }
457
+ else if (StrProp)
458
+ {
459
+ StrProp->SetPropertyValue_InContainer(SubsystemObj, TargetState);
460
+ bTransitionTriggered = true;
461
+ }
462
+ break;
463
+ }
464
+ }
465
+ }
466
+
467
+ if (!bTransitionTriggered)
468
+ {
469
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("state_not_found")) + TEXT("\n"));
470
+ return;
471
+ }
472
+
473
+ // Apply optional property overrides.
474
+ TArray<TSharedPtr<FJsonValue>> AppliedPropertiesArray;
475
+ const TSharedPtr<FJsonValue>* PropsVal = Payload->Values.Find(TEXT("properties"));
476
+ if (PropsVal && (*PropsVal)->Type == EJson::Object)
477
+ {
478
+ TSharedPtr<FJsonObject> PropsObj = (*PropsVal)->AsObject();
479
+ for (auto& KV : PropsObj->Values)
480
+ {
481
+ for (TFieldIterator<FProperty> PropIt(SubsystemObj->GetClass()); PropIt; ++PropIt)
482
+ {
483
+ FProperty* Prop = *PropIt;
484
+ if (Prop->GetName() == KV.Key)
485
+ {
486
+ // Mark modified before writing (T-28-03).
487
+ SubsystemObj->Modify();
488
+
489
+ FStrProperty* StrProp = CastField<FStrProperty>(Prop);
490
+ FNameProperty* NameProp = CastField<FNameProperty>(Prop);
491
+ FFloatProperty* FloatProp = CastField<FFloatProperty>(Prop);
492
+ FDoubleProperty* DoubleProp = CastField<FDoubleProperty>(Prop);
493
+ FBoolProperty* BoolProp = CastField<FBoolProperty>(Prop);
494
+
495
+ FString StrVal;
496
+ if (KV.Value->TryGetString(StrVal))
497
+ {
498
+ if (StrProp)
499
+ {
500
+ StrProp->SetPropertyValue_InContainer(SubsystemObj, StrVal);
501
+ }
502
+ else if (NameProp)
503
+ {
504
+ NameProp->SetPropertyValue_InContainer(SubsystemObj, FName(*StrVal));
505
+ }
506
+ }
507
+ else
508
+ {
509
+ double NumVal = 0.0;
510
+ if (KV.Value->TryGetNumber(NumVal))
511
+ {
512
+ if (FloatProp)
513
+ {
514
+ FloatProp->SetPropertyValue_InContainer(SubsystemObj, static_cast<float>(NumVal));
515
+ }
516
+ else if (DoubleProp)
517
+ {
518
+ DoubleProp->SetPropertyValue_InContainer(SubsystemObj, NumVal);
519
+ }
520
+ }
521
+ else
522
+ {
523
+ bool BoolVal = false;
524
+ if (KV.Value->TryGetBool(BoolVal) && BoolProp)
525
+ {
526
+ BoolProp->SetPropertyValue_InContainer(SubsystemObj, BoolVal);
527
+ }
528
+ }
529
+ }
530
+
531
+ TSharedPtr<FJsonObject> AppliedProp = MakeShared<FJsonObject>();
532
+ AppliedProp->SetStringField(TEXT("property"), KV.Key);
533
+ AppliedPropertiesArray.Add(MakeShared<FJsonValueObject>(AppliedProp));
534
+ break;
535
+ }
536
+ }
537
+ }
538
+ }
539
+
540
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
541
+ Data->SetStringField(TEXT("new_state"), TargetState);
542
+ Data->SetArrayField(TEXT("applied_properties"), AppliedPropertiesArray);
543
+
544
+ SendResponse(BuildMDSuccessResponse(CorrId, Data) + TEXT("\n"));
545
+ });
546
+
547
+ // -----------------------------------------------------------------------
548
+ // motiondesign.transitionLogic (MD-03)
549
+ // Inspects a UAvaTransitionTree asset's transition sequences.
550
+ // Required payload field: asset_path (string, must start with /Game/ or /Engine/).
551
+ // Returns: asset_path, transitions[] (in_label, out_label, layer_changes[],
552
+ // level_sequence_path).
553
+ // Gracefully degrades if Avalanche plugin is not loaded.
554
+ // -----------------------------------------------------------------------
555
+ Router.RegisterHandler(TEXT("motiondesign.transitionLogic"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
556
+ {
557
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
558
+
559
+ // Runtime check: ensure AvalancheTransition module is available.
560
+ if (!FModuleManager::Get().IsModuleLoaded(TEXT("AvalancheTransition")))
561
+ {
562
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("motion_design_plugin_not_enabled")) + TEXT("\n"));
563
+ return;
564
+ }
565
+
566
+ // Extract payload.
567
+ TSharedPtr<FJsonObject> Payload;
568
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
569
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
570
+ {
571
+ Payload = (*PayloadVal)->AsObject();
572
+ }
573
+
574
+ FString AssetPath;
575
+ if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
576
+ {
577
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("missing_asset_path")) + TEXT("\n"));
578
+ return;
579
+ }
580
+
581
+ // Validate path prefix (T-28-01).
582
+ if (!IsValidAssetPath(AssetPath))
583
+ {
584
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("invalid_asset_path")) + TEXT("\n"));
585
+ return;
586
+ }
587
+
588
+ // Look up the UAvaTransitionTree class by name (header-independent approach).
589
+ UClass* TransitionTreeClass = FindObject<UClass>(nullptr, TEXT("/Script/AvalancheTransition.AvaTransitionTree"));
590
+ if (!TransitionTreeClass)
591
+ {
592
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("transition_tree_class_not_found")) + TEXT("\n"));
593
+ return;
594
+ }
595
+
596
+ // Load the transition tree asset.
597
+ UObject* TransitionTreeObj = StaticLoadObject(TransitionTreeClass, nullptr, *AssetPath);
598
+ if (!TransitionTreeObj)
599
+ {
600
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("transition_tree_not_found")) + TEXT("\n"));
601
+ return;
602
+ }
603
+
604
+ // Enumerate transitions via reflection.
605
+ TArray<TSharedPtr<FJsonValue>> TransitionsArray;
606
+
607
+ // Look for an array property containing transition entries.
608
+ for (TFieldIterator<FArrayProperty> PropIt(TransitionTreeObj->GetClass()); PropIt; ++PropIt)
609
+ {
610
+ FArrayProperty* ArrayProp = *PropIt;
611
+ FStructProperty* ElemProp = CastField<FStructProperty>(ArrayProp->Inner);
612
+ if (!ElemProp)
613
+ {
614
+ continue;
615
+ }
616
+
617
+ FString StructName = ElemProp->Struct->GetName();
618
+ if (!StructName.Contains(TEXT("Transition")) && !StructName.Contains(TEXT("Entry")) && !StructName.Contains(TEXT("Sequence")))
619
+ {
620
+ continue;
621
+ }
622
+
623
+ FScriptArrayHelper ArrayHelper(ArrayProp, ArrayProp->ContainerPtrToValuePtr<void>(TransitionTreeObj));
624
+ for (int32 i = 0; i < ArrayHelper.Num(); ++i)
625
+ {
626
+ void* ElemPtr = ArrayHelper.GetRawPtr(i);
627
+ TSharedPtr<FJsonObject> TransObj = MakeShared<FJsonObject>();
628
+
629
+ // Extract in_label.
630
+ FString InLabel = TEXT("");
631
+ FNameProperty* InLabelProp = CastField<FNameProperty>(ElemProp->Struct->FindPropertyByName(TEXT("InLabel")));
632
+ FStrProperty* InLabelStrProp = CastField<FStrProperty>(ElemProp->Struct->FindPropertyByName(TEXT("InLabel")));
633
+ if (!InLabelProp)
634
+ {
635
+ InLabelProp = CastField<FNameProperty>(ElemProp->Struct->FindPropertyByName(TEXT("EnterLabel")));
636
+ }
637
+ if (InLabelProp)
638
+ {
639
+ InLabel = InLabelProp->GetPropertyValue_InContainer(ElemPtr).ToString();
640
+ }
641
+ else if (InLabelStrProp)
642
+ {
643
+ InLabel = InLabelStrProp->GetPropertyValue_InContainer(ElemPtr);
644
+ }
645
+ TransObj->SetStringField(TEXT("in_label"), InLabel);
646
+
647
+ // Extract out_label.
648
+ FString OutLabel = TEXT("");
649
+ FNameProperty* OutLabelProp = CastField<FNameProperty>(ElemProp->Struct->FindPropertyByName(TEXT("OutLabel")));
650
+ FStrProperty* OutLabelStrProp = CastField<FStrProperty>(ElemProp->Struct->FindPropertyByName(TEXT("OutLabel")));
651
+ if (!OutLabelProp)
652
+ {
653
+ OutLabelProp = CastField<FNameProperty>(ElemProp->Struct->FindPropertyByName(TEXT("ExitLabel")));
654
+ }
655
+ if (OutLabelProp)
656
+ {
657
+ OutLabel = OutLabelProp->GetPropertyValue_InContainer(ElemPtr).ToString();
658
+ }
659
+ else if (OutLabelStrProp)
660
+ {
661
+ OutLabel = OutLabelStrProp->GetPropertyValue_InContainer(ElemPtr);
662
+ }
663
+ TransObj->SetStringField(TEXT("out_label"), OutLabel);
664
+
665
+ // Extract associated level sequence path.
666
+ FString LevelSequencePath = TEXT("");
667
+ FObjectProperty* LevelSeqProp = CastField<FObjectProperty>(ElemProp->Struct->FindPropertyByName(TEXT("LevelSequence")));
668
+ if (!LevelSeqProp)
669
+ {
670
+ LevelSeqProp = CastField<FObjectProperty>(ElemProp->Struct->FindPropertyByName(TEXT("Sequence")));
671
+ }
672
+ if (LevelSeqProp)
673
+ {
674
+ UObject* LevelSeq = LevelSeqProp->GetObjectPropertyValue_InContainer(ElemPtr);
675
+ if (LevelSeq)
676
+ {
677
+ LevelSequencePath = LevelSeq->GetPathName();
678
+ }
679
+ }
680
+ TransObj->SetStringField(TEXT("level_sequence_path"), LevelSequencePath);
681
+
682
+ // Extract layer change definitions.
683
+ TArray<TSharedPtr<FJsonValue>> LayerChangesArray;
684
+ FArrayProperty* LayerChangesProp = CastField<FArrayProperty>(ElemProp->Struct->FindPropertyByName(TEXT("LayerChanges")));
685
+ if (!LayerChangesProp)
686
+ {
687
+ LayerChangesProp = CastField<FArrayProperty>(ElemProp->Struct->FindPropertyByName(TEXT("LayerTransitions")));
688
+ }
689
+ if (LayerChangesProp)
690
+ {
691
+ FScriptArrayHelper LayersHelper(LayerChangesProp, LayerChangesProp->ContainerPtrToValuePtr<void>(ElemPtr));
692
+ FStructProperty* LayerElemProp = CastField<FStructProperty>(LayerChangesProp->Inner);
693
+ for (int32 j = 0; j < LayersHelper.Num(); ++j)
694
+ {
695
+ void* LayerPtr = LayersHelper.GetRawPtr(j);
696
+ TSharedPtr<FJsonObject> LayerObj = MakeShared<FJsonObject>();
697
+
698
+ FString LayerName = FString::Printf(TEXT("Layer_%d"), j);
699
+ FString ChangeType = TEXT("");
700
+
701
+ if (LayerElemProp)
702
+ {
703
+ FNameProperty* LayerNameProp = CastField<FNameProperty>(LayerElemProp->Struct->FindPropertyByName(TEXT("LayerName")));
704
+ if (!LayerNameProp)
705
+ {
706
+ LayerNameProp = CastField<FNameProperty>(LayerElemProp->Struct->FindPropertyByName(TEXT("Name")));
707
+ }
708
+ if (LayerNameProp)
709
+ {
710
+ LayerName = LayerNameProp->GetPropertyValue_InContainer(LayerPtr).ToString();
711
+ }
712
+
713
+ FStrProperty* ChangeTypeProp = CastField<FStrProperty>(LayerElemProp->Struct->FindPropertyByName(TEXT("ChangeType")));
714
+ if (ChangeTypeProp)
715
+ {
716
+ ChangeType = ChangeTypeProp->GetPropertyValue_InContainer(LayerPtr);
717
+ }
718
+ }
719
+
720
+ LayerObj->SetStringField(TEXT("layer_name"), LayerName);
721
+ LayerObj->SetStringField(TEXT("change_type"), ChangeType);
722
+ LayerChangesArray.Add(MakeShared<FJsonValueObject>(LayerObj));
723
+ }
724
+ }
725
+ TransObj->SetArrayField(TEXT("layer_changes"), LayerChangesArray);
726
+
727
+ TransitionsArray.Add(MakeShared<FJsonValueObject>(TransObj));
728
+ }
729
+ // Only process the first matching array property.
730
+ if (!TransitionsArray.IsEmpty())
731
+ {
732
+ break;
733
+ }
734
+ }
735
+
736
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
737
+ Data->SetStringField(TEXT("asset_path"), AssetPath);
738
+ Data->SetArrayField(TEXT("transitions"), TransitionsArray);
739
+
740
+ SendResponse(BuildMDSuccessResponse(CorrId, Data) + TEXT("\n"));
741
+ });
742
+
743
+ // -----------------------------------------------------------------------
744
+ // motiondesign.remoteControl (MD-04)
745
+ // Reads or modifies Remote Control preset properties and triggers exposed
746
+ // functions.
747
+ // Required payload fields: preset_path (string), action (string: list|get|set|call).
748
+ // Action-specific:
749
+ // list: -- returns all exposed properties with names, types, current values
750
+ // and all exposed functions with names.
751
+ // get: property_name (string) -- returns single property value.
752
+ // set: property_name (string), value (variant) -- sets property; calls Modify().
753
+ // call: function_name (string), args (optional object) -- triggers the function.
754
+ // Validates preset_path with IsValidAssetPath (T-28-01).
755
+ // Calls Modify() before property writes (T-28-03, PITFALLS.md Pitfall 5).
756
+ // -----------------------------------------------------------------------
757
+ Router.RegisterHandler(TEXT("motiondesign.remoteControl"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
758
+ {
759
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
760
+
761
+ // Extract payload.
762
+ TSharedPtr<FJsonObject> Payload;
763
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
764
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
765
+ {
766
+ Payload = (*PayloadVal)->AsObject();
767
+ }
768
+
769
+ if (!Payload.IsValid())
770
+ {
771
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("missing_payload")) + TEXT("\n"));
772
+ return;
773
+ }
774
+
775
+ FString PresetPath;
776
+ FString Action;
777
+
778
+ if (!Payload->TryGetStringField(TEXT("preset_path"), PresetPath) || PresetPath.IsEmpty())
779
+ {
780
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("missing_preset_path")) + TEXT("\n"));
781
+ return;
782
+ }
783
+
784
+ if (!Payload->TryGetStringField(TEXT("action"), Action) || Action.IsEmpty())
785
+ {
786
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("missing_action")) + TEXT("\n"));
787
+ return;
788
+ }
789
+
790
+ // Validate path prefix (T-28-01).
791
+ if (!IsValidAssetPath(PresetPath))
792
+ {
793
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("invalid_preset_path")) + TEXT("\n"));
794
+ return;
795
+ }
796
+
797
+ // Load the Remote Control preset.
798
+ URemoteControlPreset* Preset = Cast<URemoteControlPreset>(StaticLoadObject(URemoteControlPreset::StaticClass(), nullptr, *PresetPath));
799
+ if (!Preset)
800
+ {
801
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("preset_not_found")) + TEXT("\n"));
802
+ return;
803
+ }
804
+
805
+ if (Action == TEXT("list"))
806
+ {
807
+ // List all exposed properties and functions.
808
+ TArray<TSharedPtr<FJsonValue>> PropertiesArray;
809
+ TArray<TSharedPtr<FJsonValue>> FunctionsArray;
810
+
811
+ // Iterate exposed fields (properties).
812
+ for (const TWeakPtr<FRemoteControlField>& WeakField : Preset->GetExposedEntities<FRemoteControlField>())
813
+ {
814
+ TSharedPtr<FRemoteControlField> Field = WeakField.Pin();
815
+ if (!Field.IsValid())
816
+ {
817
+ continue;
818
+ }
819
+
820
+ TSharedPtr<FJsonObject> PropObj = MakeShared<FJsonObject>();
821
+ PropObj->SetStringField(TEXT("name"), Field->GetLabel().ToString());
822
+ PropObj->SetStringField(TEXT("id"), Field->GetId().ToString());
823
+
824
+ // Get field type from the bound property.
825
+ FString TypeName = TEXT("Unknown");
826
+ FString CurrentValue = TEXT("");
827
+
828
+ // Access the bound objects to read the current property value.
829
+ TArray<UObject*> BoundObjects = Field->GetBoundObjects();
830
+ if (BoundObjects.Num() > 0 && BoundObjects[0] != nullptr)
831
+ {
832
+ UObject* BoundObj = BoundObjects[0];
833
+ FProperty* Prop = Field->GetProperty();
834
+ if (Prop)
835
+ {
836
+ TypeName = Prop->GetCPPType();
837
+ // Export the current value as string.
838
+ void* PropAddr = Prop->ContainerPtrToValuePtr<void>(BoundObj);
839
+ Prop->ExportTextItem_Direct(CurrentValue, PropAddr, nullptr, BoundObj, PPF_None);
840
+ }
841
+ }
842
+
843
+ PropObj->SetStringField(TEXT("type"), TypeName);
844
+ PropObj->SetStringField(TEXT("current_value"), CurrentValue);
845
+
846
+ PropertiesArray.Add(MakeShared<FJsonValueObject>(PropObj));
847
+ }
848
+
849
+ // Iterate exposed functions.
850
+ for (const TWeakPtr<FRemoteControlFunction>& WeakFunc : Preset->GetExposedEntities<FRemoteControlFunction>())
851
+ {
852
+ TSharedPtr<FRemoteControlFunction> RCFunc = WeakFunc.Pin();
853
+ if (!RCFunc.IsValid())
854
+ {
855
+ continue;
856
+ }
857
+
858
+ TSharedPtr<FJsonObject> FuncObj = MakeShared<FJsonObject>();
859
+ FuncObj->SetStringField(TEXT("name"), RCFunc->GetLabel().ToString());
860
+ FuncObj->SetStringField(TEXT("id"), RCFunc->GetId().ToString());
861
+
862
+ FunctionsArray.Add(MakeShared<FJsonValueObject>(FuncObj));
863
+ }
864
+
865
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
866
+ Data->SetStringField(TEXT("preset_path"), PresetPath);
867
+ Data->SetArrayField(TEXT("properties"), PropertiesArray);
868
+ Data->SetArrayField(TEXT("functions"), FunctionsArray);
869
+
870
+ SendResponse(BuildMDSuccessResponse(CorrId, Data) + TEXT("\n"));
871
+ }
872
+ else if (Action == TEXT("get"))
873
+ {
874
+ FString PropertyName;
875
+ if (!Payload->TryGetStringField(TEXT("property_name"), PropertyName) || PropertyName.IsEmpty())
876
+ {
877
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("missing_property_name")) + TEXT("\n"));
878
+ return;
879
+ }
880
+
881
+ // Find the exposed field by label.
882
+ bool bFound = false;
883
+ for (const TWeakPtr<FRemoteControlField>& WeakField : Preset->GetExposedEntities<FRemoteControlField>())
884
+ {
885
+ TSharedPtr<FRemoteControlField> Field = WeakField.Pin();
886
+ if (!Field.IsValid())
887
+ {
888
+ continue;
889
+ }
890
+
891
+ if (Field->GetLabel().ToString() != PropertyName)
892
+ {
893
+ continue;
894
+ }
895
+
896
+ FString TypeName = TEXT("Unknown");
897
+ FString CurrentValue = TEXT("");
898
+
899
+ TArray<UObject*> BoundObjects = Field->GetBoundObjects();
900
+ if (BoundObjects.Num() > 0 && BoundObjects[0] != nullptr)
901
+ {
902
+ FProperty* Prop = Field->GetProperty();
903
+ if (Prop)
904
+ {
905
+ TypeName = Prop->GetCPPType();
906
+ void* PropAddr = Prop->ContainerPtrToValuePtr<void>(BoundObjects[0]);
907
+ Prop->ExportTextItem_Direct(CurrentValue, PropAddr, nullptr, BoundObjects[0], PPF_None);
908
+ }
909
+ }
910
+
911
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
912
+ Data->SetStringField(TEXT("preset_path"), PresetPath);
913
+ Data->SetStringField(TEXT("property_name"), PropertyName);
914
+ Data->SetStringField(TEXT("type"), TypeName);
915
+ Data->SetStringField(TEXT("value"), CurrentValue);
916
+
917
+ SendResponse(BuildMDSuccessResponse(CorrId, Data) + TEXT("\n"));
918
+ bFound = true;
919
+ break;
920
+ }
921
+
922
+ if (!bFound)
923
+ {
924
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("property_not_found")) + TEXT("\n"));
925
+ }
926
+ }
927
+ else if (Action == TEXT("set"))
928
+ {
929
+ FString PropertyName;
930
+ if (!Payload->TryGetStringField(TEXT("property_name"), PropertyName) || PropertyName.IsEmpty())
931
+ {
932
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("missing_property_name")) + TEXT("\n"));
933
+ return;
934
+ }
935
+
936
+ const TSharedPtr<FJsonValue>* ValueJsonPtr = Payload->Values.Find(TEXT("value"));
937
+ if (!ValueJsonPtr)
938
+ {
939
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("missing_value")) + TEXT("\n"));
940
+ return;
941
+ }
942
+
943
+ // Find the exposed field by label.
944
+ bool bFound = false;
945
+ for (const TWeakPtr<FRemoteControlField>& WeakField : Preset->GetExposedEntities<FRemoteControlField>())
946
+ {
947
+ TSharedPtr<FRemoteControlField> Field = WeakField.Pin();
948
+ if (!Field.IsValid())
949
+ {
950
+ continue;
951
+ }
952
+
953
+ if (Field->GetLabel().ToString() != PropertyName)
954
+ {
955
+ continue;
956
+ }
957
+
958
+ TArray<UObject*> BoundObjects = Field->GetBoundObjects();
959
+ if (BoundObjects.Num() > 0 && BoundObjects[0] != nullptr)
960
+ {
961
+ FProperty* Prop = Field->GetProperty();
962
+ if (Prop)
963
+ {
964
+ // Mark as modified before property write (T-28-03, PITFALLS.md Pitfall 5).
965
+ BoundObjects[0]->Modify();
966
+
967
+ FString ImportText;
968
+ (*ValueJsonPtr)->TryGetString(ImportText);
969
+
970
+ void* PropAddr = Prop->ContainerPtrToValuePtr<void>(BoundObjects[0]);
971
+ Prop->ImportText_Direct(*ImportText, PropAddr, BoundObjects[0], PPF_None);
972
+
973
+ // Re-export to confirm what was written.
974
+ FString UpdatedValue;
975
+ Prop->ExportTextItem_Direct(UpdatedValue, PropAddr, nullptr, BoundObjects[0], PPF_None);
976
+
977
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
978
+ Data->SetStringField(TEXT("preset_path"), PresetPath);
979
+ Data->SetStringField(TEXT("property_name"), PropertyName);
980
+ Data->SetStringField(TEXT("updated_value"), UpdatedValue);
981
+
982
+ SendResponse(BuildMDSuccessResponse(CorrId, Data) + TEXT("\n"));
983
+ }
984
+ else
985
+ {
986
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("property_binding_invalid")) + TEXT("\n"));
987
+ }
988
+ }
989
+ else
990
+ {
991
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("no_bound_objects")) + TEXT("\n"));
992
+ }
993
+
994
+ bFound = true;
995
+ break;
996
+ }
997
+
998
+ if (!bFound)
999
+ {
1000
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("property_not_found")) + TEXT("\n"));
1001
+ }
1002
+ }
1003
+ else if (Action == TEXT("call"))
1004
+ {
1005
+ FString FunctionName;
1006
+ if (!Payload->TryGetStringField(TEXT("function_name"), FunctionName) || FunctionName.IsEmpty())
1007
+ {
1008
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("missing_function_name")) + TEXT("\n"));
1009
+ return;
1010
+ }
1011
+
1012
+ // Extract optional args.
1013
+ TSharedPtr<FJsonObject> ArgsObj;
1014
+ const TSharedPtr<FJsonValue>* ArgsVal = Payload->Values.Find(TEXT("args"));
1015
+ if (ArgsVal && (*ArgsVal)->Type == EJson::Object)
1016
+ {
1017
+ ArgsObj = (*ArgsVal)->AsObject();
1018
+ }
1019
+
1020
+ // Find the exposed function by label.
1021
+ bool bFound = false;
1022
+ for (const TWeakPtr<FRemoteControlFunction>& WeakFunc : Preset->GetExposedEntities<FRemoteControlFunction>())
1023
+ {
1024
+ TSharedPtr<FRemoteControlFunction> RCFunc = WeakFunc.Pin();
1025
+ if (!RCFunc.IsValid())
1026
+ {
1027
+ continue;
1028
+ }
1029
+
1030
+ if (RCFunc->GetLabel().ToString() != FunctionName)
1031
+ {
1032
+ continue;
1033
+ }
1034
+
1035
+ // Get the UFunction from the Remote Control function.
1036
+ UFunction* Func = RCFunc->GetFunction();
1037
+ TArray<UObject*> BoundObjects = RCFunc->GetBoundObjects();
1038
+
1039
+ if (Func && BoundObjects.Num() > 0 && BoundObjects[0] != nullptr)
1040
+ {
1041
+ UObject* BoundObj = BoundObjects[0];
1042
+
1043
+ // Prepare parameter buffer.
1044
+ TArray<uint8> Params;
1045
+ Params.SetNumZeroed(Func->ParmsSize);
1046
+
1047
+ // Apply args from JSON if provided.
1048
+ if (ArgsObj.IsValid())
1049
+ {
1050
+ for (TFieldIterator<FProperty> ParamIt(Func); ParamIt && (ParamIt->PropertyFlags & CPF_Parm); ++ParamIt)
1051
+ {
1052
+ FProperty* Param = *ParamIt;
1053
+ if (Param->PropertyFlags & CPF_ReturnParm)
1054
+ {
1055
+ continue;
1056
+ }
1057
+
1058
+ const TSharedPtr<FJsonValue>* ArgVal = ArgsObj->Values.Find(Param->GetName());
1059
+ if (ArgVal)
1060
+ {
1061
+ FString ArgStr;
1062
+ (*ArgVal)->TryGetString(ArgStr);
1063
+ void* ParamAddr = Params.GetData() + Param->GetOffset_ForInternal();
1064
+ Param->ImportText_Direct(*ArgStr, ParamAddr, BoundObj, PPF_None);
1065
+ }
1066
+ }
1067
+ }
1068
+
1069
+ BoundObj->ProcessEvent(Func, Params.GetData());
1070
+
1071
+ // Extract return value if any.
1072
+ FString ReturnValue = TEXT("");
1073
+ for (TFieldIterator<FProperty> ParamIt(Func); ParamIt && (ParamIt->PropertyFlags & CPF_Parm); ++ParamIt)
1074
+ {
1075
+ FProperty* Param = *ParamIt;
1076
+ if (Param->PropertyFlags & CPF_ReturnParm)
1077
+ {
1078
+ void* ParamAddr = Params.GetData() + Param->GetOffset_ForInternal();
1079
+ Param->ExportTextItem_Direct(ReturnValue, ParamAddr, nullptr, BoundObj, PPF_None);
1080
+ break;
1081
+ }
1082
+ }
1083
+
1084
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
1085
+ Data->SetStringField(TEXT("preset_path"), PresetPath);
1086
+ Data->SetStringField(TEXT("function_name"), FunctionName);
1087
+ Data->SetStringField(TEXT("result"), ReturnValue);
1088
+
1089
+ SendResponse(BuildMDSuccessResponse(CorrId, Data) + TEXT("\n"));
1090
+ }
1091
+ else
1092
+ {
1093
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("function_not_callable")) + TEXT("\n"));
1094
+ }
1095
+
1096
+ bFound = true;
1097
+ break;
1098
+ }
1099
+
1100
+ if (!bFound)
1101
+ {
1102
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("function_not_found")) + TEXT("\n"));
1103
+ }
1104
+ }
1105
+ else
1106
+ {
1107
+ SendResponse(BuildMDErrorResponse(CorrId, TEXT("invalid_action")) + TEXT("\n"));
1108
+ }
1109
+ });
1110
+ }