unreal-engine-mcp-server 0.2.1

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 (155) hide show
  1. package/.dockerignore +57 -0
  2. package/.env.production +25 -0
  3. package/.eslintrc.json +54 -0
  4. package/.github/workflows/publish-mcp.yml +75 -0
  5. package/Dockerfile +54 -0
  6. package/LICENSE +21 -0
  7. package/Public/icon.png +0 -0
  8. package/README.md +209 -0
  9. package/claude_desktop_config_example.json +13 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.js +7 -0
  12. package/dist/index.d.ts +31 -0
  13. package/dist/index.js +484 -0
  14. package/dist/prompts/index.d.ts +14 -0
  15. package/dist/prompts/index.js +38 -0
  16. package/dist/python-utils.d.ts +29 -0
  17. package/dist/python-utils.js +54 -0
  18. package/dist/resources/actors.d.ts +13 -0
  19. package/dist/resources/actors.js +83 -0
  20. package/dist/resources/assets.d.ts +23 -0
  21. package/dist/resources/assets.js +245 -0
  22. package/dist/resources/levels.d.ts +17 -0
  23. package/dist/resources/levels.js +94 -0
  24. package/dist/tools/actors.d.ts +51 -0
  25. package/dist/tools/actors.js +459 -0
  26. package/dist/tools/animation.d.ts +196 -0
  27. package/dist/tools/animation.js +579 -0
  28. package/dist/tools/assets.d.ts +21 -0
  29. package/dist/tools/assets.js +304 -0
  30. package/dist/tools/audio.d.ts +170 -0
  31. package/dist/tools/audio.js +416 -0
  32. package/dist/tools/blueprint.d.ts +144 -0
  33. package/dist/tools/blueprint.js +652 -0
  34. package/dist/tools/build_environment_advanced.d.ts +66 -0
  35. package/dist/tools/build_environment_advanced.js +484 -0
  36. package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
  37. package/dist/tools/consolidated-tool-definitions.js +607 -0
  38. package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
  39. package/dist/tools/consolidated-tool-handlers.js +1050 -0
  40. package/dist/tools/debug.d.ts +185 -0
  41. package/dist/tools/debug.js +265 -0
  42. package/dist/tools/editor.d.ts +88 -0
  43. package/dist/tools/editor.js +365 -0
  44. package/dist/tools/engine.d.ts +30 -0
  45. package/dist/tools/engine.js +36 -0
  46. package/dist/tools/foliage.d.ts +155 -0
  47. package/dist/tools/foliage.js +525 -0
  48. package/dist/tools/introspection.d.ts +98 -0
  49. package/dist/tools/introspection.js +683 -0
  50. package/dist/tools/landscape.d.ts +158 -0
  51. package/dist/tools/landscape.js +375 -0
  52. package/dist/tools/level.d.ts +110 -0
  53. package/dist/tools/level.js +362 -0
  54. package/dist/tools/lighting.d.ts +159 -0
  55. package/dist/tools/lighting.js +1179 -0
  56. package/dist/tools/materials.d.ts +34 -0
  57. package/dist/tools/materials.js +146 -0
  58. package/dist/tools/niagara.d.ts +145 -0
  59. package/dist/tools/niagara.js +289 -0
  60. package/dist/tools/performance.d.ts +163 -0
  61. package/dist/tools/performance.js +412 -0
  62. package/dist/tools/physics.d.ts +189 -0
  63. package/dist/tools/physics.js +784 -0
  64. package/dist/tools/rc.d.ts +110 -0
  65. package/dist/tools/rc.js +363 -0
  66. package/dist/tools/sequence.d.ts +112 -0
  67. package/dist/tools/sequence.js +675 -0
  68. package/dist/tools/tool-definitions.d.ts +4919 -0
  69. package/dist/tools/tool-definitions.js +891 -0
  70. package/dist/tools/tool-handlers.d.ts +47 -0
  71. package/dist/tools/tool-handlers.js +830 -0
  72. package/dist/tools/ui.d.ts +171 -0
  73. package/dist/tools/ui.js +337 -0
  74. package/dist/tools/visual.d.ts +29 -0
  75. package/dist/tools/visual.js +67 -0
  76. package/dist/types/env.d.ts +10 -0
  77. package/dist/types/env.js +18 -0
  78. package/dist/types/index.d.ts +323 -0
  79. package/dist/types/index.js +28 -0
  80. package/dist/types/tool-types.d.ts +274 -0
  81. package/dist/types/tool-types.js +13 -0
  82. package/dist/unreal-bridge.d.ts +126 -0
  83. package/dist/unreal-bridge.js +992 -0
  84. package/dist/utils/cache-manager.d.ts +64 -0
  85. package/dist/utils/cache-manager.js +176 -0
  86. package/dist/utils/error-handler.d.ts +66 -0
  87. package/dist/utils/error-handler.js +243 -0
  88. package/dist/utils/errors.d.ts +133 -0
  89. package/dist/utils/errors.js +256 -0
  90. package/dist/utils/http.d.ts +26 -0
  91. package/dist/utils/http.js +135 -0
  92. package/dist/utils/logger.d.ts +12 -0
  93. package/dist/utils/logger.js +32 -0
  94. package/dist/utils/normalize.d.ts +17 -0
  95. package/dist/utils/normalize.js +49 -0
  96. package/dist/utils/response-validator.d.ts +34 -0
  97. package/dist/utils/response-validator.js +121 -0
  98. package/dist/utils/safe-json.d.ts +4 -0
  99. package/dist/utils/safe-json.js +97 -0
  100. package/dist/utils/stdio-redirect.d.ts +2 -0
  101. package/dist/utils/stdio-redirect.js +20 -0
  102. package/dist/utils/validation.d.ts +50 -0
  103. package/dist/utils/validation.js +173 -0
  104. package/mcp-config-example.json +14 -0
  105. package/package.json +63 -0
  106. package/server.json +60 -0
  107. package/src/cli.ts +7 -0
  108. package/src/index.ts +543 -0
  109. package/src/prompts/index.ts +51 -0
  110. package/src/python/editor_compat.py +181 -0
  111. package/src/python-utils.ts +57 -0
  112. package/src/resources/actors.ts +92 -0
  113. package/src/resources/assets.ts +251 -0
  114. package/src/resources/levels.ts +83 -0
  115. package/src/tools/actors.ts +480 -0
  116. package/src/tools/animation.ts +713 -0
  117. package/src/tools/assets.ts +305 -0
  118. package/src/tools/audio.ts +548 -0
  119. package/src/tools/blueprint.ts +736 -0
  120. package/src/tools/build_environment_advanced.ts +526 -0
  121. package/src/tools/consolidated-tool-definitions.ts +619 -0
  122. package/src/tools/consolidated-tool-handlers.ts +1093 -0
  123. package/src/tools/debug.ts +368 -0
  124. package/src/tools/editor.ts +360 -0
  125. package/src/tools/engine.ts +32 -0
  126. package/src/tools/foliage.ts +652 -0
  127. package/src/tools/introspection.ts +778 -0
  128. package/src/tools/landscape.ts +523 -0
  129. package/src/tools/level.ts +410 -0
  130. package/src/tools/lighting.ts +1316 -0
  131. package/src/tools/materials.ts +148 -0
  132. package/src/tools/niagara.ts +312 -0
  133. package/src/tools/performance.ts +549 -0
  134. package/src/tools/physics.ts +924 -0
  135. package/src/tools/rc.ts +437 -0
  136. package/src/tools/sequence.ts +791 -0
  137. package/src/tools/tool-definitions.ts +907 -0
  138. package/src/tools/tool-handlers.ts +941 -0
  139. package/src/tools/ui.ts +499 -0
  140. package/src/tools/visual.ts +60 -0
  141. package/src/types/env.ts +27 -0
  142. package/src/types/index.ts +414 -0
  143. package/src/types/tool-types.ts +343 -0
  144. package/src/unreal-bridge.ts +1118 -0
  145. package/src/utils/cache-manager.ts +213 -0
  146. package/src/utils/error-handler.ts +320 -0
  147. package/src/utils/errors.ts +312 -0
  148. package/src/utils/http.ts +184 -0
  149. package/src/utils/logger.ts +30 -0
  150. package/src/utils/normalize.ts +54 -0
  151. package/src/utils/response-validator.ts +145 -0
  152. package/src/utils/safe-json.ts +112 -0
  153. package/src/utils/stdio-redirect.ts +18 -0
  154. package/src/utils/validation.ts +212 -0
  155. package/tsconfig.json +33 -0
@@ -0,0 +1,784 @@
1
+ import { validateAssetParams, resolveSkeletalMeshPath, concurrencyDelay } from '../utils/validation.js';
2
+ export class PhysicsTools {
3
+ bridge;
4
+ constructor(bridge) {
5
+ this.bridge = bridge;
6
+ }
7
+ /**
8
+ * Helper to find a valid skeletal mesh in the project
9
+ */
10
+ async findValidSkeletalMesh() {
11
+ const pythonScript = `
12
+ import unreal
13
+
14
+ # Common skeletal mesh paths to check
15
+ 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
22
+ ]
23
+
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}")
30
+ break
31
+ else:
32
+ # Search for any skeletal mesh in the project
33
+ asset_registry = unreal.AssetRegistryHelpers.get_asset_registry()
34
+ assets = asset_registry.get_assets_by_class('SkeletalMesh', search_sub_classes=False)
35
+ if assets:
36
+ # Use the first available skeletal mesh
37
+ first_mesh = assets[0]
38
+ obj_path = first_mesh.get_editor_property('object_path')
39
+ 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")
45
+ `;
46
+ try {
47
+ 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;
54
+ }
55
+ else {
56
+ // Fallback: stringify and still try to parse, but restrict to line content only
57
+ outputStr = JSON.stringify(response);
58
+ }
59
+ // Capture only until end-of-line to avoid trailing JSON serialization
60
+ const match = outputStr.match(/FOUND_MESH:([^\r\n]+)/);
61
+ if (match) {
62
+ return match[1].trim();
63
+ }
64
+ }
65
+ catch (error) {
66
+ console.error('Failed to find skeletal mesh:', error);
67
+ }
68
+ // Return engine fallback if nothing found
69
+ return '/Engine/EngineMeshes/SkeletalCube';
70
+ }
71
+ /**
72
+ * Setup Ragdoll Physics
73
+ * NOTE: Requires a valid skeletal mesh to create physics asset
74
+ * @param skeletonPath - Path to an existing skeletal mesh asset (required)
75
+ * @param physicsAssetName - Name for the new physics asset
76
+ * @param savePath - Directory to save the asset (default: /Game/Physics)
77
+ */
78
+ async setupRagdoll(params) {
79
+ try {
80
+ // Strong validation for physics asset name
81
+ if (!params.physicsAssetName || params.physicsAssetName.trim() === '') {
82
+ return {
83
+ success: false,
84
+ message: 'Failed to setup ragdoll: Name cannot be empty',
85
+ error: 'Name cannot be empty'
86
+ };
87
+ }
88
+ // Check for invalid characters in name
89
+ if (params.physicsAssetName.includes('@') || params.physicsAssetName.includes('#') ||
90
+ params.physicsAssetName.includes('$') || params.physicsAssetName.includes('%')) {
91
+ return {
92
+ success: false,
93
+ message: 'Failed to setup ragdoll: Name contains invalid characters',
94
+ error: 'Name contains invalid characters'
95
+ };
96
+ }
97
+ // Check if skeleton path is provided instead of skeletal mesh
98
+ if (params.skeletonPath && (params.skeletonPath.includes('_Skeleton') ||
99
+ params.skeletonPath.includes('SK_Mannequin') && !params.skeletonPath.includes('SKM_'))) {
100
+ return {
101
+ success: false,
102
+ message: 'Failed to setup ragdoll: Must specify a valid skeletal mesh',
103
+ error: 'Must specify a valid skeletal mesh, not a skeleton'
104
+ };
105
+ }
106
+ // Validate and sanitize parameters
107
+ const validation = validateAssetParams({
108
+ name: params.physicsAssetName,
109
+ savePath: params.savePath || '/Game/Physics'
110
+ });
111
+ if (!validation.valid) {
112
+ return {
113
+ success: false,
114
+ message: `Failed to setup ragdoll: ${validation.error}`,
115
+ error: validation.error
116
+ };
117
+ }
118
+ const sanitizedParams = validation.sanitized;
119
+ const path = sanitizedParams.savePath || '/Game/Physics';
120
+ // Resolve skeletal mesh path
121
+ let meshPath = params.skeletonPath;
122
+ // Try to resolve skeleton to mesh mapping
123
+ const resolvedPath = resolveSkeletalMeshPath(meshPath);
124
+ if (resolvedPath && resolvedPath !== meshPath) {
125
+ console.error(`Auto-correcting path from ${meshPath} to ${resolvedPath}`);
126
+ meshPath = resolvedPath;
127
+ }
128
+ // Auto-resolve if it looks like a skeleton path or is empty
129
+ if (!meshPath || meshPath.includes('_Skeleton') || meshPath === 'None' || meshPath === '') {
130
+ console.error('Resolving skeletal mesh path...');
131
+ const resolvedMesh = await this.findValidSkeletalMesh();
132
+ if (resolvedMesh) {
133
+ meshPath = resolvedMesh;
134
+ console.error(`Using resolved skeletal mesh: ${meshPath}`);
135
+ }
136
+ }
137
+ // Add concurrency delay to prevent race conditions
138
+ await concurrencyDelay();
139
+ // IMPORTANT: Physics assets require a SKELETAL MESH, not a skeleton
140
+ // UE5 uses: /Game/Characters/Mannequins/Meshes/SKM_Manny_Simple or SKM_Quinn_Simple
141
+ // UE4 used: /Game/Mannequin/Character/Mesh/SK_Mannequin (which no longer exists)
142
+ // Fallback: /Engine/EngineMeshes/SkeletalCube
143
+ // Common skeleton paths that should be replaced with actual skeletal mesh paths
144
+ const skeletonToMeshMap = {
145
+ '/Game/Mannequin/Character/Mesh/UE4_Mannequin_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple',
146
+ '/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'
148
+ };
149
+ // Auto-fix common incorrect paths
150
+ let actualSkeletonPath = params.skeletonPath;
151
+ if (actualSkeletonPath && skeletonToMeshMap[actualSkeletonPath]) {
152
+ console.error(`Auto-correcting path from ${actualSkeletonPath} to ${skeletonToMeshMap[actualSkeletonPath]}`);
153
+ actualSkeletonPath = skeletonToMeshMap[actualSkeletonPath];
154
+ }
155
+ if (actualSkeletonPath && (actualSkeletonPath.includes('_Skeleton') || actualSkeletonPath.includes('SK_Mannequin'))) {
156
+ // This is likely a skeleton path, not a skeletal mesh
157
+ console.error('Warning: Path appears to be a skeleton, not a skeletal mesh. Auto-correcting to SKM_Manny_Simple.');
158
+ }
159
+ // Build Python script with resolved mesh path
160
+ const pythonScript = `
161
+ import unreal
162
+ import time
163
+
164
+ # Helper function to ensure asset persistence
165
+ def ensure_asset_persistence(asset_path):
166
+ try:
167
+ asset = unreal.EditorAssetLibrary.load_asset(asset_path)
168
+ if not asset:
169
+ return False
170
+
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}")
175
+
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
182
+
183
+ # Small delay to ensure filesystem sync
184
+ time.sleep(0.1)
185
+
186
+ return saved
187
+ except Exception as e:
188
+ print(f"Error ensuring persistence: {e}")
189
+ return False
190
+
191
+ # Stop PIE if it's running
192
+ 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
199
+
200
+ # Main execution
201
+ success = False
202
+ error_msg = ""
203
+
204
+ # Log the attempt
205
+ print("Setting up ragdoll for ${meshPath}")
206
+
207
+ asset_path = "${path}"
208
+ asset_name = "${sanitizedParams.name}"
209
+ full_path = f"{asset_path}/{asset_name}"
210
+
211
+ 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
222
+
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"
244
+ 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()
250
+
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)
255
+
256
+ # Alternative approach: Create physics asset from skeletal mesh
257
+ # This is the proper way in UE5
258
+ physics_asset = unreal.EditorSkeletalMeshLibrary.create_physics_asset(skeletal_mesh)
259
+
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
266
+
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
288
+
289
+ except Exception as e:
290
+ print(f"Method 1 failed: {str(e)}")
291
+
292
+ # Method 2: Try older approach
293
+ try:
294
+ asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
295
+ factory = unreal.PhysicsAssetFactory()
296
+
297
+ # Try to initialize factory with the skeletal mesh
298
+ factory.create_physics_asset_from_skeletal_mesh = skeletal_mesh
299
+
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
+ )
306
+
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
323
+
324
+ except Exception as e:
325
+ error_msg = str(e)
326
+ print(f"Error: {error_msg}")
327
+ import traceback
328
+ traceback.print_exc()
329
+
330
+ # Output result markers for parsing
331
+ if success:
332
+ print("SUCCESS")
333
+ else:
334
+ print(f"FAILED: {error_msg}")
335
+
336
+ print("DONE")
337
+ `;
338
+ // Execute Python and parse response
339
+ try {
340
+ 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 {
346
+ success: true,
347
+ message: `Ragdoll physics setup completed for ${sanitizedParams.name}`,
348
+ path: `${path}/${sanitizedParams.name}`
349
+ };
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
+ };
372
+ }
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
+ };
380
+ }
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
+ };
387
+ }
388
+ }
389
+ catch (error) {
390
+ return {
391
+ success: false,
392
+ message: 'Failed to setup ragdoll physics',
393
+ error: String(error)
394
+ };
395
+ }
396
+ }
397
+ catch (err) {
398
+ return { success: false, error: `Failed to setup ragdoll: ${err}` };
399
+ }
400
+ }
401
+ /**
402
+ * Create Physics Constraint
403
+ */
404
+ async createConstraint(params) {
405
+ try {
406
+ // Spawn constraint actor
407
+ const spawnCmd = `spawnactor /Script/Engine.PhysicsConstraintActor ${params.location[0]} ${params.location[1]} ${params.location[2]}`;
408
+ await this.bridge.executeConsoleCommand(spawnCmd);
409
+ // Configure constraint
410
+ const commands = [
411
+ `SetConstraintActors ${params.name} ${params.actor1} ${params.actor2}`,
412
+ `SetConstraintType ${params.name} ${params.constraintType}`
413
+ ];
414
+ if (params.breakThreshold) {
415
+ commands.push(`SetConstraintBreakThreshold ${params.name} ${params.breakThreshold}`);
416
+ }
417
+ if (params.limits) {
418
+ const limits = params.limits;
419
+ if (limits.swing1 !== undefined) {
420
+ commands.push(`SetConstraintSwing1 ${params.name} ${limits.swing1}`);
421
+ }
422
+ if (limits.swing2 !== undefined) {
423
+ commands.push(`SetConstraintSwing2 ${params.name} ${limits.swing2}`);
424
+ }
425
+ if (limits.twist !== undefined) {
426
+ commands.push(`SetConstraintTwist ${params.name} ${limits.twist}`);
427
+ }
428
+ if (limits.linear !== undefined) {
429
+ commands.push(`SetConstraintLinear ${params.name} ${limits.linear}`);
430
+ }
431
+ }
432
+ for (const cmd of commands) {
433
+ await this.bridge.executeConsoleCommand(cmd);
434
+ }
435
+ return {
436
+ success: true,
437
+ message: `Physics constraint ${params.name} created between ${params.actor1} and ${params.actor2}`
438
+ };
439
+ }
440
+ catch (err) {
441
+ return { success: false, error: `Failed to create constraint: ${err}` };
442
+ }
443
+ }
444
+ /**
445
+ * Setup Chaos Destruction
446
+ */
447
+ async setupDestruction(params) {
448
+ try {
449
+ const path = params.savePath || '/Game/Destruction';
450
+ const commands = [
451
+ `CreateGeometryCollection ${params.destructionName} ${params.meshPath} ${path}`
452
+ ];
453
+ // Configure fracture
454
+ if (params.fractureSettings) {
455
+ const settings = params.fractureSettings;
456
+ commands.push(`FractureGeometry ${params.destructionName} ${settings.cellCount} ${settings.minimumVolumeSize} ${settings.seed}`);
457
+ }
458
+ // Set damage threshold
459
+ if (params.damageThreshold) {
460
+ commands.push(`SetDamageThreshold ${params.destructionName} ${params.damageThreshold}`);
461
+ }
462
+ // Set debris lifetime
463
+ if (params.debrisLifetime) {
464
+ commands.push(`SetDebrisLifetime ${params.destructionName} ${params.debrisLifetime}`);
465
+ }
466
+ for (const cmd of commands) {
467
+ await this.bridge.executeConsoleCommand(cmd);
468
+ }
469
+ return {
470
+ success: true,
471
+ message: `Chaos destruction ${params.destructionName} created`,
472
+ path: `${path}/${params.destructionName}`
473
+ };
474
+ }
475
+ catch (err) {
476
+ return { success: false, error: `Failed to setup destruction: ${err}` };
477
+ }
478
+ }
479
+ /**
480
+ * Configure Vehicle Physics
481
+ */
482
+ async configureVehicle(params) {
483
+ try {
484
+ const commands = [
485
+ `CreateVehicle ${params.vehicleName} ${params.vehicleType}`
486
+ ];
487
+ // Configure wheels
488
+ if (params.wheels) {
489
+ for (const wheel of params.wheels) {
490
+ commands.push(`AddVehicleWheel ${params.vehicleName} ${wheel.name} ${wheel.radius} ${wheel.width} ${wheel.mass}`);
491
+ if (wheel.isSteering) {
492
+ commands.push(`SetWheelSteering ${params.vehicleName} ${wheel.name} true`);
493
+ }
494
+ if (wheel.isDriving) {
495
+ commands.push(`SetWheelDriving ${params.vehicleName} ${wheel.name} true`);
496
+ }
497
+ }
498
+ }
499
+ // Configure engine
500
+ if (params.engine) {
501
+ commands.push(`SetEngineMaxRPM ${params.vehicleName} ${params.engine.maxRPM}`);
502
+ for (const [rpm, torque] of params.engine.torqueCurve) {
503
+ commands.push(`AddTorqueCurvePoint ${params.vehicleName} ${rpm} ${torque}`);
504
+ }
505
+ }
506
+ // Configure transmission
507
+ if (params.transmission) {
508
+ for (let i = 0; i < params.transmission.gears.length; i++) {
509
+ commands.push(`SetGearRatio ${params.vehicleName} ${i} ${params.transmission.gears[i]}`);
510
+ }
511
+ commands.push(`SetFinalDriveRatio ${params.vehicleName} ${params.transmission.finalDriveRatio}`);
512
+ }
513
+ for (const cmd of commands) {
514
+ await this.bridge.executeConsoleCommand(cmd);
515
+ }
516
+ return {
517
+ success: true,
518
+ message: `Vehicle ${params.vehicleName} configured`
519
+ };
520
+ }
521
+ catch (err) {
522
+ return { success: false, error: `Failed to configure vehicle: ${err}` };
523
+ }
524
+ }
525
+ /**
526
+ * Apply Force or Impulse to Actor
527
+ */
528
+ async applyForce(params) {
529
+ try {
530
+ // Use Python to apply physics forces since console commands don't exist for this
531
+ const pythonCode = `
532
+ import unreal
533
+ import json
534
+
535
+ result = {"success": False, "message": "", "actor_found": False, "physics_enabled": False}
536
+
537
+ # Check if editor is in play mode first
538
+ try:
539
+ les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
540
+ if les and les.is_in_play_in_editor():
541
+ result["message"] = "Cannot apply physics while in Play In Editor mode. Please stop PIE first."
542
+ print(f"RESULT:{json.dumps(result)}")
543
+ # Exit early from this script
544
+ raise SystemExit(0)
545
+ except SystemExit:
546
+ # Re-raise the SystemExit to exit properly
547
+ raise
548
+ except:
549
+ pass # Continue if we can't check PIE state
550
+
551
+ try:
552
+ actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
553
+ actors = actor_subsystem.get_all_level_actors()
554
+ search_name = "${params.actorName}"
555
+
556
+ for actor in actors:
557
+ if actor:
558
+ # Check both actor name and label with case-insensitive partial matching
559
+ actor_name = actor.get_name()
560
+ actor_label = actor.get_actor_label()
561
+
562
+ if (search_name.lower() in actor_label.lower() or
563
+ actor_label.lower().startswith(search_name.lower() + "_") or
564
+ actor_label.lower() == search_name.lower() or
565
+ actor_name.lower() == search_name.lower()):
566
+
567
+ result["actor_found"] = True
568
+ # Get the primitive component if it exists
569
+ root = actor.get_editor_property('root_component')
570
+
571
+ if root and isinstance(root, unreal.PrimitiveComponent):
572
+ # Check if the component is static or movable
573
+ mobility = root.get_editor_property('mobility')
574
+ if mobility == unreal.ComponentMobility.STATIC:
575
+ # Try to set to movable first
576
+ try:
577
+ root.set_editor_property('mobility', unreal.ComponentMobility.MOVABLE)
578
+ except:
579
+ result["message"] = f"Actor {actor_label} has static mobility and cannot simulate physics"
580
+ break
581
+
582
+ # Ensure physics is enabled
583
+ try:
584
+ root.set_simulate_physics(True)
585
+ result["physics_enabled"] = True
586
+ except Exception as physics_err:
587
+ # If we can't enable physics, try applying force anyway (some actors respond without physics sim)
588
+ result["physics_enabled"] = False
589
+
590
+ force = unreal.Vector(${params.vector[0]}, ${params.vector[1]}, ${params.vector[2]})
591
+
592
+ if "${params.forceType}" == "Force":
593
+ root.add_force(force, 'None', False)
594
+ result["success"] = True
595
+ result["message"] = f"Applied Force to {actor_label}: {force}"
596
+ elif "${params.forceType}" == "Impulse":
597
+ root.add_impulse(force, 'None', False)
598
+ result["success"] = True
599
+ result["message"] = f"Applied Impulse to {actor_label}: {force}"
600
+ elif "${params.forceType}" == "Velocity":
601
+ root.set_physics_linear_velocity(force)
602
+ result["success"] = True
603
+ result["message"] = f"Set Velocity on {actor_label}: {force}"
604
+ elif "${params.forceType}" == "Torque":
605
+ root.add_torque_in_radians(force, 'None', False)
606
+ result["success"] = True
607
+ result["message"] = f"Applied Torque to {actor_label}: {force}"
608
+ else:
609
+ result["message"] = f"Actor {actor_label} doesn't have a physics-enabled component"
610
+ break
611
+
612
+ if not result["actor_found"]:
613
+ result["message"] = f"Actor not found: {search_name}"
614
+ # List actors with physics enabled for debugging
615
+ physics_actors = []
616
+ for actor in actors[:20]:
617
+ if actor:
618
+ label = actor.get_actor_label()
619
+ if "mesh" in label.lower() or "cube" in label.lower() or "static" in label.lower():
620
+ physics_actors.append(label)
621
+ if physics_actors:
622
+ result["available_actors"] = physics_actors
623
+
624
+ except Exception as e:
625
+ result["message"] = f"Error applying force: {e}"
626
+
627
+ print(f"RESULT:{json.dumps(result)}")
628
+ `.trim();
629
+ 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 || '');
649
+ }
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
+ }
667
+ }
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' };
678
+ }
679
+ }
680
+ catch (err) {
681
+ return { success: false, error: `Failed to apply force: ${err}` };
682
+ }
683
+ }
684
+ /**
685
+ * Configure Cloth Simulation
686
+ */
687
+ async setupCloth(params) {
688
+ try {
689
+ const commands = [
690
+ `EnableClothSimulation ${params.meshName}`,
691
+ `SetClothPreset ${params.meshName} ${params.clothPreset}`
692
+ ];
693
+ if (params.clothPreset === 'Custom' && params.customSettings) {
694
+ const settings = params.customSettings;
695
+ if (settings.stiffness !== undefined) {
696
+ commands.push(`SetClothStiffness ${params.meshName} ${settings.stiffness}`);
697
+ }
698
+ if (settings.damping !== undefined) {
699
+ commands.push(`SetClothDamping ${params.meshName} ${settings.damping}`);
700
+ }
701
+ if (settings.friction !== undefined) {
702
+ commands.push(`SetClothFriction ${params.meshName} ${settings.friction}`);
703
+ }
704
+ if (settings.density !== undefined) {
705
+ commands.push(`SetClothDensity ${params.meshName} ${settings.density}`);
706
+ }
707
+ if (settings.gravity !== undefined) {
708
+ commands.push(`SetClothGravity ${params.meshName} ${settings.gravity}`);
709
+ }
710
+ if (settings.windVelocity) {
711
+ const wind = settings.windVelocity;
712
+ commands.push(`SetClothWind ${params.meshName} ${wind[0]} ${wind[1]} ${wind[2]}`);
713
+ }
714
+ }
715
+ for (const cmd of commands) {
716
+ await this.bridge.executeConsoleCommand(cmd);
717
+ }
718
+ return {
719
+ success: true,
720
+ message: `Cloth simulation enabled for ${params.meshName}`
721
+ };
722
+ }
723
+ catch (err) {
724
+ return { success: false, error: `Failed to setup cloth: ${err}` };
725
+ }
726
+ }
727
+ /**
728
+ * Create Fluid Simulation (Niagara-based)
729
+ */
730
+ async createFluidSimulation(params) {
731
+ try {
732
+ const locStr = `${params.location[0]} ${params.location[1]} ${params.location[2]}`;
733
+ const volStr = `${params.volume[0]} ${params.volume[1]} ${params.volume[2]}`;
734
+ const commands = [
735
+ `CreateFluidSimulation ${params.name} ${params.fluidType} ${locStr} ${volStr}`
736
+ ];
737
+ if (params.customSettings) {
738
+ const settings = params.customSettings;
739
+ if (settings.viscosity !== undefined) {
740
+ commands.push(`SetFluidViscosity ${params.name} ${settings.viscosity}`);
741
+ }
742
+ if (settings.density !== undefined) {
743
+ commands.push(`SetFluidDensity ${params.name} ${settings.density}`);
744
+ }
745
+ if (settings.temperature !== undefined) {
746
+ commands.push(`SetFluidTemperature ${params.name} ${settings.temperature}`);
747
+ }
748
+ if (settings.turbulence !== undefined) {
749
+ commands.push(`SetFluidTurbulence ${params.name} ${settings.turbulence}`);
750
+ }
751
+ if (settings.color) {
752
+ const color = settings.color;
753
+ commands.push(`SetFluidColor ${params.name} ${color[0]} ${color[1]} ${color[2]} ${color[3]}`);
754
+ }
755
+ }
756
+ for (const cmd of commands) {
757
+ await this.bridge.executeConsoleCommand(cmd);
758
+ }
759
+ return {
760
+ success: true,
761
+ message: `Fluid simulation ${params.name} created`
762
+ };
763
+ }
764
+ catch (err) {
765
+ return { success: false, error: `Failed to create fluid simulation: ${err}` };
766
+ }
767
+ }
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
+ }
784
+ //# sourceMappingURL=physics.js.map