unreal-engine-mcp-server 0.3.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/.env.production +1 -1
  2. package/.github/copilot-instructions.md +45 -0
  3. package/.github/workflows/publish-mcp.yml +1 -1
  4. package/README.md +22 -7
  5. package/dist/index.js +137 -46
  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.d.ts +3 -2
  11. package/dist/resources/assets.js +117 -109
  12. package/dist/resources/levels.d.ts +21 -3
  13. package/dist/resources/levels.js +31 -56
  14. package/dist/tools/actors.d.ts +3 -14
  15. package/dist/tools/actors.js +246 -302
  16. package/dist/tools/animation.d.ts +57 -102
  17. package/dist/tools/animation.js +429 -450
  18. package/dist/tools/assets.d.ts +13 -2
  19. package/dist/tools/assets.js +58 -46
  20. package/dist/tools/audio.d.ts +22 -13
  21. package/dist/tools/audio.js +467 -121
  22. package/dist/tools/blueprint.d.ts +32 -13
  23. package/dist/tools/blueprint.js +699 -448
  24. package/dist/tools/build_environment_advanced.d.ts +0 -1
  25. package/dist/tools/build_environment_advanced.js +236 -87
  26. package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
  27. package/dist/tools/consolidated-tool-definitions.js +124 -255
  28. package/dist/tools/consolidated-tool-handlers.js +749 -766
  29. package/dist/tools/debug.d.ts +72 -10
  30. package/dist/tools/debug.js +170 -36
  31. package/dist/tools/editor.d.ts +9 -2
  32. package/dist/tools/editor.js +30 -44
  33. package/dist/tools/foliage.d.ts +34 -15
  34. package/dist/tools/foliage.js +97 -107
  35. package/dist/tools/introspection.js +19 -21
  36. package/dist/tools/landscape.d.ts +1 -2
  37. package/dist/tools/landscape.js +311 -168
  38. package/dist/tools/level.d.ts +3 -28
  39. package/dist/tools/level.js +642 -192
  40. package/dist/tools/lighting.d.ts +14 -3
  41. package/dist/tools/lighting.js +236 -123
  42. package/dist/tools/materials.d.ts +25 -7
  43. package/dist/tools/materials.js +102 -79
  44. package/dist/tools/niagara.d.ts +10 -12
  45. package/dist/tools/niagara.js +74 -94
  46. package/dist/tools/performance.d.ts +12 -4
  47. package/dist/tools/performance.js +38 -79
  48. package/dist/tools/physics.d.ts +34 -10
  49. package/dist/tools/physics.js +364 -292
  50. package/dist/tools/rc.js +98 -24
  51. package/dist/tools/sequence.d.ts +1 -0
  52. package/dist/tools/sequence.js +146 -24
  53. package/dist/tools/ui.d.ts +31 -4
  54. package/dist/tools/ui.js +83 -66
  55. package/dist/tools/visual.d.ts +11 -0
  56. package/dist/tools/visual.js +245 -30
  57. package/dist/types/tool-types.d.ts +0 -6
  58. package/dist/types/tool-types.js +1 -8
  59. package/dist/unreal-bridge.d.ts +32 -2
  60. package/dist/unreal-bridge.js +621 -127
  61. package/dist/utils/elicitation.d.ts +57 -0
  62. package/dist/utils/elicitation.js +104 -0
  63. package/dist/utils/error-handler.d.ts +0 -33
  64. package/dist/utils/error-handler.js +4 -111
  65. package/dist/utils/http.d.ts +2 -22
  66. package/dist/utils/http.js +12 -75
  67. package/dist/utils/normalize.d.ts +4 -4
  68. package/dist/utils/normalize.js +15 -7
  69. package/dist/utils/python-output.d.ts +18 -0
  70. package/dist/utils/python-output.js +290 -0
  71. package/dist/utils/python.d.ts +2 -0
  72. package/dist/utils/python.js +4 -0
  73. package/dist/utils/response-validator.d.ts +6 -1
  74. package/dist/utils/response-validator.js +66 -13
  75. package/dist/utils/result-helpers.d.ts +27 -0
  76. package/dist/utils/result-helpers.js +147 -0
  77. package/dist/utils/safe-json.d.ts +0 -2
  78. package/dist/utils/safe-json.js +0 -43
  79. package/dist/utils/validation.d.ts +16 -0
  80. package/dist/utils/validation.js +70 -7
  81. package/mcp-config-example.json +2 -2
  82. package/package.json +11 -10
  83. package/server.json +37 -14
  84. package/src/index.ts +146 -50
  85. package/src/prompts/index.ts +211 -13
  86. package/src/resources/actors.ts +59 -44
  87. package/src/resources/assets.ts +123 -102
  88. package/src/resources/levels.ts +37 -47
  89. package/src/tools/actors.ts +269 -313
  90. package/src/tools/animation.ts +556 -539
  91. package/src/tools/assets.ts +59 -45
  92. package/src/tools/audio.ts +507 -113
  93. package/src/tools/blueprint.ts +778 -462
  94. package/src/tools/build_environment_advanced.ts +312 -106
  95. package/src/tools/consolidated-tool-definitions.ts +136 -267
  96. package/src/tools/consolidated-tool-handlers.ts +871 -795
  97. package/src/tools/debug.ts +179 -38
  98. package/src/tools/editor.ts +35 -37
  99. package/src/tools/foliage.ts +110 -104
  100. package/src/tools/introspection.ts +24 -22
  101. package/src/tools/landscape.ts +334 -181
  102. package/src/tools/level.ts +683 -182
  103. package/src/tools/lighting.ts +244 -123
  104. package/src/tools/materials.ts +114 -83
  105. package/src/tools/niagara.ts +87 -81
  106. package/src/tools/performance.ts +49 -88
  107. package/src/tools/physics.ts +393 -299
  108. package/src/tools/rc.ts +103 -25
  109. package/src/tools/sequence.ts +157 -30
  110. package/src/tools/ui.ts +101 -70
  111. package/src/tools/visual.ts +250 -29
  112. package/src/types/tool-types.ts +0 -9
  113. package/src/unreal-bridge.ts +658 -140
  114. package/src/utils/elicitation.ts +129 -0
  115. package/src/utils/error-handler.ts +4 -159
  116. package/src/utils/http.ts +16 -115
  117. package/src/utils/normalize.ts +20 -10
  118. package/src/utils/python-output.ts +351 -0
  119. package/src/utils/python.ts +3 -0
  120. package/src/utils/response-validator.ts +68 -17
  121. package/src/utils/result-helpers.ts +193 -0
  122. package/src/utils/safe-json.ts +0 -50
  123. package/src/utils/validation.ts +94 -7
  124. package/tests/run-unreal-tool-tests.mjs +720 -0
  125. package/tsconfig.json +2 -2
  126. package/dist/python-utils.d.ts +0 -29
  127. package/dist/python-utils.js +0 -54
  128. package/dist/tools/tool-definitions.d.ts +0 -4919
  129. package/dist/tools/tool-definitions.js +0 -1065
  130. package/dist/tools/tool-handlers.d.ts +0 -47
  131. package/dist/tools/tool-handlers.js +0 -863
  132. package/dist/types/index.d.ts +0 -323
  133. package/dist/types/index.js +0 -28
  134. package/dist/utils/cache-manager.d.ts +0 -64
  135. package/dist/utils/cache-manager.js +0 -176
  136. package/dist/utils/errors.d.ts +0 -133
  137. package/dist/utils/errors.js +0 -256
  138. package/src/python/editor_compat.py +0 -181
  139. package/src/python-utils.ts +0 -57
  140. package/src/tools/tool-definitions.ts +0 -1081
  141. package/src/tools/tool-handlers.ts +0 -973
  142. package/src/types/index.ts +0 -414
  143. package/src/utils/cache-manager.ts +0 -213
  144. 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 unreal.RemoteControlFunctionLibrary.expose_actor(preset, target, None)\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'
@@ -194,7 +272,7 @@ except Exception as e:
194
272
 
195
273
  // Expose a property on an object into a preset
196
274
  async exposeProperty(params: { presetPath: string; objectPath: string; propertyName: string }) {
197
- const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nobj_path = r"${params.objectPath}"\nprop_name = r"${params.propertyName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n obj = unreal.load_object(None, obj_path)\n if not preset or not obj:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset or object not found'}))\n else:\n try:\n unreal.RemoteControlFunctionLibrary.expose_property(preset, obj, prop_name, None)\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();
275
+ const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nobj_path = r"${params.objectPath}"\nprop_name = r"${params.propertyName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n obj = unreal.load_object(None, obj_path)\n if not preset or not obj:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset or object not found'}))\n else:\n try:\n # Expose with default optional args struct (cannot pass None)\n args = unreal.RemoteControlOptionalExposeArgs()\n unreal.RemoteControlFunctionLibrary.expose_property(preset, obj, prop_name, 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();
198
276
  const resp = await this.executeWithRetry(
199
277
  () => this.bridge.executePython(python),
200
278
  'exposeProperty'
@@ -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:
@@ -334,12 +385,43 @@ except Exception as e:
334
385
  * Play the current level sequence
335
386
  */
336
387
  async play(params?: { startTime?: number; loopMode?: 'once' | 'loop' | 'pingpong' }) {
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
+ }
337
401
  const py = `
338
402
  import unreal, json
403
+
404
+ # Helper to resolve SequencerLoopMode from a friendly string
405
+ def _resolve_loop_mode(mode_str):
406
+ try:
407
+ m = str(mode_str).lower()
408
+ slm = unreal.SequencerLoopMode
409
+ if m in ('once','noloop','no_loop'):
410
+ return getattr(slm, 'SLM_NoLoop', getattr(slm, 'NoLoop'))
411
+ if m in ('loop',):
412
+ return getattr(slm, 'SLM_Loop', getattr(slm, 'Loop'))
413
+ if m in ('pingpong','ping_pong'):
414
+ return getattr(slm, 'SLM_PingPong', getattr(slm, 'PingPong'))
415
+ except Exception:
416
+ pass
417
+ return None
418
+
339
419
  try:
340
420
  unreal.LevelSequenceEditorBlueprintLibrary.play()
341
- ${params?.loopMode ? `unreal.LevelSequenceEditorBlueprintLibrary.set_loop_mode('${params.loopMode}')` : ''}
342
- print('RESULT:' + json.dumps({'success': True, 'playing': True}))
421
+ loop_mode = _resolve_loop_mode('${loop}')
422
+ if loop_mode is not None:
423
+ unreal.LevelSequenceEditorBlueprintLibrary.set_loop_mode(loop_mode)
424
+ print('RESULT:' + json.dumps({'success': True, 'playing': True, 'loopMode': '${loop || 'default'}'}))
343
425
  except Exception as e:
344
426
  print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
345
427
  `.trim();
@@ -356,6 +438,13 @@ except Exception as e:
356
438
  * Pause the current level sequence
357
439
  */
358
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
+ }
359
448
  const py = `
360
449
  import unreal, json
361
450
  try:
@@ -377,6 +466,13 @@ except Exception as e:
377
466
  * Stop/close the current level sequence
378
467
  */
379
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
+ }
380
476
  const py = `
381
477
  import unreal, json
382
478
  try:
@@ -404,6 +500,37 @@ except Exception as e:
404
500
  playbackStart?: number;
405
501
  playbackEnd?: number;
406
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
+ }
407
534
  const py = `
408
535
  import unreal, json
409
536
  try: