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,5 +1,6 @@
1
1
  import { UnrealBridge } from '../unreal-bridge.js';
2
2
  import { validateAssetParams, resolveSkeletalMeshPath, concurrencyDelay } from '../utils/validation.js';
3
+ import { bestEffortInterpretedText, coerceString, coerceStringArray, interpretStandardResult } from '../utils/result-helpers.js';
3
4
 
4
5
  export class PhysicsTools {
5
6
  constructor(private bridge: UnrealBridge) {}
@@ -10,62 +11,80 @@ export class PhysicsTools {
10
11
  private async findValidSkeletalMesh(): Promise<string | null> {
11
12
  const pythonScript = `
12
13
  import unreal
14
+ import json
15
+
16
+ result = {
17
+ 'success': False,
18
+ 'meshPath': None,
19
+ 'source': None
20
+ }
13
21
 
14
- # Common skeletal mesh paths to check
15
22
  common_paths = [
16
- '/Game/Mannequin/Character/Mesh/SK_Mannequin',
17
- '/Game/Characters/Mannequin/Meshes/SK_Mannequin',
18
- '/Game/AnimStarterPack/UE4_Mannequin/Mesh/SK_Mannequin',
19
- '/Game/ThirdPerson/Meshes/SK_Mannequin',
20
- '/Game/ThirdPersonBP/Meshes/SK_Mannequin',
21
- '/Engine/EngineMeshes/SkeletalCube', # Fallback engine mesh
23
+ '/Game/Characters/Mannequins/Meshes/SKM_Manny',
24
+ '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple',
25
+ '/Game/Characters/Mannequins/Meshes/SKM_Manny_Complex',
26
+ '/Game/Characters/Mannequins/Meshes/SKM_Quinn',
27
+ '/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple',
28
+ '/Game/Characters/Mannequins/Meshes/SKM_Quinn_Complex'
22
29
  ]
23
30
 
24
- # Try to find any skeletal mesh
25
- for path in common_paths:
26
- if unreal.EditorAssetLibrary.does_asset_exist(path):
27
- asset = unreal.EditorAssetLibrary.load_asset(path)
28
- if asset and isinstance(asset, unreal.SkeletalMesh):
29
- print(f"FOUND_MESH:{path}")
31
+ for candidate in common_paths:
32
+ if unreal.EditorAssetLibrary.does_asset_exist(candidate):
33
+ mesh = unreal.EditorAssetLibrary.load_asset(candidate)
34
+ if mesh and isinstance(mesh, unreal.SkeletalMesh):
35
+ result['success'] = True
36
+ result['meshPath'] = candidate
37
+ result['source'] = 'common'
30
38
  break
31
- else:
32
- # Search for any skeletal mesh in the project
39
+
40
+ if not result['success']:
33
41
  asset_registry = unreal.AssetRegistryHelpers.get_asset_registry()
34
42
  assets = asset_registry.get_assets_by_class('SkeletalMesh', search_sub_classes=False)
35
43
  if assets:
36
- # Use the first available skeletal mesh
37
44
  first_mesh = assets[0]
38
- obj_path = first_mesh.get_editor_property('object_path')
45
+ obj_path = first_mesh.get_editor_property('object_path') if hasattr(first_mesh, 'get_editor_property') else None
46
+ if not obj_path and hasattr(first_mesh, 'object_path'):
47
+ obj_path = first_mesh.object_path
39
48
  if obj_path:
40
- print(f"FOUND_MESH:{str(obj_path).split('.')[0]}")
41
- else:
42
- print("NO_MESH_FOUND")
43
- else:
44
- print("NO_MESH_FOUND")
49
+ result['success'] = True
50
+ result['meshPath'] = str(obj_path).split('.')[0]
51
+ result['source'] = 'registry'
52
+ if hasattr(first_mesh, 'asset_name'):
53
+ result['assetName'] = str(first_mesh.asset_name)
54
+
55
+ if not result['success']:
56
+ result['fallback'] = '/Engine/EngineMeshes/SkeletalCube'
57
+
58
+ print('RESULT:' + json.dumps(result))
45
59
  `;
46
-
60
+
47
61
  try {
48
62
  const response = await this.bridge.executePython(pythonScript);
49
- let outputStr = '';
50
- if (response?.LogOutput && Array.isArray((response as any).LogOutput)) {
51
- outputStr = (response as any).LogOutput.map((l: any) => l.Output || '').join('');
52
- } else if (typeof response === 'string') {
53
- outputStr = response;
54
- } else {
55
- // Fallback: stringify and still try to parse, but restrict to line content only
56
- outputStr = JSON.stringify(response);
63
+ const interpreted = interpretStandardResult(response, {
64
+ successMessage: 'Skeletal mesh discovery complete',
65
+ failureMessage: 'Failed to discover skeletal mesh'
66
+ });
67
+
68
+ if (interpreted.success) {
69
+ const meshPath = coerceString(interpreted.payload.meshPath);
70
+ if (meshPath) {
71
+ return meshPath;
72
+ }
57
73
  }
58
-
59
- // Capture only until end-of-line to avoid trailing JSON serialization
60
- const match = outputStr.match(/FOUND_MESH:([^\r\n]+)/);
61
- if (match) {
62
- return match[1].trim();
74
+
75
+ const fallback = coerceString(interpreted.payload.fallback);
76
+ if (fallback) {
77
+ return fallback;
78
+ }
79
+
80
+ const detail = bestEffortInterpretedText(interpreted);
81
+ if (detail) {
82
+ console.error('Failed to parse skeletal mesh discovery:', detail);
63
83
  }
64
84
  } catch (error) {
65
85
  console.error('Failed to find skeletal mesh:', error);
66
86
  }
67
87
 
68
- // Return engine fallback if nothing found
69
88
  return '/Engine/EngineMeshes/SkeletalCube';
70
89
  }
71
90
 
@@ -170,7 +189,9 @@ else:
170
189
  const skeletonToMeshMap: { [key: string]: string } = {
171
190
  '/Game/Mannequin/Character/Mesh/UE4_Mannequin_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple',
172
191
  '/Game/Characters/Mannequins/Meshes/SK_Mannequin': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple',
173
- '/Game/Mannequin/Character/Mesh/SK_Mannequin': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple'
192
+ '/Game/Mannequin/Character/Mesh/SK_Mannequin': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple',
193
+ '/Game/Characters/Mannequins/Skeletons/UE5_Mannequin_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple',
194
+ '/Game/Characters/Mannequins/Skeletons/UE5_Female_Mannequin_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple'
174
195
  };
175
196
 
176
197
  // Auto-fix common incorrect paths
@@ -186,240 +207,343 @@ else:
186
207
  }
187
208
 
188
209
  // Build Python script with resolved mesh path
189
- const pythonScript = `
210
+ const pythonScript = `
190
211
  import unreal
191
212
  import time
213
+ import json
214
+
215
+ result = {
216
+ "success": False,
217
+ "path": None,
218
+ "message": "",
219
+ "error": None,
220
+ "warnings": [],
221
+ "details": [],
222
+ "existingAsset": False,
223
+ "meshPath": "${meshPath}"
224
+ }
225
+
226
+ def record_detail(message):
227
+ result["details"].append(message)
228
+
229
+ def record_warning(message):
230
+ result["warnings"].append(message)
231
+
232
+ def record_error(message):
233
+ result["error"] = message
192
234
 
193
235
  # Helper function to ensure asset persistence
194
236
  def ensure_asset_persistence(asset_path):
195
- try:
196
- asset = unreal.EditorAssetLibrary.load_asset(asset_path)
197
- if not asset:
198
- return False
237
+ try:
238
+ asset = unreal.EditorAssetLibrary.load_asset(asset_path)
239
+ if not asset:
240
+ record_warning(f"Asset persistence check failed: {asset_path} not loaded")
241
+ return False
199
242
 
200
- # Save the asset
201
- saved = unreal.EditorAssetLibrary.save_asset(asset_path, only_if_is_dirty=False)
202
- if saved:
203
- print(f"Asset saved: {asset_path}")
243
+ # Save the asset
244
+ saved = unreal.EditorAssetLibrary.save_asset(asset_path, only_if_is_dirty=False)
245
+ if saved:
246
+ print(f"Asset saved: {asset_path}")
247
+ record_detail(f"Asset saved: {asset_path}")
204
248
 
205
- # Refresh the asset registry minimally for the asset's directory
206
- try:
207
- asset_dir = asset_path.rsplit('/', 1)[0]
208
- unreal.AssetRegistryHelpers.get_asset_registry().scan_paths_synchronous([asset_dir], True)
209
- except Exception as _reg_e:
210
- pass
249
+ # Refresh the asset registry minimally for the asset's directory
250
+ try:
251
+ asset_dir = asset_path.rsplit('/', 1)[0]
252
+ unreal.AssetRegistryHelpers.get_asset_registry().scan_paths_synchronous([asset_dir], True)
253
+ except Exception as _reg_e:
254
+ record_warning(f"Asset registry refresh warning: {_reg_e}")
211
255
 
212
- # Small delay to ensure filesystem sync
213
- time.sleep(0.1)
256
+ # Small delay to ensure filesystem sync
257
+ time.sleep(0.1)
214
258
 
215
- return saved
216
- except Exception as e:
217
- print(f"Error ensuring persistence: {e}")
218
- return False
259
+ return saved
260
+ except Exception as e:
261
+ print(f"Error ensuring persistence: {e}")
262
+ record_error(f"Error ensuring persistence: {e}")
263
+ return False
219
264
 
220
- # Stop PIE if it's running
265
+ # Stop PIE if running using modern subsystems
221
266
  try:
222
- if unreal.EditorLevelLibrary.is_playing_editor():
223
- print("Stopping Play In Editor mode...")
224
- unreal.EditorLevelLibrary.editor_end_play()
225
- time.sleep(0.5)
226
- except:
227
- pass
267
+ level_subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
268
+ play_subsystem = None
269
+ try:
270
+ play_subsystem = unreal.get_editor_subsystem(unreal.EditorPlayWorldSubsystem)
271
+ except Exception:
272
+ play_subsystem = None
273
+
274
+ is_playing = False
275
+ if level_subsystem and hasattr(level_subsystem, 'is_in_play_in_editor'):
276
+ is_playing = level_subsystem.is_in_play_in_editor()
277
+ elif play_subsystem and hasattr(play_subsystem, 'is_playing_in_editor'): # type: ignore[attr-defined]
278
+ is_playing = play_subsystem.is_playing_in_editor() # type: ignore[attr-defined]
279
+
280
+ if is_playing:
281
+ print("Stopping Play In Editor mode...")
282
+ record_detail("Stopping Play In Editor mode")
283
+ if level_subsystem and hasattr(level_subsystem, 'editor_request_end_play'):
284
+ level_subsystem.editor_request_end_play()
285
+ elif play_subsystem and hasattr(play_subsystem, 'stop_playing_session'): # type: ignore[attr-defined]
286
+ play_subsystem.stop_playing_session() # type: ignore[attr-defined]
287
+ elif play_subsystem and hasattr(play_subsystem, 'end_play'): # type: ignore[attr-defined]
288
+ play_subsystem.end_play() # type: ignore[attr-defined]
289
+ else:
290
+ record_warning('Unable to stop Play In Editor via modern subsystems; please stop PIE manually.')
291
+ time.sleep(0.5)
292
+ except Exception as pie_error:
293
+ record_warning(f"PIE stop check failed: {pie_error}")
228
294
 
229
295
  # Main execution
230
296
  success = False
231
297
  error_msg = ""
298
+ new_asset = None
232
299
 
233
300
  # Log the attempt
234
301
  print("Setting up ragdoll for ${meshPath}")
302
+ record_detail("Setting up ragdoll for ${meshPath}")
235
303
 
236
304
  asset_path = "${path}"
237
305
  asset_name = "${sanitizedParams.name}"
238
306
  full_path = f"{asset_path}/{asset_name}"
239
307
 
240
308
  try:
241
- # Check if already exists
242
- if unreal.EditorAssetLibrary.does_asset_exist(full_path):
243
- print(f"Physics asset already exists at {full_path}")
244
- existing = unreal.EditorAssetLibrary.load_asset(full_path)
245
- if existing:
246
- print(f"Loaded existing PhysicsAsset: {full_path}")
247
- else:
248
- # Try to load skeletal mesh first - it's required
249
- skeletal_mesh_path = "${meshPath}"
250
- skeletal_mesh = None
251
-
252
- if skeletal_mesh_path and skeletal_mesh_path != "None":
253
- if unreal.EditorAssetLibrary.does_asset_exist(skeletal_mesh_path):
254
- asset = unreal.EditorAssetLibrary.load_asset(skeletal_mesh_path)
255
- if asset:
256
- if isinstance(asset, unreal.SkeletalMesh):
257
- skeletal_mesh = asset
258
- print(f"Loaded skeletal mesh: {skeletal_mesh_path}")
259
- elif isinstance(asset, unreal.Skeleton):
260
- error_msg = f"Provided path is a skeleton, not a skeletal mesh: {skeletal_mesh_path}"
261
- print(f"Error: {error_msg}")
262
- print(f"Error: Physics assets require a skeletal mesh, not just a skeleton")
263
- else:
264
- error_msg = f"Asset is not a skeletal mesh: {skeletal_mesh_path}"
265
- print(f"Warning: {error_msg}")
266
- else:
267
- error_msg = f"Skeletal mesh not found at {skeletal_mesh_path}"
268
- print(f"Error: {error_msg}")
309
+ # Check if already exists
310
+ if unreal.EditorAssetLibrary.does_asset_exist(full_path):
311
+ print(f"Physics asset already exists at {full_path}")
312
+ record_detail(f"Physics asset already exists at {full_path}")
313
+ existing = unreal.EditorAssetLibrary.load_asset(full_path)
314
+ if existing:
315
+ print(f"Loaded existing PhysicsAsset: {full_path}")
316
+ record_detail(f"Loaded existing PhysicsAsset: {full_path}")
317
+ success = True
318
+ result["existingAsset"] = True
319
+ result["message"] = f"Physics asset already exists at {full_path}"
320
+ else:
321
+ # Try to load skeletal mesh first - it's required
322
+ skeletal_mesh_path = "${meshPath}"
323
+ skeletal_mesh = None
269
324
 
270
- if not skeletal_mesh:
271
- if not error_msg:
272
- error_msg = "Cannot create physics asset without a valid skeletal mesh"
325
+ if skeletal_mesh_path and skeletal_mesh_path != "None":
326
+ if unreal.EditorAssetLibrary.does_asset_exist(skeletal_mesh_path):
327
+ asset = unreal.EditorAssetLibrary.load_asset(skeletal_mesh_path)
328
+ if asset:
329
+ if isinstance(asset, unreal.SkeletalMesh):
330
+ skeletal_mesh = asset
331
+ print(f"Loaded skeletal mesh: {skeletal_mesh_path}")
332
+ record_detail(f"Loaded skeletal mesh: {skeletal_mesh_path}")
333
+ elif isinstance(asset, unreal.Skeleton):
334
+ error_msg = f"Provided path is a skeleton, not a skeletal mesh: {skeletal_mesh_path}"
273
335
  print(f"Error: {error_msg}")
274
- else:
275
- # Create physics asset using a different approach
276
- # Method 1: Direct creation with initialized factory
277
- try:
278
- factory = unreal.PhysicsAssetFactory()
336
+ record_error(error_msg)
337
+ result["message"] = error_msg
338
+ print("Error: Physics assets require a skeletal mesh, not just a skeleton")
339
+ record_warning("Physics assets require a skeletal mesh, not just a skeleton")
340
+ else:
341
+ error_msg = f"Asset is not a skeletal mesh: {skeletal_mesh_path}"
342
+ print(f"Warning: {error_msg}")
343
+ record_warning(error_msg)
344
+ else:
345
+ error_msg = f"Skeletal mesh not found at {skeletal_mesh_path}"
346
+ print(f"Error: {error_msg}")
347
+ record_error(error_msg)
348
+ result["message"] = error_msg
349
+
350
+ if not skeletal_mesh:
351
+ if not error_msg:
352
+ error_msg = "Cannot create physics asset without a valid skeletal mesh"
353
+ print(f"Error: {error_msg}")
354
+ record_error(error_msg)
355
+ if not result["message"]:
356
+ result["message"] = error_msg
357
+ else:
358
+ # Create physics asset using a different approach
359
+ # Method 1: Direct creation with initialized factory
360
+ try:
361
+ factory = unreal.PhysicsAssetFactory()
279
362
 
280
- # Create a transient package for the physics asset
281
- # Ensure the directory exists
282
- if not unreal.EditorAssetLibrary.does_directory_exist(asset_path):
283
- unreal.EditorAssetLibrary.make_directory(asset_path)
363
+ # Ensure the directory exists
364
+ if not unreal.EditorAssetLibrary.does_directory_exist(asset_path):
365
+ unreal.EditorAssetLibrary.make_directory(asset_path)
284
366
 
285
- # Alternative approach: Create physics asset from skeletal mesh
286
- # This is the proper way in UE5
367
+ # Alternative approach: Create physics asset from skeletal mesh
368
+ # This is the proper way in UE5
369
+ try:
370
+ # Try modern physics asset creation methods first
371
+ try:
372
+ # Method 1: Try using SkeletalMesh editor utilities if available
373
+ if hasattr(unreal, 'SkeletalMeshEditorSubsystem'):
374
+ skel_subsystem = unreal.get_editor_subsystem(unreal.SkeletalMeshEditorSubsystem)
375
+ if hasattr(skel_subsystem, 'create_physics_asset'):
376
+ physics_asset = skel_subsystem.create_physics_asset(skeletal_mesh)
377
+ else:
378
+ # Fallback to deprecated EditorSkeletalMeshLibrary
287
379
  physics_asset = unreal.EditorSkeletalMeshLibrary.create_physics_asset(skeletal_mesh)
380
+ else:
381
+ physics_asset = unreal.EditorSkeletalMeshLibrary.create_physics_asset(skeletal_mesh)
382
+ except Exception as method1_modern_error:
383
+ record_warning(f"Modern creation path fallback: {method1_modern_error}")
384
+ # Final fallback to deprecated API
385
+ physics_asset = unreal.EditorSkeletalMeshLibrary.create_physics_asset(skeletal_mesh)
386
+ except Exception as e:
387
+ print(f"Physics asset creation failed: {str(e)}")
388
+ record_error(f"Physics asset creation failed: {str(e)}")
389
+ physics_asset = None
288
390
 
289
- if physics_asset:
290
- # Move/rename the physics asset to desired location
291
- source_path = physics_asset.get_path_name()
292
- if unreal.EditorAssetLibrary.rename_asset(source_path, full_path):
293
- print(f"Successfully created and moved PhysicsAsset to {full_path}")
294
- new_asset = physics_asset
391
+ if physics_asset:
392
+ # Move/rename the physics asset to desired location
393
+ source_path = physics_asset.get_path_name()
394
+ if unreal.EditorAssetLibrary.rename_asset(source_path, full_path):
395
+ print(f"Successfully created and moved PhysicsAsset to {full_path}")
396
+ record_detail(f"Successfully created and moved PhysicsAsset to {full_path}")
397
+ new_asset = physics_asset
295
398
 
296
- # Ensure persistence
297
- if ensure_asset_persistence(full_path):
298
- # Verify it was saved
299
- if unreal.EditorAssetLibrary.does_asset_exist(full_path):
300
- print(f"Verified PhysicsAsset exists after save: {full_path}")
301
- success = True
302
- else:
303
- error_msg = f"PhysicsAsset not found after save: {full_path}"
304
- print(f"Warning: {error_msg}")
305
- else:
306
- error_msg = "Failed to persist physics asset"
307
- print(f"Warning: {error_msg}")
308
- else:
309
- print(f"Created PhysicsAsset but couldn't move to {full_path}")
310
- # Still consider it a success if we created it
311
- new_asset = physics_asset
312
- success = True
313
- else:
314
- error_msg = "Failed to create PhysicsAsset from skeletal mesh"
315
- print(f"{error_msg}")
316
- new_asset = None
399
+ # Ensure persistence
400
+ if ensure_asset_persistence(full_path):
401
+ # Verify it was saved
402
+ if unreal.EditorAssetLibrary.does_asset_exist(full_path):
403
+ print(f"Verified PhysicsAsset exists after save: {full_path}")
404
+ record_detail(f"Verified PhysicsAsset exists after save: {full_path}")
405
+ success = True
406
+ result["message"] = f"Ragdoll physics setup completed for {asset_name}"
407
+ else:
408
+ error_msg = f"PhysicsAsset not found after save: {full_path}"
409
+ print(f"Warning: {error_msg}")
410
+ record_warning(error_msg)
411
+ else:
412
+ error_msg = "Failed to persist physics asset"
413
+ print(f"Warning: {error_msg}")
414
+ record_warning(error_msg)
415
+ else:
416
+ print(f"Created PhysicsAsset but couldn't move to {full_path}")
417
+ record_warning(f"Created PhysicsAsset but couldn't move to {full_path}")
418
+ # Still consider it a success if we created it
419
+ new_asset = physics_asset
420
+ success = True
421
+ result["message"] = f"Physics asset created but not moved to {full_path}"
422
+ else:
423
+ error_msg = "Failed to create PhysicsAsset from skeletal mesh"
424
+ print(error_msg)
425
+ record_error(error_msg)
426
+ new_asset = None
317
427
 
318
- except Exception as e:
319
- print(f"Method 1 failed: {str(e)}")
428
+ successMessage: \`Skeletal mesh discovery complete\`,
429
+ failureMessage: \`Failed to discover skeletal mesh\`
430
+ record_warning(f"Method 1 failed: {str(e)}")
320
431
 
321
- # Method 2: Try older approach
322
- try:
323
- asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
324
- factory = unreal.PhysicsAssetFactory()
432
+ # Method 2: Try older approach
433
+ try:
434
+ asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
435
+ factory = unreal.PhysicsAssetFactory()
325
436
 
326
- # Try to initialize factory with the skeletal mesh
327
- factory.create_physics_asset_from_skeletal_mesh = skeletal_mesh
437
+ # Try to initialize factory with the skeletal mesh
438
+ factory.create_physics_asset_from_skeletal_mesh = skeletal_mesh
328
439
 
329
- new_asset = asset_tools.create_asset(
330
- asset_name=asset_name,
331
- package_path=asset_path,
332
- asset_class=unreal.PhysicsAsset,
333
- factory=factory
334
- )
440
+ new_asset = asset_tools.create_asset(
441
+ asset_name=asset_name,
442
+ package_path=asset_path,
443
+ asset_class=unreal.PhysicsAsset,
444
+ factory=factory
445
+ )
335
446
 
336
- if new_asset:
337
- print(f"Successfully created PhysicsAsset at {full_path} (Method 2)")
338
- # Ensure persistence
339
- if ensure_asset_persistence(full_path):
340
- success = True
341
- except Exception as e2:
342
- error_msg = f"Method 2 also failed: {str(e2)}"
343
- print(error_msg)
344
- new_asset = None
345
-
346
- # Final check
347
- if new_asset and not success:
348
- # Try one more save
349
- if ensure_asset_persistence(full_path):
350
- if unreal.EditorAssetLibrary.does_asset_exist(full_path):
351
- success = True
447
+ if new_asset:
448
+ print(f"Successfully created PhysicsAsset at {full_path} (Method 2)")
449
+ record_detail(f"Successfully created PhysicsAsset at {full_path} (Method 2)")
450
+ # Ensure persistence
451
+ if ensure_asset_persistence(full_path):
452
+ success = True
453
+ result["message"] = f"Ragdoll physics setup completed for {asset_name}"
454
+ else:
455
+ record_warning("Persistence check failed after Method 2 creation")
456
+ except Exception as e2:
457
+ error_msg = f"Method 2 also failed: {str(e2)}"
458
+ print(error_msg)
459
+ record_error(error_msg)
460
+ new_asset = None
352
461
 
462
+ # Final check
463
+ if new_asset and not success:
464
+ # Try one more save
465
+ if ensure_asset_persistence(full_path):
466
+ if unreal.EditorAssetLibrary.does_asset_exist(full_path):
467
+ success = True
468
+ result["message"] = f"Ragdoll physics setup completed for {asset_name}"
469
+ else:
470
+ record_warning(f"Final existence check failed for {full_path}")
471
+
353
472
  except Exception as e:
354
- error_msg = str(e)
355
- print(f"Error: {error_msg}")
356
- import traceback
357
- traceback.print_exc()
358
-
359
- # Output result markers for parsing
360
- if success:
361
- print("SUCCESS")
362
- else:
363
- print(f"FAILED: {error_msg}")
364
-
365
- print("DONE")
473
+ error_msg = str(e)
474
+ print(f"Error: {error_msg}")
475
+ record_error(error_msg)
476
+ import traceback
477
+ traceback.print_exc()
478
+
479
+ # Finalize result
480
+ result["success"] = bool(success)
481
+ result["path"] = full_path if success else None
482
+
483
+ if not result["message"]:
484
+ if success:
485
+ result["message"] = f"Ragdoll physics setup completed for {asset_name}"
486
+ elif error_msg:
487
+ result["message"] = error_msg
488
+ else:
489
+ result["message"] = "Failed to setup ragdoll"
490
+
491
+ if not success:
492
+ if not result["error"]:
493
+ result["error"] = error_msg or "Unknown error"
494
+
495
+ print('RESULT:' + json.dumps(result))
366
496
  `;
367
497
 
368
498
 
369
- // Execute Python and parse response
499
+ // Execute Python and interpret response
370
500
  try {
371
501
  const response = await this.bridge.executePython(pythonScript);
372
-
373
- // Parse the response to detect actual success or failure
374
- const responseStr = typeof response === 'string' ? response : JSON.stringify(response);
375
-
376
- // Check for explicit success/failure markers
377
- if (responseStr.includes('SUCCESS')) {
378
- return {
379
- success: true,
380
- message: `Ragdoll physics setup completed for ${sanitizedParams.name}`,
381
- path: `${path}/${sanitizedParams.name}`
382
- };
383
- } else if (responseStr.includes('FAILED:')) {
384
- // Extract error message after FAILED:
385
- const failMatch = responseStr.match(/FAILED:\s*(.+)/);
386
- const errorMsg = failMatch ? failMatch[1] : 'Unknown error';
387
- return {
388
- success: false,
389
- message: `Failed to setup ragdoll: ${errorMsg}`,
390
- error: errorMsg
502
+ const interpreted = interpretStandardResult(response, {
503
+ successMessage: `Ragdoll physics setup completed for ${sanitizedParams.name}`,
504
+ failureMessage: `Failed to setup ragdoll for ${sanitizedParams.name}`
505
+ });
506
+
507
+ const warnings = interpreted.warnings ?? [];
508
+ const details = interpreted.details ?? [];
509
+
510
+ if (interpreted.success) {
511
+ const successPayload: {
512
+ success: true;
513
+ message: string;
514
+ path: string;
515
+ existingAsset?: boolean;
516
+ warnings?: string[];
517
+ details?: string[];
518
+ } = {
519
+ success: true,
520
+ message: interpreted.message,
521
+ path: coerceString(interpreted.payload.path) ?? `${path}/${sanitizedParams.name}`
391
522
  };
392
- } else {
393
- // Check legacy error detection for backwards compatibility
394
- const logOutput = response?.LogOutput || [];
395
- const hasSkeletonError = logOutput.some((log: any) =>
396
- log.Output && (log.Output.includes('skeleton, not a skeletal mesh') ||
397
- log.Output.includes('Must specify a valid skeletal mesh')));
398
-
399
- if (hasSkeletonError) {
400
- return {
401
- success: false,
402
- message: 'Failed: Must specify a valid skeletal mesh',
403
- error: 'The path points to a skeleton, not a skeletal mesh. Physics assets require a skeletal mesh.'
404
- };
523
+
524
+ if (interpreted.payload.existingAsset === true) {
525
+ successPayload.existingAsset = true;
405
526
  }
406
-
407
- // Check for other error indicators
408
- if (responseStr.includes('Error:') || responseStr.includes('error')) {
409
- return {
410
- success: false,
411
- message: 'Failed to setup ragdoll physics',
412
- error: responseStr
413
- };
527
+
528
+ if (warnings.length > 0) {
529
+ successPayload.warnings = warnings;
414
530
  }
415
-
416
- // Default to success if no errors detected
417
- return {
418
- success: true,
419
- message: `Ragdoll physics processed for ${sanitizedParams.name}`,
420
- path: `${path}/${sanitizedParams.name}`
421
- };
531
+ if (details.length > 0) {
532
+ successPayload.details = details;
533
+ }
534
+
535
+ return successPayload;
422
536
  }
537
+
538
+ const errorMessage = interpreted.error ?? `Failed to setup ragdoll for ${sanitizedParams.name}`;
539
+
540
+ return {
541
+ success: false as const,
542
+ message: errorMessage,
543
+ error: errorMessage,
544
+ warnings: warnings.length > 0 ? warnings : undefined,
545
+ details: details.length > 0 ? details : undefined
546
+ };
423
547
  } catch (error) {
424
548
  return {
425
549
  success: false,
@@ -480,9 +604,7 @@ print("DONE")
480
604
  }
481
605
  }
482
606
 
483
- for (const cmd of commands) {
484
- await this.bridge.executeConsoleCommand(cmd);
485
- }
607
+ await this.bridge.executeConsoleCommands(commands);
486
608
 
487
609
  return {
488
610
  success: true,
@@ -533,9 +655,7 @@ print("DONE")
533
655
  commands.push(`SetDebrisLifetime ${params.destructionName} ${params.debrisLifetime}`);
534
656
  }
535
657
 
536
- for (const cmd of commands) {
537
- await this.bridge.executeConsoleCommand(cmd);
538
- }
658
+ await this.bridge.executeConsoleCommands(commands);
539
659
 
540
660
  return {
541
661
  success: true,
@@ -612,9 +732,7 @@ print("DONE")
612
732
  );
613
733
  }
614
734
 
615
- for (const cmd of commands) {
616
- await this.bridge.executeConsoleCommand(cmd);
617
- }
735
+ await this.bridge.executeConsoleCommands(commands);
618
736
 
619
737
  return {
620
738
  success: true,
@@ -737,52 +855,47 @@ print(f"RESULT:{json.dumps(result)}")
737
855
  `.trim();
738
856
 
739
857
  const response = await this.bridge.executePython(pythonCode);
740
-
741
- // Extract output from Python response
742
- let outputStr = '';
743
- if (typeof response === 'object' && response !== null) {
744
- // Check if it has LogOutput (standard Python execution response)
745
- if (response.LogOutput && Array.isArray(response.LogOutput)) {
746
- // Concatenate all log outputs
747
- outputStr = response.LogOutput
748
- .map((log: any) => log.Output || '')
749
- .join('');
750
- } else if ('result' in response) {
751
- outputStr = String(response.result);
752
- } else {
753
- outputStr = JSON.stringify(response);
754
- }
755
- } else {
756
- outputStr = String(response || '');
858
+ const interpreted = interpretStandardResult(response, {
859
+ successMessage: `Applied ${params.forceType} to ${params.actorName}`,
860
+ failureMessage: 'Force application failed'
861
+ });
862
+
863
+ const availableActors = coerceStringArray(interpreted.payload.available_actors);
864
+
865
+ if (interpreted.success) {
866
+ return {
867
+ success: true,
868
+ message: interpreted.message,
869
+ availableActors,
870
+ details: interpreted.details
871
+ };
757
872
  }
758
-
759
- // Parse the result
760
- const resultMatch = outputStr.match(/RESULT:(\{.*\})/);
761
- if (resultMatch) {
762
- try {
763
- const forceResult = JSON.parse(resultMatch[1]);
764
- if (!forceResult.success) {
765
- return { success: false, error: forceResult.message };
766
- }
767
- return forceResult;
768
- } catch {
769
- // Fallback
770
- if (outputStr.includes('Applied')) {
771
- return { success: true, message: outputStr };
772
- }
773
- return { success: false, error: outputStr || 'Force application failed' };
774
- }
775
- } else {
776
- // Check for error patterns
777
- if (outputStr.includes('not found') || outputStr.includes('Error')) {
778
- return { success: false, error: outputStr || 'Force application failed' };
779
- }
780
- // Only return success if we have clear indication of success
781
- if (outputStr.includes('Applied')) {
782
- return { success: true, message: `Applied ${params.forceType} to ${params.actorName}` };
783
- }
784
- return { success: false, error: 'No valid result from Python' };
873
+
874
+ const fallbackText = bestEffortInterpretedText(interpreted) ?? '';
875
+ if (/Applied/i.test(fallbackText)) {
876
+ return {
877
+ success: true,
878
+ message: fallbackText || interpreted.message,
879
+ availableActors,
880
+ details: interpreted.details
881
+ };
785
882
  }
883
+
884
+ if (/not found/i.test(fallbackText) || /error/i.test(fallbackText)) {
885
+ return {
886
+ success: false,
887
+ error: interpreted.error ?? (fallbackText || 'Force application failed'),
888
+ availableActors,
889
+ details: interpreted.details ?? (fallbackText ? [fallbackText] : undefined)
890
+ };
891
+ }
892
+
893
+ return {
894
+ success: false,
895
+ error: interpreted.error ?? 'No valid result from Python',
896
+ availableActors,
897
+ details: interpreted.details ?? (fallbackText ? [fallbackText] : undefined)
898
+ };
786
899
  } catch (err) {
787
900
  return { success: false, error: `Failed to apply force: ${err}` };
788
901
  }
@@ -833,9 +946,7 @@ print(f"RESULT:{json.dumps(result)}")
833
946
  }
834
947
  }
835
948
 
836
- for (const cmd of commands) {
837
- await this.bridge.executeConsoleCommand(cmd);
838
- }
949
+ await this.bridge.executeConsoleCommands(commands);
839
950
 
840
951
  return {
841
952
  success: true,
@@ -893,9 +1004,7 @@ print(f"RESULT:{json.dumps(result)}")
893
1004
  }
894
1005
  }
895
1006
 
896
- for (const cmd of commands) {
897
- await this.bridge.executeConsoleCommand(cmd);
898
- }
1007
+ await this.bridge.executeConsoleCommands(commands);
899
1008
 
900
1009
  return {
901
1010
  success: true,
@@ -906,19 +1015,4 @@ print(f"RESULT:{json.dumps(result)}")
906
1015
  }
907
1016
  }
908
1017
 
909
- /**
910
- * Helper function to execute console commands
911
- */
912
- private async _executeCommand(command: string) {
913
- return this.bridge.httpCall('/remote/object/call', 'PUT', {
914
- objectPath: '/Script/Engine.Default__KismetSystemLibrary',
915
- functionName: 'ExecuteConsoleCommand',
916
- parameters: {
917
- WorldContextObject: null,
918
- Command: command,
919
- SpecificPlayer: null
920
- },
921
- generateTransaction: false
922
- });
923
- }
924
1018
  }