unreal-engine-mcp-server 0.3.1 → 0.4.3
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 +1 -1
- package/README.md +22 -7
- package/dist/index.js +137 -46
- 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.d.ts +3 -2
- package/dist/resources/assets.js +117 -109
- package/dist/resources/levels.d.ts +21 -3
- package/dist/resources/levels.js +31 -56
- 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 +58 -46
- 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 +236 -87
- package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
- package/dist/tools/consolidated-tool-definitions.js +124 -255
- package/dist/tools/consolidated-tool-handlers.js +749 -766
- package/dist/tools/debug.d.ts +72 -10
- package/dist/tools/debug.js +170 -36
- 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 +98 -24
- package/dist/tools/sequence.d.ts +1 -0
- package/dist/tools/sequence.js +146 -24
- 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.d.ts +6 -1
- package/dist/utils/response-validator.js +66 -13
- 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 +11 -10
- package/server.json +37 -14
- package/src/index.ts +146 -50
- package/src/prompts/index.ts +211 -13
- package/src/resources/actors.ts +59 -44
- package/src/resources/assets.ts +123 -102
- package/src/resources/levels.ts +37 -47
- package/src/tools/actors.ts +269 -313
- package/src/tools/animation.ts +556 -539
- package/src/tools/assets.ts +59 -45
- package/src/tools/audio.ts +507 -113
- package/src/tools/blueprint.ts +778 -462
- package/src/tools/build_environment_advanced.ts +312 -106
- package/src/tools/consolidated-tool-definitions.ts +136 -267
- package/src/tools/consolidated-tool-handlers.ts +871 -795
- package/src/tools/debug.ts +179 -38
- 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 +103 -25
- package/src/tools/sequence.ts +157 -30
- 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 +68 -17
- 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/tools/tool-definitions.d.ts +0 -4919
- package/dist/tools/tool-definitions.js +0 -1065
- package/dist/tools/tool-handlers.d.ts +0 -47
- package/dist/tools/tool-handlers.js +0 -863
- 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/tools/tool-definitions.ts +0 -1081
- package/src/tools/tool-handlers.ts +0 -973
- package/src/types/index.ts +0 -414
- package/src/utils/cache-manager.ts +0 -213
- package/src/utils/errors.ts +0 -312
package/src/tools/animation.ts
CHANGED
|
@@ -1,287 +1,88 @@
|
|
|
1
1
|
import { UnrealBridge } from '../unreal-bridge.js';
|
|
2
|
-
import { validateAssetParams
|
|
2
|
+
import { validateAssetParams } from '../utils/validation.js';
|
|
3
|
+
import {
|
|
4
|
+
interpretStandardResult,
|
|
5
|
+
coerceBoolean,
|
|
6
|
+
coerceString,
|
|
7
|
+
coerceStringArray
|
|
8
|
+
} from '../utils/result-helpers.js';
|
|
9
|
+
|
|
10
|
+
type CreateAnimationBlueprintSuccess = {
|
|
11
|
+
success: true;
|
|
12
|
+
message: string;
|
|
13
|
+
path: string;
|
|
14
|
+
exists?: boolean;
|
|
15
|
+
skeleton?: string;
|
|
16
|
+
warnings?: string[];
|
|
17
|
+
details?: string[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type CreateAnimationBlueprintFailure = {
|
|
21
|
+
success: false;
|
|
22
|
+
message: string;
|
|
23
|
+
error: string;
|
|
24
|
+
path?: string;
|
|
25
|
+
exists?: boolean;
|
|
26
|
+
skeleton?: string;
|
|
27
|
+
warnings?: string[];
|
|
28
|
+
details?: string[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type PlayAnimationSuccess = {
|
|
32
|
+
success: true;
|
|
33
|
+
message: string;
|
|
34
|
+
warnings?: string[];
|
|
35
|
+
details?: string[];
|
|
36
|
+
actorName?: string;
|
|
37
|
+
animationType?: string;
|
|
38
|
+
assetPath?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type PlayAnimationFailure = {
|
|
42
|
+
success: false;
|
|
43
|
+
message: string;
|
|
44
|
+
error: string;
|
|
45
|
+
warnings?: string[];
|
|
46
|
+
details?: string[];
|
|
47
|
+
availableActors?: string[];
|
|
48
|
+
actorName?: string;
|
|
49
|
+
animationType?: string;
|
|
50
|
+
assetPath?: string;
|
|
51
|
+
};
|
|
3
52
|
|
|
4
53
|
export class AnimationTools {
|
|
5
54
|
constructor(private bridge: UnrealBridge) {}
|
|
6
55
|
|
|
7
|
-
/**
|
|
8
|
-
* Create Animation Blueprint
|
|
9
|
-
*/
|
|
10
56
|
async createAnimationBlueprint(params: {
|
|
11
57
|
name: string;
|
|
12
58
|
skeletonPath: string;
|
|
13
59
|
savePath?: string;
|
|
14
|
-
}) {
|
|
60
|
+
}): Promise<CreateAnimationBlueprintSuccess | CreateAnimationBlueprintFailure> {
|
|
15
61
|
try {
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
62
|
+
const targetPath = params.savePath ?? '/Game/Animations';
|
|
63
|
+
const validation = validateAssetParams({ name: params.name, savePath: targetPath });
|
|
59
64
|
if (!validation.valid) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
message: `Failed to create Animation Blueprint: ${validation.error}`,
|
|
63
|
-
error: validation.error
|
|
64
|
-
};
|
|
65
|
+
const message = validation.error ?? 'Invalid asset parameters';
|
|
66
|
+
return { success: false, message, error: message };
|
|
65
67
|
}
|
|
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
68
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
}
|
|
69
|
+
const sanitized = validation.sanitized;
|
|
70
|
+
const assetName = sanitized.name;
|
|
71
|
+
const assetPath = sanitized.savePath ?? targetPath;
|
|
72
|
+
const script = this.buildCreateAnimationBlueprintScript({
|
|
73
|
+
name: assetName,
|
|
74
|
+
path: assetPath,
|
|
75
|
+
skeletonPath: params.skeletonPath
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const response = await this.bridge.executePython(script);
|
|
79
|
+
return this.parseAnimationBlueprintResponse(response, assetName, assetPath);
|
|
277
80
|
} catch (err) {
|
|
278
|
-
|
|
81
|
+
const error = `Failed to create Animation Blueprint: ${err}`;
|
|
82
|
+
return { success: false, message: error, error: String(err) };
|
|
279
83
|
}
|
|
280
84
|
}
|
|
281
85
|
|
|
282
|
-
/**
|
|
283
|
-
* Add State Machine to Animation Blueprint
|
|
284
|
-
*/
|
|
285
86
|
async addStateMachine(params: {
|
|
286
87
|
blueprintPath: string;
|
|
287
88
|
machineName: string;
|
|
@@ -296,174 +97,113 @@ print("DONE")
|
|
|
296
97
|
targetState: string;
|
|
297
98
|
condition?: string;
|
|
298
99
|
}>;
|
|
299
|
-
}) {
|
|
100
|
+
}): Promise<{ success: true; message: string } | { success: false; error: string }> {
|
|
300
101
|
try {
|
|
301
|
-
|
|
302
|
-
|
|
102
|
+
if (!params.blueprintPath || !params.machineName) {
|
|
103
|
+
return { success: false, error: 'blueprintPath and machineName are required' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const commands: string[] = [
|
|
303
107
|
`AddAnimStateMachine ${params.blueprintPath} ${params.machineName}`
|
|
304
108
|
];
|
|
305
|
-
|
|
306
|
-
// Add states
|
|
109
|
+
|
|
307
110
|
for (const state of params.states) {
|
|
111
|
+
const animationName = state.animation ?? '';
|
|
308
112
|
commands.push(
|
|
309
|
-
`AddAnimState ${params.blueprintPath} ${params.machineName} ${state.name} ${
|
|
113
|
+
`AddAnimState ${params.blueprintPath} ${params.machineName} ${state.name} ${animationName}`
|
|
310
114
|
);
|
|
311
115
|
if (state.isEntry) {
|
|
312
116
|
commands.push(`SetAnimStateEntry ${params.blueprintPath} ${params.machineName} ${state.name}`);
|
|
313
117
|
}
|
|
118
|
+
if (state.isExit) {
|
|
119
|
+
commands.push(`SetAnimStateExit ${params.blueprintPath} ${params.machineName} ${state.name}`);
|
|
120
|
+
}
|
|
314
121
|
}
|
|
315
|
-
|
|
316
|
-
// Add transitions
|
|
122
|
+
|
|
317
123
|
if (params.transitions) {
|
|
318
124
|
for (const transition of params.transitions) {
|
|
319
125
|
commands.push(
|
|
320
126
|
`AddAnimTransition ${params.blueprintPath} ${params.machineName} ${transition.sourceState} ${transition.targetState}`
|
|
321
127
|
);
|
|
128
|
+
if (transition.condition) {
|
|
129
|
+
commands.push(
|
|
130
|
+
`SetAnimTransitionRule ${params.blueprintPath} ${params.machineName} ${transition.sourceState} ${transition.targetState} ${transition.condition}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
322
133
|
}
|
|
323
134
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return {
|
|
330
|
-
success: true,
|
|
331
|
-
message: `State machine ${params.machineName} added to ${params.blueprintPath}`
|
|
135
|
+
|
|
136
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
137
|
+
return {
|
|
138
|
+
success: true,
|
|
139
|
+
message: `State machine ${params.machineName} added to ${params.blueprintPath}`
|
|
332
140
|
};
|
|
333
141
|
} catch (err) {
|
|
334
142
|
return { success: false, error: `Failed to add state machine: ${err}` };
|
|
335
143
|
}
|
|
336
144
|
}
|
|
337
145
|
|
|
338
|
-
|
|
339
|
-
* Create Animation Montage
|
|
340
|
-
*/
|
|
341
|
-
async createMontage(params: {
|
|
146
|
+
async createBlendSpace(params: {
|
|
342
147
|
name: string;
|
|
343
|
-
animationSequence: string;
|
|
344
148
|
savePath?: string;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}>;
|
|
350
|
-
|
|
351
|
-
name: string;
|
|
352
|
-
time: number;
|
|
353
|
-
}>;
|
|
354
|
-
}) {
|
|
149
|
+
dimensions?: 1 | 2;
|
|
150
|
+
skeletonPath?: string;
|
|
151
|
+
horizontalAxis?: { name: string; minValue: number; maxValue: number };
|
|
152
|
+
verticalAxis?: { name: string; minValue: number; maxValue: number };
|
|
153
|
+
samples?: Array<{ animation: string; x: number; y?: number }>;
|
|
154
|
+
}): Promise<{ success: true; message: string; path: string } | { success: false; error: string }> {
|
|
355
155
|
try {
|
|
356
|
-
const
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
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);
|
|
156
|
+
const targetPath = params.savePath ?? '/Game/Animations';
|
|
157
|
+
const validation = validateAssetParams({ name: params.name, savePath: targetPath });
|
|
158
|
+
if (!validation.valid) {
|
|
159
|
+
return { success: false, error: validation.error ?? 'Invalid asset parameters' };
|
|
382
160
|
}
|
|
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
161
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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}`
|
|
162
|
+
const sanitized = validation.sanitized;
|
|
163
|
+
const assetName = sanitized.name;
|
|
164
|
+
const assetPath = sanitized.savePath ?? targetPath;
|
|
165
|
+
const dimensions = params.dimensions === 2 ? 2 : 1;
|
|
166
|
+
const blendSpaceType = dimensions === 2 ? 'BlendSpace' : 'BlendSpace1D';
|
|
167
|
+
|
|
168
|
+
const commands: string[] = [
|
|
169
|
+
`CreateAsset ${blendSpaceType} ${assetName} ${assetPath}`,
|
|
170
|
+
`echo Creating ${blendSpaceType} ${assetName} at ${assetPath}`
|
|
425
171
|
];
|
|
426
|
-
|
|
427
|
-
|
|
172
|
+
|
|
173
|
+
if (params.skeletonPath) {
|
|
174
|
+
commands.push(`SetBlendSpaceSkeleton ${assetName} ${params.skeletonPath}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
428
177
|
if (params.horizontalAxis) {
|
|
429
178
|
commands.push(
|
|
430
|
-
`SetBlendSpaceAxis ${
|
|
179
|
+
`SetBlendSpaceAxis ${assetName} Horizontal ${params.horizontalAxis.name} ${params.horizontalAxis.minValue} ${params.horizontalAxis.maxValue}`
|
|
431
180
|
);
|
|
432
181
|
}
|
|
433
|
-
|
|
434
|
-
if (
|
|
182
|
+
|
|
183
|
+
if (dimensions === 2 && params.verticalAxis) {
|
|
435
184
|
commands.push(
|
|
436
|
-
`SetBlendSpaceAxis ${
|
|
185
|
+
`SetBlendSpaceAxis ${assetName} Vertical ${params.verticalAxis.name} ${params.verticalAxis.minValue} ${params.verticalAxis.maxValue}`
|
|
437
186
|
);
|
|
438
187
|
}
|
|
439
|
-
|
|
440
|
-
// Add sample animations
|
|
188
|
+
|
|
441
189
|
if (params.samples) {
|
|
442
190
|
for (const sample of params.samples) {
|
|
443
|
-
const coords =
|
|
444
|
-
commands.push(
|
|
445
|
-
`AddBlendSpaceSample ${params.name} ${sample.animation} ${coords}`
|
|
446
|
-
);
|
|
191
|
+
const coords = dimensions === 1 ? `${sample.x}` : `${sample.x} ${sample.y ?? 0}`;
|
|
192
|
+
commands.push(`AddBlendSpaceSample ${assetName} ${sample.animation} ${coords}`);
|
|
447
193
|
}
|
|
448
194
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
success: true,
|
|
456
|
-
message: `Blend Space ${params.name} created`,
|
|
457
|
-
path: `${path}/${params.name}`
|
|
195
|
+
|
|
196
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
197
|
+
return {
|
|
198
|
+
success: true,
|
|
199
|
+
message: `Blend Space ${assetName} created`,
|
|
200
|
+
path: `${assetPath}/${assetName}`
|
|
458
201
|
};
|
|
459
202
|
} catch (err) {
|
|
460
203
|
return { success: false, error: `Failed to create blend space: ${err}` };
|
|
461
204
|
}
|
|
462
205
|
}
|
|
463
206
|
|
|
464
|
-
/**
|
|
465
|
-
* Setup Control Rig
|
|
466
|
-
*/
|
|
467
207
|
async setupControlRig(params: {
|
|
468
208
|
name: string;
|
|
469
209
|
skeletonPath: string;
|
|
@@ -472,58 +212,50 @@ print("DONE")
|
|
|
472
212
|
name: string;
|
|
473
213
|
type: 'Transform' | 'Float' | 'Bool' | 'Vector';
|
|
474
214
|
bone?: string;
|
|
475
|
-
defaultValue?:
|
|
215
|
+
defaultValue?: unknown;
|
|
476
216
|
}>;
|
|
477
|
-
}) {
|
|
217
|
+
}): Promise<{ success: true; message: string; path: string } | { success: false; error: string }> {
|
|
478
218
|
try {
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
};
|
|
219
|
+
const targetPath = params.savePath ?? '/Game/Animations';
|
|
220
|
+
const validation = validateAssetParams({ name: params.name, savePath: targetPath });
|
|
221
|
+
if (!validation.valid) {
|
|
222
|
+
return { success: false, error: validation.error ?? 'Invalid asset parameters' };
|
|
489
223
|
}
|
|
490
|
-
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
224
|
+
|
|
225
|
+
const sanitized = validation.sanitized;
|
|
226
|
+
const assetName = sanitized.name;
|
|
227
|
+
const assetPath = sanitized.savePath ?? targetPath;
|
|
228
|
+
const fullPath = `${assetPath}/${assetName}`;
|
|
229
|
+
|
|
230
|
+
const commands: string[] = [
|
|
231
|
+
`CreateAsset ControlRig ${assetName} ${assetPath}`,
|
|
232
|
+
`SetControlRigSkeleton ${assetName} ${params.skeletonPath}`
|
|
494
233
|
];
|
|
495
|
-
|
|
496
|
-
// Add controls
|
|
234
|
+
|
|
497
235
|
if (params.controls) {
|
|
498
236
|
for (const control of params.controls) {
|
|
499
237
|
commands.push(
|
|
500
|
-
`AddControlRigControl ${
|
|
238
|
+
`AddControlRigControl ${assetName} ${control.name} ${control.type} ${control.bone ?? ''}`
|
|
501
239
|
);
|
|
502
240
|
if (control.defaultValue !== undefined) {
|
|
503
241
|
commands.push(
|
|
504
|
-
`SetControlRigDefault ${
|
|
242
|
+
`SetControlRigDefault ${assetName} ${control.name} ${JSON.stringify(control.defaultValue)}`
|
|
505
243
|
);
|
|
506
244
|
}
|
|
507
245
|
}
|
|
508
246
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
success: true,
|
|
516
|
-
message: `Control Rig ${params.name} created`,
|
|
517
|
-
path: `${path}/${params.name}`
|
|
247
|
+
|
|
248
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
249
|
+
return {
|
|
250
|
+
success: true,
|
|
251
|
+
message: `Control Rig ${assetName} created`,
|
|
252
|
+
path: fullPath
|
|
518
253
|
};
|
|
519
254
|
} catch (err) {
|
|
520
255
|
return { success: false, error: `Failed to setup control rig: ${err}` };
|
|
521
256
|
}
|
|
522
257
|
}
|
|
523
258
|
|
|
524
|
-
/**
|
|
525
|
-
* Create Level Sequence (for cinematics)
|
|
526
|
-
*/
|
|
527
259
|
async createLevelSequence(params: {
|
|
528
260
|
name: string;
|
|
529
261
|
savePath?: string;
|
|
@@ -532,56 +264,50 @@ print("DONE")
|
|
|
532
264
|
tracks?: Array<{
|
|
533
265
|
actorName: string;
|
|
534
266
|
trackType: 'Transform' | 'Animation' | 'Camera' | 'Event';
|
|
535
|
-
keyframes?: Array<{
|
|
536
|
-
time: number;
|
|
537
|
-
value: any;
|
|
538
|
-
}>;
|
|
267
|
+
keyframes?: Array<{ time: number; value: unknown }>;
|
|
539
268
|
}>;
|
|
540
|
-
}) {
|
|
269
|
+
}): Promise<{ success: true; message: string; path: string } | { success: false; error: string }> {
|
|
541
270
|
try {
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
271
|
+
const targetPath = params.savePath ?? '/Game/Cinematics';
|
|
272
|
+
const validation = validateAssetParams({ name: params.name, savePath: targetPath });
|
|
273
|
+
if (!validation.valid) {
|
|
274
|
+
return { success: false, error: validation.error ?? 'Invalid asset parameters' };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const sanitized = validation.sanitized;
|
|
278
|
+
const assetName = sanitized.name;
|
|
279
|
+
const assetPath = sanitized.savePath ?? targetPath;
|
|
280
|
+
|
|
281
|
+
const commands: string[] = [
|
|
282
|
+
`CreateAsset LevelSequence ${assetName} ${assetPath}`,
|
|
283
|
+
`SetSequenceFrameRate ${assetName} ${params.frameRate ?? 30}`,
|
|
284
|
+
`SetSequenceDuration ${assetName} ${params.duration ?? 5}`
|
|
548
285
|
];
|
|
549
|
-
|
|
550
|
-
// Add tracks
|
|
286
|
+
|
|
551
287
|
if (params.tracks) {
|
|
552
288
|
for (const track of params.tracks) {
|
|
553
|
-
commands.push(
|
|
554
|
-
`AddSequenceTrack ${params.name} ${track.actorName} ${track.trackType}`
|
|
555
|
-
);
|
|
556
|
-
|
|
557
|
-
// Add keyframes
|
|
289
|
+
commands.push(`AddSequenceTrack ${assetName} ${track.actorName} ${track.trackType}`);
|
|
558
290
|
if (track.keyframes) {
|
|
559
291
|
for (const keyframe of track.keyframes) {
|
|
560
292
|
commands.push(
|
|
561
|
-
`AddSequenceKey ${
|
|
293
|
+
`AddSequenceKey ${assetName} ${track.actorName} ${track.trackType} ${keyframe.time} ${JSON.stringify(keyframe.value)}`
|
|
562
294
|
);
|
|
563
295
|
}
|
|
564
296
|
}
|
|
565
297
|
}
|
|
566
298
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
success: true,
|
|
574
|
-
message: `Level Sequence ${params.name} created`,
|
|
575
|
-
path: `${path}/${params.name}`
|
|
299
|
+
|
|
300
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
301
|
+
return {
|
|
302
|
+
success: true,
|
|
303
|
+
message: `Level Sequence ${assetName} created`,
|
|
304
|
+
path: `${assetPath}/${assetName}`
|
|
576
305
|
};
|
|
577
306
|
} catch (err) {
|
|
578
307
|
return { success: false, error: `Failed to create level sequence: ${err}` };
|
|
579
308
|
}
|
|
580
309
|
}
|
|
581
310
|
|
|
582
|
-
/**
|
|
583
|
-
* Play Animation on Actor
|
|
584
|
-
*/
|
|
585
311
|
async playAnimation(params: {
|
|
586
312
|
actorName: string;
|
|
587
313
|
animationType: 'Montage' | 'Sequence' | 'BlendSpace';
|
|
@@ -590,124 +316,415 @@ print("DONE")
|
|
|
590
316
|
loop?: boolean;
|
|
591
317
|
blendInTime?: number;
|
|
592
318
|
blendOutTime?: number;
|
|
593
|
-
}) {
|
|
319
|
+
}): Promise<PlayAnimationSuccess | PlayAnimationFailure> {
|
|
594
320
|
try {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
321
|
+
const script = this.buildPlayAnimationScript({
|
|
322
|
+
actorName: params.actorName,
|
|
323
|
+
animationType: params.animationType,
|
|
324
|
+
animationPath: params.animationPath,
|
|
325
|
+
playRate: params.playRate ?? 1.0,
|
|
326
|
+
loop: params.loop ?? false,
|
|
327
|
+
blendInTime: params.blendInTime ?? 0.25,
|
|
328
|
+
blendOutTime: params.blendOutTime ?? 0.25
|
|
329
|
+
});
|
|
598
330
|
|
|
599
|
-
const
|
|
331
|
+
const response = await this.bridge.executePython(script);
|
|
332
|
+
const interpreted = interpretStandardResult(response, {
|
|
333
|
+
successMessage: `Animation ${params.animationType} triggered on ${params.actorName}`,
|
|
334
|
+
failureMessage: `Failed to play animation on ${params.actorName}`
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const payload = interpreted.payload ?? {};
|
|
338
|
+
const warnings = interpreted.warnings ?? coerceStringArray((payload as any).warnings) ?? undefined;
|
|
339
|
+
const details = interpreted.details ?? coerceStringArray((payload as any).details) ?? undefined;
|
|
340
|
+
const availableActors = coerceStringArray((payload as any).availableActors);
|
|
341
|
+
const actorName = coerceString((payload as any).actorName) ?? params.actorName;
|
|
342
|
+
const animationType = coerceString((payload as any).animationType) ?? params.animationType;
|
|
343
|
+
const assetPath = coerceString((payload as any).assetPath) ?? params.animationPath;
|
|
344
|
+
const errorMessage = coerceString((payload as any).error) ?? interpreted.error ?? `Animation playback failed for ${params.actorName}`;
|
|
345
|
+
|
|
346
|
+
if (interpreted.success) {
|
|
347
|
+
const result: PlayAnimationSuccess = {
|
|
348
|
+
success: true,
|
|
349
|
+
message: interpreted.message
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
if (warnings && warnings.length > 0) {
|
|
353
|
+
result.warnings = warnings;
|
|
354
|
+
}
|
|
355
|
+
if (details && details.length > 0) {
|
|
356
|
+
result.details = details;
|
|
357
|
+
}
|
|
358
|
+
if (actorName) {
|
|
359
|
+
result.actorName = actorName;
|
|
360
|
+
}
|
|
361
|
+
if (animationType) {
|
|
362
|
+
result.animationType = animationType;
|
|
363
|
+
}
|
|
364
|
+
if (assetPath) {
|
|
365
|
+
result.assetPath = assetPath;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const failure: PlayAnimationFailure = {
|
|
372
|
+
success: false,
|
|
373
|
+
message: `Failed to play animation: ${errorMessage}`,
|
|
374
|
+
error: errorMessage
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
if (warnings && warnings.length > 0) {
|
|
378
|
+
failure.warnings = warnings;
|
|
379
|
+
}
|
|
380
|
+
if (details && details.length > 0) {
|
|
381
|
+
failure.details = details;
|
|
382
|
+
}
|
|
383
|
+
if (availableActors && availableActors.length > 0) {
|
|
384
|
+
failure.availableActors = availableActors;
|
|
385
|
+
}
|
|
386
|
+
if (actorName) {
|
|
387
|
+
failure.actorName = actorName;
|
|
388
|
+
}
|
|
389
|
+
if (animationType) {
|
|
390
|
+
failure.animationType = animationType;
|
|
391
|
+
}
|
|
392
|
+
if (assetPath) {
|
|
393
|
+
failure.assetPath = assetPath;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return failure;
|
|
397
|
+
} catch (err) {
|
|
398
|
+
const error = `Failed to play animation: ${err}`;
|
|
399
|
+
return { success: false, message: error, error: String(err) };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private buildCreateAnimationBlueprintScript(args: {
|
|
404
|
+
name: string;
|
|
405
|
+
path: string;
|
|
406
|
+
skeletonPath: string;
|
|
407
|
+
}): string {
|
|
408
|
+
const payload = JSON.stringify(args);
|
|
409
|
+
return `
|
|
600
410
|
import unreal
|
|
601
411
|
import json
|
|
412
|
+
import traceback
|
|
602
413
|
|
|
603
|
-
|
|
414
|
+
params = json.loads(${JSON.stringify(payload)})
|
|
415
|
+
|
|
416
|
+
result = {
|
|
417
|
+
"success": False,
|
|
418
|
+
"message": "",
|
|
419
|
+
"error": "",
|
|
420
|
+
"warnings": [],
|
|
421
|
+
"details": [],
|
|
422
|
+
"exists": False,
|
|
423
|
+
"skeleton": params.get("skeletonPath") or ""
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
asset_path = (params.get("path") or "/Game").rstrip('/')
|
|
428
|
+
asset_name = params.get("name") or ""
|
|
429
|
+
full_path = f"{asset_path}/{asset_name}"
|
|
430
|
+
result["path"] = full_path
|
|
431
|
+
|
|
432
|
+
editor_lib = unreal.EditorAssetLibrary
|
|
433
|
+
asset_subsystem = None
|
|
434
|
+
try:
|
|
435
|
+
asset_subsystem = unreal.get_editor_subsystem(unreal.EditorAssetSubsystem)
|
|
436
|
+
except Exception:
|
|
437
|
+
asset_subsystem = None
|
|
438
|
+
|
|
439
|
+
skeleton_path = params.get("skeletonPath")
|
|
440
|
+
skeleton_asset = None
|
|
441
|
+
if skeleton_path:
|
|
442
|
+
if editor_lib.does_asset_exist(skeleton_path):
|
|
443
|
+
skeleton_asset = editor_lib.load_asset(skeleton_path)
|
|
444
|
+
if skeleton_asset and isinstance(skeleton_asset, unreal.Skeleton):
|
|
445
|
+
result["details"].append(f"Using skeleton: {skeleton_path}")
|
|
446
|
+
result["skeleton"] = skeleton_path
|
|
447
|
+
else:
|
|
448
|
+
result["error"] = f"Skeleton asset invalid at {skeleton_path}"
|
|
449
|
+
result["warnings"].append(result["error"])
|
|
450
|
+
skeleton_asset = None
|
|
451
|
+
else:
|
|
452
|
+
result["error"] = f"Skeleton not found at {skeleton_path}"
|
|
453
|
+
result["warnings"].append(result["error"])
|
|
454
|
+
|
|
455
|
+
if not skeleton_asset:
|
|
456
|
+
raise RuntimeError(result["error"] or f"Skeleton {skeleton_path} unavailable")
|
|
457
|
+
|
|
458
|
+
does_exist = False
|
|
459
|
+
try:
|
|
460
|
+
if asset_subsystem and hasattr(asset_subsystem, 'does_asset_exist'):
|
|
461
|
+
does_exist = asset_subsystem.does_asset_exist(full_path)
|
|
462
|
+
else:
|
|
463
|
+
does_exist = editor_lib.does_asset_exist(full_path)
|
|
464
|
+
except Exception:
|
|
465
|
+
does_exist = editor_lib.does_asset_exist(full_path)
|
|
466
|
+
|
|
467
|
+
if does_exist:
|
|
468
|
+
result["exists"] = True
|
|
469
|
+
loaded = editor_lib.load_asset(full_path)
|
|
470
|
+
if loaded:
|
|
471
|
+
result["success"] = True
|
|
472
|
+
result["message"] = f"Animation Blueprint already exists at {full_path}"
|
|
473
|
+
result["details"].append(result["message"])
|
|
474
|
+
else:
|
|
475
|
+
result["error"] = f"Asset exists but could not be loaded: {full_path}"
|
|
476
|
+
result["warnings"].append(result["error"])
|
|
477
|
+
else:
|
|
478
|
+
factory = unreal.AnimBlueprintFactory()
|
|
479
|
+
if skeleton_asset:
|
|
480
|
+
try:
|
|
481
|
+
factory.target_skeleton = skeleton_asset
|
|
482
|
+
except Exception as assign_error:
|
|
483
|
+
result["warnings"].append(f"Unable to assign skeleton {skeleton_path}: {assign_error}")
|
|
484
|
+
|
|
485
|
+
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
|
486
|
+
created = asset_tools.create_asset(
|
|
487
|
+
asset_name=asset_name,
|
|
488
|
+
package_path=asset_path,
|
|
489
|
+
asset_class=unreal.AnimBlueprint,
|
|
490
|
+
factory=factory
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
if created:
|
|
494
|
+
editor_lib.save_asset(full_path, only_if_is_dirty=False)
|
|
495
|
+
result["success"] = True
|
|
496
|
+
result["message"] = f"Animation Blueprint created at {full_path}"
|
|
497
|
+
result["details"].append(result["message"])
|
|
498
|
+
else:
|
|
499
|
+
result["error"] = f"Failed to create Animation Blueprint {asset_name}"
|
|
500
|
+
|
|
501
|
+
except Exception as exc:
|
|
502
|
+
result["error"] = str(exc)
|
|
503
|
+
result["warnings"].append(result["error"])
|
|
504
|
+
tb = traceback.format_exc()
|
|
505
|
+
if tb:
|
|
506
|
+
result.setdefault("details", []).append(tb)
|
|
507
|
+
|
|
508
|
+
if result["success"] and not result.get("message"):
|
|
509
|
+
result["message"] = f"Animation Blueprint created at {result.get('path')}"
|
|
510
|
+
|
|
511
|
+
if not result["success"] and not result.get("error"):
|
|
512
|
+
result["error"] = "Animation Blueprint creation failed"
|
|
513
|
+
|
|
514
|
+
if not result.get("warnings"):
|
|
515
|
+
result.pop("warnings", None)
|
|
516
|
+
if not result.get("details"):
|
|
517
|
+
result.pop("details", None)
|
|
518
|
+
if not result.get("error"):
|
|
519
|
+
result.pop("error", None)
|
|
520
|
+
|
|
521
|
+
print('RESULT:' + json.dumps(result))
|
|
522
|
+
`.trim();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private parseAnimationBlueprintResponse(
|
|
526
|
+
response: unknown,
|
|
527
|
+
assetName: string,
|
|
528
|
+
assetPath: string
|
|
529
|
+
): CreateAnimationBlueprintSuccess | CreateAnimationBlueprintFailure {
|
|
530
|
+
const interpreted = interpretStandardResult(response, {
|
|
531
|
+
successMessage: `Animation Blueprint ${assetName} created`,
|
|
532
|
+
failureMessage: `Failed to create Animation Blueprint ${assetName}`
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const payload = interpreted.payload ?? {};
|
|
536
|
+
const path = coerceString((payload as any).path) ?? `${assetPath}/${assetName}`;
|
|
537
|
+
const exists = coerceBoolean((payload as any).exists);
|
|
538
|
+
const skeleton = coerceString((payload as any).skeleton);
|
|
539
|
+
const warnings = interpreted.warnings ?? coerceStringArray((payload as any).warnings) ?? undefined;
|
|
540
|
+
const details = interpreted.details ?? coerceStringArray((payload as any).details) ?? undefined;
|
|
541
|
+
|
|
542
|
+
if (interpreted.success) {
|
|
543
|
+
const result: CreateAnimationBlueprintSuccess = {
|
|
544
|
+
success: true,
|
|
545
|
+
message: interpreted.message,
|
|
546
|
+
path
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
if (typeof exists === 'boolean') {
|
|
550
|
+
result.exists = exists;
|
|
551
|
+
}
|
|
552
|
+
if (skeleton) {
|
|
553
|
+
result.skeleton = skeleton;
|
|
554
|
+
}
|
|
555
|
+
if (warnings && warnings.length > 0) {
|
|
556
|
+
result.warnings = warnings;
|
|
557
|
+
}
|
|
558
|
+
if (details && details.length > 0) {
|
|
559
|
+
result.details = details;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return result;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const errorMessage = coerceString((payload as any).error) ?? interpreted.error ?? interpreted.message;
|
|
566
|
+
|
|
567
|
+
const failure: CreateAnimationBlueprintFailure = {
|
|
568
|
+
success: false,
|
|
569
|
+
message: `Failed to create Animation Blueprint: ${errorMessage}`,
|
|
570
|
+
error: errorMessage,
|
|
571
|
+
path
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
if (typeof exists === 'boolean') {
|
|
575
|
+
failure.exists = exists;
|
|
576
|
+
}
|
|
577
|
+
if (skeleton) {
|
|
578
|
+
failure.skeleton = skeleton;
|
|
579
|
+
}
|
|
580
|
+
if (warnings && warnings.length > 0) {
|
|
581
|
+
failure.warnings = warnings;
|
|
582
|
+
}
|
|
583
|
+
if (details && details.length > 0) {
|
|
584
|
+
failure.details = details;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return failure;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
private buildPlayAnimationScript(args: {
|
|
591
|
+
actorName: string;
|
|
592
|
+
animationType: string;
|
|
593
|
+
animationPath: string;
|
|
594
|
+
playRate: number;
|
|
595
|
+
loop: boolean;
|
|
596
|
+
blendInTime: number;
|
|
597
|
+
blendOutTime: number;
|
|
598
|
+
}): string {
|
|
599
|
+
const payload = JSON.stringify(args);
|
|
600
|
+
return `
|
|
601
|
+
import unreal
|
|
602
|
+
import json
|
|
603
|
+
import traceback
|
|
604
|
+
|
|
605
|
+
params = json.loads(${JSON.stringify(payload)})
|
|
606
|
+
|
|
607
|
+
result = {
|
|
608
|
+
"success": False,
|
|
609
|
+
"message": "",
|
|
610
|
+
"error": "",
|
|
611
|
+
"warnings": [],
|
|
612
|
+
"details": [],
|
|
613
|
+
"actorName": params.get("actorName"),
|
|
614
|
+
"animationType": params.get("animationType"),
|
|
615
|
+
"assetPath": params.get("animationPath"),
|
|
616
|
+
"availableActors": []
|
|
617
|
+
}
|
|
604
618
|
|
|
605
619
|
try:
|
|
606
620
|
actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
607
|
-
actors = actor_subsystem.get_all_level_actors()
|
|
621
|
+
actors = actor_subsystem.get_all_level_actors() if actor_subsystem else []
|
|
608
622
|
target = None
|
|
609
|
-
search =
|
|
610
|
-
|
|
611
|
-
|
|
623
|
+
search = params.get("actorName") or ""
|
|
624
|
+
search_lower = search.lower()
|
|
625
|
+
|
|
626
|
+
for actor in actors:
|
|
627
|
+
if not actor:
|
|
612
628
|
continue
|
|
613
|
-
name =
|
|
614
|
-
label =
|
|
615
|
-
if (
|
|
616
|
-
target =
|
|
629
|
+
name = (actor.get_name() or "").lower()
|
|
630
|
+
label = (actor.get_actor_label() or "").lower()
|
|
631
|
+
if search_lower and (search_lower == name or search_lower == label or search_lower in label):
|
|
632
|
+
target = actor
|
|
617
633
|
break
|
|
618
634
|
|
|
619
635
|
if not target:
|
|
620
|
-
result["
|
|
636
|
+
result["error"] = f"Actor not found: {search}"
|
|
637
|
+
result["warnings"].append("Actor search yielded no results")
|
|
638
|
+
suggestions = []
|
|
639
|
+
for actor in actors[:20]:
|
|
640
|
+
try:
|
|
641
|
+
suggestions.append(actor.get_actor_label())
|
|
642
|
+
except Exception:
|
|
643
|
+
continue
|
|
644
|
+
if suggestions:
|
|
645
|
+
result["availableActors"] = suggestions
|
|
621
646
|
else:
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
647
|
+
try:
|
|
648
|
+
display_name = target.get_actor_label() or target.get_name()
|
|
649
|
+
if display_name:
|
|
650
|
+
result["actorName"] = display_name
|
|
651
|
+
except Exception:
|
|
652
|
+
pass
|
|
653
|
+
|
|
654
|
+
skeletal_component = target.get_component_by_class(unreal.SkeletalMeshComponent)
|
|
655
|
+
if not skeletal_component:
|
|
626
656
|
try:
|
|
627
|
-
|
|
657
|
+
skeletal_component = target.get_editor_property('mesh')
|
|
628
658
|
except Exception:
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
659
|
+
skeletal_component = None
|
|
660
|
+
|
|
661
|
+
if not skeletal_component:
|
|
662
|
+
result["error"] = "No SkeletalMeshComponent found on actor"
|
|
663
|
+
result["warnings"].append("Actor lacks SkeletalMeshComponent")
|
|
632
664
|
else:
|
|
633
|
-
|
|
634
|
-
asset_path
|
|
635
|
-
|
|
636
|
-
result["
|
|
665
|
+
asset_path = params.get("animationPath")
|
|
666
|
+
if not asset_path or not unreal.EditorAssetLibrary.does_asset_exist(asset_path):
|
|
667
|
+
result["error"] = f"Animation asset not found: {asset_path}"
|
|
668
|
+
result["warnings"].append("Animation asset missing")
|
|
637
669
|
else:
|
|
638
670
|
asset = unreal.EditorAssetLibrary.load_asset(asset_path)
|
|
671
|
+
anim_type = params.get("animationType") or ""
|
|
639
672
|
if anim_type == 'Montage':
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
if not inst:
|
|
643
|
-
result["message"] = "AnimInstance not found on SkeletalMeshComponent"
|
|
644
|
-
else:
|
|
673
|
+
anim_instance = skeletal_component.get_anim_instance()
|
|
674
|
+
if anim_instance:
|
|
645
675
|
try:
|
|
646
|
-
|
|
647
|
-
inst.montage_play(asset, ${playRate})
|
|
676
|
+
anim_instance.montage_play(asset, params.get("playRate", 1.0))
|
|
648
677
|
result["success"] = True
|
|
649
|
-
result["message"] = f"Montage playing on {search}"
|
|
650
|
-
|
|
651
|
-
|
|
678
|
+
result["message"] = f"Montage playing on {result.get('actorName') or search}"
|
|
679
|
+
result["details"].append(result["message"])
|
|
680
|
+
except Exception as play_error:
|
|
681
|
+
result["error"] = f"Failed to play montage: {play_error}"
|
|
682
|
+
result["warnings"].append(result["error"])
|
|
683
|
+
else:
|
|
684
|
+
result["error"] = "AnimInstance not found on SkeletalMeshComponent"
|
|
685
|
+
result["warnings"].append(result["error"])
|
|
652
686
|
elif anim_type == 'Sequence':
|
|
653
687
|
try:
|
|
654
|
-
|
|
655
|
-
# Adjust rate if supported via play rate on AnimInstance
|
|
688
|
+
skeletal_component.play_animation(asset, bool(params.get("loop")))
|
|
656
689
|
try:
|
|
657
|
-
|
|
658
|
-
if
|
|
659
|
-
|
|
660
|
-
pass
|
|
690
|
+
anim_instance = skeletal_component.get_anim_instance()
|
|
691
|
+
if anim_instance:
|
|
692
|
+
anim_instance.set_play_rate(params.get("playRate", 1.0))
|
|
661
693
|
except Exception:
|
|
662
694
|
pass
|
|
663
695
|
result["success"] = True
|
|
664
|
-
result["message"] = f"Sequence playing on {search}"
|
|
665
|
-
|
|
666
|
-
|
|
696
|
+
result["message"] = f"Sequence playing on {result.get('actorName') or search}"
|
|
697
|
+
result["details"].append(result["message"])
|
|
698
|
+
except Exception as play_error:
|
|
699
|
+
result["error"] = f"Failed to play sequence: {play_error}"
|
|
700
|
+
result["warnings"].append(result["error"])
|
|
667
701
|
else:
|
|
668
|
-
result["
|
|
669
|
-
|
|
670
|
-
result["message"] = f"Error: {e}"
|
|
702
|
+
result["error"] = "BlendSpace playback requires Animation Blueprint support"
|
|
703
|
+
result["warnings"].append("Unsupported animation type for direct play")
|
|
671
704
|
|
|
672
|
-
|
|
673
|
-
|
|
705
|
+
except Exception as exc:
|
|
706
|
+
result["error"] = str(exc)
|
|
707
|
+
result["warnings"].append(result["error"])
|
|
708
|
+
tb = traceback.format_exc()
|
|
709
|
+
if tb:
|
|
710
|
+
result["details"].append(tb)
|
|
674
711
|
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
}
|
|
712
|
+
if result["success"] and not result.get("message"):
|
|
713
|
+
result["message"] = f"Animation {result.get('animationType')} triggered on {result.get('actorName') or params.get('actorName')}"
|
|
697
714
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
715
|
+
if not result["success"] and not result.get("error"):
|
|
716
|
+
result["error"] = "Animation playback failed"
|
|
717
|
+
|
|
718
|
+
if not result.get("warnings"):
|
|
719
|
+
result.pop("warnings", None)
|
|
720
|
+
if not result.get("details"):
|
|
721
|
+
result.pop("details", None)
|
|
722
|
+
if not result.get("availableActors"):
|
|
723
|
+
result.pop("availableActors", None)
|
|
724
|
+
if not result.get("error"):
|
|
725
|
+
result.pop("error", None)
|
|
726
|
+
|
|
727
|
+
print('RESULT:' + json.dumps(result))
|
|
728
|
+
`.trim();
|
|
712
729
|
}
|
|
713
730
|
}
|