unreal-engine-mcp-server 0.2.1

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 (155) hide show
  1. package/.dockerignore +57 -0
  2. package/.env.production +25 -0
  3. package/.eslintrc.json +54 -0
  4. package/.github/workflows/publish-mcp.yml +75 -0
  5. package/Dockerfile +54 -0
  6. package/LICENSE +21 -0
  7. package/Public/icon.png +0 -0
  8. package/README.md +209 -0
  9. package/claude_desktop_config_example.json +13 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.js +7 -0
  12. package/dist/index.d.ts +31 -0
  13. package/dist/index.js +484 -0
  14. package/dist/prompts/index.d.ts +14 -0
  15. package/dist/prompts/index.js +38 -0
  16. package/dist/python-utils.d.ts +29 -0
  17. package/dist/python-utils.js +54 -0
  18. package/dist/resources/actors.d.ts +13 -0
  19. package/dist/resources/actors.js +83 -0
  20. package/dist/resources/assets.d.ts +23 -0
  21. package/dist/resources/assets.js +245 -0
  22. package/dist/resources/levels.d.ts +17 -0
  23. package/dist/resources/levels.js +94 -0
  24. package/dist/tools/actors.d.ts +51 -0
  25. package/dist/tools/actors.js +459 -0
  26. package/dist/tools/animation.d.ts +196 -0
  27. package/dist/tools/animation.js +579 -0
  28. package/dist/tools/assets.d.ts +21 -0
  29. package/dist/tools/assets.js +304 -0
  30. package/dist/tools/audio.d.ts +170 -0
  31. package/dist/tools/audio.js +416 -0
  32. package/dist/tools/blueprint.d.ts +144 -0
  33. package/dist/tools/blueprint.js +652 -0
  34. package/dist/tools/build_environment_advanced.d.ts +66 -0
  35. package/dist/tools/build_environment_advanced.js +484 -0
  36. package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
  37. package/dist/tools/consolidated-tool-definitions.js +607 -0
  38. package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
  39. package/dist/tools/consolidated-tool-handlers.js +1050 -0
  40. package/dist/tools/debug.d.ts +185 -0
  41. package/dist/tools/debug.js +265 -0
  42. package/dist/tools/editor.d.ts +88 -0
  43. package/dist/tools/editor.js +365 -0
  44. package/dist/tools/engine.d.ts +30 -0
  45. package/dist/tools/engine.js +36 -0
  46. package/dist/tools/foliage.d.ts +155 -0
  47. package/dist/tools/foliage.js +525 -0
  48. package/dist/tools/introspection.d.ts +98 -0
  49. package/dist/tools/introspection.js +683 -0
  50. package/dist/tools/landscape.d.ts +158 -0
  51. package/dist/tools/landscape.js +375 -0
  52. package/dist/tools/level.d.ts +110 -0
  53. package/dist/tools/level.js +362 -0
  54. package/dist/tools/lighting.d.ts +159 -0
  55. package/dist/tools/lighting.js +1179 -0
  56. package/dist/tools/materials.d.ts +34 -0
  57. package/dist/tools/materials.js +146 -0
  58. package/dist/tools/niagara.d.ts +145 -0
  59. package/dist/tools/niagara.js +289 -0
  60. package/dist/tools/performance.d.ts +163 -0
  61. package/dist/tools/performance.js +412 -0
  62. package/dist/tools/physics.d.ts +189 -0
  63. package/dist/tools/physics.js +784 -0
  64. package/dist/tools/rc.d.ts +110 -0
  65. package/dist/tools/rc.js +363 -0
  66. package/dist/tools/sequence.d.ts +112 -0
  67. package/dist/tools/sequence.js +675 -0
  68. package/dist/tools/tool-definitions.d.ts +4919 -0
  69. package/dist/tools/tool-definitions.js +891 -0
  70. package/dist/tools/tool-handlers.d.ts +47 -0
  71. package/dist/tools/tool-handlers.js +830 -0
  72. package/dist/tools/ui.d.ts +171 -0
  73. package/dist/tools/ui.js +337 -0
  74. package/dist/tools/visual.d.ts +29 -0
  75. package/dist/tools/visual.js +67 -0
  76. package/dist/types/env.d.ts +10 -0
  77. package/dist/types/env.js +18 -0
  78. package/dist/types/index.d.ts +323 -0
  79. package/dist/types/index.js +28 -0
  80. package/dist/types/tool-types.d.ts +274 -0
  81. package/dist/types/tool-types.js +13 -0
  82. package/dist/unreal-bridge.d.ts +126 -0
  83. package/dist/unreal-bridge.js +992 -0
  84. package/dist/utils/cache-manager.d.ts +64 -0
  85. package/dist/utils/cache-manager.js +176 -0
  86. package/dist/utils/error-handler.d.ts +66 -0
  87. package/dist/utils/error-handler.js +243 -0
  88. package/dist/utils/errors.d.ts +133 -0
  89. package/dist/utils/errors.js +256 -0
  90. package/dist/utils/http.d.ts +26 -0
  91. package/dist/utils/http.js +135 -0
  92. package/dist/utils/logger.d.ts +12 -0
  93. package/dist/utils/logger.js +32 -0
  94. package/dist/utils/normalize.d.ts +17 -0
  95. package/dist/utils/normalize.js +49 -0
  96. package/dist/utils/response-validator.d.ts +34 -0
  97. package/dist/utils/response-validator.js +121 -0
  98. package/dist/utils/safe-json.d.ts +4 -0
  99. package/dist/utils/safe-json.js +97 -0
  100. package/dist/utils/stdio-redirect.d.ts +2 -0
  101. package/dist/utils/stdio-redirect.js +20 -0
  102. package/dist/utils/validation.d.ts +50 -0
  103. package/dist/utils/validation.js +173 -0
  104. package/mcp-config-example.json +14 -0
  105. package/package.json +63 -0
  106. package/server.json +60 -0
  107. package/src/cli.ts +7 -0
  108. package/src/index.ts +543 -0
  109. package/src/prompts/index.ts +51 -0
  110. package/src/python/editor_compat.py +181 -0
  111. package/src/python-utils.ts +57 -0
  112. package/src/resources/actors.ts +92 -0
  113. package/src/resources/assets.ts +251 -0
  114. package/src/resources/levels.ts +83 -0
  115. package/src/tools/actors.ts +480 -0
  116. package/src/tools/animation.ts +713 -0
  117. package/src/tools/assets.ts +305 -0
  118. package/src/tools/audio.ts +548 -0
  119. package/src/tools/blueprint.ts +736 -0
  120. package/src/tools/build_environment_advanced.ts +526 -0
  121. package/src/tools/consolidated-tool-definitions.ts +619 -0
  122. package/src/tools/consolidated-tool-handlers.ts +1093 -0
  123. package/src/tools/debug.ts +368 -0
  124. package/src/tools/editor.ts +360 -0
  125. package/src/tools/engine.ts +32 -0
  126. package/src/tools/foliage.ts +652 -0
  127. package/src/tools/introspection.ts +778 -0
  128. package/src/tools/landscape.ts +523 -0
  129. package/src/tools/level.ts +410 -0
  130. package/src/tools/lighting.ts +1316 -0
  131. package/src/tools/materials.ts +148 -0
  132. package/src/tools/niagara.ts +312 -0
  133. package/src/tools/performance.ts +549 -0
  134. package/src/tools/physics.ts +924 -0
  135. package/src/tools/rc.ts +437 -0
  136. package/src/tools/sequence.ts +791 -0
  137. package/src/tools/tool-definitions.ts +907 -0
  138. package/src/tools/tool-handlers.ts +941 -0
  139. package/src/tools/ui.ts +499 -0
  140. package/src/tools/visual.ts +60 -0
  141. package/src/types/env.ts +27 -0
  142. package/src/types/index.ts +414 -0
  143. package/src/types/tool-types.ts +343 -0
  144. package/src/unreal-bridge.ts +1118 -0
  145. package/src/utils/cache-manager.ts +213 -0
  146. package/src/utils/error-handler.ts +320 -0
  147. package/src/utils/errors.ts +312 -0
  148. package/src/utils/http.ts +184 -0
  149. package/src/utils/logger.ts +30 -0
  150. package/src/utils/normalize.ts +54 -0
  151. package/src/utils/response-validator.ts +145 -0
  152. package/src/utils/safe-json.ts +112 -0
  153. package/src/utils/stdio-redirect.ts +18 -0
  154. package/src/utils/validation.ts +212 -0
  155. package/tsconfig.json +33 -0
@@ -0,0 +1,713 @@
1
+ import { UnrealBridge } from '../unreal-bridge.js';
2
+ import { validateAssetParams, concurrencyDelay } from '../utils/validation.js';
3
+
4
+ export class AnimationTools {
5
+ constructor(private bridge: UnrealBridge) {}
6
+
7
+ /**
8
+ * Create Animation Blueprint
9
+ */
10
+ async createAnimationBlueprint(params: {
11
+ name: string;
12
+ skeletonPath: string;
13
+ savePath?: string;
14
+ }) {
15
+ try {
16
+ // Strong input validation with expected error messages
17
+ if (!params.name || params.name.trim() === '') {
18
+ return {
19
+ success: false,
20
+ message: 'Failed: Name cannot be empty',
21
+ error: 'Name cannot be empty'
22
+ };
23
+ }
24
+
25
+ // Check for whitespace issues
26
+ if (params.name.includes(' ') || params.name.startsWith(' ') || params.name.endsWith(' ')) {
27
+ return {
28
+ success: false,
29
+ message: 'Failed to create Animation Blueprint: Name contains invalid whitespace',
30
+ error: 'Name contains invalid whitespace'
31
+ };
32
+ }
33
+
34
+ // Check for SQL injection patterns
35
+ if (params.name.toLowerCase().includes('drop') || params.name.toLowerCase().includes('delete') ||
36
+ params.name.includes(';') || params.name.includes('--')) {
37
+ return {
38
+ success: false,
39
+ message: 'Failed to create Animation Blueprint: Name contains invalid characters',
40
+ error: 'Name contains invalid characters'
41
+ };
42
+ }
43
+
44
+ // Check save path starts with /
45
+ if (params.savePath && !params.savePath.startsWith('/')) {
46
+ return {
47
+ success: false,
48
+ message: 'Failed to create Animation Blueprint: Path must start with /',
49
+ error: 'Path must start with /'
50
+ };
51
+ }
52
+
53
+ // Now validate and sanitize for actual use
54
+ const validation = validateAssetParams({
55
+ name: params.name,
56
+ savePath: params.savePath || '/Game/Animations'
57
+ });
58
+
59
+ if (!validation.valid) {
60
+ return {
61
+ success: false,
62
+ message: `Failed to create Animation Blueprint: ${validation.error}`,
63
+ error: validation.error
64
+ };
65
+ }
66
+
67
+ const sanitizedParams = validation.sanitized;
68
+ const path = sanitizedParams.savePath || '/Game/Animations';
69
+
70
+ // Add concurrency delay to prevent race conditions
71
+ await concurrencyDelay();
72
+
73
+ // Enhanced Python script with proper persistence and error detection
74
+ const pythonScript = `
75
+ import unreal
76
+ import time
77
+
78
+ # Helper function to ensure asset persistence
79
+ def ensure_asset_persistence(asset_path):
80
+ """Ensure asset is properly saved and registered"""
81
+ try:
82
+ # Load the asset to ensure it's in memory
83
+ asset = unreal.EditorAssetLibrary.load_asset(asset_path)
84
+ if not asset:
85
+ return False
86
+
87
+ # Save the asset
88
+ saved = unreal.EditorAssetLibrary.save_asset(asset_path, only_if_is_dirty=False)
89
+ if saved:
90
+ print(f"Asset saved: {asset_path}")
91
+
92
+ # Refresh the asset registry for the asset's directory only
93
+ try:
94
+ asset_dir = asset_path.rsplit('/', 1)[0]
95
+ unreal.AssetRegistryHelpers.get_asset_registry().scan_paths_synchronous([asset_dir], True)
96
+ except Exception as _reg_e:
97
+ pass
98
+
99
+ # Small delay to ensure filesystem sync
100
+ time.sleep(0.1)
101
+
102
+ return saved
103
+ except Exception as e:
104
+ print(f"Error ensuring persistence: {e}")
105
+ return False
106
+
107
+ # Stop PIE if it's running
108
+ try:
109
+ if unreal.EditorLevelLibrary.is_playing_editor():
110
+ print("Stopping Play In Editor mode...")
111
+ unreal.EditorLevelLibrary.editor_end_play()
112
+ # Small delay to ensure editor fully exits play mode
113
+ import time as _t
114
+ _t.sleep(0.5)
115
+ except Exception as _e:
116
+ # Try alternative check
117
+ try:
118
+ play_world = unreal.EditorLevelLibrary.get_editor_world()
119
+ if play_world and play_world.is_play_in_editor():
120
+ print("Stopping PIE via alternative method...")
121
+ unreal.EditorLevelLibrary.editor_end_play()
122
+ import time as _t2
123
+ _t2.sleep(0.5)
124
+ except:
125
+ pass # Continue if we can't check/stop play mode
126
+
127
+ # Main execution
128
+ success = False
129
+ error_msg = ""
130
+
131
+ # Log the attempt
132
+ print("Creating animation blueprint: ${sanitizedParams.name}")
133
+
134
+ asset_path = "${path}"
135
+ asset_name = "${sanitizedParams.name}"
136
+ full_path = f"{asset_path}/{asset_name}"
137
+
138
+ try:
139
+ # Check if already exists
140
+ if unreal.EditorAssetLibrary.does_asset_exist(full_path):
141
+ print(f"Asset already exists at {full_path}")
142
+ # Load and return existing
143
+ existing = unreal.EditorAssetLibrary.load_asset(full_path)
144
+ if existing:
145
+ print(f"Loaded existing AnimBlueprint: {full_path}")
146
+ success = True
147
+ else:
148
+ error_msg = f"Could not load existing asset at {full_path}"
149
+ print(f"Warning: {error_msg}")
150
+ else:
151
+ # Try to create new animation blueprint
152
+ factory = unreal.AnimBlueprintFactory()
153
+
154
+ # Try to load skeleton if provided
155
+ skeleton_path = "${params.skeletonPath}"
156
+ skeleton = None
157
+ skeleton_set = False
158
+
159
+ if skeleton_path and skeleton_path != "None":
160
+ if unreal.EditorAssetLibrary.does_asset_exist(skeleton_path):
161
+ skeleton = unreal.EditorAssetLibrary.load_asset(skeleton_path)
162
+ if skeleton and isinstance(skeleton, unreal.Skeleton):
163
+ # Different Unreal versions use different attribute names
164
+ try:
165
+ factory.target_skeleton = skeleton
166
+ skeleton_set = True
167
+ print(f"Using skeleton: {skeleton_path}")
168
+ except AttributeError:
169
+ try:
170
+ factory.skeleton = skeleton
171
+ skeleton_set = True
172
+ print(f"Using skeleton (alternate): {skeleton_path}")
173
+ except AttributeError:
174
+ # In some versions, the skeleton is set differently
175
+ try:
176
+ factory.set_editor_property('target_skeleton', skeleton)
177
+ skeleton_set = True
178
+ print(f"Using skeleton (property): {skeleton_path}")
179
+ except:
180
+ print(f"Warning: Could not set skeleton on factory")
181
+ else:
182
+ error_msg = f"Invalid skeleton at {skeleton_path}"
183
+ print(f"Warning: {error_msg}")
184
+ else:
185
+ print(f"Warning: Skeleton not found at {skeleton_path}, creating without skeleton")
186
+
187
+ # Create the asset
188
+ asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
189
+ new_asset = asset_tools.create_asset(
190
+ asset_name=asset_name,
191
+ package_path=asset_path,
192
+ asset_class=unreal.AnimBlueprint,
193
+ factory=factory
194
+ )
195
+
196
+ if new_asset:
197
+ print(f"Successfully created AnimBlueprint at {full_path}")
198
+
199
+ # Ensure persistence
200
+ if ensure_asset_persistence(full_path):
201
+ # Verify it was saved
202
+ if unreal.EditorAssetLibrary.does_asset_exist(full_path):
203
+ print(f"Verified asset exists after save: {full_path}")
204
+ success = True
205
+ else:
206
+ error_msg = f"Asset not found after save: {full_path}"
207
+ print(f"Warning: {error_msg}")
208
+ else:
209
+ error_msg = "Failed to persist asset"
210
+ print(f"Warning: {error_msg}")
211
+ else:
212
+ error_msg = f"Failed to create AnimBlueprint {asset_name}"
213
+ print(error_msg)
214
+
215
+ except Exception as e:
216
+ error_msg = str(e)
217
+ print(f"Error: {error_msg}")
218
+ import traceback
219
+ traceback.print_exc()
220
+
221
+ # Output result markers for parsing
222
+ if success:
223
+ print("SUCCESS")
224
+ else:
225
+ print(f"FAILED: {error_msg}")
226
+
227
+ print("DONE")
228
+ `;
229
+
230
+ // Execute Python and parse the output
231
+ try {
232
+ const response = await this.bridge.executePython(pythonScript);
233
+
234
+ // Parse the response to detect actual success or failure
235
+ const responseStr = typeof response === 'string' ? response : JSON.stringify(response);
236
+
237
+ // Check for explicit success/failure markers
238
+ if (responseStr.includes('SUCCESS')) {
239
+ return {
240
+ success: true,
241
+ message: `Animation Blueprint ${sanitizedParams.name} created successfully`,
242
+ path: `${path}/${sanitizedParams.name}`
243
+ };
244
+ } else if (responseStr.includes('FAILED:')) {
245
+ // Extract error message after FAILED:
246
+ const failMatch = responseStr.match(/FAILED:\s*(.+)/); const errorMsg = failMatch ? failMatch[1] : 'Unknown error';
247
+ return {
248
+ success: false,
249
+ message: `Failed to create Animation Blueprint: ${errorMsg}`,
250
+ error: errorMsg
251
+ };
252
+ } else {
253
+ // If no explicit markers, check for other error indicators
254
+ if (responseStr.includes('Error:') || responseStr.includes('error') ||
255
+ responseStr.includes('failed') || responseStr.includes('Failed')) {
256
+ return {
257
+ success: false,
258
+ message: 'Failed to create Animation Blueprint',
259
+ error: responseStr
260
+ };
261
+ }
262
+
263
+ // Assume success if no errors detected
264
+ return {
265
+ success: true,
266
+ message: `Animation Blueprint ${sanitizedParams.name} processed`,
267
+ path: `${path}/${sanitizedParams.name}`
268
+ };
269
+ }
270
+ } catch (error) {
271
+ return {
272
+ success: false,
273
+ message: 'Failed to create Animation Blueprint',
274
+ error: String(error)
275
+ };
276
+ }
277
+ } catch (err) {
278
+ return { success: false, error: `Failed to create AnimBlueprint: ${err}` };
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Add State Machine to Animation Blueprint
284
+ */
285
+ async addStateMachine(params: {
286
+ blueprintPath: string;
287
+ machineName: string;
288
+ states: Array<{
289
+ name: string;
290
+ animation?: string;
291
+ isEntry?: boolean;
292
+ isExit?: boolean;
293
+ }>;
294
+ transitions?: Array<{
295
+ sourceState: string;
296
+ targetState: string;
297
+ condition?: string;
298
+ }>;
299
+ }) {
300
+ try {
301
+ // State machines are complex - we'll use console commands for basic setup
302
+ const commands = [
303
+ `AddAnimStateMachine ${params.blueprintPath} ${params.machineName}`
304
+ ];
305
+
306
+ // Add states
307
+ for (const state of params.states) {
308
+ commands.push(
309
+ `AddAnimState ${params.blueprintPath} ${params.machineName} ${state.name} ${state.animation || ''}`
310
+ );
311
+ if (state.isEntry) {
312
+ commands.push(`SetAnimStateEntry ${params.blueprintPath} ${params.machineName} ${state.name}`);
313
+ }
314
+ }
315
+
316
+ // Add transitions
317
+ if (params.transitions) {
318
+ for (const transition of params.transitions) {
319
+ commands.push(
320
+ `AddAnimTransition ${params.blueprintPath} ${params.machineName} ${transition.sourceState} ${transition.targetState}`
321
+ );
322
+ }
323
+ }
324
+
325
+ for (const cmd of commands) {
326
+ await this.bridge.executeConsoleCommand(cmd);
327
+ }
328
+
329
+ return {
330
+ success: true,
331
+ message: `State machine ${params.machineName} added to ${params.blueprintPath}`
332
+ };
333
+ } catch (err) {
334
+ return { success: false, error: `Failed to add state machine: ${err}` };
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Create Animation Montage
340
+ */
341
+ async createMontage(params: {
342
+ name: string;
343
+ animationSequence: string;
344
+ savePath?: string;
345
+ sections?: Array<{
346
+ name: string;
347
+ startTime: number;
348
+ endTime: number;
349
+ }>;
350
+ notifies?: Array<{
351
+ name: string;
352
+ time: number;
353
+ }>;
354
+ }) {
355
+ try {
356
+ const path = params.savePath || '/Game/Animations/Montages';
357
+ const commands = [
358
+ `CreateAsset AnimMontage ${params.name} ${path}`,
359
+ `SetMontageAnimation ${params.name} ${params.animationSequence}`
360
+ ];
361
+
362
+ // Add sections
363
+ if (params.sections) {
364
+ for (const section of params.sections) {
365
+ commands.push(
366
+ `AddMontageSection ${params.name} ${section.name} ${section.startTime} ${section.endTime}`
367
+ );
368
+ }
369
+ }
370
+
371
+ // Add notifies
372
+ if (params.notifies) {
373
+ for (const notify of params.notifies) {
374
+ commands.push(
375
+ `AddMontageNotify ${params.name} ${notify.name} ${notify.time}`
376
+ );
377
+ }
378
+ }
379
+
380
+ for (const cmd of commands) {
381
+ await this.bridge.executeConsoleCommand(cmd);
382
+ }
383
+
384
+ return {
385
+ success: true,
386
+ message: `Animation Montage ${params.name} created`,
387
+ path: `${path}/${params.name}`
388
+ };
389
+ } catch (err) {
390
+ return { success: false, error: `Failed to create montage: ${err}` };
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Create Blend Space
396
+ */
397
+ async createBlendSpace(params: {
398
+ name: string;
399
+ skeletonPath: string;
400
+ savePath?: string;
401
+ dimensions: 1 | 2;
402
+ horizontalAxis?: {
403
+ name: string;
404
+ minValue: number;
405
+ maxValue: number;
406
+ };
407
+ verticalAxis?: {
408
+ name: string;
409
+ minValue: number;
410
+ maxValue: number;
411
+ };
412
+ samples?: Array<{
413
+ animation: string;
414
+ x: number;
415
+ y?: number;
416
+ }>;
417
+ }) {
418
+ try {
419
+ const path = params.savePath || '/Game/Animations/BlendSpaces';
420
+ const blendSpaceType = params.dimensions === 1 ? 'BlendSpace1D' : 'BlendSpace';
421
+
422
+ // These commands don't exist, return a message about limitations
423
+ const commands = [
424
+ `echo Creating ${blendSpaceType} ${params.name} at ${path}`
425
+ ];
426
+
427
+ // Configure axes
428
+ if (params.horizontalAxis) {
429
+ commands.push(
430
+ `SetBlendSpaceAxis ${params.name} Horizontal ${params.horizontalAxis.name} ${params.horizontalAxis.minValue} ${params.horizontalAxis.maxValue}`
431
+ );
432
+ }
433
+
434
+ if (params.dimensions === 2 && params.verticalAxis) {
435
+ commands.push(
436
+ `SetBlendSpaceAxis ${params.name} Vertical ${params.verticalAxis.name} ${params.verticalAxis.minValue} ${params.verticalAxis.maxValue}`
437
+ );
438
+ }
439
+
440
+ // Add sample animations
441
+ if (params.samples) {
442
+ for (const sample of params.samples) {
443
+ const coords = params.dimensions === 1 ? `${sample.x}` : `${sample.x} ${sample.y || 0}`;
444
+ commands.push(
445
+ `AddBlendSpaceSample ${params.name} ${sample.animation} ${coords}`
446
+ );
447
+ }
448
+ }
449
+
450
+ for (const cmd of commands) {
451
+ await this.bridge.executeConsoleCommand(cmd);
452
+ }
453
+
454
+ return {
455
+ success: true,
456
+ message: `Blend Space ${params.name} created`,
457
+ path: `${path}/${params.name}`
458
+ };
459
+ } catch (err) {
460
+ return { success: false, error: `Failed to create blend space: ${err}` };
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Setup Control Rig
466
+ */
467
+ async setupControlRig(params: {
468
+ name: string;
469
+ skeletonPath: string;
470
+ savePath?: string;
471
+ controls?: Array<{
472
+ name: string;
473
+ type: 'Transform' | 'Float' | 'Bool' | 'Vector';
474
+ bone?: string;
475
+ defaultValue?: any;
476
+ }>;
477
+ }) {
478
+ try {
479
+ const path = params.savePath || '/Game/Animations';
480
+
481
+ // Validate path length (Unreal has a 260 character limit)
482
+ const fullPath = `${path}/${params.name}`;
483
+ if (fullPath.length > 260) {
484
+ return {
485
+ success: false,
486
+ message: `Failed: Path too long (${fullPath.length} characters)`,
487
+ error: 'Unreal Engine paths must be less than 260 characters'
488
+ };
489
+ }
490
+
491
+ const commands = [
492
+ `CreateAsset ControlRig ${params.name} ${path}`,
493
+ `SetControlRigSkeleton ${params.name} ${params.skeletonPath}`
494
+ ];
495
+
496
+ // Add controls
497
+ if (params.controls) {
498
+ for (const control of params.controls) {
499
+ commands.push(
500
+ `AddControlRigControl ${params.name} ${control.name} ${control.type} ${control.bone || ''}`
501
+ );
502
+ if (control.defaultValue !== undefined) {
503
+ commands.push(
504
+ `SetControlRigDefault ${params.name} ${control.name} ${JSON.stringify(control.defaultValue)}`
505
+ );
506
+ }
507
+ }
508
+ }
509
+
510
+ for (const cmd of commands) {
511
+ await this.bridge.executeConsoleCommand(cmd);
512
+ }
513
+
514
+ return {
515
+ success: true,
516
+ message: `Control Rig ${params.name} created`,
517
+ path: `${path}/${params.name}`
518
+ };
519
+ } catch (err) {
520
+ return { success: false, error: `Failed to setup control rig: ${err}` };
521
+ }
522
+ }
523
+
524
+ /**
525
+ * Create Level Sequence (for cinematics)
526
+ */
527
+ async createLevelSequence(params: {
528
+ name: string;
529
+ savePath?: string;
530
+ frameRate?: number;
531
+ duration?: number;
532
+ tracks?: Array<{
533
+ actorName: string;
534
+ trackType: 'Transform' | 'Animation' | 'Camera' | 'Event';
535
+ keyframes?: Array<{
536
+ time: number;
537
+ value: any;
538
+ }>;
539
+ }>;
540
+ }) {
541
+ try {
542
+ const path = params.savePath || '/Game/Cinematics';
543
+
544
+ const commands = [
545
+ `CreateAsset LevelSequence ${params.name} ${path}`,
546
+ `SetSequenceFrameRate ${params.name} ${params.frameRate || 30}`,
547
+ `SetSequenceDuration ${params.name} ${params.duration || 5}`
548
+ ];
549
+
550
+ // Add tracks
551
+ if (params.tracks) {
552
+ for (const track of params.tracks) {
553
+ commands.push(
554
+ `AddSequenceTrack ${params.name} ${track.actorName} ${track.trackType}`
555
+ );
556
+
557
+ // Add keyframes
558
+ if (track.keyframes) {
559
+ for (const keyframe of track.keyframes) {
560
+ commands.push(
561
+ `AddSequenceKey ${params.name} ${track.actorName} ${track.trackType} ${keyframe.time} ${JSON.stringify(keyframe.value)}`
562
+ );
563
+ }
564
+ }
565
+ }
566
+ }
567
+
568
+ for (const cmd of commands) {
569
+ await this.bridge.executeConsoleCommand(cmd);
570
+ }
571
+
572
+ return {
573
+ success: true,
574
+ message: `Level Sequence ${params.name} created`,
575
+ path: `${path}/${params.name}`
576
+ };
577
+ } catch (err) {
578
+ return { success: false, error: `Failed to create level sequence: ${err}` };
579
+ }
580
+ }
581
+
582
+ /**
583
+ * Play Animation on Actor
584
+ */
585
+ async playAnimation(params: {
586
+ actorName: string;
587
+ animationType: 'Montage' | 'Sequence' | 'BlendSpace';
588
+ animationPath: string;
589
+ playRate?: number;
590
+ loop?: boolean;
591
+ blendInTime?: number;
592
+ blendOutTime?: number;
593
+ }) {
594
+ try {
595
+ // Implement via Python for UE 5.x compatibility instead of non-existent console commands
596
+ const playRate = params.playRate ?? 1.0;
597
+ const loopFlag = params.loop ? 'True' : 'False';
598
+
599
+ const python = `
600
+ import unreal
601
+ import json
602
+
603
+ result = {"success": False, "message": ""}
604
+
605
+ try:
606
+ actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
607
+ actors = actor_subsystem.get_all_level_actors()
608
+ target = None
609
+ search = "${params.actorName}"
610
+ for a in actors:
611
+ if not a:
612
+ continue
613
+ name = a.get_name()
614
+ label = a.get_actor_label()
615
+ if (search.lower() == name.lower()) or (search.lower() == label.lower()) or (search.lower() in label.lower()):
616
+ target = a
617
+ break
618
+
619
+ if not target:
620
+ result["message"] = f"Actor not found: {search}"
621
+ else:
622
+ # Try to get a SkeletalMeshComponent from the actor
623
+ sk = target.get_component_by_class(unreal.SkeletalMeshComponent)
624
+ if not sk:
625
+ # Try commonly named properties (e.g., Character mesh)
626
+ try:
627
+ sk = target.get_editor_property('mesh')
628
+ except Exception:
629
+ sk = None
630
+ if not sk:
631
+ result["message"] = "No SkeletalMeshComponent found on actor"
632
+ else:
633
+ anim_type = "${params.animationType}"
634
+ asset_path = r"${params.animationPath}"
635
+ if not unreal.EditorAssetLibrary.does_asset_exist(asset_path):
636
+ result["message"] = f"Animation asset not found: {asset_path}"
637
+ else:
638
+ asset = unreal.EditorAssetLibrary.load_asset(asset_path)
639
+ if anim_type == 'Montage':
640
+ # Use AnimInstance montage_play
641
+ inst = sk.get_anim_instance()
642
+ if not inst:
643
+ result["message"] = "AnimInstance not found on SkeletalMeshComponent"
644
+ else:
645
+ try:
646
+ # montage_play(montage, play_rate, return_value_type, time_to_start_montage_at, stop_all_montages)
647
+ inst.montage_play(asset, ${playRate})
648
+ result["success"] = True
649
+ result["message"] = f"Montage playing on {search}"
650
+ except Exception as e:
651
+ result["message"] = f"Failed to play montage: {e}"
652
+ elif anim_type == 'Sequence':
653
+ try:
654
+ sk.play_animation(asset, ${loopFlag})
655
+ # Adjust rate if supported via play rate on AnimInstance
656
+ try:
657
+ inst = sk.get_anim_instance()
658
+ if inst:
659
+ # Not all paths support direct play rate control here; best effort only
660
+ pass
661
+ except Exception:
662
+ pass
663
+ result["success"] = True
664
+ result["message"] = f"Sequence playing on {search}"
665
+ except Exception as e:
666
+ result["message"] = f"Failed to play sequence: {e}"
667
+ else:
668
+ result["message"] = "BlendSpace playback requires an Animation Blueprint; not supported via direct play."
669
+ except Exception as e:
670
+ result["message"] = f"Error: {e}"
671
+
672
+ print("RESULT:" + json.dumps(result))
673
+ `.trim();
674
+
675
+ const resp = await this.bridge.executePython(python);
676
+ // Parse Python result
677
+ let output = '';
678
+ if (resp && typeof resp === 'object' && Array.isArray((resp as any).LogOutput)) {
679
+ output = (resp as any).LogOutput.map((l: any) => l.Output || '').join('');
680
+ } else if (typeof resp === 'string') {
681
+ output = resp;
682
+ } else {
683
+ output = JSON.stringify(resp);
684
+ }
685
+ const m = output.match(/RESULT:({.*})/);
686
+ if (m) {
687
+ try {
688
+ const parsed = JSON.parse(m[1]);
689
+ return parsed.success ? { success: true, message: parsed.message } : { success: false, error: parsed.message };
690
+ } catch {}
691
+ }
692
+ return { success: true, message: `Animation ${params.animationType} processed for ${params.actorName}` };
693
+ } catch (err) {
694
+ return { success: false, error: `Failed to play animation: ${err}` };
695
+ }
696
+ }
697
+
698
+ /**
699
+ * Helper function to execute console commands
700
+ */
701
+ private async _executeCommand(command: string) {
702
+ return this.bridge.httpCall('/remote/object/call', 'PUT', {
703
+ objectPath: '/Script/Engine.Default__KismetSystemLibrary',
704
+ functionName: 'ExecuteConsoleCommand',
705
+ parameters: {
706
+ WorldContextObject: null,
707
+ Command: command,
708
+ SpecificPlayer: null
709
+ },
710
+ generateTransaction: false
711
+ });
712
+ }
713
+ }