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.
- package/.env.production +1 -1
- package/.github/copilot-instructions.md +45 -0
- package/.github/workflows/publish-mcp.yml +1 -1
- package/README.md +22 -7
- package/dist/index.js +137 -46
- 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.d.ts +3 -2
- package/dist/resources/assets.js +117 -109
- package/dist/resources/levels.d.ts +21 -3
- package/dist/resources/levels.js +31 -56
- 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 +58 -46
- 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 +236 -87
- package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
- package/dist/tools/consolidated-tool-definitions.js +124 -255
- package/dist/tools/consolidated-tool-handlers.js +749 -766
- package/dist/tools/debug.d.ts +72 -10
- package/dist/tools/debug.js +170 -36
- 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 +98 -24
- package/dist/tools/sequence.d.ts +1 -0
- package/dist/tools/sequence.js +146 -24
- 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.d.ts +6 -1
- package/dist/utils/response-validator.js +66 -13
- 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 +11 -10
- package/server.json +37 -14
- package/src/index.ts +146 -50
- package/src/prompts/index.ts +211 -13
- package/src/resources/actors.ts +59 -44
- package/src/resources/assets.ts +123 -102
- package/src/resources/levels.ts +37 -47
- package/src/tools/actors.ts +269 -313
- package/src/tools/animation.ts +556 -539
- package/src/tools/assets.ts +59 -45
- package/src/tools/audio.ts +507 -113
- package/src/tools/blueprint.ts +778 -462
- package/src/tools/build_environment_advanced.ts +312 -106
- package/src/tools/consolidated-tool-definitions.ts +136 -267
- package/src/tools/consolidated-tool-handlers.ts +871 -795
- package/src/tools/debug.ts +179 -38
- 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 +103 -25
- package/src/tools/sequence.ts +157 -30
- 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 +68 -17
- 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/tools/tool-definitions.d.ts +0 -4919
- package/dist/tools/tool-definitions.js +0 -1065
- package/dist/tools/tool-handlers.d.ts +0 -47
- package/dist/tools/tool-handlers.js +0 -863
- 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/tools/tool-definitions.ts +0 -1081
- package/src/tools/tool-handlers.ts +0 -973
- package/src/types/index.ts +0 -414
- package/src/utils/cache-manager.ts +0 -213
- package/src/utils/errors.ts +0 -312
package/src/tools/physics.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
|
|
47
61
|
try {
|
|
48
62
|
const response = await this.bridge.executePython(pythonScript);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
213
|
-
|
|
256
|
+
# Small delay to ensure filesystem sync
|
|
257
|
+
time.sleep(0.1)
|
|
214
258
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
265
|
+
# Stop PIE if running using modern subsystems
|
|
221
266
|
try:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
except:
|
|
227
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
286
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
432
|
+
# Method 2: Try older approach
|
|
433
|
+
try:
|
|
434
|
+
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
|
435
|
+
factory = unreal.PhysicsAssetFactory()
|
|
325
436
|
|
|
326
|
-
|
|
327
|
-
|
|
437
|
+
# Try to initialize factory with the skeletal mesh
|
|
438
|
+
factory.create_physics_asset_from_skeletal_mesh = skeletal_mesh
|
|
328
439
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
else
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
499
|
+
// Execute Python and interpret response
|
|
370
500
|
try {
|
|
371
501
|
const response = await this.bridge.executePython(pythonScript);
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
408
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|