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,713 @@
|
|
|
1
|
+
import { UnrealBridge } from '../unreal-bridge.js';
|
|
2
|
+
import { validateAssetParams, concurrencyDelay } from '../utils/validation.js';
|
|
3
|
+
|
|
4
|
+
export class AnimationTools {
|
|
5
|
+
constructor(private bridge: UnrealBridge) {}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create Animation Blueprint
|
|
9
|
+
*/
|
|
10
|
+
async createAnimationBlueprint(params: {
|
|
11
|
+
name: string;
|
|
12
|
+
skeletonPath: string;
|
|
13
|
+
savePath?: string;
|
|
14
|
+
}) {
|
|
15
|
+
try {
|
|
16
|
+
// Strong input validation with expected error messages
|
|
17
|
+
if (!params.name || params.name.trim() === '') {
|
|
18
|
+
return {
|
|
19
|
+
success: false,
|
|
20
|
+
message: 'Failed: Name cannot be empty',
|
|
21
|
+
error: 'Name cannot be empty'
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check for whitespace issues
|
|
26
|
+
if (params.name.includes(' ') || params.name.startsWith(' ') || params.name.endsWith(' ')) {
|
|
27
|
+
return {
|
|
28
|
+
success: false,
|
|
29
|
+
message: 'Failed to create Animation Blueprint: Name contains invalid whitespace',
|
|
30
|
+
error: 'Name contains invalid whitespace'
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check for SQL injection patterns
|
|
35
|
+
if (params.name.toLowerCase().includes('drop') || params.name.toLowerCase().includes('delete') ||
|
|
36
|
+
params.name.includes(';') || params.name.includes('--')) {
|
|
37
|
+
return {
|
|
38
|
+
success: false,
|
|
39
|
+
message: 'Failed to create Animation Blueprint: Name contains invalid characters',
|
|
40
|
+
error: 'Name contains invalid characters'
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check save path starts with /
|
|
45
|
+
if (params.savePath && !params.savePath.startsWith('/')) {
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
message: 'Failed to create Animation Blueprint: Path must start with /',
|
|
49
|
+
error: 'Path must start with /'
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Now validate and sanitize for actual use
|
|
54
|
+
const validation = validateAssetParams({
|
|
55
|
+
name: params.name,
|
|
56
|
+
savePath: params.savePath || '/Game/Animations'
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!validation.valid) {
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
message: `Failed to create Animation Blueprint: ${validation.error}`,
|
|
63
|
+
error: validation.error
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const sanitizedParams = validation.sanitized;
|
|
68
|
+
const path = sanitizedParams.savePath || '/Game/Animations';
|
|
69
|
+
|
|
70
|
+
// Add concurrency delay to prevent race conditions
|
|
71
|
+
await concurrencyDelay();
|
|
72
|
+
|
|
73
|
+
// Enhanced Python script with proper persistence and error detection
|
|
74
|
+
const pythonScript = `
|
|
75
|
+
import unreal
|
|
76
|
+
import time
|
|
77
|
+
|
|
78
|
+
# Helper function to ensure asset persistence
|
|
79
|
+
def ensure_asset_persistence(asset_path):
|
|
80
|
+
"""Ensure asset is properly saved and registered"""
|
|
81
|
+
try:
|
|
82
|
+
# Load the asset to ensure it's in memory
|
|
83
|
+
asset = unreal.EditorAssetLibrary.load_asset(asset_path)
|
|
84
|
+
if not asset:
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
# Save the asset
|
|
88
|
+
saved = unreal.EditorAssetLibrary.save_asset(asset_path, only_if_is_dirty=False)
|
|
89
|
+
if saved:
|
|
90
|
+
print(f"Asset saved: {asset_path}")
|
|
91
|
+
|
|
92
|
+
# Refresh the asset registry for the asset's directory only
|
|
93
|
+
try:
|
|
94
|
+
asset_dir = asset_path.rsplit('/', 1)[0]
|
|
95
|
+
unreal.AssetRegistryHelpers.get_asset_registry().scan_paths_synchronous([asset_dir], True)
|
|
96
|
+
except Exception as _reg_e:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
# Small delay to ensure filesystem sync
|
|
100
|
+
time.sleep(0.1)
|
|
101
|
+
|
|
102
|
+
return saved
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f"Error ensuring persistence: {e}")
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
# Stop PIE if it's running
|
|
108
|
+
try:
|
|
109
|
+
if unreal.EditorLevelLibrary.is_playing_editor():
|
|
110
|
+
print("Stopping Play In Editor mode...")
|
|
111
|
+
unreal.EditorLevelLibrary.editor_end_play()
|
|
112
|
+
# Small delay to ensure editor fully exits play mode
|
|
113
|
+
import time as _t
|
|
114
|
+
_t.sleep(0.5)
|
|
115
|
+
except Exception as _e:
|
|
116
|
+
# Try alternative check
|
|
117
|
+
try:
|
|
118
|
+
play_world = unreal.EditorLevelLibrary.get_editor_world()
|
|
119
|
+
if play_world and play_world.is_play_in_editor():
|
|
120
|
+
print("Stopping PIE via alternative method...")
|
|
121
|
+
unreal.EditorLevelLibrary.editor_end_play()
|
|
122
|
+
import time as _t2
|
|
123
|
+
_t2.sleep(0.5)
|
|
124
|
+
except:
|
|
125
|
+
pass # Continue if we can't check/stop play mode
|
|
126
|
+
|
|
127
|
+
# Main execution
|
|
128
|
+
success = False
|
|
129
|
+
error_msg = ""
|
|
130
|
+
|
|
131
|
+
# Log the attempt
|
|
132
|
+
print("Creating animation blueprint: ${sanitizedParams.name}")
|
|
133
|
+
|
|
134
|
+
asset_path = "${path}"
|
|
135
|
+
asset_name = "${sanitizedParams.name}"
|
|
136
|
+
full_path = f"{asset_path}/{asset_name}"
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
# Check if already exists
|
|
140
|
+
if unreal.EditorAssetLibrary.does_asset_exist(full_path):
|
|
141
|
+
print(f"Asset already exists at {full_path}")
|
|
142
|
+
# Load and return existing
|
|
143
|
+
existing = unreal.EditorAssetLibrary.load_asset(full_path)
|
|
144
|
+
if existing:
|
|
145
|
+
print(f"Loaded existing AnimBlueprint: {full_path}")
|
|
146
|
+
success = True
|
|
147
|
+
else:
|
|
148
|
+
error_msg = f"Could not load existing asset at {full_path}"
|
|
149
|
+
print(f"Warning: {error_msg}")
|
|
150
|
+
else:
|
|
151
|
+
# Try to create new animation blueprint
|
|
152
|
+
factory = unreal.AnimBlueprintFactory()
|
|
153
|
+
|
|
154
|
+
# Try to load skeleton if provided
|
|
155
|
+
skeleton_path = "${params.skeletonPath}"
|
|
156
|
+
skeleton = None
|
|
157
|
+
skeleton_set = False
|
|
158
|
+
|
|
159
|
+
if skeleton_path and skeleton_path != "None":
|
|
160
|
+
if unreal.EditorAssetLibrary.does_asset_exist(skeleton_path):
|
|
161
|
+
skeleton = unreal.EditorAssetLibrary.load_asset(skeleton_path)
|
|
162
|
+
if skeleton and isinstance(skeleton, unreal.Skeleton):
|
|
163
|
+
# Different Unreal versions use different attribute names
|
|
164
|
+
try:
|
|
165
|
+
factory.target_skeleton = skeleton
|
|
166
|
+
skeleton_set = True
|
|
167
|
+
print(f"Using skeleton: {skeleton_path}")
|
|
168
|
+
except AttributeError:
|
|
169
|
+
try:
|
|
170
|
+
factory.skeleton = skeleton
|
|
171
|
+
skeleton_set = True
|
|
172
|
+
print(f"Using skeleton (alternate): {skeleton_path}")
|
|
173
|
+
except AttributeError:
|
|
174
|
+
# In some versions, the skeleton is set differently
|
|
175
|
+
try:
|
|
176
|
+
factory.set_editor_property('target_skeleton', skeleton)
|
|
177
|
+
skeleton_set = True
|
|
178
|
+
print(f"Using skeleton (property): {skeleton_path}")
|
|
179
|
+
except:
|
|
180
|
+
print(f"Warning: Could not set skeleton on factory")
|
|
181
|
+
else:
|
|
182
|
+
error_msg = f"Invalid skeleton at {skeleton_path}"
|
|
183
|
+
print(f"Warning: {error_msg}")
|
|
184
|
+
else:
|
|
185
|
+
print(f"Warning: Skeleton not found at {skeleton_path}, creating without skeleton")
|
|
186
|
+
|
|
187
|
+
# Create the asset
|
|
188
|
+
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
|
189
|
+
new_asset = asset_tools.create_asset(
|
|
190
|
+
asset_name=asset_name,
|
|
191
|
+
package_path=asset_path,
|
|
192
|
+
asset_class=unreal.AnimBlueprint,
|
|
193
|
+
factory=factory
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if new_asset:
|
|
197
|
+
print(f"Successfully created AnimBlueprint at {full_path}")
|
|
198
|
+
|
|
199
|
+
# Ensure persistence
|
|
200
|
+
if ensure_asset_persistence(full_path):
|
|
201
|
+
# Verify it was saved
|
|
202
|
+
if unreal.EditorAssetLibrary.does_asset_exist(full_path):
|
|
203
|
+
print(f"Verified asset exists after save: {full_path}")
|
|
204
|
+
success = True
|
|
205
|
+
else:
|
|
206
|
+
error_msg = f"Asset not found after save: {full_path}"
|
|
207
|
+
print(f"Warning: {error_msg}")
|
|
208
|
+
else:
|
|
209
|
+
error_msg = "Failed to persist asset"
|
|
210
|
+
print(f"Warning: {error_msg}")
|
|
211
|
+
else:
|
|
212
|
+
error_msg = f"Failed to create AnimBlueprint {asset_name}"
|
|
213
|
+
print(error_msg)
|
|
214
|
+
|
|
215
|
+
except Exception as e:
|
|
216
|
+
error_msg = str(e)
|
|
217
|
+
print(f"Error: {error_msg}")
|
|
218
|
+
import traceback
|
|
219
|
+
traceback.print_exc()
|
|
220
|
+
|
|
221
|
+
# Output result markers for parsing
|
|
222
|
+
if success:
|
|
223
|
+
print("SUCCESS")
|
|
224
|
+
else:
|
|
225
|
+
print(f"FAILED: {error_msg}")
|
|
226
|
+
|
|
227
|
+
print("DONE")
|
|
228
|
+
`;
|
|
229
|
+
|
|
230
|
+
// Execute Python and parse the output
|
|
231
|
+
try {
|
|
232
|
+
const response = await this.bridge.executePython(pythonScript);
|
|
233
|
+
|
|
234
|
+
// Parse the response to detect actual success or failure
|
|
235
|
+
const responseStr = typeof response === 'string' ? response : JSON.stringify(response);
|
|
236
|
+
|
|
237
|
+
// Check for explicit success/failure markers
|
|
238
|
+
if (responseStr.includes('SUCCESS')) {
|
|
239
|
+
return {
|
|
240
|
+
success: true,
|
|
241
|
+
message: `Animation Blueprint ${sanitizedParams.name} created successfully`,
|
|
242
|
+
path: `${path}/${sanitizedParams.name}`
|
|
243
|
+
};
|
|
244
|
+
} else if (responseStr.includes('FAILED:')) {
|
|
245
|
+
// Extract error message after FAILED:
|
|
246
|
+
const failMatch = responseStr.match(/FAILED:\s*(.+)/); const errorMsg = failMatch ? failMatch[1] : 'Unknown error';
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
message: `Failed to create Animation Blueprint: ${errorMsg}`,
|
|
250
|
+
error: errorMsg
|
|
251
|
+
};
|
|
252
|
+
} else {
|
|
253
|
+
// If no explicit markers, check for other error indicators
|
|
254
|
+
if (responseStr.includes('Error:') || responseStr.includes('error') ||
|
|
255
|
+
responseStr.includes('failed') || responseStr.includes('Failed')) {
|
|
256
|
+
return {
|
|
257
|
+
success: false,
|
|
258
|
+
message: 'Failed to create Animation Blueprint',
|
|
259
|
+
error: responseStr
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Assume success if no errors detected
|
|
264
|
+
return {
|
|
265
|
+
success: true,
|
|
266
|
+
message: `Animation Blueprint ${sanitizedParams.name} processed`,
|
|
267
|
+
path: `${path}/${sanitizedParams.name}`
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
return {
|
|
272
|
+
success: false,
|
|
273
|
+
message: 'Failed to create Animation Blueprint',
|
|
274
|
+
error: String(error)
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
} catch (err) {
|
|
278
|
+
return { success: false, error: `Failed to create AnimBlueprint: ${err}` };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Add State Machine to Animation Blueprint
|
|
284
|
+
*/
|
|
285
|
+
async addStateMachine(params: {
|
|
286
|
+
blueprintPath: string;
|
|
287
|
+
machineName: string;
|
|
288
|
+
states: Array<{
|
|
289
|
+
name: string;
|
|
290
|
+
animation?: string;
|
|
291
|
+
isEntry?: boolean;
|
|
292
|
+
isExit?: boolean;
|
|
293
|
+
}>;
|
|
294
|
+
transitions?: Array<{
|
|
295
|
+
sourceState: string;
|
|
296
|
+
targetState: string;
|
|
297
|
+
condition?: string;
|
|
298
|
+
}>;
|
|
299
|
+
}) {
|
|
300
|
+
try {
|
|
301
|
+
// State machines are complex - we'll use console commands for basic setup
|
|
302
|
+
const commands = [
|
|
303
|
+
`AddAnimStateMachine ${params.blueprintPath} ${params.machineName}`
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
// Add states
|
|
307
|
+
for (const state of params.states) {
|
|
308
|
+
commands.push(
|
|
309
|
+
`AddAnimState ${params.blueprintPath} ${params.machineName} ${state.name} ${state.animation || ''}`
|
|
310
|
+
);
|
|
311
|
+
if (state.isEntry) {
|
|
312
|
+
commands.push(`SetAnimStateEntry ${params.blueprintPath} ${params.machineName} ${state.name}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Add transitions
|
|
317
|
+
if (params.transitions) {
|
|
318
|
+
for (const transition of params.transitions) {
|
|
319
|
+
commands.push(
|
|
320
|
+
`AddAnimTransition ${params.blueprintPath} ${params.machineName} ${transition.sourceState} ${transition.targetState}`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
for (const cmd of commands) {
|
|
326
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
success: true,
|
|
331
|
+
message: `State machine ${params.machineName} added to ${params.blueprintPath}`
|
|
332
|
+
};
|
|
333
|
+
} catch (err) {
|
|
334
|
+
return { success: false, error: `Failed to add state machine: ${err}` };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Create Animation Montage
|
|
340
|
+
*/
|
|
341
|
+
async createMontage(params: {
|
|
342
|
+
name: string;
|
|
343
|
+
animationSequence: string;
|
|
344
|
+
savePath?: string;
|
|
345
|
+
sections?: Array<{
|
|
346
|
+
name: string;
|
|
347
|
+
startTime: number;
|
|
348
|
+
endTime: number;
|
|
349
|
+
}>;
|
|
350
|
+
notifies?: Array<{
|
|
351
|
+
name: string;
|
|
352
|
+
time: number;
|
|
353
|
+
}>;
|
|
354
|
+
}) {
|
|
355
|
+
try {
|
|
356
|
+
const path = params.savePath || '/Game/Animations/Montages';
|
|
357
|
+
const commands = [
|
|
358
|
+
`CreateAsset AnimMontage ${params.name} ${path}`,
|
|
359
|
+
`SetMontageAnimation ${params.name} ${params.animationSequence}`
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
// Add sections
|
|
363
|
+
if (params.sections) {
|
|
364
|
+
for (const section of params.sections) {
|
|
365
|
+
commands.push(
|
|
366
|
+
`AddMontageSection ${params.name} ${section.name} ${section.startTime} ${section.endTime}`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Add notifies
|
|
372
|
+
if (params.notifies) {
|
|
373
|
+
for (const notify of params.notifies) {
|
|
374
|
+
commands.push(
|
|
375
|
+
`AddMontageNotify ${params.name} ${notify.name} ${notify.time}`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
for (const cmd of commands) {
|
|
381
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
success: true,
|
|
386
|
+
message: `Animation Montage ${params.name} created`,
|
|
387
|
+
path: `${path}/${params.name}`
|
|
388
|
+
};
|
|
389
|
+
} catch (err) {
|
|
390
|
+
return { success: false, error: `Failed to create montage: ${err}` };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Create Blend Space
|
|
396
|
+
*/
|
|
397
|
+
async createBlendSpace(params: {
|
|
398
|
+
name: string;
|
|
399
|
+
skeletonPath: string;
|
|
400
|
+
savePath?: string;
|
|
401
|
+
dimensions: 1 | 2;
|
|
402
|
+
horizontalAxis?: {
|
|
403
|
+
name: string;
|
|
404
|
+
minValue: number;
|
|
405
|
+
maxValue: number;
|
|
406
|
+
};
|
|
407
|
+
verticalAxis?: {
|
|
408
|
+
name: string;
|
|
409
|
+
minValue: number;
|
|
410
|
+
maxValue: number;
|
|
411
|
+
};
|
|
412
|
+
samples?: Array<{
|
|
413
|
+
animation: string;
|
|
414
|
+
x: number;
|
|
415
|
+
y?: number;
|
|
416
|
+
}>;
|
|
417
|
+
}) {
|
|
418
|
+
try {
|
|
419
|
+
const path = params.savePath || '/Game/Animations/BlendSpaces';
|
|
420
|
+
const blendSpaceType = params.dimensions === 1 ? 'BlendSpace1D' : 'BlendSpace';
|
|
421
|
+
|
|
422
|
+
// These commands don't exist, return a message about limitations
|
|
423
|
+
const commands = [
|
|
424
|
+
`echo Creating ${blendSpaceType} ${params.name} at ${path}`
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
// Configure axes
|
|
428
|
+
if (params.horizontalAxis) {
|
|
429
|
+
commands.push(
|
|
430
|
+
`SetBlendSpaceAxis ${params.name} Horizontal ${params.horizontalAxis.name} ${params.horizontalAxis.minValue} ${params.horizontalAxis.maxValue}`
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (params.dimensions === 2 && params.verticalAxis) {
|
|
435
|
+
commands.push(
|
|
436
|
+
`SetBlendSpaceAxis ${params.name} Vertical ${params.verticalAxis.name} ${params.verticalAxis.minValue} ${params.verticalAxis.maxValue}`
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Add sample animations
|
|
441
|
+
if (params.samples) {
|
|
442
|
+
for (const sample of params.samples) {
|
|
443
|
+
const coords = params.dimensions === 1 ? `${sample.x}` : `${sample.x} ${sample.y || 0}`;
|
|
444
|
+
commands.push(
|
|
445
|
+
`AddBlendSpaceSample ${params.name} ${sample.animation} ${coords}`
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
for (const cmd of commands) {
|
|
451
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
success: true,
|
|
456
|
+
message: `Blend Space ${params.name} created`,
|
|
457
|
+
path: `${path}/${params.name}`
|
|
458
|
+
};
|
|
459
|
+
} catch (err) {
|
|
460
|
+
return { success: false, error: `Failed to create blend space: ${err}` };
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Setup Control Rig
|
|
466
|
+
*/
|
|
467
|
+
async setupControlRig(params: {
|
|
468
|
+
name: string;
|
|
469
|
+
skeletonPath: string;
|
|
470
|
+
savePath?: string;
|
|
471
|
+
controls?: Array<{
|
|
472
|
+
name: string;
|
|
473
|
+
type: 'Transform' | 'Float' | 'Bool' | 'Vector';
|
|
474
|
+
bone?: string;
|
|
475
|
+
defaultValue?: any;
|
|
476
|
+
}>;
|
|
477
|
+
}) {
|
|
478
|
+
try {
|
|
479
|
+
const path = params.savePath || '/Game/Animations';
|
|
480
|
+
|
|
481
|
+
// Validate path length (Unreal has a 260 character limit)
|
|
482
|
+
const fullPath = `${path}/${params.name}`;
|
|
483
|
+
if (fullPath.length > 260) {
|
|
484
|
+
return {
|
|
485
|
+
success: false,
|
|
486
|
+
message: `Failed: Path too long (${fullPath.length} characters)`,
|
|
487
|
+
error: 'Unreal Engine paths must be less than 260 characters'
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const commands = [
|
|
492
|
+
`CreateAsset ControlRig ${params.name} ${path}`,
|
|
493
|
+
`SetControlRigSkeleton ${params.name} ${params.skeletonPath}`
|
|
494
|
+
];
|
|
495
|
+
|
|
496
|
+
// Add controls
|
|
497
|
+
if (params.controls) {
|
|
498
|
+
for (const control of params.controls) {
|
|
499
|
+
commands.push(
|
|
500
|
+
`AddControlRigControl ${params.name} ${control.name} ${control.type} ${control.bone || ''}`
|
|
501
|
+
);
|
|
502
|
+
if (control.defaultValue !== undefined) {
|
|
503
|
+
commands.push(
|
|
504
|
+
`SetControlRigDefault ${params.name} ${control.name} ${JSON.stringify(control.defaultValue)}`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
for (const cmd of commands) {
|
|
511
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
success: true,
|
|
516
|
+
message: `Control Rig ${params.name} created`,
|
|
517
|
+
path: `${path}/${params.name}`
|
|
518
|
+
};
|
|
519
|
+
} catch (err) {
|
|
520
|
+
return { success: false, error: `Failed to setup control rig: ${err}` };
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Create Level Sequence (for cinematics)
|
|
526
|
+
*/
|
|
527
|
+
async createLevelSequence(params: {
|
|
528
|
+
name: string;
|
|
529
|
+
savePath?: string;
|
|
530
|
+
frameRate?: number;
|
|
531
|
+
duration?: number;
|
|
532
|
+
tracks?: Array<{
|
|
533
|
+
actorName: string;
|
|
534
|
+
trackType: 'Transform' | 'Animation' | 'Camera' | 'Event';
|
|
535
|
+
keyframes?: Array<{
|
|
536
|
+
time: number;
|
|
537
|
+
value: any;
|
|
538
|
+
}>;
|
|
539
|
+
}>;
|
|
540
|
+
}) {
|
|
541
|
+
try {
|
|
542
|
+
const path = params.savePath || '/Game/Cinematics';
|
|
543
|
+
|
|
544
|
+
const commands = [
|
|
545
|
+
`CreateAsset LevelSequence ${params.name} ${path}`,
|
|
546
|
+
`SetSequenceFrameRate ${params.name} ${params.frameRate || 30}`,
|
|
547
|
+
`SetSequenceDuration ${params.name} ${params.duration || 5}`
|
|
548
|
+
];
|
|
549
|
+
|
|
550
|
+
// Add tracks
|
|
551
|
+
if (params.tracks) {
|
|
552
|
+
for (const track of params.tracks) {
|
|
553
|
+
commands.push(
|
|
554
|
+
`AddSequenceTrack ${params.name} ${track.actorName} ${track.trackType}`
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
// Add keyframes
|
|
558
|
+
if (track.keyframes) {
|
|
559
|
+
for (const keyframe of track.keyframes) {
|
|
560
|
+
commands.push(
|
|
561
|
+
`AddSequenceKey ${params.name} ${track.actorName} ${track.trackType} ${keyframe.time} ${JSON.stringify(keyframe.value)}`
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
for (const cmd of commands) {
|
|
569
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
success: true,
|
|
574
|
+
message: `Level Sequence ${params.name} created`,
|
|
575
|
+
path: `${path}/${params.name}`
|
|
576
|
+
};
|
|
577
|
+
} catch (err) {
|
|
578
|
+
return { success: false, error: `Failed to create level sequence: ${err}` };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Play Animation on Actor
|
|
584
|
+
*/
|
|
585
|
+
async playAnimation(params: {
|
|
586
|
+
actorName: string;
|
|
587
|
+
animationType: 'Montage' | 'Sequence' | 'BlendSpace';
|
|
588
|
+
animationPath: string;
|
|
589
|
+
playRate?: number;
|
|
590
|
+
loop?: boolean;
|
|
591
|
+
blendInTime?: number;
|
|
592
|
+
blendOutTime?: number;
|
|
593
|
+
}) {
|
|
594
|
+
try {
|
|
595
|
+
// Implement via Python for UE 5.x compatibility instead of non-existent console commands
|
|
596
|
+
const playRate = params.playRate ?? 1.0;
|
|
597
|
+
const loopFlag = params.loop ? 'True' : 'False';
|
|
598
|
+
|
|
599
|
+
const python = `
|
|
600
|
+
import unreal
|
|
601
|
+
import json
|
|
602
|
+
|
|
603
|
+
result = {"success": False, "message": ""}
|
|
604
|
+
|
|
605
|
+
try:
|
|
606
|
+
actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
607
|
+
actors = actor_subsystem.get_all_level_actors()
|
|
608
|
+
target = None
|
|
609
|
+
search = "${params.actorName}"
|
|
610
|
+
for a in actors:
|
|
611
|
+
if not a:
|
|
612
|
+
continue
|
|
613
|
+
name = a.get_name()
|
|
614
|
+
label = a.get_actor_label()
|
|
615
|
+
if (search.lower() == name.lower()) or (search.lower() == label.lower()) or (search.lower() in label.lower()):
|
|
616
|
+
target = a
|
|
617
|
+
break
|
|
618
|
+
|
|
619
|
+
if not target:
|
|
620
|
+
result["message"] = f"Actor not found: {search}"
|
|
621
|
+
else:
|
|
622
|
+
# Try to get a SkeletalMeshComponent from the actor
|
|
623
|
+
sk = target.get_component_by_class(unreal.SkeletalMeshComponent)
|
|
624
|
+
if not sk:
|
|
625
|
+
# Try commonly named properties (e.g., Character mesh)
|
|
626
|
+
try:
|
|
627
|
+
sk = target.get_editor_property('mesh')
|
|
628
|
+
except Exception:
|
|
629
|
+
sk = None
|
|
630
|
+
if not sk:
|
|
631
|
+
result["message"] = "No SkeletalMeshComponent found on actor"
|
|
632
|
+
else:
|
|
633
|
+
anim_type = "${params.animationType}"
|
|
634
|
+
asset_path = r"${params.animationPath}"
|
|
635
|
+
if not unreal.EditorAssetLibrary.does_asset_exist(asset_path):
|
|
636
|
+
result["message"] = f"Animation asset not found: {asset_path}"
|
|
637
|
+
else:
|
|
638
|
+
asset = unreal.EditorAssetLibrary.load_asset(asset_path)
|
|
639
|
+
if anim_type == 'Montage':
|
|
640
|
+
# Use AnimInstance montage_play
|
|
641
|
+
inst = sk.get_anim_instance()
|
|
642
|
+
if not inst:
|
|
643
|
+
result["message"] = "AnimInstance not found on SkeletalMeshComponent"
|
|
644
|
+
else:
|
|
645
|
+
try:
|
|
646
|
+
# montage_play(montage, play_rate, return_value_type, time_to_start_montage_at, stop_all_montages)
|
|
647
|
+
inst.montage_play(asset, ${playRate})
|
|
648
|
+
result["success"] = True
|
|
649
|
+
result["message"] = f"Montage playing on {search}"
|
|
650
|
+
except Exception as e:
|
|
651
|
+
result["message"] = f"Failed to play montage: {e}"
|
|
652
|
+
elif anim_type == 'Sequence':
|
|
653
|
+
try:
|
|
654
|
+
sk.play_animation(asset, ${loopFlag})
|
|
655
|
+
# Adjust rate if supported via play rate on AnimInstance
|
|
656
|
+
try:
|
|
657
|
+
inst = sk.get_anim_instance()
|
|
658
|
+
if inst:
|
|
659
|
+
# Not all paths support direct play rate control here; best effort only
|
|
660
|
+
pass
|
|
661
|
+
except Exception:
|
|
662
|
+
pass
|
|
663
|
+
result["success"] = True
|
|
664
|
+
result["message"] = f"Sequence playing on {search}"
|
|
665
|
+
except Exception as e:
|
|
666
|
+
result["message"] = f"Failed to play sequence: {e}"
|
|
667
|
+
else:
|
|
668
|
+
result["message"] = "BlendSpace playback requires an Animation Blueprint; not supported via direct play."
|
|
669
|
+
except Exception as e:
|
|
670
|
+
result["message"] = f"Error: {e}"
|
|
671
|
+
|
|
672
|
+
print("RESULT:" + json.dumps(result))
|
|
673
|
+
`.trim();
|
|
674
|
+
|
|
675
|
+
const resp = await this.bridge.executePython(python);
|
|
676
|
+
// Parse Python result
|
|
677
|
+
let output = '';
|
|
678
|
+
if (resp && typeof resp === 'object' && Array.isArray((resp as any).LogOutput)) {
|
|
679
|
+
output = (resp as any).LogOutput.map((l: any) => l.Output || '').join('');
|
|
680
|
+
} else if (typeof resp === 'string') {
|
|
681
|
+
output = resp;
|
|
682
|
+
} else {
|
|
683
|
+
output = JSON.stringify(resp);
|
|
684
|
+
}
|
|
685
|
+
const m = output.match(/RESULT:({.*})/);
|
|
686
|
+
if (m) {
|
|
687
|
+
try {
|
|
688
|
+
const parsed = JSON.parse(m[1]);
|
|
689
|
+
return parsed.success ? { success: true, message: parsed.message } : { success: false, error: parsed.message };
|
|
690
|
+
} catch {}
|
|
691
|
+
}
|
|
692
|
+
return { success: true, message: `Animation ${params.animationType} processed for ${params.actorName}` };
|
|
693
|
+
} catch (err) {
|
|
694
|
+
return { success: false, error: `Failed to play animation: ${err}` };
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Helper function to execute console commands
|
|
700
|
+
*/
|
|
701
|
+
private async _executeCommand(command: string) {
|
|
702
|
+
return this.bridge.httpCall('/remote/object/call', 'PUT', {
|
|
703
|
+
objectPath: '/Script/Engine.Default__KismetSystemLibrary',
|
|
704
|
+
functionName: 'ExecuteConsoleCommand',
|
|
705
|
+
parameters: {
|
|
706
|
+
WorldContextObject: null,
|
|
707
|
+
Command: command,
|
|
708
|
+
SpecificPlayer: null
|
|
709
|
+
},
|
|
710
|
+
generateTransaction: false
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|