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,4 +1,5 @@
1
1
  import { validateAssetParams, resolveSkeletalMeshPath, concurrencyDelay } from '../utils/validation.js';
2
+ import { bestEffortInterpretedText, coerceString, coerceStringArray, interpretStandardResult } from '../utils/result-helpers.js';
2
3
  export class PhysicsTools {
3
4
  bridge;
4
5
  constructor(bridge) {
@@ -10,62 +11,76 @@ export class PhysicsTools {
10
11
  async findValidSkeletalMesh() {
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
  try {
47
61
  const response = await this.bridge.executePython(pythonScript);
48
- let outputStr = '';
49
- if (response?.LogOutput && Array.isArray(response.LogOutput)) {
50
- outputStr = response.LogOutput.map((l) => l.Output || '').join('');
51
- }
52
- else if (typeof response === 'string') {
53
- outputStr = response;
62
+ const interpreted = interpretStandardResult(response, {
63
+ successMessage: 'Skeletal mesh discovery complete',
64
+ failureMessage: 'Failed to discover skeletal mesh'
65
+ });
66
+ if (interpreted.success) {
67
+ const meshPath = coerceString(interpreted.payload.meshPath);
68
+ if (meshPath) {
69
+ return meshPath;
70
+ }
54
71
  }
55
- else {
56
- // Fallback: stringify and still try to parse, but restrict to line content only
57
- outputStr = JSON.stringify(response);
72
+ const fallback = coerceString(interpreted.payload.fallback);
73
+ if (fallback) {
74
+ return fallback;
58
75
  }
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();
76
+ const detail = bestEffortInterpretedText(interpreted);
77
+ if (detail) {
78
+ console.error('Failed to parse skeletal mesh discovery:', detail);
63
79
  }
64
80
  }
65
81
  catch (error) {
66
82
  console.error('Failed to find skeletal mesh:', error);
67
83
  }
68
- // Return engine fallback if nothing found
69
84
  return '/Engine/EngineMeshes/SkeletalCube';
70
85
  }
71
86
  /**
@@ -144,7 +159,9 @@ else:
144
159
  const skeletonToMeshMap = {
145
160
  '/Game/Mannequin/Character/Mesh/UE4_Mannequin_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple',
146
161
  '/Game/Characters/Mannequins/Meshes/SK_Mannequin': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple',
147
- '/Game/Mannequin/Character/Mesh/SK_Mannequin': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple'
162
+ '/Game/Mannequin/Character/Mesh/SK_Mannequin': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple',
163
+ '/Game/Characters/Mannequins/Skeletons/UE5_Mannequin_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple',
164
+ '/Game/Characters/Mannequins/Skeletons/UE5_Female_Mannequin_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple'
148
165
  };
149
166
  // Auto-fix common incorrect paths
150
167
  let actualSkeletonPath = params.skeletonPath;
@@ -160,231 +177,324 @@ else:
160
177
  const pythonScript = `
161
178
  import unreal
162
179
  import time
180
+ import json
181
+
182
+ result = {
183
+ "success": False,
184
+ "path": None,
185
+ "message": "",
186
+ "error": None,
187
+ "warnings": [],
188
+ "details": [],
189
+ "existingAsset": False,
190
+ "meshPath": "${meshPath}"
191
+ }
192
+
193
+ def record_detail(message):
194
+ result["details"].append(message)
195
+
196
+ def record_warning(message):
197
+ result["warnings"].append(message)
198
+
199
+ def record_error(message):
200
+ result["error"] = message
163
201
 
164
202
  # Helper function to ensure asset persistence
165
203
  def ensure_asset_persistence(asset_path):
166
- try:
167
- asset = unreal.EditorAssetLibrary.load_asset(asset_path)
168
- if not asset:
169
- return False
204
+ try:
205
+ asset = unreal.EditorAssetLibrary.load_asset(asset_path)
206
+ if not asset:
207
+ record_warning(f"Asset persistence check failed: {asset_path} not loaded")
208
+ return False
170
209
 
171
- # Save the asset
172
- saved = unreal.EditorAssetLibrary.save_asset(asset_path, only_if_is_dirty=False)
173
- if saved:
174
- print(f"Asset saved: {asset_path}")
210
+ # Save the asset
211
+ saved = unreal.EditorAssetLibrary.save_asset(asset_path, only_if_is_dirty=False)
212
+ if saved:
213
+ print(f"Asset saved: {asset_path}")
214
+ record_detail(f"Asset saved: {asset_path}")
175
215
 
176
- # Refresh the asset registry minimally for the asset's directory
177
- try:
178
- asset_dir = asset_path.rsplit('/', 1)[0]
179
- unreal.AssetRegistryHelpers.get_asset_registry().scan_paths_synchronous([asset_dir], True)
180
- except Exception as _reg_e:
181
- pass
216
+ # Refresh the asset registry minimally for the asset's directory
217
+ try:
218
+ asset_dir = asset_path.rsplit('/', 1)[0]
219
+ unreal.AssetRegistryHelpers.get_asset_registry().scan_paths_synchronous([asset_dir], True)
220
+ except Exception as _reg_e:
221
+ record_warning(f"Asset registry refresh warning: {_reg_e}")
182
222
 
183
- # Small delay to ensure filesystem sync
184
- time.sleep(0.1)
223
+ # Small delay to ensure filesystem sync
224
+ time.sleep(0.1)
185
225
 
186
- return saved
187
- except Exception as e:
188
- print(f"Error ensuring persistence: {e}")
189
- return False
226
+ return saved
227
+ except Exception as e:
228
+ print(f"Error ensuring persistence: {e}")
229
+ record_error(f"Error ensuring persistence: {e}")
230
+ return False
190
231
 
191
- # Stop PIE if it's running
232
+ # Stop PIE if running using modern subsystems
192
233
  try:
193
- if unreal.EditorLevelLibrary.is_playing_editor():
194
- print("Stopping Play In Editor mode...")
195
- unreal.EditorLevelLibrary.editor_end_play()
196
- time.sleep(0.5)
197
- except:
198
- pass
234
+ level_subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
235
+ play_subsystem = None
236
+ try:
237
+ play_subsystem = unreal.get_editor_subsystem(unreal.EditorPlayWorldSubsystem)
238
+ except Exception:
239
+ play_subsystem = None
240
+
241
+ is_playing = False
242
+ if level_subsystem and hasattr(level_subsystem, 'is_in_play_in_editor'):
243
+ is_playing = level_subsystem.is_in_play_in_editor()
244
+ elif play_subsystem and hasattr(play_subsystem, 'is_playing_in_editor'): # type: ignore[attr-defined]
245
+ is_playing = play_subsystem.is_playing_in_editor() # type: ignore[attr-defined]
246
+
247
+ if is_playing:
248
+ print("Stopping Play In Editor mode...")
249
+ record_detail("Stopping Play In Editor mode")
250
+ if level_subsystem and hasattr(level_subsystem, 'editor_request_end_play'):
251
+ level_subsystem.editor_request_end_play()
252
+ elif play_subsystem and hasattr(play_subsystem, 'stop_playing_session'): # type: ignore[attr-defined]
253
+ play_subsystem.stop_playing_session() # type: ignore[attr-defined]
254
+ elif play_subsystem and hasattr(play_subsystem, 'end_play'): # type: ignore[attr-defined]
255
+ play_subsystem.end_play() # type: ignore[attr-defined]
256
+ else:
257
+ record_warning('Unable to stop Play In Editor via modern subsystems; please stop PIE manually.')
258
+ time.sleep(0.5)
259
+ except Exception as pie_error:
260
+ record_warning(f"PIE stop check failed: {pie_error}")
199
261
 
200
262
  # Main execution
201
263
  success = False
202
264
  error_msg = ""
265
+ new_asset = None
203
266
 
204
267
  # Log the attempt
205
268
  print("Setting up ragdoll for ${meshPath}")
269
+ record_detail("Setting up ragdoll for ${meshPath}")
206
270
 
207
271
  asset_path = "${path}"
208
272
  asset_name = "${sanitizedParams.name}"
209
273
  full_path = f"{asset_path}/{asset_name}"
210
274
 
211
275
  try:
212
- # Check if already exists
213
- if unreal.EditorAssetLibrary.does_asset_exist(full_path):
214
- print(f"Physics asset already exists at {full_path}")
215
- existing = unreal.EditorAssetLibrary.load_asset(full_path)
216
- if existing:
217
- print(f"Loaded existing PhysicsAsset: {full_path}")
218
- else:
219
- # Try to load skeletal mesh first - it's required
220
- skeletal_mesh_path = "${meshPath}"
221
- skeletal_mesh = None
276
+ # Check if already exists
277
+ if unreal.EditorAssetLibrary.does_asset_exist(full_path):
278
+ print(f"Physics asset already exists at {full_path}")
279
+ record_detail(f"Physics asset already exists at {full_path}")
280
+ existing = unreal.EditorAssetLibrary.load_asset(full_path)
281
+ if existing:
282
+ print(f"Loaded existing PhysicsAsset: {full_path}")
283
+ record_detail(f"Loaded existing PhysicsAsset: {full_path}")
284
+ success = True
285
+ result["existingAsset"] = True
286
+ result["message"] = f"Physics asset already exists at {full_path}"
287
+ else:
288
+ # Try to load skeletal mesh first - it's required
289
+ skeletal_mesh_path = "${meshPath}"
290
+ skeletal_mesh = None
222
291
 
223
- if skeletal_mesh_path and skeletal_mesh_path != "None":
224
- if unreal.EditorAssetLibrary.does_asset_exist(skeletal_mesh_path):
225
- asset = unreal.EditorAssetLibrary.load_asset(skeletal_mesh_path)
226
- if asset:
227
- if isinstance(asset, unreal.SkeletalMesh):
228
- skeletal_mesh = asset
229
- print(f"Loaded skeletal mesh: {skeletal_mesh_path}")
230
- elif isinstance(asset, unreal.Skeleton):
231
- error_msg = f"Provided path is a skeleton, not a skeletal mesh: {skeletal_mesh_path}"
232
- print(f"Error: {error_msg}")
233
- print(f"Error: Physics assets require a skeletal mesh, not just a skeleton")
234
- else:
235
- error_msg = f"Asset is not a skeletal mesh: {skeletal_mesh_path}"
236
- print(f"Warning: {error_msg}")
237
- else:
238
- error_msg = f"Skeletal mesh not found at {skeletal_mesh_path}"
239
- print(f"Error: {error_msg}")
240
-
241
- if not skeletal_mesh:
242
- if not error_msg:
243
- error_msg = "Cannot create physics asset without a valid skeletal mesh"
292
+ if skeletal_mesh_path and skeletal_mesh_path != "None":
293
+ if unreal.EditorAssetLibrary.does_asset_exist(skeletal_mesh_path):
294
+ asset = unreal.EditorAssetLibrary.load_asset(skeletal_mesh_path)
295
+ if asset:
296
+ if isinstance(asset, unreal.SkeletalMesh):
297
+ skeletal_mesh = asset
298
+ print(f"Loaded skeletal mesh: {skeletal_mesh_path}")
299
+ record_detail(f"Loaded skeletal mesh: {skeletal_mesh_path}")
300
+ elif isinstance(asset, unreal.Skeleton):
301
+ error_msg = f"Provided path is a skeleton, not a skeletal mesh: {skeletal_mesh_path}"
244
302
  print(f"Error: {error_msg}")
245
- else:
246
- # Create physics asset using a different approach
247
- # Method 1: Direct creation with initialized factory
248
- try:
249
- factory = unreal.PhysicsAssetFactory()
303
+ record_error(error_msg)
304
+ result["message"] = error_msg
305
+ print("Error: Physics assets require a skeletal mesh, not just a skeleton")
306
+ record_warning("Physics assets require a skeletal mesh, not just a skeleton")
307
+ else:
308
+ error_msg = f"Asset is not a skeletal mesh: {skeletal_mesh_path}"
309
+ print(f"Warning: {error_msg}")
310
+ record_warning(error_msg)
311
+ else:
312
+ error_msg = f"Skeletal mesh not found at {skeletal_mesh_path}"
313
+ print(f"Error: {error_msg}")
314
+ record_error(error_msg)
315
+ result["message"] = error_msg
316
+
317
+ if not skeletal_mesh:
318
+ if not error_msg:
319
+ error_msg = "Cannot create physics asset without a valid skeletal mesh"
320
+ print(f"Error: {error_msg}")
321
+ record_error(error_msg)
322
+ if not result["message"]:
323
+ result["message"] = error_msg
324
+ else:
325
+ # Create physics asset using a different approach
326
+ # Method 1: Direct creation with initialized factory
327
+ try:
328
+ factory = unreal.PhysicsAssetFactory()
250
329
 
251
- # Create a transient package for the physics asset
252
- # Ensure the directory exists
253
- if not unreal.EditorAssetLibrary.does_directory_exist(asset_path):
254
- unreal.EditorAssetLibrary.make_directory(asset_path)
330
+ # Ensure the directory exists
331
+ if not unreal.EditorAssetLibrary.does_directory_exist(asset_path):
332
+ unreal.EditorAssetLibrary.make_directory(asset_path)
255
333
 
256
- # Alternative approach: Create physics asset from skeletal mesh
257
- # This is the proper way in UE5
334
+ # Alternative approach: Create physics asset from skeletal mesh
335
+ # This is the proper way in UE5
336
+ try:
337
+ # Try modern physics asset creation methods first
338
+ try:
339
+ # Method 1: Try using SkeletalMesh editor utilities if available
340
+ if hasattr(unreal, 'SkeletalMeshEditorSubsystem'):
341
+ skel_subsystem = unreal.get_editor_subsystem(unreal.SkeletalMeshEditorSubsystem)
342
+ if hasattr(skel_subsystem, 'create_physics_asset'):
343
+ physics_asset = skel_subsystem.create_physics_asset(skeletal_mesh)
344
+ else:
345
+ # Fallback to deprecated EditorSkeletalMeshLibrary
258
346
  physics_asset = unreal.EditorSkeletalMeshLibrary.create_physics_asset(skeletal_mesh)
347
+ else:
348
+ physics_asset = unreal.EditorSkeletalMeshLibrary.create_physics_asset(skeletal_mesh)
349
+ except Exception as method1_modern_error:
350
+ record_warning(f"Modern creation path fallback: {method1_modern_error}")
351
+ # Final fallback to deprecated API
352
+ physics_asset = unreal.EditorSkeletalMeshLibrary.create_physics_asset(skeletal_mesh)
353
+ except Exception as e:
354
+ print(f"Physics asset creation failed: {str(e)}")
355
+ record_error(f"Physics asset creation failed: {str(e)}")
356
+ physics_asset = None
259
357
 
260
- if physics_asset:
261
- # Move/rename the physics asset to desired location
262
- source_path = physics_asset.get_path_name()
263
- if unreal.EditorAssetLibrary.rename_asset(source_path, full_path):
264
- print(f"Successfully created and moved PhysicsAsset to {full_path}")
265
- new_asset = physics_asset
358
+ if physics_asset:
359
+ # Move/rename the physics asset to desired location
360
+ source_path = physics_asset.get_path_name()
361
+ if unreal.EditorAssetLibrary.rename_asset(source_path, full_path):
362
+ print(f"Successfully created and moved PhysicsAsset to {full_path}")
363
+ record_detail(f"Successfully created and moved PhysicsAsset to {full_path}")
364
+ new_asset = physics_asset
266
365
 
267
- # Ensure persistence
268
- if ensure_asset_persistence(full_path):
269
- # Verify it was saved
270
- if unreal.EditorAssetLibrary.does_asset_exist(full_path):
271
- print(f"Verified PhysicsAsset exists after save: {full_path}")
272
- success = True
273
- else:
274
- error_msg = f"PhysicsAsset not found after save: {full_path}"
275
- print(f"Warning: {error_msg}")
276
- else:
277
- error_msg = "Failed to persist physics asset"
278
- print(f"Warning: {error_msg}")
279
- else:
280
- print(f"Created PhysicsAsset but couldn't move to {full_path}")
281
- # Still consider it a success if we created it
282
- new_asset = physics_asset
283
- success = True
284
- else:
285
- error_msg = "Failed to create PhysicsAsset from skeletal mesh"
286
- print(f"{error_msg}")
287
- new_asset = None
366
+ # Ensure persistence
367
+ if ensure_asset_persistence(full_path):
368
+ # Verify it was saved
369
+ if unreal.EditorAssetLibrary.does_asset_exist(full_path):
370
+ print(f"Verified PhysicsAsset exists after save: {full_path}")
371
+ record_detail(f"Verified PhysicsAsset exists after save: {full_path}")
372
+ success = True
373
+ result["message"] = f"Ragdoll physics setup completed for {asset_name}"
374
+ else:
375
+ error_msg = f"PhysicsAsset not found after save: {full_path}"
376
+ print(f"Warning: {error_msg}")
377
+ record_warning(error_msg)
378
+ else:
379
+ error_msg = "Failed to persist physics asset"
380
+ print(f"Warning: {error_msg}")
381
+ record_warning(error_msg)
382
+ else:
383
+ print(f"Created PhysicsAsset but couldn't move to {full_path}")
384
+ record_warning(f"Created PhysicsAsset but couldn't move to {full_path}")
385
+ # Still consider it a success if we created it
386
+ new_asset = physics_asset
387
+ success = True
388
+ result["message"] = f"Physics asset created but not moved to {full_path}"
389
+ else:
390
+ error_msg = "Failed to create PhysicsAsset from skeletal mesh"
391
+ print(error_msg)
392
+ record_error(error_msg)
393
+ new_asset = None
288
394
 
289
- except Exception as e:
290
- print(f"Method 1 failed: {str(e)}")
395
+ successMessage: \`Skeletal mesh discovery complete\`,
396
+ failureMessage: \`Failed to discover skeletal mesh\`
397
+ record_warning(f"Method 1 failed: {str(e)}")
291
398
 
292
- # Method 2: Try older approach
293
- try:
294
- asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
295
- factory = unreal.PhysicsAssetFactory()
399
+ # Method 2: Try older approach
400
+ try:
401
+ asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
402
+ factory = unreal.PhysicsAssetFactory()
296
403
 
297
- # Try to initialize factory with the skeletal mesh
298
- factory.create_physics_asset_from_skeletal_mesh = skeletal_mesh
404
+ # Try to initialize factory with the skeletal mesh
405
+ factory.create_physics_asset_from_skeletal_mesh = skeletal_mesh
299
406
 
300
- new_asset = asset_tools.create_asset(
301
- asset_name=asset_name,
302
- package_path=asset_path,
303
- asset_class=unreal.PhysicsAsset,
304
- factory=factory
305
- )
407
+ new_asset = asset_tools.create_asset(
408
+ asset_name=asset_name,
409
+ package_path=asset_path,
410
+ asset_class=unreal.PhysicsAsset,
411
+ factory=factory
412
+ )
306
413
 
307
- if new_asset:
308
- print(f"Successfully created PhysicsAsset at {full_path} (Method 2)")
309
- # Ensure persistence
310
- if ensure_asset_persistence(full_path):
311
- success = True
312
- except Exception as e2:
313
- error_msg = f"Method 2 also failed: {str(e2)}"
314
- print(error_msg)
315
- new_asset = None
316
-
317
- # Final check
318
- if new_asset and not success:
319
- # Try one more save
320
- if ensure_asset_persistence(full_path):
321
- if unreal.EditorAssetLibrary.does_asset_exist(full_path):
322
- success = True
414
+ if new_asset:
415
+ print(f"Successfully created PhysicsAsset at {full_path} (Method 2)")
416
+ record_detail(f"Successfully created PhysicsAsset at {full_path} (Method 2)")
417
+ # Ensure persistence
418
+ if ensure_asset_persistence(full_path):
419
+ success = True
420
+ result["message"] = f"Ragdoll physics setup completed for {asset_name}"
421
+ else:
422
+ record_warning("Persistence check failed after Method 2 creation")
423
+ except Exception as e2:
424
+ error_msg = f"Method 2 also failed: {str(e2)}"
425
+ print(error_msg)
426
+ record_error(error_msg)
427
+ new_asset = None
323
428
 
429
+ # Final check
430
+ if new_asset and not success:
431
+ # Try one more save
432
+ if ensure_asset_persistence(full_path):
433
+ if unreal.EditorAssetLibrary.does_asset_exist(full_path):
434
+ success = True
435
+ result["message"] = f"Ragdoll physics setup completed for {asset_name}"
436
+ else:
437
+ record_warning(f"Final existence check failed for {full_path}")
438
+
324
439
  except Exception as e:
325
- error_msg = str(e)
326
- print(f"Error: {error_msg}")
327
- import traceback
328
- traceback.print_exc()
440
+ error_msg = str(e)
441
+ print(f"Error: {error_msg}")
442
+ record_error(error_msg)
443
+ import traceback
444
+ traceback.print_exc()
329
445
 
330
- # Output result markers for parsing
331
- if success:
332
- print("SUCCESS")
333
- else:
334
- print(f"FAILED: {error_msg}")
446
+ # Finalize result
447
+ result["success"] = bool(success)
448
+ result["path"] = full_path if success else None
335
449
 
336
- print("DONE")
450
+ if not result["message"]:
451
+ if success:
452
+ result["message"] = f"Ragdoll physics setup completed for {asset_name}"
453
+ elif error_msg:
454
+ result["message"] = error_msg
455
+ else:
456
+ result["message"] = "Failed to setup ragdoll"
457
+
458
+ if not success:
459
+ if not result["error"]:
460
+ result["error"] = error_msg or "Unknown error"
461
+
462
+ print('RESULT:' + json.dumps(result))
337
463
  `;
338
- // Execute Python and parse response
464
+ // Execute Python and interpret response
339
465
  try {
340
466
  const response = await this.bridge.executePython(pythonScript);
341
- // Parse the response to detect actual success or failure
342
- const responseStr = typeof response === 'string' ? response : JSON.stringify(response);
343
- // Check for explicit success/failure markers
344
- if (responseStr.includes('SUCCESS')) {
345
- return {
467
+ const interpreted = interpretStandardResult(response, {
468
+ successMessage: `Ragdoll physics setup completed for ${sanitizedParams.name}`,
469
+ failureMessage: `Failed to setup ragdoll for ${sanitizedParams.name}`
470
+ });
471
+ const warnings = interpreted.warnings ?? [];
472
+ const details = interpreted.details ?? [];
473
+ if (interpreted.success) {
474
+ const successPayload = {
346
475
  success: true,
347
- message: `Ragdoll physics setup completed for ${sanitizedParams.name}`,
348
- path: `${path}/${sanitizedParams.name}`
476
+ message: interpreted.message,
477
+ path: coerceString(interpreted.payload.path) ?? `${path}/${sanitizedParams.name}`
349
478
  };
350
- }
351
- else if (responseStr.includes('FAILED:')) {
352
- // Extract error message after FAILED:
353
- const failMatch = responseStr.match(/FAILED:\s*(.+)/);
354
- const errorMsg = failMatch ? failMatch[1] : 'Unknown error';
355
- return {
356
- success: false,
357
- message: `Failed to setup ragdoll: ${errorMsg}`,
358
- error: errorMsg
359
- };
360
- }
361
- else {
362
- // Check legacy error detection for backwards compatibility
363
- const logOutput = response?.LogOutput || [];
364
- const hasSkeletonError = logOutput.some((log) => log.Output && (log.Output.includes('skeleton, not a skeletal mesh') ||
365
- log.Output.includes('Must specify a valid skeletal mesh')));
366
- if (hasSkeletonError) {
367
- return {
368
- success: false,
369
- message: 'Failed: Must specify a valid skeletal mesh',
370
- error: 'The path points to a skeleton, not a skeletal mesh. Physics assets require a skeletal mesh.'
371
- };
479
+ if (interpreted.payload.existingAsset === true) {
480
+ successPayload.existingAsset = true;
372
481
  }
373
- // Check for other error indicators
374
- if (responseStr.includes('Error:') || responseStr.includes('error')) {
375
- return {
376
- success: false,
377
- message: 'Failed to setup ragdoll physics',
378
- error: responseStr
379
- };
482
+ if (warnings.length > 0) {
483
+ successPayload.warnings = warnings;
380
484
  }
381
- // Default to success if no errors detected
382
- return {
383
- success: true,
384
- message: `Ragdoll physics processed for ${sanitizedParams.name}`,
385
- path: `${path}/${sanitizedParams.name}`
386
- };
485
+ if (details.length > 0) {
486
+ successPayload.details = details;
487
+ }
488
+ return successPayload;
387
489
  }
490
+ const errorMessage = interpreted.error ?? `Failed to setup ragdoll for ${sanitizedParams.name}`;
491
+ return {
492
+ success: false,
493
+ message: errorMessage,
494
+ error: errorMessage,
495
+ warnings: warnings.length > 0 ? warnings : undefined,
496
+ details: details.length > 0 ? details : undefined
497
+ };
388
498
  }
389
499
  catch (error) {
390
500
  return {
@@ -429,9 +539,7 @@ print("DONE")
429
539
  commands.push(`SetConstraintLinear ${params.name} ${limits.linear}`);
430
540
  }
431
541
  }
432
- for (const cmd of commands) {
433
- await this.bridge.executeConsoleCommand(cmd);
434
- }
542
+ await this.bridge.executeConsoleCommands(commands);
435
543
  return {
436
544
  success: true,
437
545
  message: `Physics constraint ${params.name} created between ${params.actor1} and ${params.actor2}`
@@ -463,9 +571,7 @@ print("DONE")
463
571
  if (params.debrisLifetime) {
464
572
  commands.push(`SetDebrisLifetime ${params.destructionName} ${params.debrisLifetime}`);
465
573
  }
466
- for (const cmd of commands) {
467
- await this.bridge.executeConsoleCommand(cmd);
468
- }
574
+ await this.bridge.executeConsoleCommands(commands);
469
575
  return {
470
576
  success: true,
471
577
  message: `Chaos destruction ${params.destructionName} created`,
@@ -510,9 +616,7 @@ print("DONE")
510
616
  }
511
617
  commands.push(`SetFinalDriveRatio ${params.vehicleName} ${params.transmission.finalDriveRatio}`);
512
618
  }
513
- for (const cmd of commands) {
514
- await this.bridge.executeConsoleCommand(cmd);
515
- }
619
+ await this.bridge.executeConsoleCommands(commands);
516
620
  return {
517
621
  success: true,
518
622
  message: `Vehicle ${params.vehicleName} configured`
@@ -627,55 +731,42 @@ except Exception as e:
627
731
  print(f"RESULT:{json.dumps(result)}")
628
732
  `.trim();
629
733
  const response = await this.bridge.executePython(pythonCode);
630
- // Extract output from Python response
631
- let outputStr = '';
632
- if (typeof response === 'object' && response !== null) {
633
- // Check if it has LogOutput (standard Python execution response)
634
- if (response.LogOutput && Array.isArray(response.LogOutput)) {
635
- // Concatenate all log outputs
636
- outputStr = response.LogOutput
637
- .map((log) => log.Output || '')
638
- .join('');
639
- }
640
- else if ('result' in response) {
641
- outputStr = String(response.result);
642
- }
643
- else {
644
- outputStr = JSON.stringify(response);
645
- }
646
- }
647
- else {
648
- outputStr = String(response || '');
734
+ const interpreted = interpretStandardResult(response, {
735
+ successMessage: `Applied ${params.forceType} to ${params.actorName}`,
736
+ failureMessage: 'Force application failed'
737
+ });
738
+ const availableActors = coerceStringArray(interpreted.payload.available_actors);
739
+ if (interpreted.success) {
740
+ return {
741
+ success: true,
742
+ message: interpreted.message,
743
+ availableActors,
744
+ details: interpreted.details
745
+ };
649
746
  }
650
- // Parse the result
651
- const resultMatch = outputStr.match(/RESULT:(\{.*\})/);
652
- if (resultMatch) {
653
- try {
654
- const forceResult = JSON.parse(resultMatch[1]);
655
- if (!forceResult.success) {
656
- return { success: false, error: forceResult.message };
657
- }
658
- return forceResult;
659
- }
660
- catch {
661
- // Fallback
662
- if (outputStr.includes('Applied')) {
663
- return { success: true, message: outputStr };
664
- }
665
- return { success: false, error: outputStr || 'Force application failed' };
666
- }
747
+ const fallbackText = bestEffortInterpretedText(interpreted) ?? '';
748
+ if (/Applied/i.test(fallbackText)) {
749
+ return {
750
+ success: true,
751
+ message: fallbackText || interpreted.message,
752
+ availableActors,
753
+ details: interpreted.details
754
+ };
667
755
  }
668
- else {
669
- // Check for error patterns
670
- if (outputStr.includes('not found') || outputStr.includes('Error')) {
671
- return { success: false, error: outputStr || 'Force application failed' };
672
- }
673
- // Only return success if we have clear indication of success
674
- if (outputStr.includes('Applied')) {
675
- return { success: true, message: `Applied ${params.forceType} to ${params.actorName}` };
676
- }
677
- return { success: false, error: 'No valid result from Python' };
756
+ if (/not found/i.test(fallbackText) || /error/i.test(fallbackText)) {
757
+ return {
758
+ success: false,
759
+ error: interpreted.error ?? (fallbackText || 'Force application failed'),
760
+ availableActors,
761
+ details: interpreted.details ?? (fallbackText ? [fallbackText] : undefined)
762
+ };
678
763
  }
764
+ return {
765
+ success: false,
766
+ error: interpreted.error ?? 'No valid result from Python',
767
+ availableActors,
768
+ details: interpreted.details ?? (fallbackText ? [fallbackText] : undefined)
769
+ };
679
770
  }
680
771
  catch (err) {
681
772
  return { success: false, error: `Failed to apply force: ${err}` };
@@ -712,9 +803,7 @@ print(f"RESULT:{json.dumps(result)}")
712
803
  commands.push(`SetClothWind ${params.meshName} ${wind[0]} ${wind[1]} ${wind[2]}`);
713
804
  }
714
805
  }
715
- for (const cmd of commands) {
716
- await this.bridge.executeConsoleCommand(cmd);
717
- }
806
+ await this.bridge.executeConsoleCommands(commands);
718
807
  return {
719
808
  success: true,
720
809
  message: `Cloth simulation enabled for ${params.meshName}`
@@ -753,9 +842,7 @@ print(f"RESULT:{json.dumps(result)}")
753
842
  commands.push(`SetFluidColor ${params.name} ${color[0]} ${color[1]} ${color[2]} ${color[3]}`);
754
843
  }
755
844
  }
756
- for (const cmd of commands) {
757
- await this.bridge.executeConsoleCommand(cmd);
758
- }
845
+ await this.bridge.executeConsoleCommands(commands);
759
846
  return {
760
847
  success: true,
761
848
  message: `Fluid simulation ${params.name} created`
@@ -765,20 +852,5 @@ print(f"RESULT:{json.dumps(result)}")
765
852
  return { success: false, error: `Failed to create fluid simulation: ${err}` };
766
853
  }
767
854
  }
768
- /**
769
- * Helper function to execute console commands
770
- */
771
- async _executeCommand(command) {
772
- return this.bridge.httpCall('/remote/object/call', 'PUT', {
773
- objectPath: '/Script/Engine.Default__KismetSystemLibrary',
774
- functionName: 'ExecuteConsoleCommand',
775
- parameters: {
776
- WorldContextObject: null,
777
- Command: command,
778
- SpecificPlayer: null
779
- },
780
- generateTransaction: false
781
- });
782
- }
783
855
  }
784
856
  //# sourceMappingURL=physics.js.map