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