unreal-engine-mcp-server 0.3.1 → 0.4.3

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 (144) hide show
  1. package/.env.production +1 -1
  2. package/.github/copilot-instructions.md +45 -0
  3. package/.github/workflows/publish-mcp.yml +1 -1
  4. package/README.md +22 -7
  5. package/dist/index.js +137 -46
  6. package/dist/prompts/index.d.ts +10 -3
  7. package/dist/prompts/index.js +186 -7
  8. package/dist/resources/actors.d.ts +19 -1
  9. package/dist/resources/actors.js +55 -64
  10. package/dist/resources/assets.d.ts +3 -2
  11. package/dist/resources/assets.js +117 -109
  12. package/dist/resources/levels.d.ts +21 -3
  13. package/dist/resources/levels.js +31 -56
  14. package/dist/tools/actors.d.ts +3 -14
  15. package/dist/tools/actors.js +246 -302
  16. package/dist/tools/animation.d.ts +57 -102
  17. package/dist/tools/animation.js +429 -450
  18. package/dist/tools/assets.d.ts +13 -2
  19. package/dist/tools/assets.js +58 -46
  20. package/dist/tools/audio.d.ts +22 -13
  21. package/dist/tools/audio.js +467 -121
  22. package/dist/tools/blueprint.d.ts +32 -13
  23. package/dist/tools/blueprint.js +699 -448
  24. package/dist/tools/build_environment_advanced.d.ts +0 -1
  25. package/dist/tools/build_environment_advanced.js +236 -87
  26. package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
  27. package/dist/tools/consolidated-tool-definitions.js +124 -255
  28. package/dist/tools/consolidated-tool-handlers.js +749 -766
  29. package/dist/tools/debug.d.ts +72 -10
  30. package/dist/tools/debug.js +170 -36
  31. package/dist/tools/editor.d.ts +9 -2
  32. package/dist/tools/editor.js +30 -44
  33. package/dist/tools/foliage.d.ts +34 -15
  34. package/dist/tools/foliage.js +97 -107
  35. package/dist/tools/introspection.js +19 -21
  36. package/dist/tools/landscape.d.ts +1 -2
  37. package/dist/tools/landscape.js +311 -168
  38. package/dist/tools/level.d.ts +3 -28
  39. package/dist/tools/level.js +642 -192
  40. package/dist/tools/lighting.d.ts +14 -3
  41. package/dist/tools/lighting.js +236 -123
  42. package/dist/tools/materials.d.ts +25 -7
  43. package/dist/tools/materials.js +102 -79
  44. package/dist/tools/niagara.d.ts +10 -12
  45. package/dist/tools/niagara.js +74 -94
  46. package/dist/tools/performance.d.ts +12 -4
  47. package/dist/tools/performance.js +38 -79
  48. package/dist/tools/physics.d.ts +34 -10
  49. package/dist/tools/physics.js +364 -292
  50. package/dist/tools/rc.js +98 -24
  51. package/dist/tools/sequence.d.ts +1 -0
  52. package/dist/tools/sequence.js +146 -24
  53. package/dist/tools/ui.d.ts +31 -4
  54. package/dist/tools/ui.js +83 -66
  55. package/dist/tools/visual.d.ts +11 -0
  56. package/dist/tools/visual.js +245 -30
  57. package/dist/types/tool-types.d.ts +0 -6
  58. package/dist/types/tool-types.js +1 -8
  59. package/dist/unreal-bridge.d.ts +32 -2
  60. package/dist/unreal-bridge.js +621 -127
  61. package/dist/utils/elicitation.d.ts +57 -0
  62. package/dist/utils/elicitation.js +104 -0
  63. package/dist/utils/error-handler.d.ts +0 -33
  64. package/dist/utils/error-handler.js +4 -111
  65. package/dist/utils/http.d.ts +2 -22
  66. package/dist/utils/http.js +12 -75
  67. package/dist/utils/normalize.d.ts +4 -4
  68. package/dist/utils/normalize.js +15 -7
  69. package/dist/utils/python-output.d.ts +18 -0
  70. package/dist/utils/python-output.js +290 -0
  71. package/dist/utils/python.d.ts +2 -0
  72. package/dist/utils/python.js +4 -0
  73. package/dist/utils/response-validator.d.ts +6 -1
  74. package/dist/utils/response-validator.js +66 -13
  75. package/dist/utils/result-helpers.d.ts +27 -0
  76. package/dist/utils/result-helpers.js +147 -0
  77. package/dist/utils/safe-json.d.ts +0 -2
  78. package/dist/utils/safe-json.js +0 -43
  79. package/dist/utils/validation.d.ts +16 -0
  80. package/dist/utils/validation.js +70 -7
  81. package/mcp-config-example.json +2 -2
  82. package/package.json +11 -10
  83. package/server.json +37 -14
  84. package/src/index.ts +146 -50
  85. package/src/prompts/index.ts +211 -13
  86. package/src/resources/actors.ts +59 -44
  87. package/src/resources/assets.ts +123 -102
  88. package/src/resources/levels.ts +37 -47
  89. package/src/tools/actors.ts +269 -313
  90. package/src/tools/animation.ts +556 -539
  91. package/src/tools/assets.ts +59 -45
  92. package/src/tools/audio.ts +507 -113
  93. package/src/tools/blueprint.ts +778 -462
  94. package/src/tools/build_environment_advanced.ts +312 -106
  95. package/src/tools/consolidated-tool-definitions.ts +136 -267
  96. package/src/tools/consolidated-tool-handlers.ts +871 -795
  97. package/src/tools/debug.ts +179 -38
  98. package/src/tools/editor.ts +35 -37
  99. package/src/tools/foliage.ts +110 -104
  100. package/src/tools/introspection.ts +24 -22
  101. package/src/tools/landscape.ts +334 -181
  102. package/src/tools/level.ts +683 -182
  103. package/src/tools/lighting.ts +244 -123
  104. package/src/tools/materials.ts +114 -83
  105. package/src/tools/niagara.ts +87 -81
  106. package/src/tools/performance.ts +49 -88
  107. package/src/tools/physics.ts +393 -299
  108. package/src/tools/rc.ts +103 -25
  109. package/src/tools/sequence.ts +157 -30
  110. package/src/tools/ui.ts +101 -70
  111. package/src/tools/visual.ts +250 -29
  112. package/src/types/tool-types.ts +0 -9
  113. package/src/unreal-bridge.ts +658 -140
  114. package/src/utils/elicitation.ts +129 -0
  115. package/src/utils/error-handler.ts +4 -159
  116. package/src/utils/http.ts +16 -115
  117. package/src/utils/normalize.ts +20 -10
  118. package/src/utils/python-output.ts +351 -0
  119. package/src/utils/python.ts +3 -0
  120. package/src/utils/response-validator.ts +68 -17
  121. package/src/utils/result-helpers.ts +193 -0
  122. package/src/utils/safe-json.ts +0 -50
  123. package/src/utils/validation.ts +94 -7
  124. package/tests/run-unreal-tool-tests.mjs +720 -0
  125. package/tsconfig.json +2 -2
  126. package/dist/python-utils.d.ts +0 -29
  127. package/dist/python-utils.js +0 -54
  128. package/dist/tools/tool-definitions.d.ts +0 -4919
  129. package/dist/tools/tool-definitions.js +0 -1065
  130. package/dist/tools/tool-handlers.d.ts +0 -47
  131. package/dist/tools/tool-handlers.js +0 -863
  132. package/dist/types/index.d.ts +0 -323
  133. package/dist/types/index.js +0 -28
  134. package/dist/utils/cache-manager.d.ts +0 -64
  135. package/dist/utils/cache-manager.js +0 -176
  136. package/dist/utils/errors.d.ts +0 -133
  137. package/dist/utils/errors.js +0 -256
  138. package/src/python/editor_compat.py +0 -181
  139. package/src/python-utils.ts +0 -57
  140. package/src/tools/tool-definitions.ts +0 -1081
  141. package/src/tools/tool-handlers.ts +0 -973
  142. package/src/types/index.ts +0 -414
  143. package/src/utils/cache-manager.ts +0 -213
  144. package/src/utils/errors.ts +0 -312
@@ -1,287 +1,88 @@
1
1
  import { UnrealBridge } from '../unreal-bridge.js';
2
- import { validateAssetParams, concurrencyDelay } from '../utils/validation.js';
2
+ import { validateAssetParams } from '../utils/validation.js';
3
+ import {
4
+ interpretStandardResult,
5
+ coerceBoolean,
6
+ coerceString,
7
+ coerceStringArray
8
+ } from '../utils/result-helpers.js';
9
+
10
+ type CreateAnimationBlueprintSuccess = {
11
+ success: true;
12
+ message: string;
13
+ path: string;
14
+ exists?: boolean;
15
+ skeleton?: string;
16
+ warnings?: string[];
17
+ details?: string[];
18
+ };
19
+
20
+ type CreateAnimationBlueprintFailure = {
21
+ success: false;
22
+ message: string;
23
+ error: string;
24
+ path?: string;
25
+ exists?: boolean;
26
+ skeleton?: string;
27
+ warnings?: string[];
28
+ details?: string[];
29
+ };
30
+
31
+ type PlayAnimationSuccess = {
32
+ success: true;
33
+ message: string;
34
+ warnings?: string[];
35
+ details?: string[];
36
+ actorName?: string;
37
+ animationType?: string;
38
+ assetPath?: string;
39
+ };
40
+
41
+ type PlayAnimationFailure = {
42
+ success: false;
43
+ message: string;
44
+ error: string;
45
+ warnings?: string[];
46
+ details?: string[];
47
+ availableActors?: string[];
48
+ actorName?: string;
49
+ animationType?: string;
50
+ assetPath?: string;
51
+ };
3
52
 
4
53
  export class AnimationTools {
5
54
  constructor(private bridge: UnrealBridge) {}
6
55
 
7
- /**
8
- * Create Animation Blueprint
9
- */
10
56
  async createAnimationBlueprint(params: {
11
57
  name: string;
12
58
  skeletonPath: string;
13
59
  savePath?: string;
14
- }) {
60
+ }): Promise<CreateAnimationBlueprintSuccess | CreateAnimationBlueprintFailure> {
15
61
  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
-
62
+ const targetPath = params.savePath ?? '/Game/Animations';
63
+ const validation = validateAssetParams({ name: params.name, savePath: targetPath });
59
64
  if (!validation.valid) {
60
- return {
61
- success: false,
62
- message: `Failed to create Animation Blueprint: ${validation.error}`,
63
- error: validation.error
64
- };
65
+ const message = validation.error ?? 'Invalid asset parameters';
66
+ return { success: false, message, error: message };
65
67
  }
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
68
 
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
- }
69
+ const sanitized = validation.sanitized;
70
+ const assetName = sanitized.name;
71
+ const assetPath = sanitized.savePath ?? targetPath;
72
+ const script = this.buildCreateAnimationBlueprintScript({
73
+ name: assetName,
74
+ path: assetPath,
75
+ skeletonPath: params.skeletonPath
76
+ });
77
+
78
+ const response = await this.bridge.executePython(script);
79
+ return this.parseAnimationBlueprintResponse(response, assetName, assetPath);
277
80
  } catch (err) {
278
- return { success: false, error: `Failed to create AnimBlueprint: ${err}` };
81
+ const error = `Failed to create Animation Blueprint: ${err}`;
82
+ return { success: false, message: error, error: String(err) };
279
83
  }
280
84
  }
281
85
 
282
- /**
283
- * Add State Machine to Animation Blueprint
284
- */
285
86
  async addStateMachine(params: {
286
87
  blueprintPath: string;
287
88
  machineName: string;
@@ -296,174 +97,113 @@ print("DONE")
296
97
  targetState: string;
297
98
  condition?: string;
298
99
  }>;
299
- }) {
100
+ }): Promise<{ success: true; message: string } | { success: false; error: string }> {
300
101
  try {
301
- // State machines are complex - we'll use console commands for basic setup
302
- const commands = [
102
+ if (!params.blueprintPath || !params.machineName) {
103
+ return { success: false, error: 'blueprintPath and machineName are required' };
104
+ }
105
+
106
+ const commands: string[] = [
303
107
  `AddAnimStateMachine ${params.blueprintPath} ${params.machineName}`
304
108
  ];
305
-
306
- // Add states
109
+
307
110
  for (const state of params.states) {
111
+ const animationName = state.animation ?? '';
308
112
  commands.push(
309
- `AddAnimState ${params.blueprintPath} ${params.machineName} ${state.name} ${state.animation || ''}`
113
+ `AddAnimState ${params.blueprintPath} ${params.machineName} ${state.name} ${animationName}`
310
114
  );
311
115
  if (state.isEntry) {
312
116
  commands.push(`SetAnimStateEntry ${params.blueprintPath} ${params.machineName} ${state.name}`);
313
117
  }
118
+ if (state.isExit) {
119
+ commands.push(`SetAnimStateExit ${params.blueprintPath} ${params.machineName} ${state.name}`);
120
+ }
314
121
  }
315
-
316
- // Add transitions
122
+
317
123
  if (params.transitions) {
318
124
  for (const transition of params.transitions) {
319
125
  commands.push(
320
126
  `AddAnimTransition ${params.blueprintPath} ${params.machineName} ${transition.sourceState} ${transition.targetState}`
321
127
  );
128
+ if (transition.condition) {
129
+ commands.push(
130
+ `SetAnimTransitionRule ${params.blueprintPath} ${params.machineName} ${transition.sourceState} ${transition.targetState} ${transition.condition}`
131
+ );
132
+ }
322
133
  }
323
134
  }
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}`
135
+
136
+ await this.bridge.executeConsoleCommands(commands);
137
+ return {
138
+ success: true,
139
+ message: `State machine ${params.machineName} added to ${params.blueprintPath}`
332
140
  };
333
141
  } catch (err) {
334
142
  return { success: false, error: `Failed to add state machine: ${err}` };
335
143
  }
336
144
  }
337
145
 
338
- /**
339
- * Create Animation Montage
340
- */
341
- async createMontage(params: {
146
+ async createBlendSpace(params: {
342
147
  name: string;
343
- animationSequence: string;
344
148
  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
- }) {
149
+ dimensions?: 1 | 2;
150
+ skeletonPath?: string;
151
+ horizontalAxis?: { name: string; minValue: number; maxValue: number };
152
+ verticalAxis?: { name: string; minValue: number; maxValue: number };
153
+ samples?: Array<{ animation: string; x: number; y?: number }>;
154
+ }): Promise<{ success: true; message: string; path: string } | { success: false; error: string }> {
355
155
  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);
156
+ const targetPath = params.savePath ?? '/Game/Animations';
157
+ const validation = validateAssetParams({ name: params.name, savePath: targetPath });
158
+ if (!validation.valid) {
159
+ return { success: false, error: validation.error ?? 'Invalid asset parameters' };
382
160
  }
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
161
 
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}`
162
+ const sanitized = validation.sanitized;
163
+ const assetName = sanitized.name;
164
+ const assetPath = sanitized.savePath ?? targetPath;
165
+ const dimensions = params.dimensions === 2 ? 2 : 1;
166
+ const blendSpaceType = dimensions === 2 ? 'BlendSpace' : 'BlendSpace1D';
167
+
168
+ const commands: string[] = [
169
+ `CreateAsset ${blendSpaceType} ${assetName} ${assetPath}`,
170
+ `echo Creating ${blendSpaceType} ${assetName} at ${assetPath}`
425
171
  ];
426
-
427
- // Configure axes
172
+
173
+ if (params.skeletonPath) {
174
+ commands.push(`SetBlendSpaceSkeleton ${assetName} ${params.skeletonPath}`);
175
+ }
176
+
428
177
  if (params.horizontalAxis) {
429
178
  commands.push(
430
- `SetBlendSpaceAxis ${params.name} Horizontal ${params.horizontalAxis.name} ${params.horizontalAxis.minValue} ${params.horizontalAxis.maxValue}`
179
+ `SetBlendSpaceAxis ${assetName} Horizontal ${params.horizontalAxis.name} ${params.horizontalAxis.minValue} ${params.horizontalAxis.maxValue}`
431
180
  );
432
181
  }
433
-
434
- if (params.dimensions === 2 && params.verticalAxis) {
182
+
183
+ if (dimensions === 2 && params.verticalAxis) {
435
184
  commands.push(
436
- `SetBlendSpaceAxis ${params.name} Vertical ${params.verticalAxis.name} ${params.verticalAxis.minValue} ${params.verticalAxis.maxValue}`
185
+ `SetBlendSpaceAxis ${assetName} Vertical ${params.verticalAxis.name} ${params.verticalAxis.minValue} ${params.verticalAxis.maxValue}`
437
186
  );
438
187
  }
439
-
440
- // Add sample animations
188
+
441
189
  if (params.samples) {
442
190
  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
- );
191
+ const coords = dimensions === 1 ? `${sample.x}` : `${sample.x} ${sample.y ?? 0}`;
192
+ commands.push(`AddBlendSpaceSample ${assetName} ${sample.animation} ${coords}`);
447
193
  }
448
194
  }
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}`
195
+
196
+ await this.bridge.executeConsoleCommands(commands);
197
+ return {
198
+ success: true,
199
+ message: `Blend Space ${assetName} created`,
200
+ path: `${assetPath}/${assetName}`
458
201
  };
459
202
  } catch (err) {
460
203
  return { success: false, error: `Failed to create blend space: ${err}` };
461
204
  }
462
205
  }
463
206
 
464
- /**
465
- * Setup Control Rig
466
- */
467
207
  async setupControlRig(params: {
468
208
  name: string;
469
209
  skeletonPath: string;
@@ -472,58 +212,50 @@ print("DONE")
472
212
  name: string;
473
213
  type: 'Transform' | 'Float' | 'Bool' | 'Vector';
474
214
  bone?: string;
475
- defaultValue?: any;
215
+ defaultValue?: unknown;
476
216
  }>;
477
- }) {
217
+ }): Promise<{ success: true; message: string; path: string } | { success: false; error: string }> {
478
218
  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
- };
219
+ const targetPath = params.savePath ?? '/Game/Animations';
220
+ const validation = validateAssetParams({ name: params.name, savePath: targetPath });
221
+ if (!validation.valid) {
222
+ return { success: false, error: validation.error ?? 'Invalid asset parameters' };
489
223
  }
490
-
491
- const commands = [
492
- `CreateAsset ControlRig ${params.name} ${path}`,
493
- `SetControlRigSkeleton ${params.name} ${params.skeletonPath}`
224
+
225
+ const sanitized = validation.sanitized;
226
+ const assetName = sanitized.name;
227
+ const assetPath = sanitized.savePath ?? targetPath;
228
+ const fullPath = `${assetPath}/${assetName}`;
229
+
230
+ const commands: string[] = [
231
+ `CreateAsset ControlRig ${assetName} ${assetPath}`,
232
+ `SetControlRigSkeleton ${assetName} ${params.skeletonPath}`
494
233
  ];
495
-
496
- // Add controls
234
+
497
235
  if (params.controls) {
498
236
  for (const control of params.controls) {
499
237
  commands.push(
500
- `AddControlRigControl ${params.name} ${control.name} ${control.type} ${control.bone || ''}`
238
+ `AddControlRigControl ${assetName} ${control.name} ${control.type} ${control.bone ?? ''}`
501
239
  );
502
240
  if (control.defaultValue !== undefined) {
503
241
  commands.push(
504
- `SetControlRigDefault ${params.name} ${control.name} ${JSON.stringify(control.defaultValue)}`
242
+ `SetControlRigDefault ${assetName} ${control.name} ${JSON.stringify(control.defaultValue)}`
505
243
  );
506
244
  }
507
245
  }
508
246
  }
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}`
247
+
248
+ await this.bridge.executeConsoleCommands(commands);
249
+ return {
250
+ success: true,
251
+ message: `Control Rig ${assetName} created`,
252
+ path: fullPath
518
253
  };
519
254
  } catch (err) {
520
255
  return { success: false, error: `Failed to setup control rig: ${err}` };
521
256
  }
522
257
  }
523
258
 
524
- /**
525
- * Create Level Sequence (for cinematics)
526
- */
527
259
  async createLevelSequence(params: {
528
260
  name: string;
529
261
  savePath?: string;
@@ -532,56 +264,50 @@ print("DONE")
532
264
  tracks?: Array<{
533
265
  actorName: string;
534
266
  trackType: 'Transform' | 'Animation' | 'Camera' | 'Event';
535
- keyframes?: Array<{
536
- time: number;
537
- value: any;
538
- }>;
267
+ keyframes?: Array<{ time: number; value: unknown }>;
539
268
  }>;
540
- }) {
269
+ }): Promise<{ success: true; message: string; path: string } | { success: false; error: string }> {
541
270
  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}`
271
+ const targetPath = params.savePath ?? '/Game/Cinematics';
272
+ const validation = validateAssetParams({ name: params.name, savePath: targetPath });
273
+ if (!validation.valid) {
274
+ return { success: false, error: validation.error ?? 'Invalid asset parameters' };
275
+ }
276
+
277
+ const sanitized = validation.sanitized;
278
+ const assetName = sanitized.name;
279
+ const assetPath = sanitized.savePath ?? targetPath;
280
+
281
+ const commands: string[] = [
282
+ `CreateAsset LevelSequence ${assetName} ${assetPath}`,
283
+ `SetSequenceFrameRate ${assetName} ${params.frameRate ?? 30}`,
284
+ `SetSequenceDuration ${assetName} ${params.duration ?? 5}`
548
285
  ];
549
-
550
- // Add tracks
286
+
551
287
  if (params.tracks) {
552
288
  for (const track of params.tracks) {
553
- commands.push(
554
- `AddSequenceTrack ${params.name} ${track.actorName} ${track.trackType}`
555
- );
556
-
557
- // Add keyframes
289
+ commands.push(`AddSequenceTrack ${assetName} ${track.actorName} ${track.trackType}`);
558
290
  if (track.keyframes) {
559
291
  for (const keyframe of track.keyframes) {
560
292
  commands.push(
561
- `AddSequenceKey ${params.name} ${track.actorName} ${track.trackType} ${keyframe.time} ${JSON.stringify(keyframe.value)}`
293
+ `AddSequenceKey ${assetName} ${track.actorName} ${track.trackType} ${keyframe.time} ${JSON.stringify(keyframe.value)}`
562
294
  );
563
295
  }
564
296
  }
565
297
  }
566
298
  }
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}`
299
+
300
+ await this.bridge.executeConsoleCommands(commands);
301
+ return {
302
+ success: true,
303
+ message: `Level Sequence ${assetName} created`,
304
+ path: `${assetPath}/${assetName}`
576
305
  };
577
306
  } catch (err) {
578
307
  return { success: false, error: `Failed to create level sequence: ${err}` };
579
308
  }
580
309
  }
581
310
 
582
- /**
583
- * Play Animation on Actor
584
- */
585
311
  async playAnimation(params: {
586
312
  actorName: string;
587
313
  animationType: 'Montage' | 'Sequence' | 'BlendSpace';
@@ -590,124 +316,415 @@ print("DONE")
590
316
  loop?: boolean;
591
317
  blendInTime?: number;
592
318
  blendOutTime?: number;
593
- }) {
319
+ }): Promise<PlayAnimationSuccess | PlayAnimationFailure> {
594
320
  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';
321
+ const script = this.buildPlayAnimationScript({
322
+ actorName: params.actorName,
323
+ animationType: params.animationType,
324
+ animationPath: params.animationPath,
325
+ playRate: params.playRate ?? 1.0,
326
+ loop: params.loop ?? false,
327
+ blendInTime: params.blendInTime ?? 0.25,
328
+ blendOutTime: params.blendOutTime ?? 0.25
329
+ });
598
330
 
599
- const python = `
331
+ const response = await this.bridge.executePython(script);
332
+ const interpreted = interpretStandardResult(response, {
333
+ successMessage: `Animation ${params.animationType} triggered on ${params.actorName}`,
334
+ failureMessage: `Failed to play animation on ${params.actorName}`
335
+ });
336
+
337
+ const payload = interpreted.payload ?? {};
338
+ const warnings = interpreted.warnings ?? coerceStringArray((payload as any).warnings) ?? undefined;
339
+ const details = interpreted.details ?? coerceStringArray((payload as any).details) ?? undefined;
340
+ const availableActors = coerceStringArray((payload as any).availableActors);
341
+ const actorName = coerceString((payload as any).actorName) ?? params.actorName;
342
+ const animationType = coerceString((payload as any).animationType) ?? params.animationType;
343
+ const assetPath = coerceString((payload as any).assetPath) ?? params.animationPath;
344
+ const errorMessage = coerceString((payload as any).error) ?? interpreted.error ?? `Animation playback failed for ${params.actorName}`;
345
+
346
+ if (interpreted.success) {
347
+ const result: PlayAnimationSuccess = {
348
+ success: true,
349
+ message: interpreted.message
350
+ };
351
+
352
+ if (warnings && warnings.length > 0) {
353
+ result.warnings = warnings;
354
+ }
355
+ if (details && details.length > 0) {
356
+ result.details = details;
357
+ }
358
+ if (actorName) {
359
+ result.actorName = actorName;
360
+ }
361
+ if (animationType) {
362
+ result.animationType = animationType;
363
+ }
364
+ if (assetPath) {
365
+ result.assetPath = assetPath;
366
+ }
367
+
368
+ return result;
369
+ }
370
+
371
+ const failure: PlayAnimationFailure = {
372
+ success: false,
373
+ message: `Failed to play animation: ${errorMessage}`,
374
+ error: errorMessage
375
+ };
376
+
377
+ if (warnings && warnings.length > 0) {
378
+ failure.warnings = warnings;
379
+ }
380
+ if (details && details.length > 0) {
381
+ failure.details = details;
382
+ }
383
+ if (availableActors && availableActors.length > 0) {
384
+ failure.availableActors = availableActors;
385
+ }
386
+ if (actorName) {
387
+ failure.actorName = actorName;
388
+ }
389
+ if (animationType) {
390
+ failure.animationType = animationType;
391
+ }
392
+ if (assetPath) {
393
+ failure.assetPath = assetPath;
394
+ }
395
+
396
+ return failure;
397
+ } catch (err) {
398
+ const error = `Failed to play animation: ${err}`;
399
+ return { success: false, message: error, error: String(err) };
400
+ }
401
+ }
402
+
403
+ private buildCreateAnimationBlueprintScript(args: {
404
+ name: string;
405
+ path: string;
406
+ skeletonPath: string;
407
+ }): string {
408
+ const payload = JSON.stringify(args);
409
+ return `
600
410
  import unreal
601
411
  import json
412
+ import traceback
602
413
 
603
- result = {"success": False, "message": ""}
414
+ params = json.loads(${JSON.stringify(payload)})
415
+
416
+ result = {
417
+ "success": False,
418
+ "message": "",
419
+ "error": "",
420
+ "warnings": [],
421
+ "details": [],
422
+ "exists": False,
423
+ "skeleton": params.get("skeletonPath") or ""
424
+ }
425
+
426
+ try:
427
+ asset_path = (params.get("path") or "/Game").rstrip('/')
428
+ asset_name = params.get("name") or ""
429
+ full_path = f"{asset_path}/{asset_name}"
430
+ result["path"] = full_path
431
+
432
+ editor_lib = unreal.EditorAssetLibrary
433
+ asset_subsystem = None
434
+ try:
435
+ asset_subsystem = unreal.get_editor_subsystem(unreal.EditorAssetSubsystem)
436
+ except Exception:
437
+ asset_subsystem = None
438
+
439
+ skeleton_path = params.get("skeletonPath")
440
+ skeleton_asset = None
441
+ if skeleton_path:
442
+ if editor_lib.does_asset_exist(skeleton_path):
443
+ skeleton_asset = editor_lib.load_asset(skeleton_path)
444
+ if skeleton_asset and isinstance(skeleton_asset, unreal.Skeleton):
445
+ result["details"].append(f"Using skeleton: {skeleton_path}")
446
+ result["skeleton"] = skeleton_path
447
+ else:
448
+ result["error"] = f"Skeleton asset invalid at {skeleton_path}"
449
+ result["warnings"].append(result["error"])
450
+ skeleton_asset = None
451
+ else:
452
+ result["error"] = f"Skeleton not found at {skeleton_path}"
453
+ result["warnings"].append(result["error"])
454
+
455
+ if not skeleton_asset:
456
+ raise RuntimeError(result["error"] or f"Skeleton {skeleton_path} unavailable")
457
+
458
+ does_exist = False
459
+ try:
460
+ if asset_subsystem and hasattr(asset_subsystem, 'does_asset_exist'):
461
+ does_exist = asset_subsystem.does_asset_exist(full_path)
462
+ else:
463
+ does_exist = editor_lib.does_asset_exist(full_path)
464
+ except Exception:
465
+ does_exist = editor_lib.does_asset_exist(full_path)
466
+
467
+ if does_exist:
468
+ result["exists"] = True
469
+ loaded = editor_lib.load_asset(full_path)
470
+ if loaded:
471
+ result["success"] = True
472
+ result["message"] = f"Animation Blueprint already exists at {full_path}"
473
+ result["details"].append(result["message"])
474
+ else:
475
+ result["error"] = f"Asset exists but could not be loaded: {full_path}"
476
+ result["warnings"].append(result["error"])
477
+ else:
478
+ factory = unreal.AnimBlueprintFactory()
479
+ if skeleton_asset:
480
+ try:
481
+ factory.target_skeleton = skeleton_asset
482
+ except Exception as assign_error:
483
+ result["warnings"].append(f"Unable to assign skeleton {skeleton_path}: {assign_error}")
484
+
485
+ asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
486
+ created = asset_tools.create_asset(
487
+ asset_name=asset_name,
488
+ package_path=asset_path,
489
+ asset_class=unreal.AnimBlueprint,
490
+ factory=factory
491
+ )
492
+
493
+ if created:
494
+ editor_lib.save_asset(full_path, only_if_is_dirty=False)
495
+ result["success"] = True
496
+ result["message"] = f"Animation Blueprint created at {full_path}"
497
+ result["details"].append(result["message"])
498
+ else:
499
+ result["error"] = f"Failed to create Animation Blueprint {asset_name}"
500
+
501
+ except Exception as exc:
502
+ result["error"] = str(exc)
503
+ result["warnings"].append(result["error"])
504
+ tb = traceback.format_exc()
505
+ if tb:
506
+ result.setdefault("details", []).append(tb)
507
+
508
+ if result["success"] and not result.get("message"):
509
+ result["message"] = f"Animation Blueprint created at {result.get('path')}"
510
+
511
+ if not result["success"] and not result.get("error"):
512
+ result["error"] = "Animation Blueprint creation failed"
513
+
514
+ if not result.get("warnings"):
515
+ result.pop("warnings", None)
516
+ if not result.get("details"):
517
+ result.pop("details", None)
518
+ if not result.get("error"):
519
+ result.pop("error", None)
520
+
521
+ print('RESULT:' + json.dumps(result))
522
+ `.trim();
523
+ }
524
+
525
+ private parseAnimationBlueprintResponse(
526
+ response: unknown,
527
+ assetName: string,
528
+ assetPath: string
529
+ ): CreateAnimationBlueprintSuccess | CreateAnimationBlueprintFailure {
530
+ const interpreted = interpretStandardResult(response, {
531
+ successMessage: `Animation Blueprint ${assetName} created`,
532
+ failureMessage: `Failed to create Animation Blueprint ${assetName}`
533
+ });
534
+
535
+ const payload = interpreted.payload ?? {};
536
+ const path = coerceString((payload as any).path) ?? `${assetPath}/${assetName}`;
537
+ const exists = coerceBoolean((payload as any).exists);
538
+ const skeleton = coerceString((payload as any).skeleton);
539
+ const warnings = interpreted.warnings ?? coerceStringArray((payload as any).warnings) ?? undefined;
540
+ const details = interpreted.details ?? coerceStringArray((payload as any).details) ?? undefined;
541
+
542
+ if (interpreted.success) {
543
+ const result: CreateAnimationBlueprintSuccess = {
544
+ success: true,
545
+ message: interpreted.message,
546
+ path
547
+ };
548
+
549
+ if (typeof exists === 'boolean') {
550
+ result.exists = exists;
551
+ }
552
+ if (skeleton) {
553
+ result.skeleton = skeleton;
554
+ }
555
+ if (warnings && warnings.length > 0) {
556
+ result.warnings = warnings;
557
+ }
558
+ if (details && details.length > 0) {
559
+ result.details = details;
560
+ }
561
+
562
+ return result;
563
+ }
564
+
565
+ const errorMessage = coerceString((payload as any).error) ?? interpreted.error ?? interpreted.message;
566
+
567
+ const failure: CreateAnimationBlueprintFailure = {
568
+ success: false,
569
+ message: `Failed to create Animation Blueprint: ${errorMessage}`,
570
+ error: errorMessage,
571
+ path
572
+ };
573
+
574
+ if (typeof exists === 'boolean') {
575
+ failure.exists = exists;
576
+ }
577
+ if (skeleton) {
578
+ failure.skeleton = skeleton;
579
+ }
580
+ if (warnings && warnings.length > 0) {
581
+ failure.warnings = warnings;
582
+ }
583
+ if (details && details.length > 0) {
584
+ failure.details = details;
585
+ }
586
+
587
+ return failure;
588
+ }
589
+
590
+ private buildPlayAnimationScript(args: {
591
+ actorName: string;
592
+ animationType: string;
593
+ animationPath: string;
594
+ playRate: number;
595
+ loop: boolean;
596
+ blendInTime: number;
597
+ blendOutTime: number;
598
+ }): string {
599
+ const payload = JSON.stringify(args);
600
+ return `
601
+ import unreal
602
+ import json
603
+ import traceback
604
+
605
+ params = json.loads(${JSON.stringify(payload)})
606
+
607
+ result = {
608
+ "success": False,
609
+ "message": "",
610
+ "error": "",
611
+ "warnings": [],
612
+ "details": [],
613
+ "actorName": params.get("actorName"),
614
+ "animationType": params.get("animationType"),
615
+ "assetPath": params.get("animationPath"),
616
+ "availableActors": []
617
+ }
604
618
 
605
619
  try:
606
620
  actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
607
- actors = actor_subsystem.get_all_level_actors()
621
+ actors = actor_subsystem.get_all_level_actors() if actor_subsystem else []
608
622
  target = None
609
- search = "${params.actorName}"
610
- for a in actors:
611
- if not a:
623
+ search = params.get("actorName") or ""
624
+ search_lower = search.lower()
625
+
626
+ for actor in actors:
627
+ if not actor:
612
628
  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
629
+ name = (actor.get_name() or "").lower()
630
+ label = (actor.get_actor_label() or "").lower()
631
+ if search_lower and (search_lower == name or search_lower == label or search_lower in label):
632
+ target = actor
617
633
  break
618
634
 
619
635
  if not target:
620
- result["message"] = f"Actor not found: {search}"
636
+ result["error"] = f"Actor not found: {search}"
637
+ result["warnings"].append("Actor search yielded no results")
638
+ suggestions = []
639
+ for actor in actors[:20]:
640
+ try:
641
+ suggestions.append(actor.get_actor_label())
642
+ except Exception:
643
+ continue
644
+ if suggestions:
645
+ result["availableActors"] = suggestions
621
646
  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)
647
+ try:
648
+ display_name = target.get_actor_label() or target.get_name()
649
+ if display_name:
650
+ result["actorName"] = display_name
651
+ except Exception:
652
+ pass
653
+
654
+ skeletal_component = target.get_component_by_class(unreal.SkeletalMeshComponent)
655
+ if not skeletal_component:
626
656
  try:
627
- sk = target.get_editor_property('mesh')
657
+ skeletal_component = target.get_editor_property('mesh')
628
658
  except Exception:
629
- sk = None
630
- if not sk:
631
- result["message"] = "No SkeletalMeshComponent found on actor"
659
+ skeletal_component = None
660
+
661
+ if not skeletal_component:
662
+ result["error"] = "No SkeletalMeshComponent found on actor"
663
+ result["warnings"].append("Actor lacks SkeletalMeshComponent")
632
664
  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}"
665
+ asset_path = params.get("animationPath")
666
+ if not asset_path or not unreal.EditorAssetLibrary.does_asset_exist(asset_path):
667
+ result["error"] = f"Animation asset not found: {asset_path}"
668
+ result["warnings"].append("Animation asset missing")
637
669
  else:
638
670
  asset = unreal.EditorAssetLibrary.load_asset(asset_path)
671
+ anim_type = params.get("animationType") or ""
639
672
  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:
673
+ anim_instance = skeletal_component.get_anim_instance()
674
+ if anim_instance:
645
675
  try:
646
- # montage_play(montage, play_rate, return_value_type, time_to_start_montage_at, stop_all_montages)
647
- inst.montage_play(asset, ${playRate})
676
+ anim_instance.montage_play(asset, params.get("playRate", 1.0))
648
677
  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}"
678
+ result["message"] = f"Montage playing on {result.get('actorName') or search}"
679
+ result["details"].append(result["message"])
680
+ except Exception as play_error:
681
+ result["error"] = f"Failed to play montage: {play_error}"
682
+ result["warnings"].append(result["error"])
683
+ else:
684
+ result["error"] = "AnimInstance not found on SkeletalMeshComponent"
685
+ result["warnings"].append(result["error"])
652
686
  elif anim_type == 'Sequence':
653
687
  try:
654
- sk.play_animation(asset, ${loopFlag})
655
- # Adjust rate if supported via play rate on AnimInstance
688
+ skeletal_component.play_animation(asset, bool(params.get("loop")))
656
689
  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
690
+ anim_instance = skeletal_component.get_anim_instance()
691
+ if anim_instance:
692
+ anim_instance.set_play_rate(params.get("playRate", 1.0))
661
693
  except Exception:
662
694
  pass
663
695
  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}"
696
+ result["message"] = f"Sequence playing on {result.get('actorName') or search}"
697
+ result["details"].append(result["message"])
698
+ except Exception as play_error:
699
+ result["error"] = f"Failed to play sequence: {play_error}"
700
+ result["warnings"].append(result["error"])
667
701
  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}"
702
+ result["error"] = "BlendSpace playback requires Animation Blueprint support"
703
+ result["warnings"].append("Unsupported animation type for direct play")
671
704
 
672
- print("RESULT:" + json.dumps(result))
673
- `.trim();
705
+ except Exception as exc:
706
+ result["error"] = str(exc)
707
+ result["warnings"].append(result["error"])
708
+ tb = traceback.format_exc()
709
+ if tb:
710
+ result["details"].append(tb)
674
711
 
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
- }
712
+ if result["success"] and not result.get("message"):
713
+ result["message"] = f"Animation {result.get('animationType')} triggered on {result.get('actorName') or params.get('actorName')}"
697
714
 
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
- });
715
+ if not result["success"] and not result.get("error"):
716
+ result["error"] = "Animation playback failed"
717
+
718
+ if not result.get("warnings"):
719
+ result.pop("warnings", None)
720
+ if not result.get("details"):
721
+ result.pop("details", None)
722
+ if not result.get("availableActors"):
723
+ result.pop("availableActors", None)
724
+ if not result.get("error"):
725
+ result.pop("error", None)
726
+
727
+ print('RESULT:' + json.dumps(result))
728
+ `.trim();
712
729
  }
713
730
  }