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.
- package/.env.production +1 -1
- package/.github/copilot-instructions.md +45 -0
- package/.github/workflows/publish-mcp.yml +1 -1
- package/README.md +21 -5
- package/dist/index.js +124 -31
- package/dist/prompts/index.d.ts +10 -3
- package/dist/prompts/index.js +186 -7
- package/dist/resources/actors.d.ts +19 -1
- package/dist/resources/actors.js +55 -64
- package/dist/resources/assets.js +46 -62
- package/dist/resources/levels.d.ts +21 -3
- package/dist/resources/levels.js +29 -54
- package/dist/tools/actors.d.ts +3 -14
- package/dist/tools/actors.js +246 -302
- package/dist/tools/animation.d.ts +57 -102
- package/dist/tools/animation.js +429 -450
- package/dist/tools/assets.d.ts +13 -2
- package/dist/tools/assets.js +52 -44
- package/dist/tools/audio.d.ts +22 -13
- package/dist/tools/audio.js +467 -121
- package/dist/tools/blueprint.d.ts +32 -13
- package/dist/tools/blueprint.js +699 -448
- package/dist/tools/build_environment_advanced.d.ts +0 -1
- package/dist/tools/build_environment_advanced.js +190 -45
- package/dist/tools/consolidated-tool-definitions.js +78 -252
- package/dist/tools/consolidated-tool-handlers.js +506 -133
- package/dist/tools/debug.d.ts +72 -10
- package/dist/tools/debug.js +167 -31
- package/dist/tools/editor.d.ts +9 -2
- package/dist/tools/editor.js +30 -44
- package/dist/tools/foliage.d.ts +34 -15
- package/dist/tools/foliage.js +97 -107
- package/dist/tools/introspection.js +19 -21
- package/dist/tools/landscape.d.ts +1 -2
- package/dist/tools/landscape.js +311 -168
- package/dist/tools/level.d.ts +3 -28
- package/dist/tools/level.js +642 -192
- package/dist/tools/lighting.d.ts +14 -3
- package/dist/tools/lighting.js +236 -123
- package/dist/tools/materials.d.ts +25 -7
- package/dist/tools/materials.js +102 -79
- package/dist/tools/niagara.d.ts +10 -12
- package/dist/tools/niagara.js +74 -94
- package/dist/tools/performance.d.ts +12 -4
- package/dist/tools/performance.js +38 -79
- package/dist/tools/physics.d.ts +34 -10
- package/dist/tools/physics.js +364 -292
- package/dist/tools/rc.js +97 -23
- package/dist/tools/sequence.d.ts +1 -0
- package/dist/tools/sequence.js +125 -22
- package/dist/tools/ui.d.ts +31 -4
- package/dist/tools/ui.js +83 -66
- package/dist/tools/visual.d.ts +11 -0
- package/dist/tools/visual.js +245 -30
- package/dist/types/tool-types.d.ts +0 -6
- package/dist/types/tool-types.js +1 -8
- package/dist/unreal-bridge.d.ts +32 -2
- package/dist/unreal-bridge.js +621 -127
- package/dist/utils/elicitation.d.ts +57 -0
- package/dist/utils/elicitation.js +104 -0
- package/dist/utils/error-handler.d.ts +0 -33
- package/dist/utils/error-handler.js +4 -111
- package/dist/utils/http.d.ts +2 -22
- package/dist/utils/http.js +12 -75
- package/dist/utils/normalize.d.ts +4 -4
- package/dist/utils/normalize.js +15 -7
- package/dist/utils/python-output.d.ts +18 -0
- package/dist/utils/python-output.js +290 -0
- package/dist/utils/python.d.ts +2 -0
- package/dist/utils/python.js +4 -0
- package/dist/utils/response-validator.js +28 -2
- package/dist/utils/result-helpers.d.ts +27 -0
- package/dist/utils/result-helpers.js +147 -0
- package/dist/utils/safe-json.d.ts +0 -2
- package/dist/utils/safe-json.js +0 -43
- package/dist/utils/validation.d.ts +16 -0
- package/dist/utils/validation.js +70 -7
- package/mcp-config-example.json +2 -2
- package/package.json +10 -9
- package/server.json +37 -14
- package/src/index.ts +130 -33
- package/src/prompts/index.ts +211 -13
- package/src/resources/actors.ts +59 -44
- package/src/resources/assets.ts +48 -51
- package/src/resources/levels.ts +35 -45
- package/src/tools/actors.ts +269 -313
- package/src/tools/animation.ts +556 -539
- package/src/tools/assets.ts +53 -43
- package/src/tools/audio.ts +507 -113
- package/src/tools/blueprint.ts +778 -462
- package/src/tools/build_environment_advanced.ts +266 -64
- package/src/tools/consolidated-tool-definitions.ts +90 -264
- package/src/tools/consolidated-tool-handlers.ts +630 -121
- package/src/tools/debug.ts +176 -33
- package/src/tools/editor.ts +35 -37
- package/src/tools/foliage.ts +110 -104
- package/src/tools/introspection.ts +24 -22
- package/src/tools/landscape.ts +334 -181
- package/src/tools/level.ts +683 -182
- package/src/tools/lighting.ts +244 -123
- package/src/tools/materials.ts +114 -83
- package/src/tools/niagara.ts +87 -81
- package/src/tools/performance.ts +49 -88
- package/src/tools/physics.ts +393 -299
- package/src/tools/rc.ts +102 -24
- package/src/tools/sequence.ts +136 -28
- package/src/tools/ui.ts +101 -70
- package/src/tools/visual.ts +250 -29
- package/src/types/tool-types.ts +0 -9
- package/src/unreal-bridge.ts +658 -140
- package/src/utils/elicitation.ts +129 -0
- package/src/utils/error-handler.ts +4 -159
- package/src/utils/http.ts +16 -115
- package/src/utils/normalize.ts +20 -10
- package/src/utils/python-output.ts +351 -0
- package/src/utils/python.ts +3 -0
- package/src/utils/response-validator.ts +25 -2
- package/src/utils/result-helpers.ts +193 -0
- package/src/utils/safe-json.ts +0 -50
- package/src/utils/validation.ts +94 -7
- package/tests/run-unreal-tool-tests.mjs +720 -0
- package/tsconfig.json +2 -2
- package/dist/python-utils.d.ts +0 -29
- package/dist/python-utils.js +0 -54
- package/dist/types/index.d.ts +0 -323
- package/dist/types/index.js +0 -28
- package/dist/utils/cache-manager.d.ts +0 -64
- package/dist/utils/cache-manager.js +0 -176
- package/dist/utils/errors.d.ts +0 -133
- package/dist/utils/errors.js +0 -256
- package/src/python/editor_compat.py +0 -181
- package/src/python-utils.ts +0 -57
- package/src/types/index.ts +0 -414
- package/src/utils/cache-manager.ts +0 -213
- package/src/utils/errors.ts +0 -312
package/dist/tools/physics.js
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
const fallback = coerceString(interpreted.payload.fallback);
|
|
73
|
+
if (fallback) {
|
|
74
|
+
return fallback;
|
|
58
75
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
223
|
+
# Small delay to ensure filesystem sync
|
|
224
|
+
time.sleep(0.1)
|
|
185
225
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
232
|
+
# Stop PIE if running using modern subsystems
|
|
192
233
|
try:
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
except:
|
|
198
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
399
|
+
# Method 2: Try older approach
|
|
400
|
+
try:
|
|
401
|
+
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
|
402
|
+
factory = unreal.PhysicsAssetFactory()
|
|
296
403
|
|
|
297
|
-
|
|
298
|
-
|
|
404
|
+
# Try to initialize factory with the skeletal mesh
|
|
405
|
+
factory.create_physics_asset_from_skeletal_mesh = skeletal_mesh
|
|
299
406
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
#
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
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
|
|
464
|
+
// Execute Python and interpret response
|
|
339
465
|
try {
|
|
340
466
|
const response = await this.bridge.executePython(pythonScript);
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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:
|
|
348
|
-
path: `${path}/${sanitizedParams.name}`
|
|
476
|
+
message: interpreted.message,
|
|
477
|
+
path: coerceString(interpreted.payload.path) ?? `${path}/${sanitizedParams.name}`
|
|
349
478
|
};
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
374
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|