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.
- package/.dockerignore +57 -0
- package/.env.production +25 -0
- package/.eslintrc.json +54 -0
- package/.github/workflows/publish-mcp.yml +75 -0
- package/Dockerfile +54 -0
- package/LICENSE +21 -0
- package/Public/icon.png +0 -0
- package/README.md +209 -0
- package/claude_desktop_config_example.json +13 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +7 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +484 -0
- package/dist/prompts/index.d.ts +14 -0
- package/dist/prompts/index.js +38 -0
- package/dist/python-utils.d.ts +29 -0
- package/dist/python-utils.js +54 -0
- package/dist/resources/actors.d.ts +13 -0
- package/dist/resources/actors.js +83 -0
- package/dist/resources/assets.d.ts +23 -0
- package/dist/resources/assets.js +245 -0
- package/dist/resources/levels.d.ts +17 -0
- package/dist/resources/levels.js +94 -0
- package/dist/tools/actors.d.ts +51 -0
- package/dist/tools/actors.js +459 -0
- package/dist/tools/animation.d.ts +196 -0
- package/dist/tools/animation.js +579 -0
- package/dist/tools/assets.d.ts +21 -0
- package/dist/tools/assets.js +304 -0
- package/dist/tools/audio.d.ts +170 -0
- package/dist/tools/audio.js +416 -0
- package/dist/tools/blueprint.d.ts +144 -0
- package/dist/tools/blueprint.js +652 -0
- package/dist/tools/build_environment_advanced.d.ts +66 -0
- package/dist/tools/build_environment_advanced.js +484 -0
- package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
- package/dist/tools/consolidated-tool-definitions.js +607 -0
- package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
- package/dist/tools/consolidated-tool-handlers.js +1050 -0
- package/dist/tools/debug.d.ts +185 -0
- package/dist/tools/debug.js +265 -0
- package/dist/tools/editor.d.ts +88 -0
- package/dist/tools/editor.js +365 -0
- package/dist/tools/engine.d.ts +30 -0
- package/dist/tools/engine.js +36 -0
- package/dist/tools/foliage.d.ts +155 -0
- package/dist/tools/foliage.js +525 -0
- package/dist/tools/introspection.d.ts +98 -0
- package/dist/tools/introspection.js +683 -0
- package/dist/tools/landscape.d.ts +158 -0
- package/dist/tools/landscape.js +375 -0
- package/dist/tools/level.d.ts +110 -0
- package/dist/tools/level.js +362 -0
- package/dist/tools/lighting.d.ts +159 -0
- package/dist/tools/lighting.js +1179 -0
- package/dist/tools/materials.d.ts +34 -0
- package/dist/tools/materials.js +146 -0
- package/dist/tools/niagara.d.ts +145 -0
- package/dist/tools/niagara.js +289 -0
- package/dist/tools/performance.d.ts +163 -0
- package/dist/tools/performance.js +412 -0
- package/dist/tools/physics.d.ts +189 -0
- package/dist/tools/physics.js +784 -0
- package/dist/tools/rc.d.ts +110 -0
- package/dist/tools/rc.js +363 -0
- package/dist/tools/sequence.d.ts +112 -0
- package/dist/tools/sequence.js +675 -0
- package/dist/tools/tool-definitions.d.ts +4919 -0
- package/dist/tools/tool-definitions.js +891 -0
- package/dist/tools/tool-handlers.d.ts +47 -0
- package/dist/tools/tool-handlers.js +830 -0
- package/dist/tools/ui.d.ts +171 -0
- package/dist/tools/ui.js +337 -0
- package/dist/tools/visual.d.ts +29 -0
- package/dist/tools/visual.js +67 -0
- package/dist/types/env.d.ts +10 -0
- package/dist/types/env.js +18 -0
- package/dist/types/index.d.ts +323 -0
- package/dist/types/index.js +28 -0
- package/dist/types/tool-types.d.ts +274 -0
- package/dist/types/tool-types.js +13 -0
- package/dist/unreal-bridge.d.ts +126 -0
- package/dist/unreal-bridge.js +992 -0
- package/dist/utils/cache-manager.d.ts +64 -0
- package/dist/utils/cache-manager.js +176 -0
- package/dist/utils/error-handler.d.ts +66 -0
- package/dist/utils/error-handler.js +243 -0
- package/dist/utils/errors.d.ts +133 -0
- package/dist/utils/errors.js +256 -0
- package/dist/utils/http.d.ts +26 -0
- package/dist/utils/http.js +135 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +32 -0
- package/dist/utils/normalize.d.ts +17 -0
- package/dist/utils/normalize.js +49 -0
- package/dist/utils/response-validator.d.ts +34 -0
- package/dist/utils/response-validator.js +121 -0
- package/dist/utils/safe-json.d.ts +4 -0
- package/dist/utils/safe-json.js +97 -0
- package/dist/utils/stdio-redirect.d.ts +2 -0
- package/dist/utils/stdio-redirect.js +20 -0
- package/dist/utils/validation.d.ts +50 -0
- package/dist/utils/validation.js +173 -0
- package/mcp-config-example.json +14 -0
- package/package.json +63 -0
- package/server.json +60 -0
- package/src/cli.ts +7 -0
- package/src/index.ts +543 -0
- package/src/prompts/index.ts +51 -0
- package/src/python/editor_compat.py +181 -0
- package/src/python-utils.ts +57 -0
- package/src/resources/actors.ts +92 -0
- package/src/resources/assets.ts +251 -0
- package/src/resources/levels.ts +83 -0
- package/src/tools/actors.ts +480 -0
- package/src/tools/animation.ts +713 -0
- package/src/tools/assets.ts +305 -0
- package/src/tools/audio.ts +548 -0
- package/src/tools/blueprint.ts +736 -0
- package/src/tools/build_environment_advanced.ts +526 -0
- package/src/tools/consolidated-tool-definitions.ts +619 -0
- package/src/tools/consolidated-tool-handlers.ts +1093 -0
- package/src/tools/debug.ts +368 -0
- package/src/tools/editor.ts +360 -0
- package/src/tools/engine.ts +32 -0
- package/src/tools/foliage.ts +652 -0
- package/src/tools/introspection.ts +778 -0
- package/src/tools/landscape.ts +523 -0
- package/src/tools/level.ts +410 -0
- package/src/tools/lighting.ts +1316 -0
- package/src/tools/materials.ts +148 -0
- package/src/tools/niagara.ts +312 -0
- package/src/tools/performance.ts +549 -0
- package/src/tools/physics.ts +924 -0
- package/src/tools/rc.ts +437 -0
- package/src/tools/sequence.ts +791 -0
- package/src/tools/tool-definitions.ts +907 -0
- package/src/tools/tool-handlers.ts +941 -0
- package/src/tools/ui.ts +499 -0
- package/src/tools/visual.ts +60 -0
- package/src/types/env.ts +27 -0
- package/src/types/index.ts +414 -0
- package/src/types/tool-types.ts +343 -0
- package/src/unreal-bridge.ts +1118 -0
- package/src/utils/cache-manager.ts +213 -0
- package/src/utils/error-handler.ts +320 -0
- package/src/utils/errors.ts +312 -0
- package/src/utils/http.ts +184 -0
- package/src/utils/logger.ts +30 -0
- package/src/utils/normalize.ts +54 -0
- package/src/utils/response-validator.ts +145 -0
- package/src/utils/safe-json.ts +112 -0
- package/src/utils/stdio-redirect.ts +18 -0
- package/src/utils/validation.ts +212 -0
- 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
|