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,924 @@
1
+ import { UnrealBridge } from '../unreal-bridge.js';
2
+ import { validateAssetParams, resolveSkeletalMeshPath, concurrencyDelay } from '../utils/validation.js';
3
+
4
+ export class PhysicsTools {
5
+ constructor(private bridge: UnrealBridge) {}
6
+
7
+ /**
8
+ * Helper to find a valid skeletal mesh in the project
9
+ */
10
+ private async findValidSkeletalMesh(): Promise<string | null> {
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
+
47
+ try {
48
+ const response = await this.bridge.executePython(pythonScript);
49
+ let outputStr = '';
50
+ if (response?.LogOutput && Array.isArray((response as any).LogOutput)) {
51
+ outputStr = (response as any).LogOutput.map((l: any) => l.Output || '').join('');
52
+ } else if (typeof response === 'string') {
53
+ outputStr = response;
54
+ } else {
55
+ // Fallback: stringify and still try to parse, but restrict to line content only
56
+ outputStr = JSON.stringify(response);
57
+ }
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
+ } catch (error) {
65
+ console.error('Failed to find skeletal mesh:', error);
66
+ }
67
+
68
+ // Return engine fallback if nothing found
69
+ return '/Engine/EngineMeshes/SkeletalCube';
70
+ }
71
+
72
+ /**
73
+ * Setup Ragdoll Physics
74
+ * NOTE: Requires a valid skeletal mesh to create physics asset
75
+ * @param skeletonPath - Path to an existing skeletal mesh asset (required)
76
+ * @param physicsAssetName - Name for the new physics asset
77
+ * @param savePath - Directory to save the asset (default: /Game/Physics)
78
+ */
79
+ async setupRagdoll(params: {
80
+ skeletonPath: string;
81
+ physicsAssetName: string;
82
+ savePath?: string;
83
+ blendWeight?: number;
84
+ constraints?: Array<{
85
+ boneName: string;
86
+ constraintType: 'Fixed' | 'Limited' | 'Free';
87
+ limits?: {
88
+ swing1?: number;
89
+ swing2?: number;
90
+ twist?: number;
91
+ };
92
+ }>;
93
+ }) {
94
+ try {
95
+ // Strong validation for physics asset name
96
+ if (!params.physicsAssetName || params.physicsAssetName.trim() === '') {
97
+ return {
98
+ success: false,
99
+ message: 'Failed to setup ragdoll: Name cannot be empty',
100
+ error: 'Name cannot be empty'
101
+ };
102
+ }
103
+
104
+ // Check for invalid characters in name
105
+ if (params.physicsAssetName.includes('@') || params.physicsAssetName.includes('#') ||
106
+ params.physicsAssetName.includes('$') || params.physicsAssetName.includes('%')) {
107
+ return {
108
+ success: false,
109
+ message: 'Failed to setup ragdoll: Name contains invalid characters',
110
+ error: 'Name contains invalid characters'
111
+ };
112
+ }
113
+
114
+ // Check if skeleton path is provided instead of skeletal mesh
115
+ if (params.skeletonPath && (params.skeletonPath.includes('_Skeleton') ||
116
+ params.skeletonPath.includes('SK_Mannequin') && !params.skeletonPath.includes('SKM_'))) {
117
+ return {
118
+ success: false,
119
+ message: 'Failed to setup ragdoll: Must specify a valid skeletal mesh',
120
+ error: 'Must specify a valid skeletal mesh, not a skeleton'
121
+ };
122
+ }
123
+
124
+ // Validate and sanitize parameters
125
+ const validation = validateAssetParams({
126
+ name: params.physicsAssetName,
127
+ savePath: params.savePath || '/Game/Physics'
128
+ });
129
+
130
+ if (!validation.valid) {
131
+ return {
132
+ success: false,
133
+ message: `Failed to setup ragdoll: ${validation.error}`,
134
+ error: validation.error
135
+ };
136
+ }
137
+
138
+ const sanitizedParams = validation.sanitized;
139
+ const path = sanitizedParams.savePath || '/Game/Physics';
140
+
141
+ // Resolve skeletal mesh path
142
+ let meshPath = params.skeletonPath;
143
+
144
+ // Try to resolve skeleton to mesh mapping
145
+ const resolvedPath = resolveSkeletalMeshPath(meshPath);
146
+ if (resolvedPath && resolvedPath !== meshPath) {
147
+ console.error(`Auto-correcting path from ${meshPath} to ${resolvedPath}`);
148
+ meshPath = resolvedPath;
149
+ }
150
+
151
+ // Auto-resolve if it looks like a skeleton path or is empty
152
+ if (!meshPath || meshPath.includes('_Skeleton') || meshPath === 'None' || meshPath === '') {
153
+ console.error('Resolving skeletal mesh path...');
154
+ const resolvedMesh = await this.findValidSkeletalMesh();
155
+ if (resolvedMesh) {
156
+ meshPath = resolvedMesh;
157
+ console.error(`Using resolved skeletal mesh: ${meshPath}`);
158
+ }
159
+ }
160
+
161
+ // Add concurrency delay to prevent race conditions
162
+ await concurrencyDelay();
163
+
164
+ // IMPORTANT: Physics assets require a SKELETAL MESH, not a skeleton
165
+ // UE5 uses: /Game/Characters/Mannequins/Meshes/SKM_Manny_Simple or SKM_Quinn_Simple
166
+ // UE4 used: /Game/Mannequin/Character/Mesh/SK_Mannequin (which no longer exists)
167
+ // Fallback: /Engine/EngineMeshes/SkeletalCube
168
+
169
+ // Common skeleton paths that should be replaced with actual skeletal mesh paths
170
+ const skeletonToMeshMap: { [key: string]: string } = {
171
+ '/Game/Mannequin/Character/Mesh/UE4_Mannequin_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple',
172
+ '/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'
174
+ };
175
+
176
+ // Auto-fix common incorrect paths
177
+ let actualSkeletonPath = params.skeletonPath;
178
+ if (actualSkeletonPath && skeletonToMeshMap[actualSkeletonPath]) {
179
+ console.error(`Auto-correcting path from ${actualSkeletonPath} to ${skeletonToMeshMap[actualSkeletonPath]}`);
180
+ actualSkeletonPath = skeletonToMeshMap[actualSkeletonPath];
181
+ }
182
+
183
+ if (actualSkeletonPath && (actualSkeletonPath.includes('_Skeleton') || actualSkeletonPath.includes('SK_Mannequin'))) {
184
+ // This is likely a skeleton path, not a skeletal mesh
185
+ console.error('Warning: Path appears to be a skeleton, not a skeletal mesh. Auto-correcting to SKM_Manny_Simple.');
186
+ }
187
+
188
+ // Build Python script with resolved mesh path
189
+ const pythonScript = `
190
+ import unreal
191
+ import time
192
+
193
+ # Helper function to ensure asset persistence
194
+ def ensure_asset_persistence(asset_path):
195
+ try:
196
+ asset = unreal.EditorAssetLibrary.load_asset(asset_path)
197
+ if not asset:
198
+ return False
199
+
200
+ # Save the asset
201
+ saved = unreal.EditorAssetLibrary.save_asset(asset_path, only_if_is_dirty=False)
202
+ if saved:
203
+ print(f"Asset saved: {asset_path}")
204
+
205
+ # Refresh the asset registry minimally for the asset's directory
206
+ try:
207
+ asset_dir = asset_path.rsplit('/', 1)[0]
208
+ unreal.AssetRegistryHelpers.get_asset_registry().scan_paths_synchronous([asset_dir], True)
209
+ except Exception as _reg_e:
210
+ pass
211
+
212
+ # Small delay to ensure filesystem sync
213
+ time.sleep(0.1)
214
+
215
+ return saved
216
+ except Exception as e:
217
+ print(f"Error ensuring persistence: {e}")
218
+ return False
219
+
220
+ # Stop PIE if it's running
221
+ try:
222
+ if unreal.EditorLevelLibrary.is_playing_editor():
223
+ print("Stopping Play In Editor mode...")
224
+ unreal.EditorLevelLibrary.editor_end_play()
225
+ time.sleep(0.5)
226
+ except:
227
+ pass
228
+
229
+ # Main execution
230
+ success = False
231
+ error_msg = ""
232
+
233
+ # Log the attempt
234
+ print("Setting up ragdoll for ${meshPath}")
235
+
236
+ asset_path = "${path}"
237
+ asset_name = "${sanitizedParams.name}"
238
+ full_path = f"{asset_path}/{asset_name}"
239
+
240
+ try:
241
+ # Check if already exists
242
+ if unreal.EditorAssetLibrary.does_asset_exist(full_path):
243
+ print(f"Physics asset already exists at {full_path}")
244
+ existing = unreal.EditorAssetLibrary.load_asset(full_path)
245
+ if existing:
246
+ print(f"Loaded existing PhysicsAsset: {full_path}")
247
+ else:
248
+ # Try to load skeletal mesh first - it's required
249
+ skeletal_mesh_path = "${meshPath}"
250
+ skeletal_mesh = None
251
+
252
+ if skeletal_mesh_path and skeletal_mesh_path != "None":
253
+ if unreal.EditorAssetLibrary.does_asset_exist(skeletal_mesh_path):
254
+ asset = unreal.EditorAssetLibrary.load_asset(skeletal_mesh_path)
255
+ if asset:
256
+ if isinstance(asset, unreal.SkeletalMesh):
257
+ skeletal_mesh = asset
258
+ print(f"Loaded skeletal mesh: {skeletal_mesh_path}")
259
+ elif isinstance(asset, unreal.Skeleton):
260
+ error_msg = f"Provided path is a skeleton, not a skeletal mesh: {skeletal_mesh_path}"
261
+ print(f"Error: {error_msg}")
262
+ print(f"Error: Physics assets require a skeletal mesh, not just a skeleton")
263
+ else:
264
+ error_msg = f"Asset is not a skeletal mesh: {skeletal_mesh_path}"
265
+ print(f"Warning: {error_msg}")
266
+ else:
267
+ error_msg = f"Skeletal mesh not found at {skeletal_mesh_path}"
268
+ print(f"Error: {error_msg}")
269
+
270
+ if not skeletal_mesh:
271
+ if not error_msg:
272
+ error_msg = "Cannot create physics asset without a valid skeletal mesh"
273
+ print(f"Error: {error_msg}")
274
+ else:
275
+ # Create physics asset using a different approach
276
+ # Method 1: Direct creation with initialized factory
277
+ try:
278
+ factory = unreal.PhysicsAssetFactory()
279
+
280
+ # Create a transient package for the physics asset
281
+ # Ensure the directory exists
282
+ if not unreal.EditorAssetLibrary.does_directory_exist(asset_path):
283
+ unreal.EditorAssetLibrary.make_directory(asset_path)
284
+
285
+ # Alternative approach: Create physics asset from skeletal mesh
286
+ # This is the proper way in UE5
287
+ physics_asset = unreal.EditorSkeletalMeshLibrary.create_physics_asset(skeletal_mesh)
288
+
289
+ if physics_asset:
290
+ # Move/rename the physics asset to desired location
291
+ source_path = physics_asset.get_path_name()
292
+ if unreal.EditorAssetLibrary.rename_asset(source_path, full_path):
293
+ print(f"Successfully created and moved PhysicsAsset to {full_path}")
294
+ new_asset = physics_asset
295
+
296
+ # Ensure persistence
297
+ if ensure_asset_persistence(full_path):
298
+ # Verify it was saved
299
+ if unreal.EditorAssetLibrary.does_asset_exist(full_path):
300
+ print(f"Verified PhysicsAsset exists after save: {full_path}")
301
+ success = True
302
+ else:
303
+ error_msg = f"PhysicsAsset not found after save: {full_path}"
304
+ print(f"Warning: {error_msg}")
305
+ else:
306
+ error_msg = "Failed to persist physics asset"
307
+ print(f"Warning: {error_msg}")
308
+ else:
309
+ print(f"Created PhysicsAsset but couldn't move to {full_path}")
310
+ # Still consider it a success if we created it
311
+ new_asset = physics_asset
312
+ success = True
313
+ else:
314
+ error_msg = "Failed to create PhysicsAsset from skeletal mesh"
315
+ print(f"{error_msg}")
316
+ new_asset = None
317
+
318
+ except Exception as e:
319
+ print(f"Method 1 failed: {str(e)}")
320
+
321
+ # Method 2: Try older approach
322
+ try:
323
+ asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
324
+ factory = unreal.PhysicsAssetFactory()
325
+
326
+ # Try to initialize factory with the skeletal mesh
327
+ factory.create_physics_asset_from_skeletal_mesh = skeletal_mesh
328
+
329
+ new_asset = asset_tools.create_asset(
330
+ asset_name=asset_name,
331
+ package_path=asset_path,
332
+ asset_class=unreal.PhysicsAsset,
333
+ factory=factory
334
+ )
335
+
336
+ if new_asset:
337
+ print(f"Successfully created PhysicsAsset at {full_path} (Method 2)")
338
+ # Ensure persistence
339
+ if ensure_asset_persistence(full_path):
340
+ success = True
341
+ except Exception as e2:
342
+ error_msg = f"Method 2 also failed: {str(e2)}"
343
+ print(error_msg)
344
+ new_asset = None
345
+
346
+ # Final check
347
+ if new_asset and not success:
348
+ # Try one more save
349
+ if ensure_asset_persistence(full_path):
350
+ if unreal.EditorAssetLibrary.does_asset_exist(full_path):
351
+ success = True
352
+
353
+ except Exception as e:
354
+ error_msg = str(e)
355
+ print(f"Error: {error_msg}")
356
+ import traceback
357
+ traceback.print_exc()
358
+
359
+ # Output result markers for parsing
360
+ if success:
361
+ print("SUCCESS")
362
+ else:
363
+ print(f"FAILED: {error_msg}")
364
+
365
+ print("DONE")
366
+ `;
367
+
368
+
369
+ // Execute Python and parse response
370
+ try {
371
+ const response = await this.bridge.executePython(pythonScript);
372
+
373
+ // Parse the response to detect actual success or failure
374
+ const responseStr = typeof response === 'string' ? response : JSON.stringify(response);
375
+
376
+ // Check for explicit success/failure markers
377
+ if (responseStr.includes('SUCCESS')) {
378
+ return {
379
+ success: true,
380
+ message: `Ragdoll physics setup completed for ${sanitizedParams.name}`,
381
+ path: `${path}/${sanitizedParams.name}`
382
+ };
383
+ } else if (responseStr.includes('FAILED:')) {
384
+ // Extract error message after FAILED:
385
+ const failMatch = responseStr.match(/FAILED:\s*(.+)/);
386
+ const errorMsg = failMatch ? failMatch[1] : 'Unknown error';
387
+ return {
388
+ success: false,
389
+ message: `Failed to setup ragdoll: ${errorMsg}`,
390
+ error: errorMsg
391
+ };
392
+ } else {
393
+ // Check legacy error detection for backwards compatibility
394
+ const logOutput = response?.LogOutput || [];
395
+ const hasSkeletonError = logOutput.some((log: any) =>
396
+ log.Output && (log.Output.includes('skeleton, not a skeletal mesh') ||
397
+ log.Output.includes('Must specify a valid skeletal mesh')));
398
+
399
+ if (hasSkeletonError) {
400
+ return {
401
+ success: false,
402
+ message: 'Failed: Must specify a valid skeletal mesh',
403
+ error: 'The path points to a skeleton, not a skeletal mesh. Physics assets require a skeletal mesh.'
404
+ };
405
+ }
406
+
407
+ // Check for other error indicators
408
+ if (responseStr.includes('Error:') || responseStr.includes('error')) {
409
+ return {
410
+ success: false,
411
+ message: 'Failed to setup ragdoll physics',
412
+ error: responseStr
413
+ };
414
+ }
415
+
416
+ // Default to success if no errors detected
417
+ return {
418
+ success: true,
419
+ message: `Ragdoll physics processed for ${sanitizedParams.name}`,
420
+ path: `${path}/${sanitizedParams.name}`
421
+ };
422
+ }
423
+ } catch (error) {
424
+ return {
425
+ success: false,
426
+ message: 'Failed to setup ragdoll physics',
427
+ error: String(error)
428
+ };
429
+ }
430
+ } catch (err) {
431
+ return { success: false, error: `Failed to setup ragdoll: ${err}` };
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Create Physics Constraint
437
+ */
438
+ async createConstraint(params: {
439
+ name: string;
440
+ actor1: string;
441
+ actor2: string;
442
+ constraintType: 'Fixed' | 'Hinge' | 'Prismatic' | 'Ball' | 'Cone';
443
+ location: [number, number, number];
444
+ breakThreshold?: number;
445
+ limits?: {
446
+ swing1?: number;
447
+ swing2?: number;
448
+ twist?: number;
449
+ linear?: number;
450
+ };
451
+ }) {
452
+ try {
453
+ // Spawn constraint actor
454
+ const spawnCmd = `spawnactor /Script/Engine.PhysicsConstraintActor ${params.location[0]} ${params.location[1]} ${params.location[2]}`;
455
+ await this.bridge.executeConsoleCommand(spawnCmd);
456
+
457
+ // Configure constraint
458
+ const commands = [
459
+ `SetConstraintActors ${params.name} ${params.actor1} ${params.actor2}`,
460
+ `SetConstraintType ${params.name} ${params.constraintType}`
461
+ ];
462
+
463
+ if (params.breakThreshold) {
464
+ commands.push(`SetConstraintBreakThreshold ${params.name} ${params.breakThreshold}`);
465
+ }
466
+
467
+ if (params.limits) {
468
+ const limits = params.limits;
469
+ if (limits.swing1 !== undefined) {
470
+ commands.push(`SetConstraintSwing1 ${params.name} ${limits.swing1}`);
471
+ }
472
+ if (limits.swing2 !== undefined) {
473
+ commands.push(`SetConstraintSwing2 ${params.name} ${limits.swing2}`);
474
+ }
475
+ if (limits.twist !== undefined) {
476
+ commands.push(`SetConstraintTwist ${params.name} ${limits.twist}`);
477
+ }
478
+ if (limits.linear !== undefined) {
479
+ commands.push(`SetConstraintLinear ${params.name} ${limits.linear}`);
480
+ }
481
+ }
482
+
483
+ for (const cmd of commands) {
484
+ await this.bridge.executeConsoleCommand(cmd);
485
+ }
486
+
487
+ return {
488
+ success: true,
489
+ message: `Physics constraint ${params.name} created between ${params.actor1} and ${params.actor2}`
490
+ };
491
+ } catch (err) {
492
+ return { success: false, error: `Failed to create constraint: ${err}` };
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Setup Chaos Destruction
498
+ */
499
+ async setupDestruction(params: {
500
+ meshPath: string;
501
+ destructionName: string;
502
+ savePath?: string;
503
+ fractureSettings?: {
504
+ cellCount: number;
505
+ minimumVolumeSize: number;
506
+ seed: number;
507
+ };
508
+ damageThreshold?: number;
509
+ debrisLifetime?: number;
510
+ }) {
511
+ try {
512
+ const path = params.savePath || '/Game/Destruction';
513
+
514
+ const commands = [
515
+ `CreateGeometryCollection ${params.destructionName} ${params.meshPath} ${path}`
516
+ ];
517
+
518
+ // Configure fracture
519
+ if (params.fractureSettings) {
520
+ const settings = params.fractureSettings;
521
+ commands.push(
522
+ `FractureGeometry ${params.destructionName} ${settings.cellCount} ${settings.minimumVolumeSize} ${settings.seed}`
523
+ );
524
+ }
525
+
526
+ // Set damage threshold
527
+ if (params.damageThreshold) {
528
+ commands.push(`SetDamageThreshold ${params.destructionName} ${params.damageThreshold}`);
529
+ }
530
+
531
+ // Set debris lifetime
532
+ if (params.debrisLifetime) {
533
+ commands.push(`SetDebrisLifetime ${params.destructionName} ${params.debrisLifetime}`);
534
+ }
535
+
536
+ for (const cmd of commands) {
537
+ await this.bridge.executeConsoleCommand(cmd);
538
+ }
539
+
540
+ return {
541
+ success: true,
542
+ message: `Chaos destruction ${params.destructionName} created`,
543
+ path: `${path}/${params.destructionName}`
544
+ };
545
+ } catch (err) {
546
+ return { success: false, error: `Failed to setup destruction: ${err}` };
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Configure Vehicle Physics
552
+ */
553
+ async configureVehicle(params: {
554
+ vehicleName: string;
555
+ vehicleType: 'Car' | 'Bike' | 'Tank' | 'Aircraft';
556
+ wheels?: Array<{
557
+ name: string;
558
+ radius: number;
559
+ width: number;
560
+ mass: number;
561
+ isSteering: boolean;
562
+ isDriving: boolean;
563
+ }>;
564
+ engine?: {
565
+ maxRPM: number;
566
+ torqueCurve: Array<[number, number]>;
567
+ };
568
+ transmission?: {
569
+ gears: number[];
570
+ finalDriveRatio: number;
571
+ };
572
+ }) {
573
+ try {
574
+ const commands = [
575
+ `CreateVehicle ${params.vehicleName} ${params.vehicleType}`
576
+ ];
577
+
578
+ // Configure wheels
579
+ if (params.wheels) {
580
+ for (const wheel of params.wheels) {
581
+ commands.push(
582
+ `AddVehicleWheel ${params.vehicleName} ${wheel.name} ${wheel.radius} ${wheel.width} ${wheel.mass}`
583
+ );
584
+
585
+ if (wheel.isSteering) {
586
+ commands.push(`SetWheelSteering ${params.vehicleName} ${wheel.name} true`);
587
+ }
588
+ if (wheel.isDriving) {
589
+ commands.push(`SetWheelDriving ${params.vehicleName} ${wheel.name} true`);
590
+ }
591
+ }
592
+ }
593
+
594
+ // Configure engine
595
+ if (params.engine) {
596
+ commands.push(`SetEngineMaxRPM ${params.vehicleName} ${params.engine.maxRPM}`);
597
+
598
+ for (const [rpm, torque] of params.engine.torqueCurve) {
599
+ commands.push(`AddTorqueCurvePoint ${params.vehicleName} ${rpm} ${torque}`);
600
+ }
601
+ }
602
+
603
+ // Configure transmission
604
+ if (params.transmission) {
605
+ for (let i = 0; i < params.transmission.gears.length; i++) {
606
+ commands.push(
607
+ `SetGearRatio ${params.vehicleName} ${i} ${params.transmission.gears[i]}`
608
+ );
609
+ }
610
+ commands.push(
611
+ `SetFinalDriveRatio ${params.vehicleName} ${params.transmission.finalDriveRatio}`
612
+ );
613
+ }
614
+
615
+ for (const cmd of commands) {
616
+ await this.bridge.executeConsoleCommand(cmd);
617
+ }
618
+
619
+ return {
620
+ success: true,
621
+ message: `Vehicle ${params.vehicleName} configured`
622
+ };
623
+ } catch (err) {
624
+ return { success: false, error: `Failed to configure vehicle: ${err}` };
625
+ }
626
+ }
627
+
628
+ /**
629
+ * Apply Force or Impulse to Actor
630
+ */
631
+ async applyForce(params: {
632
+ actorName: string;
633
+ forceType: 'Force' | 'Impulse' | 'Velocity' | 'Torque';
634
+ vector: [number, number, number];
635
+ boneName?: string;
636
+ isLocal?: boolean;
637
+ }) {
638
+ try {
639
+ // Use Python to apply physics forces since console commands don't exist for this
640
+ const pythonCode = `
641
+ import unreal
642
+ import json
643
+
644
+ result = {"success": False, "message": "", "actor_found": False, "physics_enabled": False}
645
+
646
+ # Check if editor is in play mode first
647
+ try:
648
+ les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
649
+ if les and les.is_in_play_in_editor():
650
+ result["message"] = "Cannot apply physics while in Play In Editor mode. Please stop PIE first."
651
+ print(f"RESULT:{json.dumps(result)}")
652
+ # Exit early from this script
653
+ raise SystemExit(0)
654
+ except SystemExit:
655
+ # Re-raise the SystemExit to exit properly
656
+ raise
657
+ except:
658
+ pass # Continue if we can't check PIE state
659
+
660
+ try:
661
+ actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
662
+ actors = actor_subsystem.get_all_level_actors()
663
+ search_name = "${params.actorName}"
664
+
665
+ for actor in actors:
666
+ if actor:
667
+ # Check both actor name and label with case-insensitive partial matching
668
+ actor_name = actor.get_name()
669
+ actor_label = actor.get_actor_label()
670
+
671
+ if (search_name.lower() in actor_label.lower() or
672
+ actor_label.lower().startswith(search_name.lower() + "_") or
673
+ actor_label.lower() == search_name.lower() or
674
+ actor_name.lower() == search_name.lower()):
675
+
676
+ result["actor_found"] = True
677
+ # Get the primitive component if it exists
678
+ root = actor.get_editor_property('root_component')
679
+
680
+ if root and isinstance(root, unreal.PrimitiveComponent):
681
+ # Check if the component is static or movable
682
+ mobility = root.get_editor_property('mobility')
683
+ if mobility == unreal.ComponentMobility.STATIC:
684
+ # Try to set to movable first
685
+ try:
686
+ root.set_editor_property('mobility', unreal.ComponentMobility.MOVABLE)
687
+ except:
688
+ result["message"] = f"Actor {actor_label} has static mobility and cannot simulate physics"
689
+ break
690
+
691
+ # Ensure physics is enabled
692
+ try:
693
+ root.set_simulate_physics(True)
694
+ result["physics_enabled"] = True
695
+ except Exception as physics_err:
696
+ # If we can't enable physics, try applying force anyway (some actors respond without physics sim)
697
+ result["physics_enabled"] = False
698
+
699
+ force = unreal.Vector(${params.vector[0]}, ${params.vector[1]}, ${params.vector[2]})
700
+
701
+ if "${params.forceType}" == "Force":
702
+ root.add_force(force, 'None', False)
703
+ result["success"] = True
704
+ result["message"] = f"Applied Force to {actor_label}: {force}"
705
+ elif "${params.forceType}" == "Impulse":
706
+ root.add_impulse(force, 'None', False)
707
+ result["success"] = True
708
+ result["message"] = f"Applied Impulse to {actor_label}: {force}"
709
+ elif "${params.forceType}" == "Velocity":
710
+ root.set_physics_linear_velocity(force)
711
+ result["success"] = True
712
+ result["message"] = f"Set Velocity on {actor_label}: {force}"
713
+ elif "${params.forceType}" == "Torque":
714
+ root.add_torque_in_radians(force, 'None', False)
715
+ result["success"] = True
716
+ result["message"] = f"Applied Torque to {actor_label}: {force}"
717
+ else:
718
+ result["message"] = f"Actor {actor_label} doesn't have a physics-enabled component"
719
+ break
720
+
721
+ if not result["actor_found"]:
722
+ result["message"] = f"Actor not found: {search_name}"
723
+ # List actors with physics enabled for debugging
724
+ physics_actors = []
725
+ for actor in actors[:20]:
726
+ if actor:
727
+ label = actor.get_actor_label()
728
+ if "mesh" in label.lower() or "cube" in label.lower() or "static" in label.lower():
729
+ physics_actors.append(label)
730
+ if physics_actors:
731
+ result["available_actors"] = physics_actors
732
+
733
+ except Exception as e:
734
+ result["message"] = f"Error applying force: {e}"
735
+
736
+ print(f"RESULT:{json.dumps(result)}")
737
+ `.trim();
738
+
739
+ const response = await this.bridge.executePython(pythonCode);
740
+
741
+ // Extract output from Python response
742
+ let outputStr = '';
743
+ if (typeof response === 'object' && response !== null) {
744
+ // Check if it has LogOutput (standard Python execution response)
745
+ if (response.LogOutput && Array.isArray(response.LogOutput)) {
746
+ // Concatenate all log outputs
747
+ outputStr = response.LogOutput
748
+ .map((log: any) => log.Output || '')
749
+ .join('');
750
+ } else if ('result' in response) {
751
+ outputStr = String(response.result);
752
+ } else {
753
+ outputStr = JSON.stringify(response);
754
+ }
755
+ } else {
756
+ outputStr = String(response || '');
757
+ }
758
+
759
+ // Parse the result
760
+ const resultMatch = outputStr.match(/RESULT:(\{.*\})/);
761
+ if (resultMatch) {
762
+ try {
763
+ const forceResult = JSON.parse(resultMatch[1]);
764
+ if (!forceResult.success) {
765
+ return { success: false, error: forceResult.message };
766
+ }
767
+ return forceResult;
768
+ } catch {
769
+ // Fallback
770
+ if (outputStr.includes('Applied')) {
771
+ return { success: true, message: outputStr };
772
+ }
773
+ return { success: false, error: outputStr || 'Force application failed' };
774
+ }
775
+ } else {
776
+ // Check for error patterns
777
+ if (outputStr.includes('not found') || outputStr.includes('Error')) {
778
+ return { success: false, error: outputStr || 'Force application failed' };
779
+ }
780
+ // Only return success if we have clear indication of success
781
+ if (outputStr.includes('Applied')) {
782
+ return { success: true, message: `Applied ${params.forceType} to ${params.actorName}` };
783
+ }
784
+ return { success: false, error: 'No valid result from Python' };
785
+ }
786
+ } catch (err) {
787
+ return { success: false, error: `Failed to apply force: ${err}` };
788
+ }
789
+ }
790
+
791
+ /**
792
+ * Configure Cloth Simulation
793
+ */
794
+ async setupCloth(params: {
795
+ meshName: string;
796
+ clothPreset: 'Silk' | 'Leather' | 'Denim' | 'Rubber' | 'Custom';
797
+ customSettings?: {
798
+ stiffness?: number;
799
+ damping?: number;
800
+ friction?: number;
801
+ density?: number;
802
+ gravity?: number;
803
+ windVelocity?: [number, number, number];
804
+ };
805
+ }) {
806
+ try {
807
+ const commands = [
808
+ `EnableClothSimulation ${params.meshName}`,
809
+ `SetClothPreset ${params.meshName} ${params.clothPreset}`
810
+ ];
811
+
812
+ if (params.clothPreset === 'Custom' && params.customSettings) {
813
+ const settings = params.customSettings;
814
+
815
+ if (settings.stiffness !== undefined) {
816
+ commands.push(`SetClothStiffness ${params.meshName} ${settings.stiffness}`);
817
+ }
818
+ if (settings.damping !== undefined) {
819
+ commands.push(`SetClothDamping ${params.meshName} ${settings.damping}`);
820
+ }
821
+ if (settings.friction !== undefined) {
822
+ commands.push(`SetClothFriction ${params.meshName} ${settings.friction}`);
823
+ }
824
+ if (settings.density !== undefined) {
825
+ commands.push(`SetClothDensity ${params.meshName} ${settings.density}`);
826
+ }
827
+ if (settings.gravity !== undefined) {
828
+ commands.push(`SetClothGravity ${params.meshName} ${settings.gravity}`);
829
+ }
830
+ if (settings.windVelocity) {
831
+ const wind = settings.windVelocity;
832
+ commands.push(`SetClothWind ${params.meshName} ${wind[0]} ${wind[1]} ${wind[2]}`);
833
+ }
834
+ }
835
+
836
+ for (const cmd of commands) {
837
+ await this.bridge.executeConsoleCommand(cmd);
838
+ }
839
+
840
+ return {
841
+ success: true,
842
+ message: `Cloth simulation enabled for ${params.meshName}`
843
+ };
844
+ } catch (err) {
845
+ return { success: false, error: `Failed to setup cloth: ${err}` };
846
+ }
847
+ }
848
+
849
+ /**
850
+ * Create Fluid Simulation (Niagara-based)
851
+ */
852
+ async createFluidSimulation(params: {
853
+ name: string;
854
+ fluidType: 'Water' | 'Smoke' | 'Fire' | 'Lava' | 'Custom';
855
+ location: [number, number, number];
856
+ volume: [number, number, number];
857
+ customSettings?: {
858
+ viscosity?: number;
859
+ density?: number;
860
+ temperature?: number;
861
+ turbulence?: number;
862
+ color?: [number, number, number, number];
863
+ };
864
+ }) {
865
+ try {
866
+ const locStr = `${params.location[0]} ${params.location[1]} ${params.location[2]}`;
867
+ const volStr = `${params.volume[0]} ${params.volume[1]} ${params.volume[2]}`;
868
+
869
+ const commands = [
870
+ `CreateFluidSimulation ${params.name} ${params.fluidType} ${locStr} ${volStr}`
871
+ ];
872
+
873
+ if (params.customSettings) {
874
+ const settings = params.customSettings;
875
+
876
+ if (settings.viscosity !== undefined) {
877
+ commands.push(`SetFluidViscosity ${params.name} ${settings.viscosity}`);
878
+ }
879
+ if (settings.density !== undefined) {
880
+ commands.push(`SetFluidDensity ${params.name} ${settings.density}`);
881
+ }
882
+ if (settings.temperature !== undefined) {
883
+ commands.push(`SetFluidTemperature ${params.name} ${settings.temperature}`);
884
+ }
885
+ if (settings.turbulence !== undefined) {
886
+ commands.push(`SetFluidTurbulence ${params.name} ${settings.turbulence}`);
887
+ }
888
+ if (settings.color) {
889
+ const color = settings.color;
890
+ commands.push(
891
+ `SetFluidColor ${params.name} ${color[0]} ${color[1]} ${color[2]} ${color[3]}`
892
+ );
893
+ }
894
+ }
895
+
896
+ for (const cmd of commands) {
897
+ await this.bridge.executeConsoleCommand(cmd);
898
+ }
899
+
900
+ return {
901
+ success: true,
902
+ message: `Fluid simulation ${params.name} created`
903
+ };
904
+ } catch (err) {
905
+ return { success: false, error: `Failed to create fluid simulation: ${err}` };
906
+ }
907
+ }
908
+
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
+ }