unreal-engine-mcp-server 0.4.0 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.production +1 -1
- package/.github/copilot-instructions.md +45 -0
- package/.github/workflows/publish-mcp.yml +3 -2
- package/README.md +21 -5
- package/dist/index.js +124 -31
- package/dist/prompts/index.d.ts +10 -3
- package/dist/prompts/index.js +186 -7
- package/dist/resources/actors.d.ts +19 -1
- package/dist/resources/actors.js +55 -64
- package/dist/resources/assets.js +46 -62
- package/dist/resources/levels.d.ts +21 -3
- package/dist/resources/levels.js +29 -54
- package/dist/tools/actors.d.ts +3 -14
- package/dist/tools/actors.js +246 -302
- package/dist/tools/animation.d.ts +57 -102
- package/dist/tools/animation.js +429 -450
- package/dist/tools/assets.d.ts +13 -2
- package/dist/tools/assets.js +52 -44
- package/dist/tools/audio.d.ts +22 -13
- package/dist/tools/audio.js +467 -121
- package/dist/tools/blueprint.d.ts +32 -13
- package/dist/tools/blueprint.js +699 -448
- package/dist/tools/build_environment_advanced.d.ts +0 -1
- package/dist/tools/build_environment_advanced.js +190 -45
- package/dist/tools/consolidated-tool-definitions.js +78 -252
- package/dist/tools/consolidated-tool-handlers.js +506 -133
- package/dist/tools/debug.d.ts +72 -10
- package/dist/tools/debug.js +167 -31
- package/dist/tools/editor.d.ts +9 -2
- package/dist/tools/editor.js +30 -44
- package/dist/tools/foliage.d.ts +34 -15
- package/dist/tools/foliage.js +97 -107
- package/dist/tools/introspection.js +19 -21
- package/dist/tools/landscape.d.ts +1 -2
- package/dist/tools/landscape.js +311 -168
- package/dist/tools/level.d.ts +3 -28
- package/dist/tools/level.js +642 -192
- package/dist/tools/lighting.d.ts +14 -3
- package/dist/tools/lighting.js +236 -123
- package/dist/tools/materials.d.ts +25 -7
- package/dist/tools/materials.js +102 -79
- package/dist/tools/niagara.d.ts +10 -12
- package/dist/tools/niagara.js +74 -94
- package/dist/tools/performance.d.ts +12 -4
- package/dist/tools/performance.js +38 -79
- package/dist/tools/physics.d.ts +34 -10
- package/dist/tools/physics.js +364 -292
- package/dist/tools/rc.js +97 -23
- package/dist/tools/sequence.d.ts +1 -0
- package/dist/tools/sequence.js +125 -22
- package/dist/tools/ui.d.ts +31 -4
- package/dist/tools/ui.js +83 -66
- package/dist/tools/visual.d.ts +11 -0
- package/dist/tools/visual.js +245 -30
- package/dist/types/tool-types.d.ts +0 -6
- package/dist/types/tool-types.js +1 -8
- package/dist/unreal-bridge.d.ts +32 -2
- package/dist/unreal-bridge.js +621 -127
- package/dist/utils/elicitation.d.ts +57 -0
- package/dist/utils/elicitation.js +104 -0
- package/dist/utils/error-handler.d.ts +0 -33
- package/dist/utils/error-handler.js +4 -111
- package/dist/utils/http.d.ts +2 -22
- package/dist/utils/http.js +12 -75
- package/dist/utils/normalize.d.ts +4 -4
- package/dist/utils/normalize.js +15 -7
- package/dist/utils/python-output.d.ts +18 -0
- package/dist/utils/python-output.js +290 -0
- package/dist/utils/python.d.ts +2 -0
- package/dist/utils/python.js +4 -0
- package/dist/utils/response-validator.js +28 -2
- package/dist/utils/result-helpers.d.ts +27 -0
- package/dist/utils/result-helpers.js +147 -0
- package/dist/utils/safe-json.d.ts +0 -2
- package/dist/utils/safe-json.js +0 -43
- package/dist/utils/validation.d.ts +16 -0
- package/dist/utils/validation.js +70 -7
- package/mcp-config-example.json +2 -2
- package/package.json +10 -9
- package/server.json +37 -14
- package/src/index.ts +130 -33
- package/src/prompts/index.ts +211 -13
- package/src/resources/actors.ts +59 -44
- package/src/resources/assets.ts +48 -51
- package/src/resources/levels.ts +35 -45
- package/src/tools/actors.ts +269 -313
- package/src/tools/animation.ts +556 -539
- package/src/tools/assets.ts +53 -43
- package/src/tools/audio.ts +507 -113
- package/src/tools/blueprint.ts +778 -462
- package/src/tools/build_environment_advanced.ts +266 -64
- package/src/tools/consolidated-tool-definitions.ts +90 -264
- package/src/tools/consolidated-tool-handlers.ts +630 -121
- package/src/tools/debug.ts +176 -33
- package/src/tools/editor.ts +35 -37
- package/src/tools/foliage.ts +110 -104
- package/src/tools/introspection.ts +24 -22
- package/src/tools/landscape.ts +334 -181
- package/src/tools/level.ts +683 -182
- package/src/tools/lighting.ts +244 -123
- package/src/tools/materials.ts +114 -83
- package/src/tools/niagara.ts +87 -81
- package/src/tools/performance.ts +49 -88
- package/src/tools/physics.ts +393 -299
- package/src/tools/rc.ts +102 -24
- package/src/tools/sequence.ts +136 -28
- package/src/tools/ui.ts +101 -70
- package/src/tools/visual.ts +250 -29
- package/src/types/tool-types.ts +0 -9
- package/src/unreal-bridge.ts +658 -140
- package/src/utils/elicitation.ts +129 -0
- package/src/utils/error-handler.ts +4 -159
- package/src/utils/http.ts +16 -115
- package/src/utils/normalize.ts +20 -10
- package/src/utils/python-output.ts +351 -0
- package/src/utils/python.ts +3 -0
- package/src/utils/response-validator.ts +25 -2
- package/src/utils/result-helpers.ts +193 -0
- package/src/utils/safe-json.ts +0 -50
- package/src/utils/validation.ts +94 -7
- package/tests/run-unreal-tool-tests.mjs +720 -0
- package/tsconfig.json +2 -2
- package/dist/python-utils.d.ts +0 -29
- package/dist/python-utils.js +0 -54
- package/dist/types/index.d.ts +0 -323
- package/dist/types/index.js +0 -28
- package/dist/utils/cache-manager.d.ts +0 -64
- package/dist/utils/cache-manager.js +0 -176
- package/dist/utils/errors.d.ts +0 -133
- package/dist/utils/errors.js +0 -256
- package/src/python/editor_compat.py +0 -181
- package/src/python-utils.ts +0 -57
- package/src/types/index.ts +0 -414
- package/src/utils/cache-manager.ts +0 -213
- package/src/utils/errors.ts +0 -312
package/dist/tools/animation.js
CHANGED
|
@@ -1,297 +1,61 @@
|
|
|
1
|
-
import { validateAssetParams
|
|
1
|
+
import { validateAssetParams } from '../utils/validation.js';
|
|
2
|
+
import { interpretStandardResult, coerceBoolean, coerceString, coerceStringArray } from '../utils/result-helpers.js';
|
|
2
3
|
export class AnimationTools {
|
|
3
4
|
bridge;
|
|
4
5
|
constructor(bridge) {
|
|
5
6
|
this.bridge = bridge;
|
|
6
7
|
}
|
|
7
|
-
/**
|
|
8
|
-
* Create Animation Blueprint
|
|
9
|
-
*/
|
|
10
8
|
async createAnimationBlueprint(params) {
|
|
11
9
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
success: false,
|
|
16
|
-
message: 'Failed: Name cannot be empty',
|
|
17
|
-
error: 'Name cannot be empty'
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
// Check for whitespace issues
|
|
21
|
-
if (params.name.includes(' ') || params.name.startsWith(' ') || params.name.endsWith(' ')) {
|
|
22
|
-
return {
|
|
23
|
-
success: false,
|
|
24
|
-
message: 'Failed to create Animation Blueprint: Name contains invalid whitespace',
|
|
25
|
-
error: 'Name contains invalid whitespace'
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
// Check for SQL injection patterns
|
|
29
|
-
if (params.name.toLowerCase().includes('drop') || params.name.toLowerCase().includes('delete') ||
|
|
30
|
-
params.name.includes(';') || params.name.includes('--')) {
|
|
31
|
-
return {
|
|
32
|
-
success: false,
|
|
33
|
-
message: 'Failed to create Animation Blueprint: Name contains invalid characters',
|
|
34
|
-
error: 'Name contains invalid characters'
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
// Check save path starts with /
|
|
38
|
-
if (params.savePath && !params.savePath.startsWith('/')) {
|
|
39
|
-
return {
|
|
40
|
-
success: false,
|
|
41
|
-
message: 'Failed to create Animation Blueprint: Path must start with /',
|
|
42
|
-
error: 'Path must start with /'
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
// Now validate and sanitize for actual use
|
|
46
|
-
const validation = validateAssetParams({
|
|
47
|
-
name: params.name,
|
|
48
|
-
savePath: params.savePath || '/Game/Animations'
|
|
49
|
-
});
|
|
10
|
+
const targetPath = params.savePath ?? '/Game/Animations';
|
|
11
|
+
const validation = validateAssetParams({ name: params.name, savePath: targetPath });
|
|
50
12
|
if (!validation.valid) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
message: `Failed to create Animation Blueprint: ${validation.error}`,
|
|
54
|
-
error: validation.error
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
const sanitizedParams = validation.sanitized;
|
|
58
|
-
const path = sanitizedParams.savePath || '/Game/Animations';
|
|
59
|
-
// Add concurrency delay to prevent race conditions
|
|
60
|
-
await concurrencyDelay();
|
|
61
|
-
// Enhanced Python script with proper persistence and error detection
|
|
62
|
-
const pythonScript = `
|
|
63
|
-
import unreal
|
|
64
|
-
import time
|
|
65
|
-
|
|
66
|
-
# Helper function to ensure asset persistence
|
|
67
|
-
def ensure_asset_persistence(asset_path):
|
|
68
|
-
"""Ensure asset is properly saved and registered"""
|
|
69
|
-
try:
|
|
70
|
-
# Load the asset to ensure it's in memory
|
|
71
|
-
asset = unreal.EditorAssetLibrary.load_asset(asset_path)
|
|
72
|
-
if not asset:
|
|
73
|
-
return False
|
|
74
|
-
|
|
75
|
-
# Save the asset
|
|
76
|
-
saved = unreal.EditorAssetLibrary.save_asset(asset_path, only_if_is_dirty=False)
|
|
77
|
-
if saved:
|
|
78
|
-
print(f"Asset saved: {asset_path}")
|
|
79
|
-
|
|
80
|
-
# Refresh the asset registry for the asset's directory only
|
|
81
|
-
try:
|
|
82
|
-
asset_dir = asset_path.rsplit('/', 1)[0]
|
|
83
|
-
unreal.AssetRegistryHelpers.get_asset_registry().scan_paths_synchronous([asset_dir], True)
|
|
84
|
-
except Exception as _reg_e:
|
|
85
|
-
pass
|
|
86
|
-
|
|
87
|
-
# Small delay to ensure filesystem sync
|
|
88
|
-
time.sleep(0.1)
|
|
89
|
-
|
|
90
|
-
return saved
|
|
91
|
-
except Exception as e:
|
|
92
|
-
print(f"Error ensuring persistence: {e}")
|
|
93
|
-
return False
|
|
94
|
-
|
|
95
|
-
# Stop PIE if it's running
|
|
96
|
-
try:
|
|
97
|
-
if unreal.EditorLevelLibrary.is_playing_editor():
|
|
98
|
-
print("Stopping Play In Editor mode...")
|
|
99
|
-
unreal.EditorLevelLibrary.editor_end_play()
|
|
100
|
-
# Small delay to ensure editor fully exits play mode
|
|
101
|
-
import time as _t
|
|
102
|
-
_t.sleep(0.5)
|
|
103
|
-
except Exception as _e:
|
|
104
|
-
# Try alternative check
|
|
105
|
-
try:
|
|
106
|
-
play_world = unreal.EditorLevelLibrary.get_editor_world()
|
|
107
|
-
if play_world and play_world.is_play_in_editor():
|
|
108
|
-
print("Stopping PIE via alternative method...")
|
|
109
|
-
unreal.EditorLevelLibrary.editor_end_play()
|
|
110
|
-
import time as _t2
|
|
111
|
-
_t2.sleep(0.5)
|
|
112
|
-
except:
|
|
113
|
-
pass # Continue if we can't check/stop play mode
|
|
114
|
-
|
|
115
|
-
# Main execution
|
|
116
|
-
success = False
|
|
117
|
-
error_msg = ""
|
|
118
|
-
|
|
119
|
-
# Log the attempt
|
|
120
|
-
print("Creating animation blueprint: ${sanitizedParams.name}")
|
|
121
|
-
|
|
122
|
-
asset_path = "${path}"
|
|
123
|
-
asset_name = "${sanitizedParams.name}"
|
|
124
|
-
full_path = f"{asset_path}/{asset_name}"
|
|
125
|
-
|
|
126
|
-
try:
|
|
127
|
-
# Check if already exists
|
|
128
|
-
if unreal.EditorAssetLibrary.does_asset_exist(full_path):
|
|
129
|
-
print(f"Asset already exists at {full_path}")
|
|
130
|
-
# Load and return existing
|
|
131
|
-
existing = unreal.EditorAssetLibrary.load_asset(full_path)
|
|
132
|
-
if existing:
|
|
133
|
-
print(f"Loaded existing AnimBlueprint: {full_path}")
|
|
134
|
-
success = True
|
|
135
|
-
else:
|
|
136
|
-
error_msg = f"Could not load existing asset at {full_path}"
|
|
137
|
-
print(f"Warning: {error_msg}")
|
|
138
|
-
else:
|
|
139
|
-
# Try to create new animation blueprint
|
|
140
|
-
factory = unreal.AnimBlueprintFactory()
|
|
141
|
-
|
|
142
|
-
# Try to load skeleton if provided
|
|
143
|
-
skeleton_path = "${params.skeletonPath}"
|
|
144
|
-
skeleton = None
|
|
145
|
-
skeleton_set = False
|
|
146
|
-
|
|
147
|
-
if skeleton_path and skeleton_path != "None":
|
|
148
|
-
if unreal.EditorAssetLibrary.does_asset_exist(skeleton_path):
|
|
149
|
-
skeleton = unreal.EditorAssetLibrary.load_asset(skeleton_path)
|
|
150
|
-
if skeleton and isinstance(skeleton, unreal.Skeleton):
|
|
151
|
-
# Different Unreal versions use different attribute names
|
|
152
|
-
try:
|
|
153
|
-
factory.target_skeleton = skeleton
|
|
154
|
-
skeleton_set = True
|
|
155
|
-
print(f"Using skeleton: {skeleton_path}")
|
|
156
|
-
except AttributeError:
|
|
157
|
-
try:
|
|
158
|
-
factory.skeleton = skeleton
|
|
159
|
-
skeleton_set = True
|
|
160
|
-
print(f"Using skeleton (alternate): {skeleton_path}")
|
|
161
|
-
except AttributeError:
|
|
162
|
-
# In some versions, the skeleton is set differently
|
|
163
|
-
try:
|
|
164
|
-
factory.set_editor_property('target_skeleton', skeleton)
|
|
165
|
-
skeleton_set = True
|
|
166
|
-
print(f"Using skeleton (property): {skeleton_path}")
|
|
167
|
-
except:
|
|
168
|
-
print(f"Warning: Could not set skeleton on factory")
|
|
169
|
-
else:
|
|
170
|
-
error_msg = f"Invalid skeleton at {skeleton_path}"
|
|
171
|
-
print(f"Warning: {error_msg}")
|
|
172
|
-
else:
|
|
173
|
-
print(f"Warning: Skeleton not found at {skeleton_path}, creating without skeleton")
|
|
174
|
-
|
|
175
|
-
# Create the asset
|
|
176
|
-
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
|
177
|
-
new_asset = asset_tools.create_asset(
|
|
178
|
-
asset_name=asset_name,
|
|
179
|
-
package_path=asset_path,
|
|
180
|
-
asset_class=unreal.AnimBlueprint,
|
|
181
|
-
factory=factory
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
if new_asset:
|
|
185
|
-
print(f"Successfully created AnimBlueprint at {full_path}")
|
|
186
|
-
|
|
187
|
-
# Ensure persistence
|
|
188
|
-
if ensure_asset_persistence(full_path):
|
|
189
|
-
# Verify it was saved
|
|
190
|
-
if unreal.EditorAssetLibrary.does_asset_exist(full_path):
|
|
191
|
-
print(f"Verified asset exists after save: {full_path}")
|
|
192
|
-
success = True
|
|
193
|
-
else:
|
|
194
|
-
error_msg = f"Asset not found after save: {full_path}"
|
|
195
|
-
print(f"Warning: {error_msg}")
|
|
196
|
-
else:
|
|
197
|
-
error_msg = "Failed to persist asset"
|
|
198
|
-
print(f"Warning: {error_msg}")
|
|
199
|
-
else:
|
|
200
|
-
error_msg = f"Failed to create AnimBlueprint {asset_name}"
|
|
201
|
-
print(error_msg)
|
|
202
|
-
|
|
203
|
-
except Exception as e:
|
|
204
|
-
error_msg = str(e)
|
|
205
|
-
print(f"Error: {error_msg}")
|
|
206
|
-
import traceback
|
|
207
|
-
traceback.print_exc()
|
|
208
|
-
|
|
209
|
-
# Output result markers for parsing
|
|
210
|
-
if success:
|
|
211
|
-
print("SUCCESS")
|
|
212
|
-
else:
|
|
213
|
-
print(f"FAILED: {error_msg}")
|
|
214
|
-
|
|
215
|
-
print("DONE")
|
|
216
|
-
`;
|
|
217
|
-
// Execute Python and parse the output
|
|
218
|
-
try {
|
|
219
|
-
const response = await this.bridge.executePython(pythonScript);
|
|
220
|
-
// Parse the response to detect actual success or failure
|
|
221
|
-
const responseStr = typeof response === 'string' ? response : JSON.stringify(response);
|
|
222
|
-
// Check for explicit success/failure markers
|
|
223
|
-
if (responseStr.includes('SUCCESS')) {
|
|
224
|
-
return {
|
|
225
|
-
success: true,
|
|
226
|
-
message: `Animation Blueprint ${sanitizedParams.name} created successfully`,
|
|
227
|
-
path: `${path}/${sanitizedParams.name}`
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
else if (responseStr.includes('FAILED:')) {
|
|
231
|
-
// Extract error message after FAILED:
|
|
232
|
-
const failMatch = responseStr.match(/FAILED:\s*(.+)/);
|
|
233
|
-
const errorMsg = failMatch ? failMatch[1] : 'Unknown error';
|
|
234
|
-
return {
|
|
235
|
-
success: false,
|
|
236
|
-
message: `Failed to create Animation Blueprint: ${errorMsg}`,
|
|
237
|
-
error: errorMsg
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
else {
|
|
241
|
-
// If no explicit markers, check for other error indicators
|
|
242
|
-
if (responseStr.includes('Error:') || responseStr.includes('error') ||
|
|
243
|
-
responseStr.includes('failed') || responseStr.includes('Failed')) {
|
|
244
|
-
return {
|
|
245
|
-
success: false,
|
|
246
|
-
message: 'Failed to create Animation Blueprint',
|
|
247
|
-
error: responseStr
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
// Assume success if no errors detected
|
|
251
|
-
return {
|
|
252
|
-
success: true,
|
|
253
|
-
message: `Animation Blueprint ${sanitizedParams.name} processed`,
|
|
254
|
-
path: `${path}/${sanitizedParams.name}`
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
catch (error) {
|
|
259
|
-
return {
|
|
260
|
-
success: false,
|
|
261
|
-
message: 'Failed to create Animation Blueprint',
|
|
262
|
-
error: String(error)
|
|
263
|
-
};
|
|
13
|
+
const message = validation.error ?? 'Invalid asset parameters';
|
|
14
|
+
return { success: false, message, error: message };
|
|
264
15
|
}
|
|
16
|
+
const sanitized = validation.sanitized;
|
|
17
|
+
const assetName = sanitized.name;
|
|
18
|
+
const assetPath = sanitized.savePath ?? targetPath;
|
|
19
|
+
const script = this.buildCreateAnimationBlueprintScript({
|
|
20
|
+
name: assetName,
|
|
21
|
+
path: assetPath,
|
|
22
|
+
skeletonPath: params.skeletonPath
|
|
23
|
+
});
|
|
24
|
+
const response = await this.bridge.executePython(script);
|
|
25
|
+
return this.parseAnimationBlueprintResponse(response, assetName, assetPath);
|
|
265
26
|
}
|
|
266
27
|
catch (err) {
|
|
267
|
-
|
|
28
|
+
const error = `Failed to create Animation Blueprint: ${err}`;
|
|
29
|
+
return { success: false, message: error, error: String(err) };
|
|
268
30
|
}
|
|
269
31
|
}
|
|
270
|
-
/**
|
|
271
|
-
* Add State Machine to Animation Blueprint
|
|
272
|
-
*/
|
|
273
32
|
async addStateMachine(params) {
|
|
274
33
|
try {
|
|
275
|
-
|
|
34
|
+
if (!params.blueprintPath || !params.machineName) {
|
|
35
|
+
return { success: false, error: 'blueprintPath and machineName are required' };
|
|
36
|
+
}
|
|
276
37
|
const commands = [
|
|
277
38
|
`AddAnimStateMachine ${params.blueprintPath} ${params.machineName}`
|
|
278
39
|
];
|
|
279
|
-
// Add states
|
|
280
40
|
for (const state of params.states) {
|
|
281
|
-
|
|
41
|
+
const animationName = state.animation ?? '';
|
|
42
|
+
commands.push(`AddAnimState ${params.blueprintPath} ${params.machineName} ${state.name} ${animationName}`);
|
|
282
43
|
if (state.isEntry) {
|
|
283
44
|
commands.push(`SetAnimStateEntry ${params.blueprintPath} ${params.machineName} ${state.name}`);
|
|
284
45
|
}
|
|
46
|
+
if (state.isExit) {
|
|
47
|
+
commands.push(`SetAnimStateExit ${params.blueprintPath} ${params.machineName} ${state.name}`);
|
|
48
|
+
}
|
|
285
49
|
}
|
|
286
|
-
// Add transitions
|
|
287
50
|
if (params.transitions) {
|
|
288
51
|
for (const transition of params.transitions) {
|
|
289
52
|
commands.push(`AddAnimTransition ${params.blueprintPath} ${params.machineName} ${transition.sourceState} ${transition.targetState}`);
|
|
53
|
+
if (transition.condition) {
|
|
54
|
+
commands.push(`SetAnimTransitionRule ${params.blueprintPath} ${params.machineName} ${transition.sourceState} ${transition.targetState} ${transition.condition}`);
|
|
55
|
+
}
|
|
290
56
|
}
|
|
291
57
|
}
|
|
292
|
-
|
|
293
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
294
|
-
}
|
|
58
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
295
59
|
return {
|
|
296
60
|
success: true,
|
|
297
61
|
message: `State machine ${params.machineName} added to ${params.blueprintPath}`
|
|
@@ -301,279 +65,494 @@ print("DONE")
|
|
|
301
65
|
return { success: false, error: `Failed to add state machine: ${err}` };
|
|
302
66
|
}
|
|
303
67
|
}
|
|
304
|
-
/**
|
|
305
|
-
* Create Animation Montage
|
|
306
|
-
*/
|
|
307
|
-
async createMontage(params) {
|
|
308
|
-
try {
|
|
309
|
-
const path = params.savePath || '/Game/Animations/Montages';
|
|
310
|
-
const commands = [
|
|
311
|
-
`CreateAsset AnimMontage ${params.name} ${path}`,
|
|
312
|
-
`SetMontageAnimation ${params.name} ${params.animationSequence}`
|
|
313
|
-
];
|
|
314
|
-
// Add sections
|
|
315
|
-
if (params.sections) {
|
|
316
|
-
for (const section of params.sections) {
|
|
317
|
-
commands.push(`AddMontageSection ${params.name} ${section.name} ${section.startTime} ${section.endTime}`);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
// Add notifies
|
|
321
|
-
if (params.notifies) {
|
|
322
|
-
for (const notify of params.notifies) {
|
|
323
|
-
commands.push(`AddMontageNotify ${params.name} ${notify.name} ${notify.time}`);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
for (const cmd of commands) {
|
|
327
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
328
|
-
}
|
|
329
|
-
return {
|
|
330
|
-
success: true,
|
|
331
|
-
message: `Animation Montage ${params.name} created`,
|
|
332
|
-
path: `${path}/${params.name}`
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
catch (err) {
|
|
336
|
-
return { success: false, error: `Failed to create montage: ${err}` };
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Create Blend Space
|
|
341
|
-
*/
|
|
342
68
|
async createBlendSpace(params) {
|
|
343
69
|
try {
|
|
344
|
-
const
|
|
345
|
-
const
|
|
346
|
-
|
|
70
|
+
const targetPath = params.savePath ?? '/Game/Animations';
|
|
71
|
+
const validation = validateAssetParams({ name: params.name, savePath: targetPath });
|
|
72
|
+
if (!validation.valid) {
|
|
73
|
+
return { success: false, error: validation.error ?? 'Invalid asset parameters' };
|
|
74
|
+
}
|
|
75
|
+
const sanitized = validation.sanitized;
|
|
76
|
+
const assetName = sanitized.name;
|
|
77
|
+
const assetPath = sanitized.savePath ?? targetPath;
|
|
78
|
+
const dimensions = params.dimensions === 2 ? 2 : 1;
|
|
79
|
+
const blendSpaceType = dimensions === 2 ? 'BlendSpace' : 'BlendSpace1D';
|
|
347
80
|
const commands = [
|
|
348
|
-
`
|
|
81
|
+
`CreateAsset ${blendSpaceType} ${assetName} ${assetPath}`,
|
|
82
|
+
`echo Creating ${blendSpaceType} ${assetName} at ${assetPath}`
|
|
349
83
|
];
|
|
350
|
-
|
|
84
|
+
if (params.skeletonPath) {
|
|
85
|
+
commands.push(`SetBlendSpaceSkeleton ${assetName} ${params.skeletonPath}`);
|
|
86
|
+
}
|
|
351
87
|
if (params.horizontalAxis) {
|
|
352
|
-
commands.push(`SetBlendSpaceAxis ${
|
|
88
|
+
commands.push(`SetBlendSpaceAxis ${assetName} Horizontal ${params.horizontalAxis.name} ${params.horizontalAxis.minValue} ${params.horizontalAxis.maxValue}`);
|
|
353
89
|
}
|
|
354
|
-
if (
|
|
355
|
-
commands.push(`SetBlendSpaceAxis ${
|
|
90
|
+
if (dimensions === 2 && params.verticalAxis) {
|
|
91
|
+
commands.push(`SetBlendSpaceAxis ${assetName} Vertical ${params.verticalAxis.name} ${params.verticalAxis.minValue} ${params.verticalAxis.maxValue}`);
|
|
356
92
|
}
|
|
357
|
-
// Add sample animations
|
|
358
93
|
if (params.samples) {
|
|
359
94
|
for (const sample of params.samples) {
|
|
360
|
-
const coords =
|
|
361
|
-
commands.push(`AddBlendSpaceSample ${
|
|
95
|
+
const coords = dimensions === 1 ? `${sample.x}` : `${sample.x} ${sample.y ?? 0}`;
|
|
96
|
+
commands.push(`AddBlendSpaceSample ${assetName} ${sample.animation} ${coords}`);
|
|
362
97
|
}
|
|
363
98
|
}
|
|
364
|
-
|
|
365
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
366
|
-
}
|
|
99
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
367
100
|
return {
|
|
368
101
|
success: true,
|
|
369
|
-
message: `Blend Space ${
|
|
370
|
-
path: `${
|
|
102
|
+
message: `Blend Space ${assetName} created`,
|
|
103
|
+
path: `${assetPath}/${assetName}`
|
|
371
104
|
};
|
|
372
105
|
}
|
|
373
106
|
catch (err) {
|
|
374
107
|
return { success: false, error: `Failed to create blend space: ${err}` };
|
|
375
108
|
}
|
|
376
109
|
}
|
|
377
|
-
/**
|
|
378
|
-
* Setup Control Rig
|
|
379
|
-
*/
|
|
380
110
|
async setupControlRig(params) {
|
|
381
111
|
try {
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
return {
|
|
387
|
-
success: false,
|
|
388
|
-
message: `Failed: Path too long (${fullPath.length} characters)`,
|
|
389
|
-
error: 'Unreal Engine paths must be less than 260 characters'
|
|
390
|
-
};
|
|
112
|
+
const targetPath = params.savePath ?? '/Game/Animations';
|
|
113
|
+
const validation = validateAssetParams({ name: params.name, savePath: targetPath });
|
|
114
|
+
if (!validation.valid) {
|
|
115
|
+
return { success: false, error: validation.error ?? 'Invalid asset parameters' };
|
|
391
116
|
}
|
|
117
|
+
const sanitized = validation.sanitized;
|
|
118
|
+
const assetName = sanitized.name;
|
|
119
|
+
const assetPath = sanitized.savePath ?? targetPath;
|
|
120
|
+
const fullPath = `${assetPath}/${assetName}`;
|
|
392
121
|
const commands = [
|
|
393
|
-
`CreateAsset ControlRig ${
|
|
394
|
-
`SetControlRigSkeleton ${
|
|
122
|
+
`CreateAsset ControlRig ${assetName} ${assetPath}`,
|
|
123
|
+
`SetControlRigSkeleton ${assetName} ${params.skeletonPath}`
|
|
395
124
|
];
|
|
396
|
-
// Add controls
|
|
397
125
|
if (params.controls) {
|
|
398
126
|
for (const control of params.controls) {
|
|
399
|
-
commands.push(`AddControlRigControl ${
|
|
127
|
+
commands.push(`AddControlRigControl ${assetName} ${control.name} ${control.type} ${control.bone ?? ''}`);
|
|
400
128
|
if (control.defaultValue !== undefined) {
|
|
401
|
-
commands.push(`SetControlRigDefault ${
|
|
129
|
+
commands.push(`SetControlRigDefault ${assetName} ${control.name} ${JSON.stringify(control.defaultValue)}`);
|
|
402
130
|
}
|
|
403
131
|
}
|
|
404
132
|
}
|
|
405
|
-
|
|
406
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
407
|
-
}
|
|
133
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
408
134
|
return {
|
|
409
135
|
success: true,
|
|
410
|
-
message: `Control Rig ${
|
|
411
|
-
path:
|
|
136
|
+
message: `Control Rig ${assetName} created`,
|
|
137
|
+
path: fullPath
|
|
412
138
|
};
|
|
413
139
|
}
|
|
414
140
|
catch (err) {
|
|
415
141
|
return { success: false, error: `Failed to setup control rig: ${err}` };
|
|
416
142
|
}
|
|
417
143
|
}
|
|
418
|
-
/**
|
|
419
|
-
* Create Level Sequence (for cinematics)
|
|
420
|
-
*/
|
|
421
144
|
async createLevelSequence(params) {
|
|
422
145
|
try {
|
|
423
|
-
const
|
|
146
|
+
const targetPath = params.savePath ?? '/Game/Cinematics';
|
|
147
|
+
const validation = validateAssetParams({ name: params.name, savePath: targetPath });
|
|
148
|
+
if (!validation.valid) {
|
|
149
|
+
return { success: false, error: validation.error ?? 'Invalid asset parameters' };
|
|
150
|
+
}
|
|
151
|
+
const sanitized = validation.sanitized;
|
|
152
|
+
const assetName = sanitized.name;
|
|
153
|
+
const assetPath = sanitized.savePath ?? targetPath;
|
|
424
154
|
const commands = [
|
|
425
|
-
`CreateAsset LevelSequence ${
|
|
426
|
-
`SetSequenceFrameRate ${
|
|
427
|
-
`SetSequenceDuration ${
|
|
155
|
+
`CreateAsset LevelSequence ${assetName} ${assetPath}`,
|
|
156
|
+
`SetSequenceFrameRate ${assetName} ${params.frameRate ?? 30}`,
|
|
157
|
+
`SetSequenceDuration ${assetName} ${params.duration ?? 5}`
|
|
428
158
|
];
|
|
429
|
-
// Add tracks
|
|
430
159
|
if (params.tracks) {
|
|
431
160
|
for (const track of params.tracks) {
|
|
432
|
-
commands.push(`AddSequenceTrack ${
|
|
433
|
-
// Add keyframes
|
|
161
|
+
commands.push(`AddSequenceTrack ${assetName} ${track.actorName} ${track.trackType}`);
|
|
434
162
|
if (track.keyframes) {
|
|
435
163
|
for (const keyframe of track.keyframes) {
|
|
436
|
-
commands.push(`AddSequenceKey ${
|
|
164
|
+
commands.push(`AddSequenceKey ${assetName} ${track.actorName} ${track.trackType} ${keyframe.time} ${JSON.stringify(keyframe.value)}`);
|
|
437
165
|
}
|
|
438
166
|
}
|
|
439
167
|
}
|
|
440
168
|
}
|
|
441
|
-
|
|
442
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
443
|
-
}
|
|
169
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
444
170
|
return {
|
|
445
171
|
success: true,
|
|
446
|
-
message: `Level Sequence ${
|
|
447
|
-
path: `${
|
|
172
|
+
message: `Level Sequence ${assetName} created`,
|
|
173
|
+
path: `${assetPath}/${assetName}`
|
|
448
174
|
};
|
|
449
175
|
}
|
|
450
176
|
catch (err) {
|
|
451
177
|
return { success: false, error: `Failed to create level sequence: ${err}` };
|
|
452
178
|
}
|
|
453
179
|
}
|
|
454
|
-
/**
|
|
455
|
-
* Play Animation on Actor
|
|
456
|
-
*/
|
|
457
180
|
async playAnimation(params) {
|
|
458
181
|
try {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
182
|
+
const script = this.buildPlayAnimationScript({
|
|
183
|
+
actorName: params.actorName,
|
|
184
|
+
animationType: params.animationType,
|
|
185
|
+
animationPath: params.animationPath,
|
|
186
|
+
playRate: params.playRate ?? 1.0,
|
|
187
|
+
loop: params.loop ?? false,
|
|
188
|
+
blendInTime: params.blendInTime ?? 0.25,
|
|
189
|
+
blendOutTime: params.blendOutTime ?? 0.25
|
|
190
|
+
});
|
|
191
|
+
const response = await this.bridge.executePython(script);
|
|
192
|
+
const interpreted = interpretStandardResult(response, {
|
|
193
|
+
successMessage: `Animation ${params.animationType} triggered on ${params.actorName}`,
|
|
194
|
+
failureMessage: `Failed to play animation on ${params.actorName}`
|
|
195
|
+
});
|
|
196
|
+
const payload = interpreted.payload ?? {};
|
|
197
|
+
const warnings = interpreted.warnings ?? coerceStringArray(payload.warnings) ?? undefined;
|
|
198
|
+
const details = interpreted.details ?? coerceStringArray(payload.details) ?? undefined;
|
|
199
|
+
const availableActors = coerceStringArray(payload.availableActors);
|
|
200
|
+
const actorName = coerceString(payload.actorName) ?? params.actorName;
|
|
201
|
+
const animationType = coerceString(payload.animationType) ?? params.animationType;
|
|
202
|
+
const assetPath = coerceString(payload.assetPath) ?? params.animationPath;
|
|
203
|
+
const errorMessage = coerceString(payload.error) ?? interpreted.error ?? `Animation playback failed for ${params.actorName}`;
|
|
204
|
+
if (interpreted.success) {
|
|
205
|
+
const result = {
|
|
206
|
+
success: true,
|
|
207
|
+
message: interpreted.message
|
|
208
|
+
};
|
|
209
|
+
if (warnings && warnings.length > 0) {
|
|
210
|
+
result.warnings = warnings;
|
|
211
|
+
}
|
|
212
|
+
if (details && details.length > 0) {
|
|
213
|
+
result.details = details;
|
|
214
|
+
}
|
|
215
|
+
if (actorName) {
|
|
216
|
+
result.actorName = actorName;
|
|
217
|
+
}
|
|
218
|
+
if (animationType) {
|
|
219
|
+
result.animationType = animationType;
|
|
220
|
+
}
|
|
221
|
+
if (assetPath) {
|
|
222
|
+
result.assetPath = assetPath;
|
|
223
|
+
}
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
const failure = {
|
|
227
|
+
success: false,
|
|
228
|
+
message: `Failed to play animation: ${errorMessage}`,
|
|
229
|
+
error: errorMessage
|
|
230
|
+
};
|
|
231
|
+
if (warnings && warnings.length > 0) {
|
|
232
|
+
failure.warnings = warnings;
|
|
233
|
+
}
|
|
234
|
+
if (details && details.length > 0) {
|
|
235
|
+
failure.details = details;
|
|
236
|
+
}
|
|
237
|
+
if (availableActors && availableActors.length > 0) {
|
|
238
|
+
failure.availableActors = availableActors;
|
|
239
|
+
}
|
|
240
|
+
if (actorName) {
|
|
241
|
+
failure.actorName = actorName;
|
|
242
|
+
}
|
|
243
|
+
if (animationType) {
|
|
244
|
+
failure.animationType = animationType;
|
|
245
|
+
}
|
|
246
|
+
if (assetPath) {
|
|
247
|
+
failure.assetPath = assetPath;
|
|
248
|
+
}
|
|
249
|
+
return failure;
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
const error = `Failed to play animation: ${err}`;
|
|
253
|
+
return { success: false, message: error, error: String(err) };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
buildCreateAnimationBlueprintScript(args) {
|
|
257
|
+
const payload = JSON.stringify(args);
|
|
258
|
+
return `
|
|
259
|
+
import unreal
|
|
260
|
+
import json
|
|
261
|
+
import traceback
|
|
262
|
+
|
|
263
|
+
params = json.loads(${JSON.stringify(payload)})
|
|
264
|
+
|
|
265
|
+
result = {
|
|
266
|
+
"success": False,
|
|
267
|
+
"message": "",
|
|
268
|
+
"error": "",
|
|
269
|
+
"warnings": [],
|
|
270
|
+
"details": [],
|
|
271
|
+
"exists": False,
|
|
272
|
+
"skeleton": params.get("skeletonPath") or ""
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
asset_path = (params.get("path") or "/Game").rstrip('/')
|
|
277
|
+
asset_name = params.get("name") or ""
|
|
278
|
+
full_path = f"{asset_path}/{asset_name}"
|
|
279
|
+
result["path"] = full_path
|
|
280
|
+
|
|
281
|
+
editor_lib = unreal.EditorAssetLibrary
|
|
282
|
+
asset_subsystem = None
|
|
283
|
+
try:
|
|
284
|
+
asset_subsystem = unreal.get_editor_subsystem(unreal.EditorAssetSubsystem)
|
|
285
|
+
except Exception:
|
|
286
|
+
asset_subsystem = None
|
|
287
|
+
|
|
288
|
+
skeleton_path = params.get("skeletonPath")
|
|
289
|
+
skeleton_asset = None
|
|
290
|
+
if skeleton_path:
|
|
291
|
+
if editor_lib.does_asset_exist(skeleton_path):
|
|
292
|
+
skeleton_asset = editor_lib.load_asset(skeleton_path)
|
|
293
|
+
if skeleton_asset and isinstance(skeleton_asset, unreal.Skeleton):
|
|
294
|
+
result["details"].append(f"Using skeleton: {skeleton_path}")
|
|
295
|
+
result["skeleton"] = skeleton_path
|
|
296
|
+
else:
|
|
297
|
+
result["error"] = f"Skeleton asset invalid at {skeleton_path}"
|
|
298
|
+
result["warnings"].append(result["error"])
|
|
299
|
+
skeleton_asset = None
|
|
300
|
+
else:
|
|
301
|
+
result["error"] = f"Skeleton not found at {skeleton_path}"
|
|
302
|
+
result["warnings"].append(result["error"])
|
|
303
|
+
|
|
304
|
+
if not skeleton_asset:
|
|
305
|
+
raise RuntimeError(result["error"] or f"Skeleton {skeleton_path} unavailable")
|
|
306
|
+
|
|
307
|
+
does_exist = False
|
|
308
|
+
try:
|
|
309
|
+
if asset_subsystem and hasattr(asset_subsystem, 'does_asset_exist'):
|
|
310
|
+
does_exist = asset_subsystem.does_asset_exist(full_path)
|
|
311
|
+
else:
|
|
312
|
+
does_exist = editor_lib.does_asset_exist(full_path)
|
|
313
|
+
except Exception:
|
|
314
|
+
does_exist = editor_lib.does_asset_exist(full_path)
|
|
315
|
+
|
|
316
|
+
if does_exist:
|
|
317
|
+
result["exists"] = True
|
|
318
|
+
loaded = editor_lib.load_asset(full_path)
|
|
319
|
+
if loaded:
|
|
320
|
+
result["success"] = True
|
|
321
|
+
result["message"] = f"Animation Blueprint already exists at {full_path}"
|
|
322
|
+
result["details"].append(result["message"])
|
|
323
|
+
else:
|
|
324
|
+
result["error"] = f"Asset exists but could not be loaded: {full_path}"
|
|
325
|
+
result["warnings"].append(result["error"])
|
|
326
|
+
else:
|
|
327
|
+
factory = unreal.AnimBlueprintFactory()
|
|
328
|
+
if skeleton_asset:
|
|
329
|
+
try:
|
|
330
|
+
factory.target_skeleton = skeleton_asset
|
|
331
|
+
except Exception as assign_error:
|
|
332
|
+
result["warnings"].append(f"Unable to assign skeleton {skeleton_path}: {assign_error}")
|
|
333
|
+
|
|
334
|
+
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
|
335
|
+
created = asset_tools.create_asset(
|
|
336
|
+
asset_name=asset_name,
|
|
337
|
+
package_path=asset_path,
|
|
338
|
+
asset_class=unreal.AnimBlueprint,
|
|
339
|
+
factory=factory
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if created:
|
|
343
|
+
editor_lib.save_asset(full_path, only_if_is_dirty=False)
|
|
344
|
+
result["success"] = True
|
|
345
|
+
result["message"] = f"Animation Blueprint created at {full_path}"
|
|
346
|
+
result["details"].append(result["message"])
|
|
347
|
+
else:
|
|
348
|
+
result["error"] = f"Failed to create Animation Blueprint {asset_name}"
|
|
349
|
+
|
|
350
|
+
except Exception as exc:
|
|
351
|
+
result["error"] = str(exc)
|
|
352
|
+
result["warnings"].append(result["error"])
|
|
353
|
+
tb = traceback.format_exc()
|
|
354
|
+
if tb:
|
|
355
|
+
result.setdefault("details", []).append(tb)
|
|
356
|
+
|
|
357
|
+
if result["success"] and not result.get("message"):
|
|
358
|
+
result["message"] = f"Animation Blueprint created at {result.get('path')}"
|
|
359
|
+
|
|
360
|
+
if not result["success"] and not result.get("error"):
|
|
361
|
+
result["error"] = "Animation Blueprint creation failed"
|
|
362
|
+
|
|
363
|
+
if not result.get("warnings"):
|
|
364
|
+
result.pop("warnings", None)
|
|
365
|
+
if not result.get("details"):
|
|
366
|
+
result.pop("details", None)
|
|
367
|
+
if not result.get("error"):
|
|
368
|
+
result.pop("error", None)
|
|
369
|
+
|
|
370
|
+
print('RESULT:' + json.dumps(result))
|
|
371
|
+
`.trim();
|
|
372
|
+
}
|
|
373
|
+
parseAnimationBlueprintResponse(response, assetName, assetPath) {
|
|
374
|
+
const interpreted = interpretStandardResult(response, {
|
|
375
|
+
successMessage: `Animation Blueprint ${assetName} created`,
|
|
376
|
+
failureMessage: `Failed to create Animation Blueprint ${assetName}`
|
|
377
|
+
});
|
|
378
|
+
const payload = interpreted.payload ?? {};
|
|
379
|
+
const path = coerceString(payload.path) ?? `${assetPath}/${assetName}`;
|
|
380
|
+
const exists = coerceBoolean(payload.exists);
|
|
381
|
+
const skeleton = coerceString(payload.skeleton);
|
|
382
|
+
const warnings = interpreted.warnings ?? coerceStringArray(payload.warnings) ?? undefined;
|
|
383
|
+
const details = interpreted.details ?? coerceStringArray(payload.details) ?? undefined;
|
|
384
|
+
if (interpreted.success) {
|
|
385
|
+
const result = {
|
|
386
|
+
success: true,
|
|
387
|
+
message: interpreted.message,
|
|
388
|
+
path
|
|
389
|
+
};
|
|
390
|
+
if (typeof exists === 'boolean') {
|
|
391
|
+
result.exists = exists;
|
|
392
|
+
}
|
|
393
|
+
if (skeleton) {
|
|
394
|
+
result.skeleton = skeleton;
|
|
395
|
+
}
|
|
396
|
+
if (warnings && warnings.length > 0) {
|
|
397
|
+
result.warnings = warnings;
|
|
398
|
+
}
|
|
399
|
+
if (details && details.length > 0) {
|
|
400
|
+
result.details = details;
|
|
401
|
+
}
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
404
|
+
const errorMessage = coerceString(payload.error) ?? interpreted.error ?? interpreted.message;
|
|
405
|
+
const failure = {
|
|
406
|
+
success: false,
|
|
407
|
+
message: `Failed to create Animation Blueprint: ${errorMessage}`,
|
|
408
|
+
error: errorMessage,
|
|
409
|
+
path
|
|
410
|
+
};
|
|
411
|
+
if (typeof exists === 'boolean') {
|
|
412
|
+
failure.exists = exists;
|
|
413
|
+
}
|
|
414
|
+
if (skeleton) {
|
|
415
|
+
failure.skeleton = skeleton;
|
|
416
|
+
}
|
|
417
|
+
if (warnings && warnings.length > 0) {
|
|
418
|
+
failure.warnings = warnings;
|
|
419
|
+
}
|
|
420
|
+
if (details && details.length > 0) {
|
|
421
|
+
failure.details = details;
|
|
422
|
+
}
|
|
423
|
+
return failure;
|
|
424
|
+
}
|
|
425
|
+
buildPlayAnimationScript(args) {
|
|
426
|
+
const payload = JSON.stringify(args);
|
|
427
|
+
return `
|
|
463
428
|
import unreal
|
|
464
429
|
import json
|
|
430
|
+
import traceback
|
|
465
431
|
|
|
466
|
-
|
|
432
|
+
params = json.loads(${JSON.stringify(payload)})
|
|
433
|
+
|
|
434
|
+
result = {
|
|
435
|
+
"success": False,
|
|
436
|
+
"message": "",
|
|
437
|
+
"error": "",
|
|
438
|
+
"warnings": [],
|
|
439
|
+
"details": [],
|
|
440
|
+
"actorName": params.get("actorName"),
|
|
441
|
+
"animationType": params.get("animationType"),
|
|
442
|
+
"assetPath": params.get("animationPath"),
|
|
443
|
+
"availableActors": []
|
|
444
|
+
}
|
|
467
445
|
|
|
468
446
|
try:
|
|
469
447
|
actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
470
|
-
actors = actor_subsystem.get_all_level_actors()
|
|
448
|
+
actors = actor_subsystem.get_all_level_actors() if actor_subsystem else []
|
|
471
449
|
target = None
|
|
472
|
-
search =
|
|
473
|
-
|
|
474
|
-
|
|
450
|
+
search = params.get("actorName") or ""
|
|
451
|
+
search_lower = search.lower()
|
|
452
|
+
|
|
453
|
+
for actor in actors:
|
|
454
|
+
if not actor:
|
|
475
455
|
continue
|
|
476
|
-
name =
|
|
477
|
-
label =
|
|
478
|
-
if (
|
|
479
|
-
target =
|
|
456
|
+
name = (actor.get_name() or "").lower()
|
|
457
|
+
label = (actor.get_actor_label() or "").lower()
|
|
458
|
+
if search_lower and (search_lower == name or search_lower == label or search_lower in label):
|
|
459
|
+
target = actor
|
|
480
460
|
break
|
|
481
461
|
|
|
482
462
|
if not target:
|
|
483
|
-
result["
|
|
463
|
+
result["error"] = f"Actor not found: {search}"
|
|
464
|
+
result["warnings"].append("Actor search yielded no results")
|
|
465
|
+
suggestions = []
|
|
466
|
+
for actor in actors[:20]:
|
|
467
|
+
try:
|
|
468
|
+
suggestions.append(actor.get_actor_label())
|
|
469
|
+
except Exception:
|
|
470
|
+
continue
|
|
471
|
+
if suggestions:
|
|
472
|
+
result["availableActors"] = suggestions
|
|
484
473
|
else:
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
474
|
+
try:
|
|
475
|
+
display_name = target.get_actor_label() or target.get_name()
|
|
476
|
+
if display_name:
|
|
477
|
+
result["actorName"] = display_name
|
|
478
|
+
except Exception:
|
|
479
|
+
pass
|
|
480
|
+
|
|
481
|
+
skeletal_component = target.get_component_by_class(unreal.SkeletalMeshComponent)
|
|
482
|
+
if not skeletal_component:
|
|
489
483
|
try:
|
|
490
|
-
|
|
484
|
+
skeletal_component = target.get_editor_property('mesh')
|
|
491
485
|
except Exception:
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
486
|
+
skeletal_component = None
|
|
487
|
+
|
|
488
|
+
if not skeletal_component:
|
|
489
|
+
result["error"] = "No SkeletalMeshComponent found on actor"
|
|
490
|
+
result["warnings"].append("Actor lacks SkeletalMeshComponent")
|
|
495
491
|
else:
|
|
496
|
-
|
|
497
|
-
asset_path
|
|
498
|
-
|
|
499
|
-
result["
|
|
492
|
+
asset_path = params.get("animationPath")
|
|
493
|
+
if not asset_path or not unreal.EditorAssetLibrary.does_asset_exist(asset_path):
|
|
494
|
+
result["error"] = f"Animation asset not found: {asset_path}"
|
|
495
|
+
result["warnings"].append("Animation asset missing")
|
|
500
496
|
else:
|
|
501
497
|
asset = unreal.EditorAssetLibrary.load_asset(asset_path)
|
|
498
|
+
anim_type = params.get("animationType") or ""
|
|
502
499
|
if anim_type == 'Montage':
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if not inst:
|
|
506
|
-
result["message"] = "AnimInstance not found on SkeletalMeshComponent"
|
|
507
|
-
else:
|
|
500
|
+
anim_instance = skeletal_component.get_anim_instance()
|
|
501
|
+
if anim_instance:
|
|
508
502
|
try:
|
|
509
|
-
|
|
510
|
-
inst.montage_play(asset, ${playRate})
|
|
503
|
+
anim_instance.montage_play(asset, params.get("playRate", 1.0))
|
|
511
504
|
result["success"] = True
|
|
512
|
-
result["message"] = f"Montage playing on {search}"
|
|
513
|
-
|
|
514
|
-
|
|
505
|
+
result["message"] = f"Montage playing on {result.get('actorName') or search}"
|
|
506
|
+
result["details"].append(result["message"])
|
|
507
|
+
except Exception as play_error:
|
|
508
|
+
result["error"] = f"Failed to play montage: {play_error}"
|
|
509
|
+
result["warnings"].append(result["error"])
|
|
510
|
+
else:
|
|
511
|
+
result["error"] = "AnimInstance not found on SkeletalMeshComponent"
|
|
512
|
+
result["warnings"].append(result["error"])
|
|
515
513
|
elif anim_type == 'Sequence':
|
|
516
514
|
try:
|
|
517
|
-
|
|
518
|
-
# Adjust rate if supported via play rate on AnimInstance
|
|
515
|
+
skeletal_component.play_animation(asset, bool(params.get("loop")))
|
|
519
516
|
try:
|
|
520
|
-
|
|
521
|
-
if
|
|
522
|
-
|
|
523
|
-
pass
|
|
517
|
+
anim_instance = skeletal_component.get_anim_instance()
|
|
518
|
+
if anim_instance:
|
|
519
|
+
anim_instance.set_play_rate(params.get("playRate", 1.0))
|
|
524
520
|
except Exception:
|
|
525
521
|
pass
|
|
526
522
|
result["success"] = True
|
|
527
|
-
result["message"] = f"Sequence playing on {search}"
|
|
528
|
-
|
|
529
|
-
|
|
523
|
+
result["message"] = f"Sequence playing on {result.get('actorName') or search}"
|
|
524
|
+
result["details"].append(result["message"])
|
|
525
|
+
except Exception as play_error:
|
|
526
|
+
result["error"] = f"Failed to play sequence: {play_error}"
|
|
527
|
+
result["warnings"].append(result["error"])
|
|
530
528
|
else:
|
|
531
|
-
result["
|
|
532
|
-
|
|
533
|
-
result["message"] = f"Error: {e}"
|
|
529
|
+
result["error"] = "BlendSpace playback requires Animation Blueprint support"
|
|
530
|
+
result["warnings"].append("Unsupported animation type for direct play")
|
|
534
531
|
|
|
535
|
-
|
|
532
|
+
except Exception as exc:
|
|
533
|
+
result["error"] = str(exc)
|
|
534
|
+
result["warnings"].append(result["error"])
|
|
535
|
+
tb = traceback.format_exc()
|
|
536
|
+
if tb:
|
|
537
|
+
result["details"].append(tb)
|
|
538
|
+
|
|
539
|
+
if result["success"] and not result.get("message"):
|
|
540
|
+
result["message"] = f"Animation {result.get('animationType')} triggered on {result.get('actorName') or params.get('actorName')}"
|
|
541
|
+
|
|
542
|
+
if not result["success"] and not result.get("error"):
|
|
543
|
+
result["error"] = "Animation playback failed"
|
|
544
|
+
|
|
545
|
+
if not result.get("warnings"):
|
|
546
|
+
result.pop("warnings", None)
|
|
547
|
+
if not result.get("details"):
|
|
548
|
+
result.pop("details", None)
|
|
549
|
+
if not result.get("availableActors"):
|
|
550
|
+
result.pop("availableActors", None)
|
|
551
|
+
if not result.get("error"):
|
|
552
|
+
result.pop("error", None)
|
|
553
|
+
|
|
554
|
+
print('RESULT:' + json.dumps(result))
|
|
536
555
|
`.trim();
|
|
537
|
-
const resp = await this.bridge.executePython(python);
|
|
538
|
-
// Parse Python result
|
|
539
|
-
let output = '';
|
|
540
|
-
if (resp && typeof resp === 'object' && Array.isArray(resp.LogOutput)) {
|
|
541
|
-
output = resp.LogOutput.map((l) => l.Output || '').join('');
|
|
542
|
-
}
|
|
543
|
-
else if (typeof resp === 'string') {
|
|
544
|
-
output = resp;
|
|
545
|
-
}
|
|
546
|
-
else {
|
|
547
|
-
output = JSON.stringify(resp);
|
|
548
|
-
}
|
|
549
|
-
const m = output.match(/RESULT:({.*})/);
|
|
550
|
-
if (m) {
|
|
551
|
-
try {
|
|
552
|
-
const parsed = JSON.parse(m[1]);
|
|
553
|
-
return parsed.success ? { success: true, message: parsed.message } : { success: false, error: parsed.message };
|
|
554
|
-
}
|
|
555
|
-
catch { }
|
|
556
|
-
}
|
|
557
|
-
return { success: true, message: `Animation ${params.animationType} processed for ${params.actorName}` };
|
|
558
|
-
}
|
|
559
|
-
catch (err) {
|
|
560
|
-
return { success: false, error: `Failed to play animation: ${err}` };
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
/**
|
|
564
|
-
* Helper function to execute console commands
|
|
565
|
-
*/
|
|
566
|
-
async _executeCommand(command) {
|
|
567
|
-
return this.bridge.httpCall('/remote/object/call', 'PUT', {
|
|
568
|
-
objectPath: '/Script/Engine.Default__KismetSystemLibrary',
|
|
569
|
-
functionName: 'ExecuteConsoleCommand',
|
|
570
|
-
parameters: {
|
|
571
|
-
WorldContextObject: null,
|
|
572
|
-
Command: command,
|
|
573
|
-
SpecificPlayer: null
|
|
574
|
-
},
|
|
575
|
-
generateTransaction: false
|
|
576
|
-
});
|
|
577
556
|
}
|
|
578
557
|
}
|
|
579
558
|
//# sourceMappingURL=animation.js.map
|