unreal-engine-mcp-server 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +57 -0
- package/.env.production +25 -0
- package/.eslintrc.json +54 -0
- package/.github/workflows/publish-mcp.yml +75 -0
- package/Dockerfile +54 -0
- package/LICENSE +21 -0
- package/Public/icon.png +0 -0
- package/README.md +209 -0
- package/claude_desktop_config_example.json +13 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +7 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +484 -0
- package/dist/prompts/index.d.ts +14 -0
- package/dist/prompts/index.js +38 -0
- package/dist/python-utils.d.ts +29 -0
- package/dist/python-utils.js +54 -0
- package/dist/resources/actors.d.ts +13 -0
- package/dist/resources/actors.js +83 -0
- package/dist/resources/assets.d.ts +23 -0
- package/dist/resources/assets.js +245 -0
- package/dist/resources/levels.d.ts +17 -0
- package/dist/resources/levels.js +94 -0
- package/dist/tools/actors.d.ts +51 -0
- package/dist/tools/actors.js +459 -0
- package/dist/tools/animation.d.ts +196 -0
- package/dist/tools/animation.js +579 -0
- package/dist/tools/assets.d.ts +21 -0
- package/dist/tools/assets.js +304 -0
- package/dist/tools/audio.d.ts +170 -0
- package/dist/tools/audio.js +416 -0
- package/dist/tools/blueprint.d.ts +144 -0
- package/dist/tools/blueprint.js +652 -0
- package/dist/tools/build_environment_advanced.d.ts +66 -0
- package/dist/tools/build_environment_advanced.js +484 -0
- package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
- package/dist/tools/consolidated-tool-definitions.js +607 -0
- package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
- package/dist/tools/consolidated-tool-handlers.js +1050 -0
- package/dist/tools/debug.d.ts +185 -0
- package/dist/tools/debug.js +265 -0
- package/dist/tools/editor.d.ts +88 -0
- package/dist/tools/editor.js +365 -0
- package/dist/tools/engine.d.ts +30 -0
- package/dist/tools/engine.js +36 -0
- package/dist/tools/foliage.d.ts +155 -0
- package/dist/tools/foliage.js +525 -0
- package/dist/tools/introspection.d.ts +98 -0
- package/dist/tools/introspection.js +683 -0
- package/dist/tools/landscape.d.ts +158 -0
- package/dist/tools/landscape.js +375 -0
- package/dist/tools/level.d.ts +110 -0
- package/dist/tools/level.js +362 -0
- package/dist/tools/lighting.d.ts +159 -0
- package/dist/tools/lighting.js +1179 -0
- package/dist/tools/materials.d.ts +34 -0
- package/dist/tools/materials.js +146 -0
- package/dist/tools/niagara.d.ts +145 -0
- package/dist/tools/niagara.js +289 -0
- package/dist/tools/performance.d.ts +163 -0
- package/dist/tools/performance.js +412 -0
- package/dist/tools/physics.d.ts +189 -0
- package/dist/tools/physics.js +784 -0
- package/dist/tools/rc.d.ts +110 -0
- package/dist/tools/rc.js +363 -0
- package/dist/tools/sequence.d.ts +112 -0
- package/dist/tools/sequence.js +675 -0
- package/dist/tools/tool-definitions.d.ts +4919 -0
- package/dist/tools/tool-definitions.js +891 -0
- package/dist/tools/tool-handlers.d.ts +47 -0
- package/dist/tools/tool-handlers.js +830 -0
- package/dist/tools/ui.d.ts +171 -0
- package/dist/tools/ui.js +337 -0
- package/dist/tools/visual.d.ts +29 -0
- package/dist/tools/visual.js +67 -0
- package/dist/types/env.d.ts +10 -0
- package/dist/types/env.js +18 -0
- package/dist/types/index.d.ts +323 -0
- package/dist/types/index.js +28 -0
- package/dist/types/tool-types.d.ts +274 -0
- package/dist/types/tool-types.js +13 -0
- package/dist/unreal-bridge.d.ts +126 -0
- package/dist/unreal-bridge.js +992 -0
- package/dist/utils/cache-manager.d.ts +64 -0
- package/dist/utils/cache-manager.js +176 -0
- package/dist/utils/error-handler.d.ts +66 -0
- package/dist/utils/error-handler.js +243 -0
- package/dist/utils/errors.d.ts +133 -0
- package/dist/utils/errors.js +256 -0
- package/dist/utils/http.d.ts +26 -0
- package/dist/utils/http.js +135 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +32 -0
- package/dist/utils/normalize.d.ts +17 -0
- package/dist/utils/normalize.js +49 -0
- package/dist/utils/response-validator.d.ts +34 -0
- package/dist/utils/response-validator.js +121 -0
- package/dist/utils/safe-json.d.ts +4 -0
- package/dist/utils/safe-json.js +97 -0
- package/dist/utils/stdio-redirect.d.ts +2 -0
- package/dist/utils/stdio-redirect.js +20 -0
- package/dist/utils/validation.d.ts +50 -0
- package/dist/utils/validation.js +173 -0
- package/mcp-config-example.json +14 -0
- package/package.json +63 -0
- package/server.json +60 -0
- package/src/cli.ts +7 -0
- package/src/index.ts +543 -0
- package/src/prompts/index.ts +51 -0
- package/src/python/editor_compat.py +181 -0
- package/src/python-utils.ts +57 -0
- package/src/resources/actors.ts +92 -0
- package/src/resources/assets.ts +251 -0
- package/src/resources/levels.ts +83 -0
- package/src/tools/actors.ts +480 -0
- package/src/tools/animation.ts +713 -0
- package/src/tools/assets.ts +305 -0
- package/src/tools/audio.ts +548 -0
- package/src/tools/blueprint.ts +736 -0
- package/src/tools/build_environment_advanced.ts +526 -0
- package/src/tools/consolidated-tool-definitions.ts +619 -0
- package/src/tools/consolidated-tool-handlers.ts +1093 -0
- package/src/tools/debug.ts +368 -0
- package/src/tools/editor.ts +360 -0
- package/src/tools/engine.ts +32 -0
- package/src/tools/foliage.ts +652 -0
- package/src/tools/introspection.ts +778 -0
- package/src/tools/landscape.ts +523 -0
- package/src/tools/level.ts +410 -0
- package/src/tools/lighting.ts +1316 -0
- package/src/tools/materials.ts +148 -0
- package/src/tools/niagara.ts +312 -0
- package/src/tools/performance.ts +549 -0
- package/src/tools/physics.ts +924 -0
- package/src/tools/rc.ts +437 -0
- package/src/tools/sequence.ts +791 -0
- package/src/tools/tool-definitions.ts +907 -0
- package/src/tools/tool-handlers.ts +941 -0
- package/src/tools/ui.ts +499 -0
- package/src/tools/visual.ts +60 -0
- package/src/types/env.ts +27 -0
- package/src/types/index.ts +414 -0
- package/src/types/tool-types.ts +343 -0
- package/src/unreal-bridge.ts +1118 -0
- package/src/utils/cache-manager.ts +213 -0
- package/src/utils/error-handler.ts +320 -0
- package/src/utils/errors.ts +312 -0
- package/src/utils/http.ts +184 -0
- package/src/utils/logger.ts +30 -0
- package/src/utils/normalize.ts +54 -0
- package/src/utils/response-validator.ts +145 -0
- package/src/utils/safe-json.ts +112 -0
- package/src/utils/stdio-redirect.ts +18 -0
- package/src/utils/validation.ts +212 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import { Logger } from '../utils/logger.js';
|
|
2
|
+
export class SequenceTools {
|
|
3
|
+
bridge;
|
|
4
|
+
log = new Logger('SequenceTools');
|
|
5
|
+
sequenceCache = new Map();
|
|
6
|
+
retryAttempts = 3;
|
|
7
|
+
retryDelay = 1000;
|
|
8
|
+
constructor(bridge) {
|
|
9
|
+
this.bridge = bridge;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Execute with retry logic for transient failures
|
|
13
|
+
*/
|
|
14
|
+
async executeWithRetry(operation, operationName) {
|
|
15
|
+
let lastError;
|
|
16
|
+
for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
|
|
17
|
+
try {
|
|
18
|
+
return await operation();
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
lastError = error;
|
|
22
|
+
this.log.warn(`${operationName} attempt ${attempt} failed: ${error.message || error}`);
|
|
23
|
+
if (attempt < this.retryAttempts) {
|
|
24
|
+
await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
throw lastError;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse Python execution result with better error handling
|
|
32
|
+
*/
|
|
33
|
+
parsePythonResult(resp, operationName) {
|
|
34
|
+
let out = '';
|
|
35
|
+
if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
|
|
36
|
+
out = resp.LogOutput.map((l) => l.Output || '').join('');
|
|
37
|
+
}
|
|
38
|
+
else if (typeof resp === 'string') {
|
|
39
|
+
out = resp;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
out = JSON.stringify(resp);
|
|
43
|
+
}
|
|
44
|
+
const m = out.match(/RESULT:({.*})/);
|
|
45
|
+
if (m) {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(m[1]);
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
this.log.error(`Failed to parse ${operationName} result: ${e}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Check for common error patterns
|
|
54
|
+
if (out.includes('ModuleNotFoundError')) {
|
|
55
|
+
return { success: false, error: 'Sequencer module not available. Ensure Sequencer is enabled.' };
|
|
56
|
+
}
|
|
57
|
+
if (out.includes('AttributeError')) {
|
|
58
|
+
return { success: false, error: 'Sequencer API method not found. Check Unreal Engine version compatibility.' };
|
|
59
|
+
}
|
|
60
|
+
return { success: false, error: `${operationName} did not return a valid result: ${out.substring(0, 200)}` };
|
|
61
|
+
}
|
|
62
|
+
async create(params) {
|
|
63
|
+
const name = params.name?.trim();
|
|
64
|
+
const base = (params.path || '/Game/Sequences').replace(/\/$/, '');
|
|
65
|
+
if (!name)
|
|
66
|
+
return { success: false, error: 'name is required' };
|
|
67
|
+
const py = `
|
|
68
|
+
import unreal, json
|
|
69
|
+
name = r"${name}"
|
|
70
|
+
base = r"${base}"
|
|
71
|
+
full = f"{base}/{name}"
|
|
72
|
+
try:
|
|
73
|
+
# Ensure directory exists
|
|
74
|
+
try:
|
|
75
|
+
if not unreal.EditorAssetLibrary.does_directory_exist(base):
|
|
76
|
+
unreal.EditorAssetLibrary.make_directory(base)
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
if unreal.EditorAssetLibrary.does_asset_exist(full):
|
|
81
|
+
print('RESULT:' + json.dumps({'success': True, 'sequencePath': full, 'existing': True}))
|
|
82
|
+
else:
|
|
83
|
+
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
|
84
|
+
factory = unreal.LevelSequenceFactoryNew()
|
|
85
|
+
seq = asset_tools.create_asset(asset_name=name, package_path=base, asset_class=unreal.LevelSequence, factory=factory)
|
|
86
|
+
if seq:
|
|
87
|
+
unreal.EditorAssetLibrary.save_asset(full)
|
|
88
|
+
print('RESULT:' + json.dumps({'success': True, 'sequencePath': full}))
|
|
89
|
+
else:
|
|
90
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Create returned None'}))
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
93
|
+
`.trim();
|
|
94
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'createSequence');
|
|
95
|
+
const result = this.parsePythonResult(resp, 'createSequence');
|
|
96
|
+
// Cache the sequence if successful
|
|
97
|
+
if (result.success && result.sequencePath) {
|
|
98
|
+
const sequence = {
|
|
99
|
+
path: result.sequencePath,
|
|
100
|
+
name: name
|
|
101
|
+
};
|
|
102
|
+
this.sequenceCache.set(sequence.path, sequence);
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
async open(params) {
|
|
107
|
+
const py = `
|
|
108
|
+
import unreal, json
|
|
109
|
+
path = r"${params.path}"
|
|
110
|
+
try:
|
|
111
|
+
seq = unreal.load_asset(path)
|
|
112
|
+
if not seq:
|
|
113
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Sequence not found'}))
|
|
114
|
+
else:
|
|
115
|
+
unreal.LevelSequenceEditorBlueprintLibrary.open_level_sequence(seq)
|
|
116
|
+
print('RESULT:' + json.dumps({'success': True, 'sequencePath': path}))
|
|
117
|
+
except Exception as e:
|
|
118
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
119
|
+
`.trim();
|
|
120
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'openSequence');
|
|
121
|
+
return this.parsePythonResult(resp, 'openSequence');
|
|
122
|
+
}
|
|
123
|
+
async addCamera(params) {
|
|
124
|
+
const py = `
|
|
125
|
+
import unreal, json
|
|
126
|
+
try:
|
|
127
|
+
ls = unreal.get_editor_subsystem(unreal.LevelSequenceEditorSubsystem)
|
|
128
|
+
if not ls:
|
|
129
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'LevelSequenceEditorSubsystem unavailable'}))
|
|
130
|
+
else:
|
|
131
|
+
# create_camera returns tuple: (binding_proxy, camera_actor)
|
|
132
|
+
result = ls.create_camera(spawnable=${params.spawnable !== false ? 'True' : 'False'})
|
|
133
|
+
binding_id = ''
|
|
134
|
+
camera_name = ''
|
|
135
|
+
|
|
136
|
+
if result and len(result) >= 2:
|
|
137
|
+
binding_proxy = result[0]
|
|
138
|
+
camera_actor = result[1]
|
|
139
|
+
|
|
140
|
+
# Get the current sequence
|
|
141
|
+
seq = unreal.LevelSequenceEditorBlueprintLibrary.get_focused_level_sequence()
|
|
142
|
+
|
|
143
|
+
if seq and binding_proxy:
|
|
144
|
+
try:
|
|
145
|
+
# Get GUID directly from binding proxy - this is more reliable
|
|
146
|
+
binding_guid = unreal.MovieSceneBindingExtensions.get_id(binding_proxy)
|
|
147
|
+
# The GUID itself is what we need
|
|
148
|
+
binding_id = str(binding_guid).replace('<Guid ', '').replace('>', '').split(' ')[0] if str(binding_guid).startswith('<') else str(binding_guid)
|
|
149
|
+
|
|
150
|
+
# If that didn't work, try the binding object
|
|
151
|
+
if binding_id.startswith('<') or not binding_id:
|
|
152
|
+
binding_obj = unreal.MovieSceneSequenceExtensions.get_binding_id(seq, binding_proxy)
|
|
153
|
+
# Try to extract GUID from the object representation
|
|
154
|
+
obj_str = str(binding_obj)
|
|
155
|
+
if 'guid=' in obj_str:
|
|
156
|
+
binding_id = obj_str.split('guid=')[1].split(',')[0].split('}')[0].strip()
|
|
157
|
+
elif hasattr(binding_obj, 'guid'):
|
|
158
|
+
binding_id = str(binding_obj.guid)
|
|
159
|
+
else:
|
|
160
|
+
# Use a hash of the binding for a consistent ID
|
|
161
|
+
import hashlib
|
|
162
|
+
binding_id = hashlib.md5(str(binding_proxy).encode()).hexdigest()[:8]
|
|
163
|
+
except Exception as e:
|
|
164
|
+
# Generate a unique ID based on camera
|
|
165
|
+
import hashlib
|
|
166
|
+
camera_str = camera_actor.get_name() if camera_actor else 'spawned'
|
|
167
|
+
binding_id = f'cam_{hashlib.md5(camera_str.encode()).hexdigest()[:8]}'
|
|
168
|
+
|
|
169
|
+
if camera_actor:
|
|
170
|
+
try:
|
|
171
|
+
camera_name = camera_actor.get_actor_label()
|
|
172
|
+
except:
|
|
173
|
+
camera_name = 'CineCamera'
|
|
174
|
+
|
|
175
|
+
print('RESULT:' + json.dumps({
|
|
176
|
+
'success': True,
|
|
177
|
+
'cameraBindingId': binding_id,
|
|
178
|
+
'cameraName': camera_name
|
|
179
|
+
}))
|
|
180
|
+
else:
|
|
181
|
+
# Even if result format is different, camera might still be created
|
|
182
|
+
print('RESULT:' + json.dumps({
|
|
183
|
+
'success': True,
|
|
184
|
+
'cameraBindingId': 'camera_created',
|
|
185
|
+
'warning': 'Camera created but binding format unexpected'
|
|
186
|
+
}))
|
|
187
|
+
except Exception as e:
|
|
188
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
189
|
+
`.trim();
|
|
190
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'addCamera');
|
|
191
|
+
return this.parsePythonResult(resp, 'addCamera');
|
|
192
|
+
}
|
|
193
|
+
async addActor(params) {
|
|
194
|
+
const py = `
|
|
195
|
+
import unreal, json
|
|
196
|
+
try:
|
|
197
|
+
actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
198
|
+
ls = unreal.get_editor_subsystem(unreal.LevelSequenceEditorSubsystem)
|
|
199
|
+
if not ls or not actor_sub:
|
|
200
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Subsystem unavailable'}))
|
|
201
|
+
else:
|
|
202
|
+
target = None
|
|
203
|
+
actors = actor_sub.get_all_level_actors()
|
|
204
|
+
for a in actors:
|
|
205
|
+
if not a: continue
|
|
206
|
+
label = a.get_actor_label()
|
|
207
|
+
name = a.get_name()
|
|
208
|
+
# Check label, name, and partial matches
|
|
209
|
+
if label == r"${params.actorName}" or name == r"${params.actorName}" or label.startswith(r"${params.actorName}"):
|
|
210
|
+
target = a
|
|
211
|
+
break
|
|
212
|
+
|
|
213
|
+
if not target:
|
|
214
|
+
# Try to find any actors to debug
|
|
215
|
+
actor_info = []
|
|
216
|
+
for a in actors[:5]:
|
|
217
|
+
if a:
|
|
218
|
+
actor_info.append({'label': a.get_actor_label(), 'name': a.get_name()})
|
|
219
|
+
print('RESULT:' + json.dumps({'success': False, 'error': f'Actor "${params.actorName}" not found. Sample actors: {actor_info}'}))
|
|
220
|
+
else:
|
|
221
|
+
# Make sure we have a focused sequence
|
|
222
|
+
seq = unreal.LevelSequenceEditorBlueprintLibrary.get_focused_level_sequence()
|
|
223
|
+
if seq:
|
|
224
|
+
# Use add_actors method which returns binding proxies
|
|
225
|
+
bindings = ls.add_actors([target])
|
|
226
|
+
binding_info = []
|
|
227
|
+
|
|
228
|
+
# bindings might be a list or might be empty if actor already exists
|
|
229
|
+
if bindings and len(bindings) > 0:
|
|
230
|
+
for binding in bindings:
|
|
231
|
+
try:
|
|
232
|
+
# Get binding name and GUID
|
|
233
|
+
binding_name = unreal.MovieSceneBindingExtensions.get_name(binding)
|
|
234
|
+
binding_guid = unreal.MovieSceneBindingExtensions.get_id(binding)
|
|
235
|
+
|
|
236
|
+
# Extract clean GUID string
|
|
237
|
+
guid_str = str(binding_guid)
|
|
238
|
+
if guid_str.startswith('<Guid '):
|
|
239
|
+
# Extract the actual GUID value from <Guid 'XXXX-XXXX-XXXX-XXXX'>
|
|
240
|
+
guid_clean = guid_str.replace('<Guid ', '').replace('>', '').replace("'", '').split(' ')[0]
|
|
241
|
+
else:
|
|
242
|
+
guid_clean = guid_str
|
|
243
|
+
|
|
244
|
+
binding_info.append({
|
|
245
|
+
'id': guid_clean,
|
|
246
|
+
'guid': guid_clean,
|
|
247
|
+
'name': binding_name if binding_name else target.get_actor_label()
|
|
248
|
+
})
|
|
249
|
+
except Exception as e:
|
|
250
|
+
# If binding methods fail, still count it
|
|
251
|
+
binding_info.append({
|
|
252
|
+
'id': 'binding_' + str(len(binding_info)),
|
|
253
|
+
'name': target.get_actor_label(),
|
|
254
|
+
'error': str(e)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
print('RESULT:' + json.dumps({
|
|
258
|
+
'success': True,
|
|
259
|
+
'count': len(bindings),
|
|
260
|
+
'actorAdded': target.get_actor_label(),
|
|
261
|
+
'bindings': binding_info
|
|
262
|
+
}))
|
|
263
|
+
else:
|
|
264
|
+
# Actor was likely added but no new binding returned (might already exist)
|
|
265
|
+
# Still report success since the actor is in the sequence
|
|
266
|
+
print('RESULT:' + json.dumps({
|
|
267
|
+
'success': True,
|
|
268
|
+
'count': 1,
|
|
269
|
+
'actorAdded': target.get_actor_label(),
|
|
270
|
+
'bindings': [{'name': target.get_actor_label(), 'note': 'Actor added to sequence'}],
|
|
271
|
+
'info': 'Actor processed successfully'
|
|
272
|
+
}))
|
|
273
|
+
else:
|
|
274
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'No sequence is currently focused'}))
|
|
275
|
+
except Exception as e:
|
|
276
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
277
|
+
`.trim();
|
|
278
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'addActor');
|
|
279
|
+
return this.parsePythonResult(resp, 'addActor');
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Play the current level sequence
|
|
283
|
+
*/
|
|
284
|
+
async play(params) {
|
|
285
|
+
const py = `
|
|
286
|
+
import unreal, json
|
|
287
|
+
try:
|
|
288
|
+
unreal.LevelSequenceEditorBlueprintLibrary.play()
|
|
289
|
+
${params?.loopMode ? `unreal.LevelSequenceEditorBlueprintLibrary.set_loop_mode('${params.loopMode}')` : ''}
|
|
290
|
+
print('RESULT:' + json.dumps({'success': True, 'playing': True}))
|
|
291
|
+
except Exception as e:
|
|
292
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
293
|
+
`.trim();
|
|
294
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'playSequence');
|
|
295
|
+
return this.parsePythonResult(resp, 'playSequence');
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Pause the current level sequence
|
|
299
|
+
*/
|
|
300
|
+
async pause() {
|
|
301
|
+
const py = `
|
|
302
|
+
import unreal, json
|
|
303
|
+
try:
|
|
304
|
+
unreal.LevelSequenceEditorBlueprintLibrary.pause()
|
|
305
|
+
print('RESULT:' + json.dumps({'success': True, 'paused': True}))
|
|
306
|
+
except Exception as e:
|
|
307
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
308
|
+
`.trim();
|
|
309
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'pauseSequence');
|
|
310
|
+
return this.parsePythonResult(resp, 'pauseSequence');
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Stop/close the current level sequence
|
|
314
|
+
*/
|
|
315
|
+
async stop() {
|
|
316
|
+
const py = `
|
|
317
|
+
import unreal, json
|
|
318
|
+
try:
|
|
319
|
+
unreal.LevelSequenceEditorBlueprintLibrary.close_level_sequence()
|
|
320
|
+
print('RESULT:' + json.dumps({'success': True, 'stopped': True}))
|
|
321
|
+
except Exception as e:
|
|
322
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
323
|
+
`.trim();
|
|
324
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'stopSequence');
|
|
325
|
+
return this.parsePythonResult(resp, 'stopSequence');
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Set sequence properties including frame rate and length
|
|
329
|
+
*/
|
|
330
|
+
async setSequenceProperties(params) {
|
|
331
|
+
const py = `
|
|
332
|
+
import unreal, json
|
|
333
|
+
try:
|
|
334
|
+
# Load the sequence
|
|
335
|
+
seq_path = r"${params.path || ''}"
|
|
336
|
+
if seq_path:
|
|
337
|
+
seq = unreal.load_asset(seq_path)
|
|
338
|
+
else:
|
|
339
|
+
# Try to get the currently open sequence
|
|
340
|
+
seq = unreal.LevelSequenceEditorBlueprintLibrary.get_focused_level_sequence()
|
|
341
|
+
|
|
342
|
+
if not seq:
|
|
343
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'No sequence found or loaded'}))
|
|
344
|
+
else:
|
|
345
|
+
result = {'success': True, 'changes': []}
|
|
346
|
+
|
|
347
|
+
# Set frame rate if provided
|
|
348
|
+
${params.frameRate ? `
|
|
349
|
+
frame_rate = unreal.FrameRate(numerator=${params.frameRate}, denominator=1)
|
|
350
|
+
unreal.MovieSceneSequenceExtensions.set_display_rate(seq, frame_rate)
|
|
351
|
+
result['changes'].append({'property': 'frameRate', 'value': ${params.frameRate}})
|
|
352
|
+
` : ''}
|
|
353
|
+
|
|
354
|
+
# Set playback range if provided
|
|
355
|
+
${(params.playbackStart !== undefined || params.playbackEnd !== undefined) ? `
|
|
356
|
+
current_range = unreal.MovieSceneSequenceExtensions.get_playback_range(seq)
|
|
357
|
+
start = ${params.playbackStart !== undefined ? params.playbackStart : 'current_range.get_start_frame()'}
|
|
358
|
+
end = ${params.playbackEnd !== undefined ? params.playbackEnd : 'current_range.get_end_frame()'}
|
|
359
|
+
# Use set_playback_start and set_playback_end instead
|
|
360
|
+
if ${params.playbackStart !== undefined}:
|
|
361
|
+
unreal.MovieSceneSequenceExtensions.set_playback_start(seq, ${params.playbackStart})
|
|
362
|
+
if ${params.playbackEnd !== undefined}:
|
|
363
|
+
unreal.MovieSceneSequenceExtensions.set_playback_end(seq, ${params.playbackEnd})
|
|
364
|
+
result['changes'].append({'property': 'playbackRange', 'start': start, 'end': end})
|
|
365
|
+
` : ''}
|
|
366
|
+
|
|
367
|
+
# Set total length in frames if provided
|
|
368
|
+
${params.lengthInFrames ? `
|
|
369
|
+
# This sets the playback end to match the desired length
|
|
370
|
+
start = unreal.MovieSceneSequenceExtensions.get_playback_start(seq)
|
|
371
|
+
end = start + ${params.lengthInFrames}
|
|
372
|
+
unreal.MovieSceneSequenceExtensions.set_playback_end(seq, end)
|
|
373
|
+
result['changes'].append({'property': 'lengthInFrames', 'value': ${params.lengthInFrames}})
|
|
374
|
+
` : ''}
|
|
375
|
+
|
|
376
|
+
# Get final properties for confirmation
|
|
377
|
+
final_rate = unreal.MovieSceneSequenceExtensions.get_display_rate(seq)
|
|
378
|
+
final_range = unreal.MovieSceneSequenceExtensions.get_playback_range(seq)
|
|
379
|
+
result['finalProperties'] = {
|
|
380
|
+
'frameRate': {'numerator': final_rate.numerator, 'denominator': final_rate.denominator},
|
|
381
|
+
'playbackStart': final_range.get_start_frame(),
|
|
382
|
+
'playbackEnd': final_range.get_end_frame(),
|
|
383
|
+
'duration': final_range.get_end_frame() - final_range.get_start_frame()
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
print('RESULT:' + json.dumps(result))
|
|
387
|
+
except Exception as e:
|
|
388
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
389
|
+
`.trim();
|
|
390
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'setSequenceProperties');
|
|
391
|
+
return this.parsePythonResult(resp, 'setSequenceProperties');
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Get sequence properties
|
|
395
|
+
*/
|
|
396
|
+
async getSequenceProperties(params) {
|
|
397
|
+
const py = `
|
|
398
|
+
import unreal, json
|
|
399
|
+
try:
|
|
400
|
+
# Load the sequence
|
|
401
|
+
seq_path = r"${params.path || ''}"
|
|
402
|
+
if seq_path:
|
|
403
|
+
seq = unreal.load_asset(seq_path)
|
|
404
|
+
else:
|
|
405
|
+
# Try to get the currently open sequence
|
|
406
|
+
seq = unreal.LevelSequenceEditorBlueprintLibrary.get_focused_level_sequence()
|
|
407
|
+
|
|
408
|
+
if not seq:
|
|
409
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'No sequence found or loaded'}))
|
|
410
|
+
else:
|
|
411
|
+
# Get all properties
|
|
412
|
+
display_rate = unreal.MovieSceneSequenceExtensions.get_display_rate(seq)
|
|
413
|
+
playback_range = unreal.MovieSceneSequenceExtensions.get_playback_range(seq)
|
|
414
|
+
|
|
415
|
+
# Get marked frames if any
|
|
416
|
+
marked_frames = []
|
|
417
|
+
try:
|
|
418
|
+
frames = unreal.MovieSceneSequenceExtensions.get_marked_frames(seq)
|
|
419
|
+
marked_frames = [{'frame': f.frame_number.value, 'label': f.label} for f in frames]
|
|
420
|
+
except:
|
|
421
|
+
pass
|
|
422
|
+
|
|
423
|
+
result = {
|
|
424
|
+
'success': True,
|
|
425
|
+
'path': seq.get_path_name(),
|
|
426
|
+
'name': seq.get_name(),
|
|
427
|
+
'frameRate': {
|
|
428
|
+
'numerator': display_rate.numerator,
|
|
429
|
+
'denominator': display_rate.denominator,
|
|
430
|
+
'fps': float(display_rate.numerator) / float(display_rate.denominator) if display_rate.denominator > 0 else 0
|
|
431
|
+
},
|
|
432
|
+
'playbackStart': playback_range.get_start_frame(),
|
|
433
|
+
'playbackEnd': playback_range.get_end_frame(),
|
|
434
|
+
'duration': playback_range.get_end_frame() - playback_range.get_start_frame(),
|
|
435
|
+
'markedFrames': marked_frames
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
print('RESULT:' + json.dumps(result))
|
|
439
|
+
except Exception as e:
|
|
440
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
441
|
+
`.trim();
|
|
442
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'getSequenceProperties');
|
|
443
|
+
return this.parsePythonResult(resp, 'getSequenceProperties');
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Set playback speed/rate
|
|
447
|
+
*/
|
|
448
|
+
async setPlaybackSpeed(params) {
|
|
449
|
+
const py = `
|
|
450
|
+
import unreal, json
|
|
451
|
+
try:
|
|
452
|
+
unreal.LevelSequenceEditorBlueprintLibrary.set_playback_speed(${params.speed})
|
|
453
|
+
print('RESULT:' + json.dumps({'success': True, 'playbackSpeed': ${params.speed}}))
|
|
454
|
+
except Exception as e:
|
|
455
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
456
|
+
`.trim();
|
|
457
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'setPlaybackSpeed');
|
|
458
|
+
return this.parsePythonResult(resp, 'setPlaybackSpeed');
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Get all bindings in the current sequence
|
|
462
|
+
*/
|
|
463
|
+
async getBindings(params) {
|
|
464
|
+
const py = `
|
|
465
|
+
import unreal, json
|
|
466
|
+
try:
|
|
467
|
+
# Load the sequence
|
|
468
|
+
seq_path = r"${params?.path || ''}"
|
|
469
|
+
if seq_path:
|
|
470
|
+
seq = unreal.load_asset(seq_path)
|
|
471
|
+
else:
|
|
472
|
+
# Try to get the currently open sequence
|
|
473
|
+
seq = unreal.LevelSequenceEditorBlueprintLibrary.get_focused_level_sequence()
|
|
474
|
+
|
|
475
|
+
if not seq:
|
|
476
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'No sequence found or loaded'}))
|
|
477
|
+
else:
|
|
478
|
+
bindings = unreal.MovieSceneSequenceExtensions.get_bindings(seq)
|
|
479
|
+
binding_list = []
|
|
480
|
+
for binding in bindings:
|
|
481
|
+
try:
|
|
482
|
+
binding_name = unreal.MovieSceneBindingExtensions.get_name(binding)
|
|
483
|
+
binding_guid = unreal.MovieSceneBindingExtensions.get_id(binding)
|
|
484
|
+
|
|
485
|
+
# Extract clean GUID string
|
|
486
|
+
guid_str = str(binding_guid)
|
|
487
|
+
if guid_str.startswith('<Guid '):
|
|
488
|
+
# Extract the actual GUID value from <Guid 'XXXX-XXXX-XXXX-XXXX'>
|
|
489
|
+
guid_clean = guid_str.replace('<Guid ', '').replace('>', '').replace("'", '').split(' ')[0]
|
|
490
|
+
else:
|
|
491
|
+
guid_clean = guid_str
|
|
492
|
+
|
|
493
|
+
binding_list.append({
|
|
494
|
+
'id': guid_clean,
|
|
495
|
+
'name': binding_name,
|
|
496
|
+
'guid': guid_clean
|
|
497
|
+
})
|
|
498
|
+
except:
|
|
499
|
+
pass
|
|
500
|
+
|
|
501
|
+
print('RESULT:' + json.dumps({'success': True, 'bindings': binding_list, 'count': len(binding_list)}))
|
|
502
|
+
except Exception as e:
|
|
503
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
504
|
+
`.trim();
|
|
505
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'getBindings');
|
|
506
|
+
return this.parsePythonResult(resp, 'getBindings');
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Add multiple actors to sequence at once
|
|
510
|
+
*/
|
|
511
|
+
async addActors(params) {
|
|
512
|
+
const py = `
|
|
513
|
+
import unreal, json
|
|
514
|
+
try:
|
|
515
|
+
actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
516
|
+
ls = unreal.get_editor_subsystem(unreal.LevelSequenceEditorSubsystem)
|
|
517
|
+
if not ls or not actor_sub:
|
|
518
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Subsystem unavailable'}))
|
|
519
|
+
else:
|
|
520
|
+
actor_names = ${JSON.stringify(params.actorNames)}
|
|
521
|
+
actors_to_add = []
|
|
522
|
+
not_found = []
|
|
523
|
+
|
|
524
|
+
all_actors = actor_sub.get_all_level_actors()
|
|
525
|
+
for name in actor_names:
|
|
526
|
+
found = False
|
|
527
|
+
for a in all_actors:
|
|
528
|
+
if not a: continue
|
|
529
|
+
label = a.get_actor_label()
|
|
530
|
+
actor_name = a.get_name()
|
|
531
|
+
if label == name or actor_name == name or label.startswith(name):
|
|
532
|
+
actors_to_add.append(a)
|
|
533
|
+
found = True
|
|
534
|
+
break
|
|
535
|
+
if not found:
|
|
536
|
+
not_found.append(name)
|
|
537
|
+
|
|
538
|
+
# Make sure we have a focused sequence
|
|
539
|
+
seq = unreal.LevelSequenceEditorBlueprintLibrary.get_focused_level_sequence()
|
|
540
|
+
if not seq:
|
|
541
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'No sequence is currently focused'}))
|
|
542
|
+
elif len(actors_to_add) == 0:
|
|
543
|
+
print('RESULT:' + json.dumps({'success': False, 'error': f'No actors found: {not_found}'}))
|
|
544
|
+
else:
|
|
545
|
+
# Add all actors at once
|
|
546
|
+
bindings = ls.add_actors(actors_to_add)
|
|
547
|
+
added_actors = [a.get_actor_label() for a in actors_to_add]
|
|
548
|
+
print('RESULT:' + json.dumps({
|
|
549
|
+
'success': True,
|
|
550
|
+
'count': len(bindings) if bindings else len(actors_to_add),
|
|
551
|
+
'actorsAdded': added_actors,
|
|
552
|
+
'notFound': not_found
|
|
553
|
+
}))
|
|
554
|
+
except Exception as e:
|
|
555
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
556
|
+
`.trim();
|
|
557
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'addActors');
|
|
558
|
+
return this.parsePythonResult(resp, 'addActors');
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Remove actors from binding
|
|
562
|
+
*/
|
|
563
|
+
async removeActors(params) {
|
|
564
|
+
const py = `
|
|
565
|
+
import unreal, json
|
|
566
|
+
try:
|
|
567
|
+
actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
568
|
+
ls = unreal.get_editor_subsystem(unreal.LevelSequenceEditorSubsystem)
|
|
569
|
+
|
|
570
|
+
if not ls or not actor_sub:
|
|
571
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Subsystem unavailable'}))
|
|
572
|
+
else:
|
|
573
|
+
# Get current sequence
|
|
574
|
+
seq = unreal.LevelSequenceEditorBlueprintLibrary.get_focused_level_sequence()
|
|
575
|
+
if not seq:
|
|
576
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'No sequence is currently focused'}))
|
|
577
|
+
else:
|
|
578
|
+
actor_names = ${JSON.stringify(params.actorNames)}
|
|
579
|
+
actors_to_remove = []
|
|
580
|
+
|
|
581
|
+
all_actors = actor_sub.get_all_level_actors()
|
|
582
|
+
for name in actor_names:
|
|
583
|
+
for a in all_actors:
|
|
584
|
+
if not a: continue
|
|
585
|
+
label = a.get_actor_label()
|
|
586
|
+
actor_name = a.get_name()
|
|
587
|
+
if label == name or actor_name == name:
|
|
588
|
+
actors_to_remove.append(a)
|
|
589
|
+
break
|
|
590
|
+
|
|
591
|
+
# Get all bindings and remove matching actors
|
|
592
|
+
bindings = unreal.MovieSceneSequenceExtensions.get_bindings(seq)
|
|
593
|
+
removed_count = 0
|
|
594
|
+
for binding in bindings:
|
|
595
|
+
try:
|
|
596
|
+
ls.remove_actors_from_binding(actors_to_remove, binding)
|
|
597
|
+
removed_count += 1
|
|
598
|
+
except:
|
|
599
|
+
pass
|
|
600
|
+
|
|
601
|
+
print('RESULT:' + json.dumps({
|
|
602
|
+
'success': True,
|
|
603
|
+
'removedActors': [a.get_actor_label() for a in actors_to_remove],
|
|
604
|
+
'bindingsProcessed': removed_count
|
|
605
|
+
}))
|
|
606
|
+
except Exception as e:
|
|
607
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
608
|
+
`.trim();
|
|
609
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'removeActors');
|
|
610
|
+
return this.parsePythonResult(resp, 'removeActors');
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Create a spawnable from an actor class
|
|
614
|
+
*/
|
|
615
|
+
async addSpawnableFromClass(params) {
|
|
616
|
+
const py = `
|
|
617
|
+
import unreal, json
|
|
618
|
+
try:
|
|
619
|
+
ls = unreal.get_editor_subsystem(unreal.LevelSequenceEditorSubsystem)
|
|
620
|
+
|
|
621
|
+
# Load the sequence
|
|
622
|
+
seq_path = r"${params.path || ''}"
|
|
623
|
+
if seq_path:
|
|
624
|
+
seq = unreal.load_asset(seq_path)
|
|
625
|
+
else:
|
|
626
|
+
seq = unreal.LevelSequenceEditorBlueprintLibrary.get_focused_level_sequence()
|
|
627
|
+
|
|
628
|
+
if not seq:
|
|
629
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'No sequence found'}))
|
|
630
|
+
else:
|
|
631
|
+
# Try to find the class
|
|
632
|
+
class_name = r"${params.className}"
|
|
633
|
+
actor_class = None
|
|
634
|
+
|
|
635
|
+
# Try common actor classes
|
|
636
|
+
if class_name == "StaticMeshActor":
|
|
637
|
+
actor_class = unreal.StaticMeshActor
|
|
638
|
+
elif class_name == "CineCameraActor":
|
|
639
|
+
actor_class = unreal.CineCameraActor
|
|
640
|
+
elif class_name == "CameraActor":
|
|
641
|
+
actor_class = unreal.CameraActor
|
|
642
|
+
elif class_name == "PointLight":
|
|
643
|
+
actor_class = unreal.PointLight
|
|
644
|
+
elif class_name == "DirectionalLight":
|
|
645
|
+
actor_class = unreal.DirectionalLight
|
|
646
|
+
elif class_name == "SpotLight":
|
|
647
|
+
actor_class = unreal.SpotLight
|
|
648
|
+
else:
|
|
649
|
+
# Try to load as a blueprint class
|
|
650
|
+
try:
|
|
651
|
+
actor_class = unreal.EditorAssetLibrary.load_asset(class_name)
|
|
652
|
+
except:
|
|
653
|
+
pass
|
|
654
|
+
|
|
655
|
+
if not actor_class:
|
|
656
|
+
print('RESULT:' + json.dumps({'success': False, 'error': f'Class {class_name} not found'}))
|
|
657
|
+
else:
|
|
658
|
+
spawnable = ls.add_spawnable_from_class(seq, actor_class)
|
|
659
|
+
if spawnable:
|
|
660
|
+
binding_id = unreal.MovieSceneSequenceExtensions.get_binding_id(seq, spawnable)
|
|
661
|
+
print('RESULT:' + json.dumps({
|
|
662
|
+
'success': True,
|
|
663
|
+
'spawnableId': str(binding_id),
|
|
664
|
+
'className': class_name
|
|
665
|
+
}))
|
|
666
|
+
else:
|
|
667
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Failed to create spawnable'}))
|
|
668
|
+
except Exception as e:
|
|
669
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
670
|
+
`.trim();
|
|
671
|
+
const resp = await this.executeWithRetry(() => this.bridge.executePython(py), 'addSpawnableFromClass');
|
|
672
|
+
return this.parsePythonResult(resp, 'addSpawnableFromClass');
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
//# sourceMappingURL=sequence.js.map
|