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