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.
- package/.env.production +1 -1
- package/.github/copilot-instructions.md +45 -0
- package/.github/workflows/publish-mcp.yml +3 -2
- package/README.md +21 -5
- package/dist/index.js +124 -31
- package/dist/prompts/index.d.ts +10 -3
- package/dist/prompts/index.js +186 -7
- package/dist/resources/actors.d.ts +19 -1
- package/dist/resources/actors.js +55 -64
- package/dist/resources/assets.js +46 -62
- package/dist/resources/levels.d.ts +21 -3
- package/dist/resources/levels.js +29 -54
- package/dist/tools/actors.d.ts +3 -14
- package/dist/tools/actors.js +246 -302
- package/dist/tools/animation.d.ts +57 -102
- package/dist/tools/animation.js +429 -450
- package/dist/tools/assets.d.ts +13 -2
- package/dist/tools/assets.js +52 -44
- package/dist/tools/audio.d.ts +22 -13
- package/dist/tools/audio.js +467 -121
- package/dist/tools/blueprint.d.ts +32 -13
- package/dist/tools/blueprint.js +699 -448
- package/dist/tools/build_environment_advanced.d.ts +0 -1
- package/dist/tools/build_environment_advanced.js +190 -45
- package/dist/tools/consolidated-tool-definitions.js +78 -252
- package/dist/tools/consolidated-tool-handlers.js +506 -133
- package/dist/tools/debug.d.ts +72 -10
- package/dist/tools/debug.js +167 -31
- package/dist/tools/editor.d.ts +9 -2
- package/dist/tools/editor.js +30 -44
- package/dist/tools/foliage.d.ts +34 -15
- package/dist/tools/foliage.js +97 -107
- package/dist/tools/introspection.js +19 -21
- package/dist/tools/landscape.d.ts +1 -2
- package/dist/tools/landscape.js +311 -168
- package/dist/tools/level.d.ts +3 -28
- package/dist/tools/level.js +642 -192
- package/dist/tools/lighting.d.ts +14 -3
- package/dist/tools/lighting.js +236 -123
- package/dist/tools/materials.d.ts +25 -7
- package/dist/tools/materials.js +102 -79
- package/dist/tools/niagara.d.ts +10 -12
- package/dist/tools/niagara.js +74 -94
- package/dist/tools/performance.d.ts +12 -4
- package/dist/tools/performance.js +38 -79
- package/dist/tools/physics.d.ts +34 -10
- package/dist/tools/physics.js +364 -292
- package/dist/tools/rc.js +97 -23
- package/dist/tools/sequence.d.ts +1 -0
- package/dist/tools/sequence.js +125 -22
- package/dist/tools/ui.d.ts +31 -4
- package/dist/tools/ui.js +83 -66
- package/dist/tools/visual.d.ts +11 -0
- package/dist/tools/visual.js +245 -30
- package/dist/types/tool-types.d.ts +0 -6
- package/dist/types/tool-types.js +1 -8
- package/dist/unreal-bridge.d.ts +32 -2
- package/dist/unreal-bridge.js +621 -127
- package/dist/utils/elicitation.d.ts +57 -0
- package/dist/utils/elicitation.js +104 -0
- package/dist/utils/error-handler.d.ts +0 -33
- package/dist/utils/error-handler.js +4 -111
- package/dist/utils/http.d.ts +2 -22
- package/dist/utils/http.js +12 -75
- package/dist/utils/normalize.d.ts +4 -4
- package/dist/utils/normalize.js +15 -7
- package/dist/utils/python-output.d.ts +18 -0
- package/dist/utils/python-output.js +290 -0
- package/dist/utils/python.d.ts +2 -0
- package/dist/utils/python.js +4 -0
- package/dist/utils/response-validator.js +28 -2
- package/dist/utils/result-helpers.d.ts +27 -0
- package/dist/utils/result-helpers.js +147 -0
- package/dist/utils/safe-json.d.ts +0 -2
- package/dist/utils/safe-json.js +0 -43
- package/dist/utils/validation.d.ts +16 -0
- package/dist/utils/validation.js +70 -7
- package/mcp-config-example.json +2 -2
- package/package.json +10 -9
- package/server.json +37 -14
- package/src/index.ts +130 -33
- package/src/prompts/index.ts +211 -13
- package/src/resources/actors.ts +59 -44
- package/src/resources/assets.ts +48 -51
- package/src/resources/levels.ts +35 -45
- package/src/tools/actors.ts +269 -313
- package/src/tools/animation.ts +556 -539
- package/src/tools/assets.ts +53 -43
- package/src/tools/audio.ts +507 -113
- package/src/tools/blueprint.ts +778 -462
- package/src/tools/build_environment_advanced.ts +266 -64
- package/src/tools/consolidated-tool-definitions.ts +90 -264
- package/src/tools/consolidated-tool-handlers.ts +630 -121
- package/src/tools/debug.ts +176 -33
- package/src/tools/editor.ts +35 -37
- package/src/tools/foliage.ts +110 -104
- package/src/tools/introspection.ts +24 -22
- package/src/tools/landscape.ts +334 -181
- package/src/tools/level.ts +683 -182
- package/src/tools/lighting.ts +244 -123
- package/src/tools/materials.ts +114 -83
- package/src/tools/niagara.ts +87 -81
- package/src/tools/performance.ts +49 -88
- package/src/tools/physics.ts +393 -299
- package/src/tools/rc.ts +102 -24
- package/src/tools/sequence.ts +136 -28
- package/src/tools/ui.ts +101 -70
- package/src/tools/visual.ts +250 -29
- package/src/types/tool-types.ts +0 -9
- package/src/unreal-bridge.ts +658 -140
- package/src/utils/elicitation.ts +129 -0
- package/src/utils/error-handler.ts +4 -159
- package/src/utils/http.ts +16 -115
- package/src/utils/normalize.ts +20 -10
- package/src/utils/python-output.ts +351 -0
- package/src/utils/python.ts +3 -0
- package/src/utils/response-validator.ts +25 -2
- package/src/utils/result-helpers.ts +193 -0
- package/src/utils/safe-json.ts +0 -50
- package/src/utils/validation.ts +94 -7
- package/tests/run-unreal-tool-tests.mjs +720 -0
- package/tsconfig.json +2 -2
- package/dist/python-utils.d.ts +0 -29
- package/dist/python-utils.js +0 -54
- package/dist/types/index.d.ts +0 -323
- package/dist/types/index.js +0 -28
- package/dist/utils/cache-manager.d.ts +0 -64
- package/dist/utils/cache-manager.js +0 -176
- package/dist/utils/errors.d.ts +0 -133
- package/dist/utils/errors.js +0 -256
- package/src/python/editor_compat.py +0 -181
- package/src/python-utils.ts +0 -57
- package/src/types/index.ts +0 -414
- package/src/utils/cache-manager.ts +0 -213
- package/src/utils/errors.ts +0 -312
package/dist/tools/audio.js
CHANGED
|
@@ -1,67 +1,304 @@
|
|
|
1
|
+
// Audio tools for Unreal Engine
|
|
2
|
+
import JSON5 from 'json5';
|
|
3
|
+
import { escapePythonString } from '../utils/python.js';
|
|
1
4
|
export class AudioTools {
|
|
2
5
|
bridge;
|
|
3
6
|
constructor(bridge) {
|
|
4
7
|
this.bridge = bridge;
|
|
5
8
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
interpretResult(resp, defaults) {
|
|
10
|
+
const normalizePayload = (payload) => {
|
|
11
|
+
const warningsValue = payload?.warnings;
|
|
12
|
+
const warningsText = Array.isArray(warningsValue)
|
|
13
|
+
? (warningsValue.length > 0 ? warningsValue.join('; ') : undefined)
|
|
14
|
+
: typeof warningsValue === 'string' && warningsValue.trim() !== ''
|
|
15
|
+
? warningsValue
|
|
16
|
+
: undefined;
|
|
17
|
+
if (payload?.success === true) {
|
|
18
|
+
const message = typeof payload?.message === 'string' && payload.message.trim() !== ''
|
|
19
|
+
? payload.message
|
|
20
|
+
: defaults.successMessage;
|
|
21
|
+
return {
|
|
22
|
+
success: true,
|
|
23
|
+
message,
|
|
24
|
+
details: warningsText
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const error = (typeof payload?.error === 'string' && payload.error.trim() !== ''
|
|
28
|
+
? payload.error
|
|
29
|
+
: undefined)
|
|
30
|
+
?? (typeof payload?.message === 'string' && payload.message.trim() !== ''
|
|
31
|
+
? payload.message
|
|
32
|
+
: undefined)
|
|
33
|
+
?? defaults.failureMessage;
|
|
34
|
+
return {
|
|
35
|
+
success: false,
|
|
36
|
+
error,
|
|
37
|
+
details: warningsText
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
if (resp && typeof resp === 'object') {
|
|
41
|
+
const payload = resp;
|
|
42
|
+
if ('success' in payload || 'error' in payload || 'message' in payload || 'warnings' in payload) {
|
|
43
|
+
return normalizePayload(payload);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const raw = typeof resp === 'string' ? resp : JSON.stringify(resp);
|
|
47
|
+
const extractJson = (input) => {
|
|
48
|
+
const marker = 'RESULT:';
|
|
49
|
+
const markerIndex = input.lastIndexOf(marker);
|
|
50
|
+
if (markerIndex === -1) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const afterMarker = input.slice(markerIndex + marker.length);
|
|
54
|
+
const firstBraceIndex = afterMarker.indexOf('{');
|
|
55
|
+
if (firstBraceIndex === -1) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
let depth = 0;
|
|
59
|
+
let inString = false;
|
|
60
|
+
let escapeNext = false;
|
|
61
|
+
for (let i = firstBraceIndex; i < afterMarker.length; i++) {
|
|
62
|
+
const char = afterMarker[i];
|
|
63
|
+
if (inString) {
|
|
64
|
+
if (escapeNext) {
|
|
65
|
+
escapeNext = false;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (char === '\\') {
|
|
69
|
+
escapeNext = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (char === '"') {
|
|
73
|
+
inString = false;
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (char === '"') {
|
|
78
|
+
inString = true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (char === '{') {
|
|
82
|
+
depth += 1;
|
|
83
|
+
}
|
|
84
|
+
else if (char === '}') {
|
|
85
|
+
depth -= 1;
|
|
86
|
+
if (depth === 0) {
|
|
87
|
+
return afterMarker.slice(firstBraceIndex, i + 1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const fallbackMatch = /\{[\s\S]*\}/.exec(afterMarker);
|
|
92
|
+
return fallbackMatch ? fallbackMatch[0] : undefined;
|
|
93
|
+
};
|
|
94
|
+
const jsonPayload = extractJson(raw);
|
|
95
|
+
if (jsonPayload) {
|
|
96
|
+
const parseAttempts = [
|
|
97
|
+
{
|
|
98
|
+
label: 'json',
|
|
99
|
+
parser: () => JSON.parse(jsonPayload)
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
label: 'json5',
|
|
103
|
+
parser: () => JSON5.parse(jsonPayload)
|
|
104
|
+
}
|
|
105
|
+
];
|
|
106
|
+
const sanitizedForJson5 = jsonPayload
|
|
107
|
+
.replace(/\bTrue\b/g, 'true')
|
|
108
|
+
.replace(/\bFalse\b/g, 'false')
|
|
109
|
+
.replace(/\bNone\b/g, 'null');
|
|
110
|
+
if (sanitizedForJson5 !== jsonPayload) {
|
|
111
|
+
parseAttempts.push({ label: 'json5-sanitized', parser: () => JSON5.parse(sanitizedForJson5) });
|
|
112
|
+
}
|
|
113
|
+
const parseErrors = [];
|
|
114
|
+
for (const attempt of parseAttempts) {
|
|
115
|
+
try {
|
|
116
|
+
const parsed = attempt.parser();
|
|
117
|
+
if (parsed && typeof parsed === 'object') {
|
|
118
|
+
return normalizePayload(parsed);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
parseErrors.push(`${attempt.label}: ${err instanceof Error ? err.message : String(err)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const errorMatch = /["']error["']\s*:\s*(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)')/i.exec(jsonPayload);
|
|
126
|
+
const messageMatch = /["']message["']\s*:\s*(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)')/i.exec(jsonPayload);
|
|
127
|
+
const fallbackText = errorMatch?.[1] ?? errorMatch?.[2] ?? messageMatch?.[1] ?? messageMatch?.[2];
|
|
128
|
+
const errorText = fallbackText && fallbackText.trim().length > 0
|
|
129
|
+
? fallbackText.trim()
|
|
130
|
+
: `${defaults.failureMessage}: ${parseErrors[0] ?? 'Unable to parse RESULT payload'}`;
|
|
131
|
+
const snippet = jsonPayload.length > 240 ? `${jsonPayload.slice(0, 240)}…` : jsonPayload;
|
|
132
|
+
const detailsParts = [];
|
|
133
|
+
if (parseErrors.length > 0) {
|
|
134
|
+
detailsParts.push(`Parse attempts failed: ${parseErrors.join('; ')}`);
|
|
135
|
+
}
|
|
136
|
+
detailsParts.push(`Raw payload: ${snippet}`);
|
|
137
|
+
const detailsText = detailsParts.join(' | ');
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
error: errorText,
|
|
141
|
+
details: detailsText
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return { success: false, error: defaults.failureMessage };
|
|
18
145
|
}
|
|
19
146
|
// Create sound cue
|
|
20
147
|
async createSoundCue(params) {
|
|
148
|
+
const escapePyString = (value) => value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
149
|
+
const toPyNumber = (value) => value === undefined || value === null || !Number.isFinite(value) ? 'None' : String(value);
|
|
150
|
+
const toPyBool = (value) => value === undefined || value === null ? 'None' : value ? 'True' : 'False';
|
|
21
151
|
const path = params.savePath || '/Game/Audio/Cues';
|
|
152
|
+
const wavePath = params.wavePath || '';
|
|
153
|
+
const attenuationPath = params.settings?.attenuationSettings || '';
|
|
154
|
+
const volumeLiteral = toPyNumber(params.settings?.volume);
|
|
155
|
+
const pitchLiteral = toPyNumber(params.settings?.pitch);
|
|
156
|
+
const loopingLiteral = toPyBool(params.settings?.looping);
|
|
22
157
|
const py = `
|
|
23
158
|
import unreal
|
|
24
159
|
import json
|
|
25
|
-
|
|
26
|
-
|
|
160
|
+
|
|
161
|
+
name = r"${escapePyString(params.name)}"
|
|
162
|
+
package_path = r"${escapePyString(path)}"
|
|
163
|
+
wave_path = r"${escapePyString(wavePath)}"
|
|
164
|
+
attenuation_path = r"${escapePyString(attenuationPath)}"
|
|
165
|
+
attach_wave = ${params.wavePath ? 'True' : 'False'}
|
|
166
|
+
volume_override = ${volumeLiteral}
|
|
167
|
+
pitch_override = ${pitchLiteral}
|
|
168
|
+
looping_override = ${loopingLiteral}
|
|
169
|
+
|
|
170
|
+
result = {
|
|
171
|
+
"success": False,
|
|
172
|
+
"message": "",
|
|
173
|
+
"error": "",
|
|
174
|
+
"warnings": []
|
|
175
|
+
}
|
|
176
|
+
|
|
27
177
|
try:
|
|
28
|
-
|
|
178
|
+
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
|
179
|
+
if not asset_tools:
|
|
180
|
+
result["error"] = "AssetToolsHelpers unavailable"
|
|
181
|
+
raise SystemExit(0)
|
|
182
|
+
|
|
183
|
+
factory = None
|
|
184
|
+
try:
|
|
185
|
+
factory = unreal.SoundCueFactoryNew()
|
|
186
|
+
except Exception:
|
|
187
|
+
factory = None
|
|
188
|
+
|
|
189
|
+
if not factory:
|
|
190
|
+
result["error"] = "SoundCueFactoryNew unavailable"
|
|
191
|
+
raise SystemExit(0)
|
|
192
|
+
|
|
193
|
+
package_path = package_path.rstrip('/') if package_path else package_path
|
|
194
|
+
|
|
195
|
+
asset = asset_tools.create_asset(
|
|
196
|
+
asset_name=name,
|
|
197
|
+
package_path=package_path,
|
|
198
|
+
asset_class=unreal.SoundCue,
|
|
199
|
+
factory=factory
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if not asset:
|
|
203
|
+
result["error"] = "Failed to create SoundCue"
|
|
204
|
+
raise SystemExit(0)
|
|
205
|
+
|
|
206
|
+
asset_subsystem = None
|
|
207
|
+
try:
|
|
208
|
+
asset_subsystem = unreal.get_editor_subsystem(unreal.EditorAssetSubsystem)
|
|
209
|
+
except Exception:
|
|
210
|
+
asset_subsystem = None
|
|
211
|
+
|
|
212
|
+
editor_library = unreal.EditorAssetLibrary
|
|
213
|
+
|
|
214
|
+
if attach_wave:
|
|
215
|
+
wave_exists = False
|
|
29
216
|
try:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
217
|
+
if asset_subsystem and hasattr(asset_subsystem, "does_asset_exist"):
|
|
218
|
+
wave_exists = asset_subsystem.does_asset_exist(wave_path)
|
|
219
|
+
else:
|
|
220
|
+
wave_exists = editor_library.does_asset_exist(wave_path)
|
|
221
|
+
except Exception as existence_error:
|
|
222
|
+
result["warnings"].append(f"Wave lookup failed: {existence_error}")
|
|
223
|
+
|
|
224
|
+
if not wave_exists:
|
|
225
|
+
result["warnings"].append(f"Wave asset not found: {wave_path}")
|
|
35
226
|
else:
|
|
36
|
-
|
|
37
|
-
if
|
|
38
|
-
|
|
39
|
-
try:
|
|
40
|
-
wave_path = r"${params.wavePath || ''}"
|
|
41
|
-
if wave_path and unreal.EditorAssetLibrary.does_asset_exist(wave_path):
|
|
42
|
-
snd = unreal.EditorAssetLibrary.load_asset(wave_path)
|
|
43
|
-
# Simple node hookup via SoundCueGraph is non-trivial via Python; leave as empty cue
|
|
44
|
-
except Exception:
|
|
45
|
-
pass
|
|
46
|
-
unreal.EditorAssetLibrary.save_asset(f"{path}/{name}")
|
|
47
|
-
print('RESULT:' + json.dumps({'success': True}))
|
|
227
|
+
try:
|
|
228
|
+
if asset_subsystem and hasattr(asset_subsystem, "load_asset"):
|
|
229
|
+
wave_asset = asset_subsystem.load_asset(wave_path)
|
|
48
230
|
else:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
231
|
+
wave_asset = editor_library.load_asset(wave_path)
|
|
232
|
+
if wave_asset:
|
|
233
|
+
# Hooking up cue nodes via Python is non-trivial; surface warning for manual setup
|
|
234
|
+
result["warnings"].append("Sound cue created without automatic wave node hookup")
|
|
235
|
+
except Exception as wave_error:
|
|
236
|
+
result["warnings"].append(f"Failed to load wave asset: {wave_error}")
|
|
237
|
+
|
|
238
|
+
if volume_override is not None and hasattr(asset, "volume_multiplier"):
|
|
239
|
+
asset.volume_multiplier = volume_override
|
|
240
|
+
if pitch_override is not None and hasattr(asset, "pitch_multiplier"):
|
|
241
|
+
asset.pitch_multiplier = pitch_override
|
|
242
|
+
if looping_override is not None and hasattr(asset, "b_looping"):
|
|
243
|
+
asset.b_looping = looping_override
|
|
244
|
+
|
|
245
|
+
if attenuation_path:
|
|
246
|
+
try:
|
|
247
|
+
attenuation_asset = editor_library.load_asset(attenuation_path)
|
|
248
|
+
if attenuation_asset:
|
|
249
|
+
applied = False
|
|
250
|
+
if hasattr(asset, "set_attenuation_settings"):
|
|
251
|
+
try:
|
|
252
|
+
asset.set_attenuation_settings(attenuation_asset)
|
|
253
|
+
applied = True
|
|
254
|
+
except Exception:
|
|
255
|
+
applied = False
|
|
256
|
+
if not applied and hasattr(asset, "attenuation_settings"):
|
|
257
|
+
asset.attenuation_settings = attenuation_asset
|
|
258
|
+
applied = True
|
|
259
|
+
if not applied:
|
|
260
|
+
result["warnings"].append("Attenuation asset loaded but could not be applied automatically")
|
|
261
|
+
except Exception as attenuation_error:
|
|
262
|
+
result["warnings"].append(f"Failed to apply attenuation: {attenuation_error}")
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
save_target = f"{package_path}/{name}" if package_path else name
|
|
266
|
+
if asset_subsystem and hasattr(asset_subsystem, "save_asset"):
|
|
267
|
+
asset_subsystem.save_asset(save_target)
|
|
268
|
+
else:
|
|
269
|
+
editor_library.save_asset(save_target)
|
|
270
|
+
except Exception as save_error:
|
|
271
|
+
result["warnings"].append(f"Save failed: {save_error}")
|
|
272
|
+
|
|
273
|
+
result["success"] = True
|
|
274
|
+
result["message"] = "Sound cue created"
|
|
275
|
+
|
|
276
|
+
except SystemExit:
|
|
277
|
+
pass
|
|
278
|
+
except Exception as error:
|
|
279
|
+
result["error"] = str(error)
|
|
280
|
+
|
|
281
|
+
finally:
|
|
282
|
+
payload = dict(result)
|
|
283
|
+
if payload.get("success"):
|
|
284
|
+
if not payload.get("message"):
|
|
285
|
+
payload["message"] = "Sound cue created"
|
|
286
|
+
payload.pop("error", None)
|
|
287
|
+
else:
|
|
288
|
+
if not payload.get("error"):
|
|
289
|
+
payload["error"] = payload.get("message") or "Failed to create SoundCue"
|
|
290
|
+
if not payload.get("message"):
|
|
291
|
+
payload["message"] = payload["error"]
|
|
292
|
+
if not payload.get("warnings"):
|
|
293
|
+
payload.pop("warnings", None)
|
|
294
|
+
print('RESULT:' + json.dumps(payload))
|
|
52
295
|
`.trim();
|
|
53
296
|
try {
|
|
54
297
|
const resp = await this.bridge.executePython(py);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const parsed = JSON.parse(m[1]);
|
|
60
|
-
return parsed.success ? { success: true, message: 'Sound cue created' } : { success: false, error: parsed.error };
|
|
61
|
-
}
|
|
62
|
-
catch { }
|
|
63
|
-
}
|
|
64
|
-
return { success: true, message: 'Sound cue creation attempted' };
|
|
298
|
+
return this.interpretResult(resp, {
|
|
299
|
+
successMessage: 'Sound cue created',
|
|
300
|
+
failureMessage: 'Failed to create SoundCue'
|
|
301
|
+
});
|
|
65
302
|
}
|
|
66
303
|
catch (e) {
|
|
67
304
|
return { success: false, error: `Failed to create sound cue: ${e}` };
|
|
@@ -72,43 +309,82 @@ except Exception as e:
|
|
|
72
309
|
const volume = params.volume ?? 1.0;
|
|
73
310
|
const pitch = params.pitch ?? 1.0;
|
|
74
311
|
const startTime = params.startTime ?? 0.0;
|
|
312
|
+
const soundPath = params.soundPath ?? '';
|
|
75
313
|
const py = `
|
|
76
314
|
import unreal
|
|
77
315
|
import json
|
|
78
|
-
|
|
79
|
-
|
|
316
|
+
|
|
317
|
+
result = {
|
|
318
|
+
"success": False,
|
|
319
|
+
"message": "",
|
|
320
|
+
"error": "",
|
|
321
|
+
"warnings": []
|
|
322
|
+
}
|
|
323
|
+
|
|
80
324
|
try:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
325
|
+
path = "${escapePythonString(soundPath)}"
|
|
326
|
+
if not unreal.EditorAssetLibrary.does_asset_exist(path):
|
|
327
|
+
result["error"] = "Sound asset not found"
|
|
328
|
+
raise SystemExit(0)
|
|
329
|
+
|
|
330
|
+
snd = unreal.EditorAssetLibrary.load_asset(path)
|
|
331
|
+
if not snd:
|
|
332
|
+
result["error"] = f"Failed to load sound asset: {path}"
|
|
333
|
+
raise SystemExit(0)
|
|
334
|
+
|
|
335
|
+
world = None
|
|
336
|
+
try:
|
|
337
|
+
world = unreal.EditorUtilityLibrary.get_editor_world()
|
|
338
|
+
except Exception:
|
|
339
|
+
world = None
|
|
340
|
+
|
|
341
|
+
if not world:
|
|
342
|
+
editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
343
|
+
if editor_subsystem and hasattr(editor_subsystem, 'get_editor_world'):
|
|
344
|
+
world = editor_subsystem.get_editor_world()
|
|
345
|
+
|
|
346
|
+
if not world:
|
|
347
|
+
try:
|
|
348
|
+
world = unreal.EditorSubsystemLibrary.get_editor_world()
|
|
349
|
+
except Exception:
|
|
350
|
+
world = None
|
|
351
|
+
|
|
352
|
+
if not world:
|
|
353
|
+
result["error"] = "Unable to resolve editor world. Start PIE and ensure Editor Scripting Utilities is enabled."
|
|
354
|
+
raise SystemExit(0)
|
|
355
|
+
|
|
356
|
+
loc = unreal.Vector(${params.location[0]}, ${params.location[1]}, ${params.location[2]})
|
|
357
|
+
rot = unreal.Rotator(0.0, 0.0, 0.0)
|
|
358
|
+
unreal.GameplayStatics.spawn_sound_at_location(world, snd, loc, rot, ${volume}, ${pitch}, ${startTime})
|
|
359
|
+
|
|
360
|
+
result["success"] = True
|
|
361
|
+
result["message"] = "Sound played"
|
|
362
|
+
|
|
363
|
+
except SystemExit:
|
|
364
|
+
pass
|
|
97
365
|
except Exception as e:
|
|
98
|
-
|
|
366
|
+
result["error"] = str(e)
|
|
367
|
+
finally:
|
|
368
|
+
payload = dict(result)
|
|
369
|
+
if payload.get("success"):
|
|
370
|
+
if not payload.get("message"):
|
|
371
|
+
payload["message"] = "Sound played"
|
|
372
|
+
payload.pop("error", None)
|
|
373
|
+
else:
|
|
374
|
+
if not payload.get("error"):
|
|
375
|
+
payload["error"] = payload.get("message") or "Failed to play sound"
|
|
376
|
+
if not payload.get("message"):
|
|
377
|
+
payload["message"] = payload["error"]
|
|
378
|
+
if not payload.get("warnings"):
|
|
379
|
+
payload.pop("warnings", None)
|
|
380
|
+
print('RESULT:' + json.dumps(payload))
|
|
99
381
|
`.trim();
|
|
100
382
|
try {
|
|
101
|
-
const resp = await this.bridge.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const parsed = JSON.parse(m[1]);
|
|
107
|
-
return parsed.success ? { success: true, message: 'Sound played' } : { success: false, error: parsed.error };
|
|
108
|
-
}
|
|
109
|
-
catch { }
|
|
110
|
-
}
|
|
111
|
-
return { success: true, message: 'Sound play attempted' };
|
|
383
|
+
const resp = await this.bridge.executePythonWithResult(py);
|
|
384
|
+
return this.interpretResult(resp, {
|
|
385
|
+
successMessage: 'Sound played',
|
|
386
|
+
failureMessage: 'Failed to play sound'
|
|
387
|
+
});
|
|
112
388
|
}
|
|
113
389
|
catch (e) {
|
|
114
390
|
return { success: false, error: `Failed to play sound: ${e}` };
|
|
@@ -119,57 +395,119 @@ except Exception as e:
|
|
|
119
395
|
const volume = params.volume ?? 1.0;
|
|
120
396
|
const pitch = params.pitch ?? 1.0;
|
|
121
397
|
const startTime = params.startTime ?? 0.0;
|
|
398
|
+
const soundPath = params.soundPath ?? '';
|
|
122
399
|
const py = `
|
|
123
400
|
import unreal
|
|
124
401
|
import json
|
|
125
|
-
|
|
402
|
+
|
|
403
|
+
result = {
|
|
404
|
+
"success": False,
|
|
405
|
+
"message": "",
|
|
406
|
+
"error": "",
|
|
407
|
+
"warnings": []
|
|
408
|
+
}
|
|
409
|
+
|
|
126
410
|
try:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
411
|
+
path = "${escapePythonString(soundPath)}"
|
|
412
|
+
if not unreal.EditorAssetLibrary.does_asset_exist(path):
|
|
413
|
+
result["error"] = "Sound asset not found"
|
|
414
|
+
raise SystemExit(0)
|
|
415
|
+
|
|
416
|
+
snd = unreal.EditorAssetLibrary.load_asset(path)
|
|
417
|
+
if not snd:
|
|
418
|
+
result["error"] = f"Failed to load sound asset: {path}"
|
|
419
|
+
raise SystemExit(0)
|
|
420
|
+
|
|
421
|
+
world = None
|
|
422
|
+
try:
|
|
423
|
+
world = unreal.EditorUtilityLibrary.get_editor_world()
|
|
424
|
+
except Exception:
|
|
425
|
+
world = None
|
|
426
|
+
|
|
427
|
+
if not world:
|
|
428
|
+
editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
429
|
+
if editor_subsystem and hasattr(editor_subsystem, 'get_editor_world'):
|
|
430
|
+
world = editor_subsystem.get_editor_world()
|
|
431
|
+
|
|
432
|
+
if not world:
|
|
433
|
+
try:
|
|
434
|
+
world = unreal.EditorSubsystemLibrary.get_editor_world()
|
|
435
|
+
except Exception:
|
|
436
|
+
world = None
|
|
437
|
+
|
|
438
|
+
if not world:
|
|
439
|
+
result["error"] = "Unable to resolve editor world. Start PIE and ensure Editor Scripting Utilities is enabled."
|
|
440
|
+
raise SystemExit(0)
|
|
441
|
+
|
|
442
|
+
ok = False
|
|
443
|
+
try:
|
|
444
|
+
unreal.GameplayStatics.spawn_sound_2d(world, snd, ${volume}, ${pitch}, ${startTime})
|
|
445
|
+
ok = True
|
|
446
|
+
except AttributeError:
|
|
447
|
+
try:
|
|
448
|
+
unreal.GameplayStatics.play_sound_2d(world, snd, ${volume}, ${pitch}, ${startTime})
|
|
449
|
+
ok = True
|
|
450
|
+
except AttributeError:
|
|
451
|
+
pass
|
|
452
|
+
|
|
453
|
+
if not ok:
|
|
454
|
+
cam_loc = unreal.Vector(0.0, 0.0, 0.0)
|
|
455
|
+
try:
|
|
456
|
+
editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
457
|
+
if editor_subsystem and hasattr(editor_subsystem, 'get_level_viewport_camera_info'):
|
|
458
|
+
info = editor_subsystem.get_level_viewport_camera_info()
|
|
459
|
+
if isinstance(info, (list, tuple)) and len(info) > 0:
|
|
460
|
+
cam_loc = info[0]
|
|
461
|
+
except Exception:
|
|
462
|
+
try:
|
|
463
|
+
controller = world.get_first_player_controller()
|
|
464
|
+
if controller:
|
|
465
|
+
pawn = controller.get_pawn()
|
|
466
|
+
if pawn:
|
|
467
|
+
cam_loc = pawn.get_actor_location()
|
|
468
|
+
except Exception:
|
|
469
|
+
pass
|
|
470
|
+
|
|
471
|
+
try:
|
|
472
|
+
rot = unreal.Rotator(0.0, 0.0, 0.0)
|
|
473
|
+
unreal.GameplayStatics.spawn_sound_at_location(world, snd, cam_loc, rot, ${volume}, ${pitch}, ${startTime})
|
|
474
|
+
ok = True
|
|
475
|
+
result["warnings"].append("Fell back to 3D playback at camera location")
|
|
476
|
+
except Exception as location_error:
|
|
477
|
+
result["warnings"].append(f"Failed fallback playback: {location_error}")
|
|
478
|
+
|
|
479
|
+
if not ok:
|
|
480
|
+
result["error"] = "Failed to play sound in 2D or fallback configuration"
|
|
481
|
+
raise SystemExit(0)
|
|
482
|
+
|
|
483
|
+
result["success"] = True
|
|
484
|
+
result["message"] = "Sound2D played"
|
|
485
|
+
|
|
486
|
+
except SystemExit:
|
|
487
|
+
pass
|
|
158
488
|
except Exception as e:
|
|
159
|
-
|
|
489
|
+
result["error"] = str(e)
|
|
490
|
+
finally:
|
|
491
|
+
payload = dict(result)
|
|
492
|
+
if payload.get("success"):
|
|
493
|
+
if not payload.get("message"):
|
|
494
|
+
payload["message"] = "Sound2D played"
|
|
495
|
+
payload.pop("error", None)
|
|
496
|
+
else:
|
|
497
|
+
if not payload.get("error"):
|
|
498
|
+
payload["error"] = payload.get("message") or "Failed to play sound2D"
|
|
499
|
+
if not payload.get("message"):
|
|
500
|
+
payload["message"] = payload["error"]
|
|
501
|
+
if not payload.get("warnings"):
|
|
502
|
+
payload.pop("warnings", None)
|
|
503
|
+
print('RESULT:' + json.dumps(payload))
|
|
160
504
|
`.trim();
|
|
161
505
|
try {
|
|
162
|
-
const resp = await this.bridge.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const parsed = JSON.parse(m[1]);
|
|
168
|
-
return parsed.success ? { success: true, message: 'Sound2D played' } : { success: false, error: parsed.error };
|
|
169
|
-
}
|
|
170
|
-
catch { }
|
|
171
|
-
}
|
|
172
|
-
return { success: true, message: 'Sound2D play attempted' };
|
|
506
|
+
const resp = await this.bridge.executePythonWithResult(py);
|
|
507
|
+
return this.interpretResult(resp, {
|
|
508
|
+
successMessage: 'Sound2D played',
|
|
509
|
+
failureMessage: 'Failed to play sound2D'
|
|
510
|
+
});
|
|
173
511
|
}
|
|
174
512
|
catch (e) {
|
|
175
513
|
return { success: false, error: `Failed to play sound2D: ${e}` };
|
|
@@ -292,9 +630,17 @@ except Exception as e:
|
|
|
292
630
|
unreal.AudioMixerBlueprintLibrary.set_overall_volume_multiplier(${vol})
|
|
293
631
|
print('RESULT:' + json.dumps({'success': True}))
|
|
294
632
|
except AttributeError:
|
|
295
|
-
# Fallback to GameplayStatics method
|
|
633
|
+
# Fallback to GameplayStatics method using modern subsystems
|
|
296
634
|
try:
|
|
297
|
-
|
|
635
|
+
# Try modern subsystem first
|
|
636
|
+
try:
|
|
637
|
+
editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
638
|
+
if hasattr(editor_subsystem, 'get_editor_world'):
|
|
639
|
+
world = editor_subsystem.get_editor_world()
|
|
640
|
+
else:
|
|
641
|
+
world = unreal.EditorLevelLibrary.get_editor_world()
|
|
642
|
+
except Exception:
|
|
643
|
+
world = unreal.EditorLevelLibrary.get_editor_world()
|
|
298
644
|
unreal.GameplayStatics.set_global_pitch_modulation(world, 1.0, 0.0) # Reset pitch
|
|
299
645
|
unreal.GameplayStatics.set_global_time_dilation(world, 1.0) # Reset time
|
|
300
646
|
# Note: There's no direct master volume in GameplayStatics, use sound class
|