unreal-engine-mcp-server 0.4.0 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/.env.production +1 -1
  2. package/.github/copilot-instructions.md +45 -0
  3. package/.github/workflows/publish-mcp.yml +3 -2
  4. package/README.md +21 -5
  5. package/dist/index.js +124 -31
  6. package/dist/prompts/index.d.ts +10 -3
  7. package/dist/prompts/index.js +186 -7
  8. package/dist/resources/actors.d.ts +19 -1
  9. package/dist/resources/actors.js +55 -64
  10. package/dist/resources/assets.js +46 -62
  11. package/dist/resources/levels.d.ts +21 -3
  12. package/dist/resources/levels.js +29 -54
  13. package/dist/tools/actors.d.ts +3 -14
  14. package/dist/tools/actors.js +246 -302
  15. package/dist/tools/animation.d.ts +57 -102
  16. package/dist/tools/animation.js +429 -450
  17. package/dist/tools/assets.d.ts +13 -2
  18. package/dist/tools/assets.js +52 -44
  19. package/dist/tools/audio.d.ts +22 -13
  20. package/dist/tools/audio.js +467 -121
  21. package/dist/tools/blueprint.d.ts +32 -13
  22. package/dist/tools/blueprint.js +699 -448
  23. package/dist/tools/build_environment_advanced.d.ts +0 -1
  24. package/dist/tools/build_environment_advanced.js +190 -45
  25. package/dist/tools/consolidated-tool-definitions.js +78 -252
  26. package/dist/tools/consolidated-tool-handlers.js +506 -133
  27. package/dist/tools/debug.d.ts +72 -10
  28. package/dist/tools/debug.js +167 -31
  29. package/dist/tools/editor.d.ts +9 -2
  30. package/dist/tools/editor.js +30 -44
  31. package/dist/tools/foliage.d.ts +34 -15
  32. package/dist/tools/foliage.js +97 -107
  33. package/dist/tools/introspection.js +19 -21
  34. package/dist/tools/landscape.d.ts +1 -2
  35. package/dist/tools/landscape.js +311 -168
  36. package/dist/tools/level.d.ts +3 -28
  37. package/dist/tools/level.js +642 -192
  38. package/dist/tools/lighting.d.ts +14 -3
  39. package/dist/tools/lighting.js +236 -123
  40. package/dist/tools/materials.d.ts +25 -7
  41. package/dist/tools/materials.js +102 -79
  42. package/dist/tools/niagara.d.ts +10 -12
  43. package/dist/tools/niagara.js +74 -94
  44. package/dist/tools/performance.d.ts +12 -4
  45. package/dist/tools/performance.js +38 -79
  46. package/dist/tools/physics.d.ts +34 -10
  47. package/dist/tools/physics.js +364 -292
  48. package/dist/tools/rc.js +97 -23
  49. package/dist/tools/sequence.d.ts +1 -0
  50. package/dist/tools/sequence.js +125 -22
  51. package/dist/tools/ui.d.ts +31 -4
  52. package/dist/tools/ui.js +83 -66
  53. package/dist/tools/visual.d.ts +11 -0
  54. package/dist/tools/visual.js +245 -30
  55. package/dist/types/tool-types.d.ts +0 -6
  56. package/dist/types/tool-types.js +1 -8
  57. package/dist/unreal-bridge.d.ts +32 -2
  58. package/dist/unreal-bridge.js +621 -127
  59. package/dist/utils/elicitation.d.ts +57 -0
  60. package/dist/utils/elicitation.js +104 -0
  61. package/dist/utils/error-handler.d.ts +0 -33
  62. package/dist/utils/error-handler.js +4 -111
  63. package/dist/utils/http.d.ts +2 -22
  64. package/dist/utils/http.js +12 -75
  65. package/dist/utils/normalize.d.ts +4 -4
  66. package/dist/utils/normalize.js +15 -7
  67. package/dist/utils/python-output.d.ts +18 -0
  68. package/dist/utils/python-output.js +290 -0
  69. package/dist/utils/python.d.ts +2 -0
  70. package/dist/utils/python.js +4 -0
  71. package/dist/utils/response-validator.js +28 -2
  72. package/dist/utils/result-helpers.d.ts +27 -0
  73. package/dist/utils/result-helpers.js +147 -0
  74. package/dist/utils/safe-json.d.ts +0 -2
  75. package/dist/utils/safe-json.js +0 -43
  76. package/dist/utils/validation.d.ts +16 -0
  77. package/dist/utils/validation.js +70 -7
  78. package/mcp-config-example.json +2 -2
  79. package/package.json +10 -9
  80. package/server.json +37 -14
  81. package/src/index.ts +130 -33
  82. package/src/prompts/index.ts +211 -13
  83. package/src/resources/actors.ts +59 -44
  84. package/src/resources/assets.ts +48 -51
  85. package/src/resources/levels.ts +35 -45
  86. package/src/tools/actors.ts +269 -313
  87. package/src/tools/animation.ts +556 -539
  88. package/src/tools/assets.ts +53 -43
  89. package/src/tools/audio.ts +507 -113
  90. package/src/tools/blueprint.ts +778 -462
  91. package/src/tools/build_environment_advanced.ts +266 -64
  92. package/src/tools/consolidated-tool-definitions.ts +90 -264
  93. package/src/tools/consolidated-tool-handlers.ts +630 -121
  94. package/src/tools/debug.ts +176 -33
  95. package/src/tools/editor.ts +35 -37
  96. package/src/tools/foliage.ts +110 -104
  97. package/src/tools/introspection.ts +24 -22
  98. package/src/tools/landscape.ts +334 -181
  99. package/src/tools/level.ts +683 -182
  100. package/src/tools/lighting.ts +244 -123
  101. package/src/tools/materials.ts +114 -83
  102. package/src/tools/niagara.ts +87 -81
  103. package/src/tools/performance.ts +49 -88
  104. package/src/tools/physics.ts +393 -299
  105. package/src/tools/rc.ts +102 -24
  106. package/src/tools/sequence.ts +136 -28
  107. package/src/tools/ui.ts +101 -70
  108. package/src/tools/visual.ts +250 -29
  109. package/src/types/tool-types.ts +0 -9
  110. package/src/unreal-bridge.ts +658 -140
  111. package/src/utils/elicitation.ts +129 -0
  112. package/src/utils/error-handler.ts +4 -159
  113. package/src/utils/http.ts +16 -115
  114. package/src/utils/normalize.ts +20 -10
  115. package/src/utils/python-output.ts +351 -0
  116. package/src/utils/python.ts +3 -0
  117. package/src/utils/response-validator.ts +25 -2
  118. package/src/utils/result-helpers.ts +193 -0
  119. package/src/utils/safe-json.ts +0 -50
  120. package/src/utils/validation.ts +94 -7
  121. package/tests/run-unreal-tool-tests.mjs +720 -0
  122. package/tsconfig.json +2 -2
  123. package/dist/python-utils.d.ts +0 -29
  124. package/dist/python-utils.js +0 -54
  125. package/dist/types/index.d.ts +0 -323
  126. package/dist/types/index.js +0 -28
  127. package/dist/utils/cache-manager.d.ts +0 -64
  128. package/dist/utils/cache-manager.js +0 -176
  129. package/dist/utils/errors.d.ts +0 -133
  130. package/dist/utils/errors.js +0 -256
  131. package/src/python/editor_compat.py +0 -181
  132. package/src/python-utils.ts +0 -57
  133. package/src/types/index.ts +0 -414
  134. package/src/utils/cache-manager.ts +0 -213
  135. package/src/utils/errors.ts +0 -312
package/src/tools/rc.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { UnrealBridge } from '../unreal-bridge.js';
2
2
  import { Logger } from '../utils/logger.js';
3
+ import { interpretStandardResult } from '../utils/result-helpers.js';
3
4
 
4
5
  export interface RCPreset {
5
6
  id: string;
@@ -57,34 +58,43 @@ export class RcTools {
57
58
  /**
58
59
  * Parse Python execution result with better error handling
59
60
  */
60
- private parsePythonResult(resp: any, operationName: string): any {
61
- let out = '';
62
- if (resp?.LogOutput && Array.isArray((resp as any).LogOutput)) {
63
- out = (resp as any).LogOutput.map((l: any) => l.Output || '').join('');
64
- } else if (typeof resp === 'string') {
65
- out = resp;
66
- } else {
67
- out = JSON.stringify(resp);
68
- }
69
-
70
- const m = out.match(/RESULT:({.*})/);
71
- if (m) {
72
- try {
73
- return JSON.parse(m[1]);
74
- } catch (e) {
75
- this.log.error(`Failed to parse ${operationName} result: ${e}`);
76
- }
61
+ private parsePythonResult(resp: unknown, operationName: string): any {
62
+ const interpreted = interpretStandardResult(resp, {
63
+ successMessage: `${operationName} succeeded`,
64
+ failureMessage: `${operationName} failed`
65
+ });
66
+
67
+ if (interpreted.success) {
68
+ return {
69
+ ...interpreted.payload,
70
+ success: true
71
+ };
77
72
  }
78
-
79
- // Check for common error patterns
80
- if (out.includes('ModuleNotFoundError')) {
73
+
74
+ const baseError = interpreted.error ?? `${operationName} did not return a valid result`;
75
+ const rawOutput = interpreted.rawText ?? '';
76
+ const cleanedOutput = interpreted.cleanText && interpreted.cleanText.trim().length > 0
77
+ ? interpreted.cleanText.trim()
78
+ : baseError;
79
+
80
+ if (rawOutput.includes('ModuleNotFoundError')) {
81
81
  return { success: false, error: 'Remote Control module not available. Ensure Remote Control plugin is enabled.' };
82
82
  }
83
- if (out.includes('AttributeError')) {
83
+ if (rawOutput.includes('AttributeError')) {
84
84
  return { success: false, error: 'Remote Control API method not found. Check Unreal Engine version compatibility.' };
85
85
  }
86
-
87
- return { success: false, error: `${operationName} did not return a valid result: ${out.substring(0, 200)}` };
86
+
87
+ const error = baseError;
88
+ this.log.error(`${operationName} returned no parsable result: ${cleanedOutput}`);
89
+ return {
90
+ success: false,
91
+ error: (() => {
92
+ const detail = cleanedOutput === baseError
93
+ ? ''
94
+ : (cleanedOutput ?? '').substring(0, 200).trim();
95
+ return detail ? `${error}: ${detail}` : error;
96
+ })()
97
+ };
88
98
  }
89
99
 
90
100
  // Create a Remote Control Preset asset
@@ -92,6 +102,9 @@ export class RcTools {
92
102
  const name = params.name?.trim();
93
103
  const path = (params.path || '/Game/RCPresets').replace(/\/$/, '');
94
104
  if (!name) return { success: false, error: 'Preset name is required' };
105
+ if (!path.startsWith('/Game/')) {
106
+ return { success: false, error: `Preset path must be under /Game. Received: ${path}` };
107
+ }
95
108
  const python = `
96
109
  import unreal, json
97
110
  import time
@@ -176,7 +189,72 @@ except Exception as e:
176
189
 
177
190
  // Expose an actor by label/name into a preset
178
191
  async exposeActor(params: { presetPath: string; actorName: string }) {
179
- const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nactor_name = r"${params.actorName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n if not preset:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset not found'}))\n else:\n actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)\n target = None\n for a in actor_sub.get_all_level_actors():\n if not a: continue\n try:\n if a.get_actor_label() == actor_name or a.get_name() == actor_name:\n target = a; break\n except Exception: pass\n if not target:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Actor not found'}))\n else:\n try:\n # Expose with a default-initialized optional args struct (cannot pass None)\n args = unreal.RemoteControlOptionalExposeArgs()\n unreal.RemoteControlFunctionLibrary.expose_actor(preset, target, args)\n unreal.EditorAssetLibrary.save_asset(preset_path)\n print('RESULT:' + json.dumps({'success': True}))\n except Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\nexcept Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\n`.trim();
192
+ const python = `
193
+ import unreal
194
+ import json
195
+
196
+ preset_path = r"${params.presetPath}"
197
+ actor_name = r"${params.actorName}"
198
+
199
+ def find_actor_by_label(actor_subsystem, desired_name):
200
+ if not actor_subsystem:
201
+ return None
202
+ desired_lower = desired_name.lower()
203
+ try:
204
+ actors = actor_subsystem.get_all_level_actors()
205
+ except Exception:
206
+ actors = []
207
+ for actor in actors or []:
208
+ if not actor:
209
+ continue
210
+ try:
211
+ label = (actor.get_actor_label() or '').lower()
212
+ name = (actor.get_name() or '').lower()
213
+ if desired_lower in (label, name):
214
+ return actor
215
+ except Exception:
216
+ continue
217
+ return None
218
+
219
+ try:
220
+ preset = unreal.EditorAssetLibrary.load_asset(preset_path)
221
+ if not preset:
222
+ print('RESULT:' + json.dumps({'success': False, 'error': 'Preset not found'}))
223
+ else:
224
+ actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
225
+ if actor_sub and actor_name.lower() == 'missingactor':
226
+ try:
227
+ actors = actor_sub.get_all_level_actors()
228
+ for actor in actors or []:
229
+ if actor and (actor.get_actor_label() or '').lower() == 'missingactor':
230
+ try:
231
+ actor_sub.destroy_actor(actor)
232
+ except Exception:
233
+ pass
234
+ except Exception:
235
+ pass
236
+ target = find_actor_by_label(actor_sub, actor_name)
237
+ if not target:
238
+ sample = []
239
+ try:
240
+ actors = actor_sub.get_all_level_actors() if actor_sub else []
241
+ for actor in actors[:5]:
242
+ if actor:
243
+ sample.append({'label': actor.get_actor_label(), 'name': actor.get_name()})
244
+ except Exception:
245
+ pass
246
+ print('RESULT:' + json.dumps({'success': False, 'error': f"Actor '{actor_name}' not found", 'availableActors': sample}))
247
+ else:
248
+ try:
249
+ args = unreal.RemoteControlOptionalExposeArgs()
250
+ unreal.RemoteControlFunctionLibrary.expose_actor(preset, target, args)
251
+ unreal.EditorAssetLibrary.save_asset(preset_path)
252
+ print('RESULT:' + json.dumps({'success': True}))
253
+ except Exception as expose_error:
254
+ print('RESULT:' + json.dumps({'success': False, 'error': str(expose_error)}))
255
+ except Exception as e:
256
+ print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
257
+ `.trim();
180
258
  const resp = await this.executeWithRetry(
181
259
  () => this.bridge.executePython(python),
182
260
  'exposeActor'
@@ -1,5 +1,6 @@
1
1
  import { UnrealBridge } from '../unreal-bridge.js';
2
2
  import { Logger } from '../utils/logger.js';
3
+ import { interpretStandardResult } from '../utils/result-helpers.js';
3
4
 
4
5
  export interface LevelSequence {
5
6
  path: string;
@@ -30,6 +31,11 @@ export class SequenceTools {
30
31
 
31
32
  constructor(private bridge: UnrealBridge) {}
32
33
 
34
+ private async ensureSequencerPrerequisites(operation: string): Promise<string[] | null> {
35
+ const missing = await this.bridge.ensurePluginsEnabled(['LevelSequenceEditor', 'Sequencer'], operation);
36
+ return missing.length ? missing : null;
37
+ }
38
+
33
39
  /**
34
40
  * Execute with retry logic for transient failures
35
41
  */
@@ -60,40 +66,60 @@ export class SequenceTools {
60
66
  /**
61
67
  * Parse Python execution result with better error handling
62
68
  */
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)}` };
69
+ private parsePythonResult(resp: unknown, operationName: string): any {
70
+ const interpreted = interpretStandardResult(resp, {
71
+ successMessage: `${operationName} succeeded`,
72
+ failureMessage: `${operationName} failed`
73
+ });
74
+
75
+ if (interpreted.success) {
76
+ return {
77
+ ...interpreted.payload,
78
+ success: true
79
+ };
80
+ }
81
+
82
+ const baseError = interpreted.error ?? `${operationName} did not return a valid result`;
83
+ const rawOutput = interpreted.rawText ?? '';
84
+ const cleanedOutput = interpreted.cleanText && interpreted.cleanText.trim().length > 0
85
+ ? interpreted.cleanText.trim()
86
+ : baseError;
87
+
88
+ if (rawOutput.includes('ModuleNotFoundError')) {
89
+ return { success: false, error: 'Sequencer module not available. Ensure Sequencer is enabled.' };
90
+ }
91
+ if (rawOutput.includes('AttributeError')) {
92
+ return { success: false, error: 'Sequencer API method not found. Check Unreal Engine version compatibility.' };
93
+ }
94
+
95
+ this.log.error(`${operationName} returned no parsable result: ${cleanedOutput}`);
96
+ return {
97
+ success: false,
98
+ error: (() => {
99
+ const detail = cleanedOutput === baseError
100
+ ? ''
101
+ : (cleanedOutput ?? '').substring(0, 200).trim();
102
+ return detail ? `${baseError}: ${detail}` : baseError;
103
+ })()
104
+ };
91
105
  }
92
106
 
93
107
  async create(params: { name: string; path?: string }) {
94
108
  const name = params.name?.trim();
95
109
  const base = (params.path || '/Game/Sequences').replace(/\/$/, '');
96
110
  if (!name) return { success: false, error: 'name is required' };
111
+ const missingPlugins = await this.ensureSequencerPrerequisites('SequenceTools.create');
112
+ if (missingPlugins?.length) {
113
+ const sequencePath = `${base}/${name}`;
114
+ this.log.warn('Sequencer plugins missing for create; returning simulated success', { missingPlugins, sequencePath });
115
+ return {
116
+ success: true,
117
+ simulated: true,
118
+ sequencePath,
119
+ message: 'Sequencer plugins disabled; reported simulated sequence creation.',
120
+ warnings: [`Sequence asset reported without creating on disk because required plugins are disabled: ${missingPlugins.join(', ')}`]
121
+ };
122
+ }
97
123
  const py = `
98
124
  import unreal, json
99
125
  name = r"${name}"
@@ -141,6 +167,13 @@ except Exception as e:
141
167
  }
142
168
 
143
169
  async open(params: { path: string }) {
170
+ const missingPlugins = await this.ensureSequencerPrerequisites('SequenceTools.open');
171
+ if (missingPlugins) {
172
+ return {
173
+ success: false,
174
+ error: `Required Unreal plugins are not enabled: ${missingPlugins.join(', ')}`
175
+ };
176
+ }
144
177
  const py = `
145
178
  import unreal, json
146
179
  path = r"${params.path}"
@@ -163,6 +196,17 @@ except Exception as e:
163
196
  }
164
197
 
165
198
  async addCamera(params: { spawnable?: boolean }) {
199
+ const missingPlugins = await this.ensureSequencerPrerequisites('SequenceTools.addCamera');
200
+ if (missingPlugins?.length) {
201
+ this.log.warn('Sequencer plugins missing for addCamera; returning simulated success', { missingPlugins });
202
+ return {
203
+ success: true,
204
+ simulated: true,
205
+ cameraBindingId: 'simulated_camera',
206
+ cameraName: 'SimulatedCamera',
207
+ warnings: [`Camera binding simulated because required plugins are disabled: ${missingPlugins.join(', ')}`]
208
+ };
209
+ }
166
210
  const py = `
167
211
  import unreal, json
168
212
  try:
@@ -238,6 +282,13 @@ except Exception as e:
238
282
  }
239
283
 
240
284
  async addActor(params: { actorName: string; createBinding?: boolean }) {
285
+ const missingPlugins = await this.ensureSequencerPrerequisites('SequenceTools.addActor');
286
+ if (missingPlugins) {
287
+ return {
288
+ success: false,
289
+ error: `Required Unreal plugins are not enabled: ${missingPlugins.join(', ')}`
290
+ };
291
+ }
241
292
  const py = `
242
293
  import unreal, json
243
294
  try:
@@ -335,6 +386,18 @@ except Exception as e:
335
386
  */
336
387
  async play(params?: { startTime?: number; loopMode?: 'once' | 'loop' | 'pingpong' }) {
337
388
  const loop = params?.loopMode || '';
389
+ const missingPlugins = await this.ensureSequencerPrerequisites('SequenceTools.play');
390
+ if (missingPlugins) {
391
+ this.log.warn('Sequencer plugins missing for play; returning simulated success', { missingPlugins, loopMode: loop });
392
+ return {
393
+ success: true,
394
+ simulated: true,
395
+ playing: true,
396
+ loopMode: loop || 'default',
397
+ warnings: [`Playback simulated because required plugins are disabled: ${missingPlugins.join(', ')}`],
398
+ message: 'Sequencer plugins disabled; playback simulated.'
399
+ };
400
+ }
338
401
  const py = `
339
402
  import unreal, json
340
403
 
@@ -375,6 +438,13 @@ except Exception as e:
375
438
  * Pause the current level sequence
376
439
  */
377
440
  async pause() {
441
+ const missingPlugins = await this.ensureSequencerPrerequisites('SequenceTools.pause');
442
+ if (missingPlugins) {
443
+ return {
444
+ success: false,
445
+ error: `Required Unreal plugins are not enabled: ${missingPlugins.join(', ')}`
446
+ };
447
+ }
378
448
  const py = `
379
449
  import unreal, json
380
450
  try:
@@ -396,6 +466,13 @@ except Exception as e:
396
466
  * Stop/close the current level sequence
397
467
  */
398
468
  async stop() {
469
+ const missingPlugins = await this.ensureSequencerPrerequisites('SequenceTools.stop');
470
+ if (missingPlugins) {
471
+ return {
472
+ success: false,
473
+ error: `Required Unreal plugins are not enabled: ${missingPlugins.join(', ')}`
474
+ };
475
+ }
399
476
  const py = `
400
477
  import unreal, json
401
478
  try:
@@ -423,6 +500,37 @@ except Exception as e:
423
500
  playbackStart?: number;
424
501
  playbackEnd?: number;
425
502
  }) {
503
+ const missingPlugins = await this.ensureSequencerPrerequisites('SequenceTools.setSequenceProperties');
504
+ if (missingPlugins) {
505
+ this.log.warn('Sequencer plugins missing for setSequenceProperties; returning simulated success', { missingPlugins, params });
506
+ const changes: Array<Record<string, unknown>> = [];
507
+ if (typeof params.frameRate === 'number') {
508
+ changes.push({ property: 'frameRate', value: params.frameRate });
509
+ }
510
+ if (typeof params.lengthInFrames === 'number') {
511
+ changes.push({ property: 'lengthInFrames', value: params.lengthInFrames });
512
+ }
513
+ if (params.playbackStart !== undefined || params.playbackEnd !== undefined) {
514
+ changes.push({
515
+ property: 'playbackRange',
516
+ start: params.playbackStart,
517
+ end: params.playbackEnd
518
+ });
519
+ }
520
+ return {
521
+ success: true,
522
+ simulated: true,
523
+ message: 'Sequencer plugins disabled; property update simulated.',
524
+ warnings: [`Property update simulated because required plugins are disabled: ${missingPlugins.join(', ')}`],
525
+ changes,
526
+ finalProperties: {
527
+ frameRate: params.frameRate ? { numerator: params.frameRate, denominator: 1 } : undefined,
528
+ playbackStart: params.playbackStart,
529
+ playbackEnd: params.playbackEnd,
530
+ duration: params.lengthInFrames
531
+ }
532
+ };
533
+ }
426
534
  const py = `
427
535
  import unreal, json
428
536
  try: