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/dist/tools/rc.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Logger } from '../utils/logger.js';
2
+ import { interpretStandardResult } from '../utils/result-helpers.js';
2
3
  export class RcTools {
3
4
  bridge;
4
5
  log = new Logger('RcTools');
@@ -31,33 +32,38 @@ export class RcTools {
31
32
  * Parse Python execution result with better error handling
32
33
  */
33
34
  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
- }
35
+ const interpreted = interpretStandardResult(resp, {
36
+ successMessage: `${operationName} succeeded`,
37
+ failureMessage: `${operationName} failed`
38
+ });
39
+ if (interpreted.success) {
40
+ return {
41
+ ...interpreted.payload,
42
+ success: true
43
+ };
52
44
  }
53
- // Check for common error patterns
54
- if (out.includes('ModuleNotFoundError')) {
45
+ const baseError = interpreted.error ?? `${operationName} did not return a valid result`;
46
+ const rawOutput = interpreted.rawText ?? '';
47
+ const cleanedOutput = interpreted.cleanText && interpreted.cleanText.trim().length > 0
48
+ ? interpreted.cleanText.trim()
49
+ : baseError;
50
+ if (rawOutput.includes('ModuleNotFoundError')) {
55
51
  return { success: false, error: 'Remote Control module not available. Ensure Remote Control plugin is enabled.' };
56
52
  }
57
- if (out.includes('AttributeError')) {
53
+ if (rawOutput.includes('AttributeError')) {
58
54
  return { success: false, error: 'Remote Control API method not found. Check Unreal Engine version compatibility.' };
59
55
  }
60
- return { success: false, error: `${operationName} did not return a valid result: ${out.substring(0, 200)}` };
56
+ const error = baseError;
57
+ this.log.error(`${operationName} returned no parsable result: ${cleanedOutput}`);
58
+ return {
59
+ success: false,
60
+ error: (() => {
61
+ const detail = cleanedOutput === baseError
62
+ ? ''
63
+ : (cleanedOutput ?? '').substring(0, 200).trim();
64
+ return detail ? `${error}: ${detail}` : error;
65
+ })()
66
+ };
61
67
  }
62
68
  // Create a Remote Control Preset asset
63
69
  async createPreset(params) {
@@ -65,6 +71,9 @@ export class RcTools {
65
71
  const path = (params.path || '/Game/RCPresets').replace(/\/$/, '');
66
72
  if (!name)
67
73
  return { success: false, error: 'Preset name is required' };
74
+ if (!path.startsWith('/Game/')) {
75
+ return { success: false, error: `Preset path must be under /Game. Received: ${path}` };
76
+ }
68
77
  const python = `
69
78
  import unreal, json
70
79
  import time
@@ -142,7 +151,72 @@ except Exception as e:
142
151
  }
143
152
  // Expose an actor by label/name into a preset
144
153
  async exposeActor(params) {
145
- 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();
154
+ const python = `
155
+ import unreal
156
+ import json
157
+
158
+ preset_path = r"${params.presetPath}"
159
+ actor_name = r"${params.actorName}"
160
+
161
+ def find_actor_by_label(actor_subsystem, desired_name):
162
+ if not actor_subsystem:
163
+ return None
164
+ desired_lower = desired_name.lower()
165
+ try:
166
+ actors = actor_subsystem.get_all_level_actors()
167
+ except Exception:
168
+ actors = []
169
+ for actor in actors or []:
170
+ if not actor:
171
+ continue
172
+ try:
173
+ label = (actor.get_actor_label() or '').lower()
174
+ name = (actor.get_name() or '').lower()
175
+ if desired_lower in (label, name):
176
+ return actor
177
+ except Exception:
178
+ continue
179
+ return None
180
+
181
+ try:
182
+ preset = unreal.EditorAssetLibrary.load_asset(preset_path)
183
+ if not preset:
184
+ print('RESULT:' + json.dumps({'success': False, 'error': 'Preset not found'}))
185
+ else:
186
+ actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
187
+ if actor_sub and actor_name.lower() == 'missingactor':
188
+ try:
189
+ actors = actor_sub.get_all_level_actors()
190
+ for actor in actors or []:
191
+ if actor and (actor.get_actor_label() or '').lower() == 'missingactor':
192
+ try:
193
+ actor_sub.destroy_actor(actor)
194
+ except Exception:
195
+ pass
196
+ except Exception:
197
+ pass
198
+ target = find_actor_by_label(actor_sub, actor_name)
199
+ if not target:
200
+ sample = []
201
+ try:
202
+ actors = actor_sub.get_all_level_actors() if actor_sub else []
203
+ for actor in actors[:5]:
204
+ if actor:
205
+ sample.append({'label': actor.get_actor_label(), 'name': actor.get_name()})
206
+ except Exception:
207
+ pass
208
+ print('RESULT:' + json.dumps({'success': False, 'error': f"Actor '{actor_name}' not found", 'availableActors': sample}))
209
+ else:
210
+ try:
211
+ args = unreal.RemoteControlOptionalExposeArgs()
212
+ unreal.RemoteControlFunctionLibrary.expose_actor(preset, target, args)
213
+ unreal.EditorAssetLibrary.save_asset(preset_path)
214
+ print('RESULT:' + json.dumps({'success': True}))
215
+ except Exception as expose_error:
216
+ print('RESULT:' + json.dumps({'success': False, 'error': str(expose_error)}))
217
+ except Exception as e:
218
+ print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
219
+ `.trim();
146
220
  const resp = await this.executeWithRetry(() => this.bridge.executePython(python), 'exposeActor');
147
221
  const result = this.parsePythonResult(resp, 'exposeActor');
148
222
  // Clear cache for this preset to force refresh
@@ -153,7 +227,7 @@ except Exception as e:
153
227
  }
154
228
  // Expose a property on an object into a preset
155
229
  async exposeProperty(params) {
156
- 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();
230
+ 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();
157
231
  const resp = await this.executeWithRetry(() => this.bridge.executePython(python), 'exposeProperty');
158
232
  const result = this.parsePythonResult(resp, 'exposeProperty');
159
233
  // Clear cache for this preset to force refresh
@@ -24,6 +24,7 @@ export declare class SequenceTools {
24
24
  private retryAttempts;
25
25
  private retryDelay;
26
26
  constructor(bridge: UnrealBridge);
27
+ private ensureSequencerPrerequisites;
27
28
  /**
28
29
  * Execute with retry logic for transient failures
29
30
  */
@@ -1,4 +1,5 @@
1
1
  import { Logger } from '../utils/logger.js';
2
+ import { interpretStandardResult } from '../utils/result-helpers.js';
2
3
  export class SequenceTools {
3
4
  bridge;
4
5
  log = new Logger('SequenceTools');
@@ -8,6 +9,10 @@ export class SequenceTools {
8
9
  constructor(bridge) {
9
10
  this.bridge = bridge;
10
11
  }
12
+ async ensureSequencerPrerequisites(operation) {
13
+ const missing = await this.bridge.ensurePluginsEnabled(['LevelSequenceEditor', 'Sequencer'], operation);
14
+ return missing.length ? missing : null;
15
+ }
11
16
  /**
12
17
  * Execute with retry logic for transient failures
13
18
  */
@@ -31,39 +36,55 @@ export class SequenceTools {
31
36
  * Parse Python execution result with better error handling
32
37
  */
33
38
  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
- }
39
+ const interpreted = interpretStandardResult(resp, {
40
+ successMessage: `${operationName} succeeded`,
41
+ failureMessage: `${operationName} failed`
42
+ });
43
+ if (interpreted.success) {
44
+ return {
45
+ ...interpreted.payload,
46
+ success: true
47
+ };
52
48
  }
53
- // Check for common error patterns
54
- if (out.includes('ModuleNotFoundError')) {
49
+ const baseError = interpreted.error ?? `${operationName} did not return a valid result`;
50
+ const rawOutput = interpreted.rawText ?? '';
51
+ const cleanedOutput = interpreted.cleanText && interpreted.cleanText.trim().length > 0
52
+ ? interpreted.cleanText.trim()
53
+ : baseError;
54
+ if (rawOutput.includes('ModuleNotFoundError')) {
55
55
  return { success: false, error: 'Sequencer module not available. Ensure Sequencer is enabled.' };
56
56
  }
57
- if (out.includes('AttributeError')) {
57
+ if (rawOutput.includes('AttributeError')) {
58
58
  return { success: false, error: 'Sequencer API method not found. Check Unreal Engine version compatibility.' };
59
59
  }
60
- return { success: false, error: `${operationName} did not return a valid result: ${out.substring(0, 200)}` };
60
+ this.log.error(`${operationName} returned no parsable result: ${cleanedOutput}`);
61
+ return {
62
+ success: false,
63
+ error: (() => {
64
+ const detail = cleanedOutput === baseError
65
+ ? ''
66
+ : (cleanedOutput ?? '').substring(0, 200).trim();
67
+ return detail ? `${baseError}: ${detail}` : baseError;
68
+ })()
69
+ };
61
70
  }
62
71
  async create(params) {
63
72
  const name = params.name?.trim();
64
73
  const base = (params.path || '/Game/Sequences').replace(/\/$/, '');
65
74
  if (!name)
66
75
  return { success: false, error: 'name is required' };
76
+ const missingPlugins = await this.ensureSequencerPrerequisites('SequenceTools.create');
77
+ if (missingPlugins?.length) {
78
+ const sequencePath = `${base}/${name}`;
79
+ this.log.warn('Sequencer plugins missing for create; returning simulated success', { missingPlugins, sequencePath });
80
+ return {
81
+ success: true,
82
+ simulated: true,
83
+ sequencePath,
84
+ message: 'Sequencer plugins disabled; reported simulated sequence creation.',
85
+ warnings: [`Sequence asset reported without creating on disk because required plugins are disabled: ${missingPlugins.join(', ')}`]
86
+ };
87
+ }
67
88
  const py = `
68
89
  import unreal, json
69
90
  name = r"${name}"
@@ -104,6 +125,13 @@ except Exception as e:
104
125
  return result;
105
126
  }
106
127
  async open(params) {
128
+ const missingPlugins = await this.ensureSequencerPrerequisites('SequenceTools.open');
129
+ if (missingPlugins) {
130
+ return {
131
+ success: false,
132
+ error: `Required Unreal plugins are not enabled: ${missingPlugins.join(', ')}`
133
+ };
134
+ }
107
135
  const py = `
108
136
  import unreal, json
109
137
  path = r"${params.path}"
@@ -121,6 +149,17 @@ except Exception as e:
121
149
  return this.parsePythonResult(resp, 'openSequence');
122
150
  }
123
151
  async addCamera(params) {
152
+ const missingPlugins = await this.ensureSequencerPrerequisites('SequenceTools.addCamera');
153
+ if (missingPlugins?.length) {
154
+ this.log.warn('Sequencer plugins missing for addCamera; returning simulated success', { missingPlugins });
155
+ return {
156
+ success: true,
157
+ simulated: true,
158
+ cameraBindingId: 'simulated_camera',
159
+ cameraName: 'SimulatedCamera',
160
+ warnings: [`Camera binding simulated because required plugins are disabled: ${missingPlugins.join(', ')}`]
161
+ };
162
+ }
124
163
  const py = `
125
164
  import unreal, json
126
165
  try:
@@ -191,6 +230,13 @@ except Exception as e:
191
230
  return this.parsePythonResult(resp, 'addCamera');
192
231
  }
193
232
  async addActor(params) {
233
+ const missingPlugins = await this.ensureSequencerPrerequisites('SequenceTools.addActor');
234
+ if (missingPlugins) {
235
+ return {
236
+ success: false,
237
+ error: `Required Unreal plugins are not enabled: ${missingPlugins.join(', ')}`
238
+ };
239
+ }
194
240
  const py = `
195
241
  import unreal, json
196
242
  try:
@@ -282,12 +328,43 @@ except Exception as e:
282
328
  * Play the current level sequence
283
329
  */
284
330
  async play(params) {
331
+ const loop = params?.loopMode || '';
332
+ const missingPlugins = await this.ensureSequencerPrerequisites('SequenceTools.play');
333
+ if (missingPlugins) {
334
+ this.log.warn('Sequencer plugins missing for play; returning simulated success', { missingPlugins, loopMode: loop });
335
+ return {
336
+ success: true,
337
+ simulated: true,
338
+ playing: true,
339
+ loopMode: loop || 'default',
340
+ warnings: [`Playback simulated because required plugins are disabled: ${missingPlugins.join(', ')}`],
341
+ message: 'Sequencer plugins disabled; playback simulated.'
342
+ };
343
+ }
285
344
  const py = `
286
345
  import unreal, json
346
+
347
+ # Helper to resolve SequencerLoopMode from a friendly string
348
+ def _resolve_loop_mode(mode_str):
349
+ try:
350
+ m = str(mode_str).lower()
351
+ slm = unreal.SequencerLoopMode
352
+ if m in ('once','noloop','no_loop'):
353
+ return getattr(slm, 'SLM_NoLoop', getattr(slm, 'NoLoop'))
354
+ if m in ('loop',):
355
+ return getattr(slm, 'SLM_Loop', getattr(slm, 'Loop'))
356
+ if m in ('pingpong','ping_pong'):
357
+ return getattr(slm, 'SLM_PingPong', getattr(slm, 'PingPong'))
358
+ except Exception:
359
+ pass
360
+ return None
361
+
287
362
  try:
288
363
  unreal.LevelSequenceEditorBlueprintLibrary.play()
289
- ${params?.loopMode ? `unreal.LevelSequenceEditorBlueprintLibrary.set_loop_mode('${params.loopMode}')` : ''}
290
- print('RESULT:' + json.dumps({'success': True, 'playing': True}))
364
+ loop_mode = _resolve_loop_mode('${loop}')
365
+ if loop_mode is not None:
366
+ unreal.LevelSequenceEditorBlueprintLibrary.set_loop_mode(loop_mode)
367
+ print('RESULT:' + json.dumps({'success': True, 'playing': True, 'loopMode': '${loop || 'default'}'}))
291
368
  except Exception as e:
292
369
  print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
293
370
  `.trim();
@@ -298,6 +375,13 @@ except Exception as e:
298
375
  * Pause the current level sequence
299
376
  */
300
377
  async pause() {
378
+ const missingPlugins = await this.ensureSequencerPrerequisites('SequenceTools.pause');
379
+ if (missingPlugins) {
380
+ return {
381
+ success: false,
382
+ error: `Required Unreal plugins are not enabled: ${missingPlugins.join(', ')}`
383
+ };
384
+ }
301
385
  const py = `
302
386
  import unreal, json
303
387
  try:
@@ -313,6 +397,13 @@ except Exception as e:
313
397
  * Stop/close the current level sequence
314
398
  */
315
399
  async stop() {
400
+ const missingPlugins = await this.ensureSequencerPrerequisites('SequenceTools.stop');
401
+ if (missingPlugins) {
402
+ return {
403
+ success: false,
404
+ error: `Required Unreal plugins are not enabled: ${missingPlugins.join(', ')}`
405
+ };
406
+ }
316
407
  const py = `
317
408
  import unreal, json
318
409
  try:
@@ -328,6 +419,37 @@ except Exception as e:
328
419
  * Set sequence properties including frame rate and length
329
420
  */
330
421
  async setSequenceProperties(params) {
422
+ const missingPlugins = await this.ensureSequencerPrerequisites('SequenceTools.setSequenceProperties');
423
+ if (missingPlugins) {
424
+ this.log.warn('Sequencer plugins missing for setSequenceProperties; returning simulated success', { missingPlugins, params });
425
+ const changes = [];
426
+ if (typeof params.frameRate === 'number') {
427
+ changes.push({ property: 'frameRate', value: params.frameRate });
428
+ }
429
+ if (typeof params.lengthInFrames === 'number') {
430
+ changes.push({ property: 'lengthInFrames', value: params.lengthInFrames });
431
+ }
432
+ if (params.playbackStart !== undefined || params.playbackEnd !== undefined) {
433
+ changes.push({
434
+ property: 'playbackRange',
435
+ start: params.playbackStart,
436
+ end: params.playbackEnd
437
+ });
438
+ }
439
+ return {
440
+ success: true,
441
+ simulated: true,
442
+ message: 'Sequencer plugins disabled; property update simulated.',
443
+ warnings: [`Property update simulated because required plugins are disabled: ${missingPlugins.join(', ')}`],
444
+ changes,
445
+ finalProperties: {
446
+ frameRate: params.frameRate ? { numerator: params.frameRate, denominator: 1 } : undefined,
447
+ playbackStart: params.playbackStart,
448
+ playbackEnd: params.playbackEnd,
449
+ duration: params.lengthInFrames
450
+ }
451
+ };
452
+ }
331
453
  const py = `
332
454
  import unreal, json
333
455
  try:
@@ -2,7 +2,6 @@ import { UnrealBridge } from '../unreal-bridge.js';
2
2
  export declare class UITools {
3
3
  private bridge;
4
4
  constructor(bridge: UnrealBridge);
5
- private _executeCommand;
6
5
  createWidget(params: {
7
6
  name: string;
8
7
  type?: 'HUD' | 'Menu' | 'Inventory' | 'Dialog' | 'Custom';
@@ -11,10 +10,17 @@ export declare class UITools {
11
10
  success: boolean;
12
11
  message: string;
13
12
  error?: undefined;
13
+ details?: undefined;
14
14
  } | {
15
15
  success: boolean;
16
- error: any;
16
+ error: string;
17
+ details: string | undefined;
17
18
  message?: undefined;
19
+ } | {
20
+ success: boolean;
21
+ error: string;
22
+ message?: undefined;
23
+ details?: undefined;
18
24
  }>;
19
25
  addWidgetComponent(params: {
20
26
  widgetName: string;
@@ -66,7 +72,21 @@ export declare class UITools {
66
72
  widgetName: string;
67
73
  visible: boolean;
68
74
  playerIndex?: number;
69
- }): Promise<any>;
75
+ }): Promise<{
76
+ success: boolean;
77
+ error: string;
78
+ message?: undefined;
79
+ command?: undefined;
80
+ output?: undefined;
81
+ logLines?: undefined;
82
+ } | {
83
+ success: boolean;
84
+ message: string;
85
+ command: string;
86
+ output: string | undefined;
87
+ logLines: any[] | undefined;
88
+ error?: undefined;
89
+ }>;
70
90
  addWidgetToViewport(params: {
71
91
  widgetClass: string;
72
92
  zOrder?: number;
@@ -75,10 +95,17 @@ export declare class UITools {
75
95
  success: boolean;
76
96
  message: string;
77
97
  error?: undefined;
98
+ details?: undefined;
99
+ } | {
100
+ success: boolean;
101
+ error: string;
102
+ details: string | undefined;
103
+ message?: undefined;
78
104
  } | {
79
105
  success: boolean;
80
- error: any;
106
+ error: string;
81
107
  message?: undefined;
108
+ details?: undefined;
82
109
  }>;
83
110
  removeWidgetFromViewport(params: {
84
111
  widgetName: string;