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,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