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.
- package/.env.production +1 -1
- package/.github/copilot-instructions.md +45 -0
- package/.github/workflows/publish-mcp.yml +1 -1
- package/README.md +22 -7
- package/dist/index.js +137 -46
- 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.d.ts +3 -2
- package/dist/resources/assets.js +117 -109
- package/dist/resources/levels.d.ts +21 -3
- package/dist/resources/levels.js +31 -56
- 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 +58 -46
- 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 +236 -87
- package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
- package/dist/tools/consolidated-tool-definitions.js +124 -255
- package/dist/tools/consolidated-tool-handlers.js +749 -766
- package/dist/tools/debug.d.ts +72 -10
- package/dist/tools/debug.js +170 -36
- 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 +98 -24
- package/dist/tools/sequence.d.ts +1 -0
- package/dist/tools/sequence.js +146 -24
- 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.d.ts +6 -1
- package/dist/utils/response-validator.js +66 -13
- 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 +11 -10
- package/server.json +37 -14
- package/src/index.ts +146 -50
- package/src/prompts/index.ts +211 -13
- package/src/resources/actors.ts +59 -44
- package/src/resources/assets.ts +123 -102
- package/src/resources/levels.ts +37 -47
- package/src/tools/actors.ts +269 -313
- package/src/tools/animation.ts +556 -539
- package/src/tools/assets.ts +59 -45
- package/src/tools/audio.ts +507 -113
- package/src/tools/blueprint.ts +778 -462
- package/src/tools/build_environment_advanced.ts +312 -106
- package/src/tools/consolidated-tool-definitions.ts +136 -267
- package/src/tools/consolidated-tool-handlers.ts +871 -795
- package/src/tools/debug.ts +179 -38
- 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 +103 -25
- package/src/tools/sequence.ts +157 -30
- 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 +68 -17
- 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/tools/tool-definitions.d.ts +0 -4919
- package/dist/tools/tool-definitions.js +0 -1065
- package/dist/tools/tool-handlers.d.ts +0 -47
- package/dist/tools/tool-handlers.js +0 -863
- 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/tools/tool-definitions.ts +0 -1081
- package/src/tools/tool-handlers.ts +0 -973
- package/src/types/index.ts +0 -414
- package/src/utils/cache-manager.ts +0 -213
- package/src/utils/errors.ts +0 -312
package/src/tools/audio.ts
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
if
|
|
50
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
362
|
+
const py = `
|
|
89
363
|
import unreal
|
|
90
364
|
import json
|
|
91
|
-
|
|
92
|
-
|
|
365
|
+
|
|
366
|
+
result = {
|
|
367
|
+
"success": False,
|
|
368
|
+
"message": "",
|
|
369
|
+
"error": "",
|
|
370
|
+
"warnings": []
|
|
371
|
+
}
|
|
372
|
+
|
|
93
373
|
try:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
454
|
+
const py = `
|
|
136
455
|
import unreal
|
|
137
456
|
import json
|
|
138
|
-
|
|
457
|
+
|
|
458
|
+
result = {
|
|
459
|
+
"success": False,
|
|
460
|
+
"message": "",
|
|
461
|
+
"error": "",
|
|
462
|
+
"warnings": []
|
|
463
|
+
}
|
|
464
|
+
|
|
139
465
|
try:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|