unreal-engine-mcp-server 0.4.0 → 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 (135) 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 +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
@@ -1,21 +1,174 @@
1
1
  // Audio tools for Unreal Engine
2
+ import JSON5 from 'json5';
2
3
  import { UnrealBridge } from '../unreal-bridge.js';
4
+ import { escapePythonString } from '../utils/python.js';
3
5
 
4
6
  export class AudioTools {
5
7
  constructor(private bridge: UnrealBridge) {}
6
8
 
7
- // Execute console command
8
- private async _executeCommand(command: string) {
9
- return this.bridge.httpCall('/remote/object/call', 'PUT', {
10
- objectPath: '/Script/Engine.Default__KismetSystemLibrary',
11
- functionName: 'ExecuteConsoleCommand',
12
- parameters: {
13
- WorldContextObject: null,
14
- Command: command,
15
- SpecificPlayer: null
16
- },
17
- generateTransaction: false
18
- });
9
+ private interpretResult(
10
+ resp: unknown,
11
+ defaults: { successMessage: string; failureMessage: string }
12
+ ): { success: true; message: string; details?: string } | { success: false; error: string; details?: string } {
13
+ const normalizePayload = (
14
+ payload: Record<string, unknown>
15
+ ): { success: true; message: string; details?: string } | { success: false; error: string; details?: string } => {
16
+ const warningsValue = payload?.warnings;
17
+ const warningsText = Array.isArray(warningsValue)
18
+ ? (warningsValue.length > 0 ? warningsValue.join('; ') : undefined)
19
+ : typeof warningsValue === 'string' && warningsValue.trim() !== ''
20
+ ? warningsValue
21
+ : undefined;
22
+
23
+ if (payload?.success === true) {
24
+ const message = typeof payload?.message === 'string' && payload.message.trim() !== ''
25
+ ? payload.message
26
+ : defaults.successMessage;
27
+ return {
28
+ success: true as const,
29
+ message,
30
+ details: warningsText
31
+ };
32
+ }
33
+
34
+ const error =
35
+ (typeof payload?.error === 'string' && payload.error.trim() !== ''
36
+ ? payload.error
37
+ : undefined)
38
+ ?? (typeof payload?.message === 'string' && payload.message.trim() !== ''
39
+ ? payload.message
40
+ : undefined)
41
+ ?? defaults.failureMessage;
42
+
43
+ return {
44
+ success: false as const,
45
+ error,
46
+ details: warningsText
47
+ };
48
+ };
49
+
50
+ if (resp && typeof resp === 'object') {
51
+ const payload = resp as Record<string, unknown>;
52
+ if ('success' in payload || 'error' in payload || 'message' in payload || 'warnings' in payload) {
53
+ return normalizePayload(payload);
54
+ }
55
+ }
56
+
57
+ const raw = typeof resp === 'string' ? resp : JSON.stringify(resp);
58
+
59
+ const extractJson = (input: string): string | undefined => {
60
+ const marker = 'RESULT:';
61
+ const markerIndex = input.lastIndexOf(marker);
62
+ if (markerIndex === -1) {
63
+ return undefined;
64
+ }
65
+
66
+ const afterMarker = input.slice(markerIndex + marker.length);
67
+ const firstBraceIndex = afterMarker.indexOf('{');
68
+ if (firstBraceIndex === -1) {
69
+ return undefined;
70
+ }
71
+
72
+ let depth = 0;
73
+ let inString = false;
74
+ let escapeNext = false;
75
+
76
+ for (let i = firstBraceIndex; i < afterMarker.length; i++) {
77
+ const char = afterMarker[i];
78
+
79
+ if (inString) {
80
+ if (escapeNext) {
81
+ escapeNext = false;
82
+ continue;
83
+ }
84
+ if (char === '\\') {
85
+ escapeNext = true;
86
+ continue;
87
+ }
88
+ if (char === '"') {
89
+ inString = false;
90
+ }
91
+ continue;
92
+ }
93
+
94
+ if (char === '"') {
95
+ inString = true;
96
+ continue;
97
+ }
98
+
99
+ if (char === '{') {
100
+ depth += 1;
101
+ } else if (char === '}') {
102
+ depth -= 1;
103
+ if (depth === 0) {
104
+ return afterMarker.slice(firstBraceIndex, i + 1);
105
+ }
106
+ }
107
+ }
108
+
109
+ const fallbackMatch = /\{[\s\S]*\}/.exec(afterMarker);
110
+ return fallbackMatch ? fallbackMatch[0] : undefined;
111
+ };
112
+
113
+ const jsonPayload = extractJson(raw);
114
+
115
+ if (jsonPayload) {
116
+ const parseAttempts: Array<{ label: string; parser: () => unknown }> = [
117
+ {
118
+ label: 'json',
119
+ parser: () => JSON.parse(jsonPayload)
120
+ },
121
+ {
122
+ label: 'json5',
123
+ parser: () => JSON5.parse(jsonPayload)
124
+ }
125
+ ];
126
+
127
+ const sanitizedForJson5 = jsonPayload
128
+ .replace(/\bTrue\b/g, 'true')
129
+ .replace(/\bFalse\b/g, 'false')
130
+ .replace(/\bNone\b/g, 'null');
131
+
132
+ if (sanitizedForJson5 !== jsonPayload) {
133
+ parseAttempts.push({ label: 'json5-sanitized', parser: () => JSON5.parse(sanitizedForJson5) });
134
+ }
135
+
136
+ const parseErrors: string[] = [];
137
+
138
+ for (const attempt of parseAttempts) {
139
+ try {
140
+ const parsed = attempt.parser();
141
+ if (parsed && typeof parsed === 'object') {
142
+ return normalizePayload(parsed as Record<string, unknown>);
143
+ }
144
+ } catch (err) {
145
+ parseErrors.push(`${attempt.label}: ${err instanceof Error ? err.message : String(err)}`);
146
+ }
147
+ }
148
+
149
+ const errorMatch = /["']error["']\s*:\s*(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)')/i.exec(jsonPayload);
150
+ const messageMatch = /["']message["']\s*:\s*(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)')/i.exec(jsonPayload);
151
+ const fallbackText = errorMatch?.[1] ?? errorMatch?.[2] ?? messageMatch?.[1] ?? messageMatch?.[2];
152
+ const errorText = fallbackText && fallbackText.trim().length > 0
153
+ ? fallbackText.trim()
154
+ : `${defaults.failureMessage}: ${parseErrors[0] ?? 'Unable to parse RESULT payload'}`;
155
+
156
+ const snippet = jsonPayload.length > 240 ? `${jsonPayload.slice(0, 240)}…` : jsonPayload;
157
+ const detailsParts: string[] = [];
158
+ if (parseErrors.length > 0) {
159
+ detailsParts.push(`Parse attempts failed: ${parseErrors.join('; ')}`);
160
+ }
161
+ detailsParts.push(`Raw payload: ${snippet}`);
162
+ const detailsText = detailsParts.join(' | ');
163
+
164
+ return {
165
+ success: false as const,
166
+ error: errorText,
167
+ details: detailsText
168
+ };
169
+ }
170
+
171
+ return { success: false as const, error: defaults.failureMessage };
19
172
  }
20
173
 
21
174
  // Create sound cue
@@ -30,44 +183,164 @@ export class AudioTools {
30
183
  attenuationSettings?: string;
31
184
  };
32
185
  }) {
33
- const path = params.savePath || '/Game/Audio/Cues';
34
- const py = `
186
+ const escapePyString = (value: string) => value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
187
+ const toPyNumber = (value?: number) =>
188
+ value === undefined || value === null || !Number.isFinite(value) ? 'None' : String(value);
189
+ const toPyBool = (value?: boolean) =>
190
+ value === undefined || value === null ? 'None' : value ? 'True' : 'False';
191
+
192
+ const path = params.savePath || '/Game/Audio/Cues';
193
+ const wavePath = params.wavePath || '';
194
+ const attenuationPath = params.settings?.attenuationSettings || '';
195
+ const volumeLiteral = toPyNumber(params.settings?.volume);
196
+ const pitchLiteral = toPyNumber(params.settings?.pitch);
197
+ const loopingLiteral = toPyBool(params.settings?.looping);
198
+
199
+ const py = `
35
200
  import unreal
36
201
  import json
37
- name = r"${params.name}"
38
- path = r"${path}"
202
+
203
+ name = r"${escapePyString(params.name)}"
204
+ package_path = r"${escapePyString(path)}"
205
+ wave_path = r"${escapePyString(wavePath)}"
206
+ attenuation_path = r"${escapePyString(attenuationPath)}"
207
+ attach_wave = ${params.wavePath ? 'True' : 'False'}
208
+ volume_override = ${volumeLiteral}
209
+ pitch_override = ${pitchLiteral}
210
+ looping_override = ${loopingLiteral}
211
+
212
+ result = {
213
+ "success": False,
214
+ "message": "",
215
+ "error": "",
216
+ "warnings": []
217
+ }
218
+
39
219
  try:
40
- asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
220
+ asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
221
+ if not asset_tools:
222
+ result["error"] = "AssetToolsHelpers unavailable"
223
+ raise SystemExit(0)
224
+
225
+ factory = None
226
+ try:
227
+ factory = unreal.SoundCueFactoryNew()
228
+ except Exception:
229
+ factory = None
230
+
231
+ if not factory:
232
+ result["error"] = "SoundCueFactoryNew unavailable"
233
+ raise SystemExit(0)
234
+
235
+ package_path = package_path.rstrip('/') if package_path else package_path
236
+
237
+ asset = asset_tools.create_asset(
238
+ asset_name=name,
239
+ package_path=package_path,
240
+ asset_class=unreal.SoundCue,
241
+ factory=factory
242
+ )
243
+
244
+ if not asset:
245
+ result["error"] = "Failed to create SoundCue"
246
+ raise SystemExit(0)
247
+
248
+ asset_subsystem = None
249
+ try:
250
+ asset_subsystem = unreal.get_editor_subsystem(unreal.EditorAssetSubsystem)
251
+ except Exception:
252
+ asset_subsystem = None
253
+
254
+ editor_library = unreal.EditorAssetLibrary
255
+
256
+ if attach_wave:
257
+ wave_exists = False
41
258
  try:
42
- factory = unreal.SoundCueFactoryNew()
43
- except Exception:
44
- factory = None
45
- if not factory:
46
- print('RESULT:' + json.dumps({'success': False, 'error': 'SoundCueFactoryNew unavailable'}))
259
+ if asset_subsystem and hasattr(asset_subsystem, "does_asset_exist"):
260
+ wave_exists = asset_subsystem.does_asset_exist(wave_path)
261
+ else:
262
+ wave_exists = editor_library.does_asset_exist(wave_path)
263
+ except Exception as existence_error:
264
+ result["warnings"].append(f"Wave lookup failed: {existence_error}")
265
+
266
+ if not wave_exists:
267
+ result["warnings"].append(f"Wave asset not found: {wave_path}")
47
268
  else:
48
- asset = asset_tools.create_asset(asset_name=name, package_path=path, asset_class=unreal.SoundCue, factory=factory)
49
- if asset:
50
- if ${params.wavePath !== undefined ? 'True' : 'False'}:
51
- try:
52
- wave_path = r"${params.wavePath || ''}"
53
- if wave_path and unreal.EditorAssetLibrary.does_asset_exist(wave_path):
54
- snd = unreal.EditorAssetLibrary.load_asset(wave_path)
55
- # Simple node hookup via SoundCueGraph is non-trivial via Python; leave as empty cue
56
- except Exception:
57
- pass
58
- unreal.EditorAssetLibrary.save_asset(f"{path}/{name}")
59
- print('RESULT:' + json.dumps({'success': True}))
269
+ try:
270
+ if asset_subsystem and hasattr(asset_subsystem, "load_asset"):
271
+ wave_asset = asset_subsystem.load_asset(wave_path)
60
272
  else:
61
- print('RESULT:' + json.dumps({'success': False, 'error': 'Failed to create SoundCue'}))
62
- except Exception as e:
63
- print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
273
+ wave_asset = editor_library.load_asset(wave_path)
274
+ if wave_asset:
275
+ # Hooking up cue nodes via Python is non-trivial; surface warning for manual setup
276
+ result["warnings"].append("Sound cue created without automatic wave node hookup")
277
+ except Exception as wave_error:
278
+ result["warnings"].append(f"Failed to load wave asset: {wave_error}")
279
+
280
+ if volume_override is not None and hasattr(asset, "volume_multiplier"):
281
+ asset.volume_multiplier = volume_override
282
+ if pitch_override is not None and hasattr(asset, "pitch_multiplier"):
283
+ asset.pitch_multiplier = pitch_override
284
+ if looping_override is not None and hasattr(asset, "b_looping"):
285
+ asset.b_looping = looping_override
286
+
287
+ if attenuation_path:
288
+ try:
289
+ attenuation_asset = editor_library.load_asset(attenuation_path)
290
+ if attenuation_asset:
291
+ applied = False
292
+ if hasattr(asset, "set_attenuation_settings"):
293
+ try:
294
+ asset.set_attenuation_settings(attenuation_asset)
295
+ applied = True
296
+ except Exception:
297
+ applied = False
298
+ if not applied and hasattr(asset, "attenuation_settings"):
299
+ asset.attenuation_settings = attenuation_asset
300
+ applied = True
301
+ if not applied:
302
+ result["warnings"].append("Attenuation asset loaded but could not be applied automatically")
303
+ except Exception as attenuation_error:
304
+ result["warnings"].append(f"Failed to apply attenuation: {attenuation_error}")
305
+
306
+ try:
307
+ save_target = f"{package_path}/{name}" if package_path else name
308
+ if asset_subsystem and hasattr(asset_subsystem, "save_asset"):
309
+ asset_subsystem.save_asset(save_target)
310
+ else:
311
+ editor_library.save_asset(save_target)
312
+ except Exception as save_error:
313
+ result["warnings"].append(f"Save failed: {save_error}")
314
+
315
+ result["success"] = True
316
+ result["message"] = "Sound cue created"
317
+
318
+ except SystemExit:
319
+ pass
320
+ except Exception as error:
321
+ result["error"] = str(error)
322
+
323
+ finally:
324
+ payload = dict(result)
325
+ if payload.get("success"):
326
+ if not payload.get("message"):
327
+ payload["message"] = "Sound cue created"
328
+ payload.pop("error", None)
329
+ else:
330
+ if not payload.get("error"):
331
+ payload["error"] = payload.get("message") or "Failed to create SoundCue"
332
+ if not payload.get("message"):
333
+ payload["message"] = payload["error"]
334
+ if not payload.get("warnings"):
335
+ payload.pop("warnings", None)
336
+ print('RESULT:' + json.dumps(payload))
64
337
  `.trim();
65
338
  try {
66
339
  const resp = await this.bridge.executePython(py);
67
- const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
68
- const m = out.match(/RESULT:({.*})/);
69
- if (m) { try { const parsed = JSON.parse(m[1]); return parsed.success ? { success: true, message: 'Sound cue created' } : { success: false, error: parsed.error }; } catch {} }
70
- return { success: true, message: 'Sound cue creation attempted' };
340
+ return this.interpretResult(resp, {
341
+ successMessage: 'Sound cue created',
342
+ failureMessage: 'Failed to create SoundCue'
343
+ });
71
344
  } catch (e) {
72
345
  return { success: false, error: `Failed to create sound cue: ${e}` };
73
346
  }
@@ -84,41 +357,86 @@ except Exception as e:
84
357
  const volume = params.volume ?? 1.0;
85
358
  const pitch = params.pitch ?? 1.0;
86
359
  const startTime = params.startTime ?? 0.0;
360
+ const soundPath = params.soundPath ?? '';
87
361
 
88
- const py = `
362
+ const py = `
89
363
  import unreal
90
364
  import json
91
- loc = unreal.Vector(${params.location[0]}, ${params.location[1]}, ${params.location[2]})
92
- path = r"${params.soundPath}"
365
+
366
+ result = {
367
+ "success": False,
368
+ "message": "",
369
+ "error": "",
370
+ "warnings": []
371
+ }
372
+
93
373
  try:
94
- if not unreal.EditorAssetLibrary.does_asset_exist(path):
95
- print('RESULT:' + json.dumps({'success': False, 'error': 'Sound asset not found'}))
96
- else:
97
- snd = unreal.EditorAssetLibrary.load_asset(path)
98
- # Get editor world via EditorSubsystem first to avoid deprecation
99
- try:
100
- world = unreal.EditorSubsystemLibrary.get_editor_world()
101
- except Exception:
102
- try:
103
- world = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem).get_editor_world()
104
- except Exception:
105
- world = unreal.EditorLevelLibrary.get_editor_world()
106
- rot = unreal.Rotator(0.0, 0.0, 0.0)
107
- # Use spawn_* variant with explicit rotation before optional floats
108
- unreal.GameplayStatics.spawn_sound_at_location(world, snd, loc, rot, ${volume}, ${pitch}, ${startTime})
109
- print('RESULT:' + json.dumps({'success': True}))
374
+ path = "${escapePythonString(soundPath)}"
375
+ if not unreal.EditorAssetLibrary.does_asset_exist(path):
376
+ result["error"] = "Sound asset not found"
377
+ raise SystemExit(0)
378
+
379
+ snd = unreal.EditorAssetLibrary.load_asset(path)
380
+ if not snd:
381
+ result["error"] = f"Failed to load sound asset: {path}"
382
+ raise SystemExit(0)
383
+
384
+ world = None
385
+ try:
386
+ world = unreal.EditorUtilityLibrary.get_editor_world()
387
+ except Exception:
388
+ world = None
389
+
390
+ if not world:
391
+ editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
392
+ if editor_subsystem and hasattr(editor_subsystem, 'get_editor_world'):
393
+ world = editor_subsystem.get_editor_world()
394
+
395
+ if not world:
396
+ try:
397
+ world = unreal.EditorSubsystemLibrary.get_editor_world()
398
+ except Exception:
399
+ world = None
400
+
401
+ if not world:
402
+ result["error"] = "Unable to resolve editor world. Start PIE and ensure Editor Scripting Utilities is enabled."
403
+ raise SystemExit(0)
404
+
405
+ loc = unreal.Vector(${params.location[0]}, ${params.location[1]}, ${params.location[2]})
406
+ rot = unreal.Rotator(0.0, 0.0, 0.0)
407
+ unreal.GameplayStatics.spawn_sound_at_location(world, snd, loc, rot, ${volume}, ${pitch}, ${startTime})
408
+
409
+ result["success"] = True
410
+ result["message"] = "Sound played"
411
+
412
+ except SystemExit:
413
+ pass
110
414
  except Exception as e:
111
- print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
415
+ result["error"] = str(e)
416
+ finally:
417
+ payload = dict(result)
418
+ if payload.get("success"):
419
+ if not payload.get("message"):
420
+ payload["message"] = "Sound played"
421
+ payload.pop("error", None)
422
+ else:
423
+ if not payload.get("error"):
424
+ payload["error"] = payload.get("message") or "Failed to play sound"
425
+ if not payload.get("message"):
426
+ payload["message"] = payload["error"]
427
+ if not payload.get("warnings"):
428
+ payload.pop("warnings", None)
429
+ print('RESULT:' + json.dumps(payload))
112
430
  `.trim();
113
431
  try {
114
- const resp = await this.bridge.executePython(py);
115
- const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
116
- const m = out.match(/RESULT:({.*})/);
117
- if (m) { try { const parsed = JSON.parse(m[1]); return parsed.success ? { success: true, message: 'Sound played' } : { success: false, error: parsed.error }; } catch {} }
118
- return { success: true, message: 'Sound play attempted' };
119
- } catch (e) {
120
- return { success: false, error: `Failed to play sound: ${e}` };
121
- }
432
+ const resp = await this.bridge.executePythonWithResult(py);
433
+ return this.interpretResult(resp, {
434
+ successMessage: 'Sound played',
435
+ failureMessage: 'Failed to play sound'
436
+ });
437
+ } catch (e) {
438
+ return { success: false, error: `Failed to play sound: ${e}` };
439
+ }
122
440
  }
123
441
 
124
442
  // Play sound 2D
@@ -131,55 +449,123 @@ except Exception as e:
131
449
  const volume = params.volume ?? 1.0;
132
450
  const pitch = params.pitch ?? 1.0;
133
451
  const startTime = params.startTime ?? 0.0;
452
+ const soundPath = params.soundPath ?? '';
134
453
 
135
- const py = `
454
+ const py = `
136
455
  import unreal
137
456
  import json
138
- path = r"${params.soundPath}"
457
+
458
+ result = {
459
+ "success": False,
460
+ "message": "",
461
+ "error": "",
462
+ "warnings": []
463
+ }
464
+
139
465
  try:
140
- if not unreal.EditorAssetLibrary.does_asset_exist(path):
141
- print('RESULT:' + json.dumps({'success': False, 'error': 'Sound asset not found'}))
142
- else:
143
- snd = unreal.EditorAssetLibrary.load_asset(path)
144
- # Get editor world via EditorSubsystem first to avoid deprecation
145
- try:
146
- world = unreal.EditorSubsystemLibrary.get_editor_world()
147
- except Exception:
148
- try:
149
- world = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem).get_editor_world()
150
- except Exception:
151
- world = unreal.EditorLevelLibrary.get_editor_world()
152
- ok = False
153
- try:
154
- unreal.GameplayStatics.spawn_sound_2d(world, snd, ${volume}, ${pitch}, ${startTime})
155
- ok = True
156
- except AttributeError:
157
- try:
158
- unreal.GameplayStatics.play_sound_2d(world, snd, ${volume}, ${pitch}, ${startTime})
159
- ok = True
160
- except AttributeError:
161
- # Fallback: play at camera location as 2D substitute
162
- try:
163
- info = unreal.EditorLevelLibrary.get_level_viewport_camera_info()
164
- cam_loc = info[0] if isinstance(info, (list, tuple)) and len(info) > 0 else unreal.Vector(0.0, 0.0, 0.0)
165
- except Exception:
166
- cam_loc = unreal.Vector(0.0, 0.0, 0.0)
167
- rot = unreal.Rotator(0.0, 0.0, 0.0)
168
- unreal.GameplayStatics.spawn_sound_at_location(world, snd, cam_loc, rot, ${volume}, ${pitch}, ${startTime})
169
- ok = True
170
- print('RESULT:' + json.dumps({'success': True} if ok else {'success': False, 'error': 'No suitable 2D playback method found'}))
466
+ path = "${escapePythonString(soundPath)}"
467
+ if not unreal.EditorAssetLibrary.does_asset_exist(path):
468
+ result["error"] = "Sound asset not found"
469
+ raise SystemExit(0)
470
+
471
+ snd = unreal.EditorAssetLibrary.load_asset(path)
472
+ if not snd:
473
+ result["error"] = f"Failed to load sound asset: {path}"
474
+ raise SystemExit(0)
475
+
476
+ world = None
477
+ try:
478
+ world = unreal.EditorUtilityLibrary.get_editor_world()
479
+ except Exception:
480
+ world = None
481
+
482
+ if not world:
483
+ editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
484
+ if editor_subsystem and hasattr(editor_subsystem, 'get_editor_world'):
485
+ world = editor_subsystem.get_editor_world()
486
+
487
+ if not world:
488
+ try:
489
+ world = unreal.EditorSubsystemLibrary.get_editor_world()
490
+ except Exception:
491
+ world = None
492
+
493
+ if not world:
494
+ result["error"] = "Unable to resolve editor world. Start PIE and ensure Editor Scripting Utilities is enabled."
495
+ raise SystemExit(0)
496
+
497
+ ok = False
498
+ try:
499
+ unreal.GameplayStatics.spawn_sound_2d(world, snd, ${volume}, ${pitch}, ${startTime})
500
+ ok = True
501
+ except AttributeError:
502
+ try:
503
+ unreal.GameplayStatics.play_sound_2d(world, snd, ${volume}, ${pitch}, ${startTime})
504
+ ok = True
505
+ except AttributeError:
506
+ pass
507
+
508
+ if not ok:
509
+ cam_loc = unreal.Vector(0.0, 0.0, 0.0)
510
+ try:
511
+ editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
512
+ if editor_subsystem and hasattr(editor_subsystem, 'get_level_viewport_camera_info'):
513
+ info = editor_subsystem.get_level_viewport_camera_info()
514
+ if isinstance(info, (list, tuple)) and len(info) > 0:
515
+ cam_loc = info[0]
516
+ except Exception:
517
+ try:
518
+ controller = world.get_first_player_controller()
519
+ if controller:
520
+ pawn = controller.get_pawn()
521
+ if pawn:
522
+ cam_loc = pawn.get_actor_location()
523
+ except Exception:
524
+ pass
525
+
526
+ try:
527
+ rot = unreal.Rotator(0.0, 0.0, 0.0)
528
+ unreal.GameplayStatics.spawn_sound_at_location(world, snd, cam_loc, rot, ${volume}, ${pitch}, ${startTime})
529
+ ok = True
530
+ result["warnings"].append("Fell back to 3D playback at camera location")
531
+ except Exception as location_error:
532
+ result["warnings"].append(f"Failed fallback playback: {location_error}")
533
+
534
+ if not ok:
535
+ result["error"] = "Failed to play sound in 2D or fallback configuration"
536
+ raise SystemExit(0)
537
+
538
+ result["success"] = True
539
+ result["message"] = "Sound2D played"
540
+
541
+ except SystemExit:
542
+ pass
171
543
  except Exception as e:
172
- print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
544
+ result["error"] = str(e)
545
+ finally:
546
+ payload = dict(result)
547
+ if payload.get("success"):
548
+ if not payload.get("message"):
549
+ payload["message"] = "Sound2D played"
550
+ payload.pop("error", None)
551
+ else:
552
+ if not payload.get("error"):
553
+ payload["error"] = payload.get("message") or "Failed to play sound2D"
554
+ if not payload.get("message"):
555
+ payload["message"] = payload["error"]
556
+ if not payload.get("warnings"):
557
+ payload.pop("warnings", None)
558
+ print('RESULT:' + json.dumps(payload))
173
559
  `.trim();
174
560
  try {
175
- const resp = await this.bridge.executePython(py);
176
- const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
177
- const m = out.match(/RESULT:({.*})/);
178
- if (m) { try { const parsed = JSON.parse(m[1]); return parsed.success ? { success: true, message: 'Sound2D played' } : { success: false, error: parsed.error }; } catch {} }
179
- return { success: true, message: 'Sound2D play attempted' };
180
- } catch (e) {
181
- return { success: false, error: `Failed to play sound2D: ${e}` };
182
- }
561
+ const resp = await this.bridge.executePythonWithResult(py);
562
+ return this.interpretResult(resp, {
563
+ successMessage: 'Sound2D played',
564
+ failureMessage: 'Failed to play sound2D'
565
+ });
566
+ } catch (e) {
567
+ return { success: false, error: `Failed to play sound2D: ${e}` };
568
+ }
183
569
  }
184
570
 
185
571
  // Create audio component
@@ -363,9 +749,17 @@ except Exception as e:
363
749
  unreal.AudioMixerBlueprintLibrary.set_overall_volume_multiplier(${vol})
364
750
  print('RESULT:' + json.dumps({'success': True}))
365
751
  except AttributeError:
366
- # Fallback to GameplayStatics method
752
+ # Fallback to GameplayStatics method using modern subsystems
367
753
  try:
368
- world = unreal.EditorLevelLibrary.get_editor_world()
754
+ # Try modern subsystem first
755
+ try:
756
+ editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
757
+ if hasattr(editor_subsystem, 'get_editor_world'):
758
+ world = editor_subsystem.get_editor_world()
759
+ else:
760
+ world = unreal.EditorLevelLibrary.get_editor_world()
761
+ except Exception:
762
+ world = unreal.EditorLevelLibrary.get_editor_world()
369
763
  unreal.GameplayStatics.set_global_pitch_modulation(world, 1.0, 0.0) # Reset pitch
370
764
  unreal.GameplayStatics.set_global_time_dilation(world, 1.0) # Reset time
371
765
  # Note: There's no direct master volume in GameplayStatics, use sound class