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,133 +1,149 @@
1
1
  import { UnrealBridge } from '../unreal-bridge.js';
2
+ import { coerceBoolean, interpretStandardResult } from '../utils/result-helpers.js';
3
+ import { escapePythonString } from '../utils/python.js';
2
4
 
3
5
  export class MaterialTools {
4
6
  constructor(private bridge: UnrealBridge) {}
5
7
 
6
8
  async createMaterial(name: string, path: string) {
7
9
  try {
8
- // Validate name
9
10
  if (!name || name.trim() === '') {
10
11
  return { success: false, error: 'Material name cannot be empty' };
11
12
  }
12
-
13
- // Check name length (Unreal has 260 char path limit)
13
+
14
14
  if (name.length > 100) {
15
15
  return { success: false, error: `Material name too long (${name.length} chars). Maximum is 100 characters.` };
16
16
  }
17
-
18
- // Validate name doesn't contain invalid characters
19
- // Unreal Engine doesn't allow: spaces, dots, slashes, backslashes, pipes, angle brackets,
20
- // curly braces, square brackets, parentheses, @, #, etc.
17
+
21
18
  const invalidChars = /[\s./<>|{}[\]()@#\\]/;
22
19
  if (invalidChars.test(name)) {
23
20
  const foundChars = name.match(invalidChars);
24
21
  return { success: false, error: `Material name contains invalid characters: '${foundChars?.[0]}'. Avoid spaces, dots, slashes, backslashes, brackets, and special symbols.` };
25
22
  }
26
-
27
- // Validate path type
23
+
28
24
  if (typeof path !== 'string') {
29
25
  return { success: false, error: `Invalid path type: expected string, got ${typeof path}` };
30
26
  }
31
-
32
- // Clean up path - remove trailing slashes
33
- const cleanPath = path.replace(/\/$/, '');
34
-
35
- // Validate path starts with /Game or /Engine
27
+
28
+ const trimmedPath = path.trim();
29
+ const effectivePath = trimmedPath.length === 0 ? '/Game' : trimmedPath;
30
+ const cleanPath = effectivePath.replace(/\/$/, '');
31
+
36
32
  if (!cleanPath.startsWith('/Game') && !cleanPath.startsWith('/Engine')) {
37
33
  return { success: false, error: `Invalid path: must start with /Game or /Engine, got ${cleanPath}` };
38
34
  }
39
-
40
- // Use Python API to create material
41
- const materialPath = `${cleanPath}/${name}`;
42
- // Use the correct Unreal Engine 5 Python API
35
+
36
+ const normalizedPath = cleanPath.toLowerCase();
37
+ const restrictedPrefixes = ['/engine/restricted', '/engine/generated', '/engine/transient'];
38
+ if (restrictedPrefixes.some(prefix => normalizedPath.startsWith(prefix))) {
39
+ const errorMessage = `Destination path is read-only and cannot be used for material creation: ${cleanPath}`;
40
+ return { success: false, error: errorMessage, message: errorMessage };
41
+ }
42
+
43
+ const materialPath = `${cleanPath}/${name}`;
44
+ const payload = { name, cleanPath, materialPath };
45
+ const escapedName = escapePythonString(name);
43
46
  const pythonCode = `
44
- import unreal
45
- import json
47
+ import unreal, json
48
+
49
+ payload = json.loads(r'''${JSON.stringify(payload)}''')
50
+ result = {
51
+ 'success': False,
52
+ 'message': '',
53
+ 'error': '',
54
+ 'warnings': [],
55
+ 'details': [],
56
+ 'name': payload.get('name') or "${escapedName}",
57
+ 'path': payload.get('materialPath')
58
+ }
59
+
60
+ material_path = result['path']
61
+ clean_path = payload.get('cleanPath') or '/Game'
46
62
 
47
63
  try:
48
- # Check if material already exists
49
- material_path = '${materialPath}'
50
64
  if unreal.EditorAssetLibrary.does_asset_exist(material_path):
51
- print(json.dumps({"success": True, "exists": True, "path": material_path}))
65
+ result['success'] = True
66
+ result['exists'] = True
67
+ result['message'] = f"Material already exists at {material_path}"
52
68
  else:
53
- # Get the AssetTools
54
69
  asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
55
-
56
- # Create a MaterialFactoryNew
57
70
  factory = unreal.MaterialFactoryNew()
58
-
59
- # Clean up the path - remove trailing slashes
60
- clean_path = '${cleanPath}'.rstrip('/')
61
-
62
- # Create the material asset at the specified path
63
- # The path should be: /Game/FolderName and asset name separately
64
71
  asset = asset_tools.create_asset(
65
- asset_name='${name}',
72
+ asset_name=payload.get('name'),
66
73
  package_path=clean_path,
67
74
  asset_class=unreal.Material,
68
75
  factory=factory
69
76
  )
70
-
71
77
  if asset:
72
- # Save the package
73
78
  unreal.EditorAssetLibrary.save_asset(material_path)
74
- print(json.dumps({"success": True, "created": True, "path": material_path}))
79
+ result['success'] = True
80
+ result['created'] = True
81
+ result['message'] = f"Material created at {material_path}"
75
82
  else:
76
- print(json.dumps({"success": False, "error": "Failed to create material"}))
77
- except Exception as e:
78
- print(json.dumps({"success": False, "error": str(e)}))
83
+ result['error'] = 'Failed to create material'
84
+ result['message'] = result['error']
85
+ except Exception as exc:
86
+ result['error'] = str(exc)
87
+ if not result['message']:
88
+ result['message'] = result['error']
89
+
90
+ print('RESULT:' + json.dumps(result))
79
91
  `.trim();
80
-
92
+
81
93
  const pyResult = await this.bridge.executePython(pythonCode);
82
-
83
- // Parse the Python response
84
- let responseStr = '';
85
- if (pyResult?.LogOutput && Array.isArray(pyResult.LogOutput)) {
86
- responseStr = pyResult.LogOutput.map((log: any) => log.Output || '').join('');
87
- } else if (typeof pyResult === 'string') {
88
- responseStr = pyResult;
89
- } else {
90
- responseStr = JSON.stringify(pyResult);
94
+ const interpreted = interpretStandardResult(pyResult, {
95
+ successMessage: `Material ${name} processed`,
96
+ failureMessage: 'Failed to create material'
97
+ });
98
+
99
+ if (interpreted.success) {
100
+ const exists = coerceBoolean(interpreted.payload.exists, false) === true;
101
+ const created = coerceBoolean(interpreted.payload.created, false) === true;
102
+ if (exists) {
103
+ return { success: true, path: materialPath, message: `Material ${name} already exists at ${materialPath}` };
104
+ }
105
+ if (created) {
106
+ return { success: true, path: materialPath, message: `Material ${name} created at ${materialPath}` };
107
+ }
108
+ return { success: true, path: materialPath, message: interpreted.message };
91
109
  }
92
-
93
- // Try to extract JSON response
94
- try {
95
- // Look for JSON in the output
96
- const jsonMatch = responseStr.match(/\{.*\}/s);
97
- if (jsonMatch) {
98
- const result = JSON.parse(jsonMatch[0]);
99
- if (result.success) {
100
- if (result.exists) {
101
- return { success: true, path: materialPath, message: `Material ${name} already exists at ${path}` };
102
- } else if (result.created) {
103
- return { success: true, path: materialPath, message: `Material ${name} created at ${path}` };
104
- }
105
- } else {
106
- return { success: false, error: result.error || 'Failed to create material' };
107
- }
110
+
111
+ if (interpreted.error) {
112
+ const exists = await this.assetExists(materialPath);
113
+ if (exists) {
114
+ return {
115
+ success: true,
116
+ path: materialPath,
117
+ message: `Material ${name} created at ${materialPath}`,
118
+ warnings: interpreted.warnings,
119
+ details: interpreted.details
120
+ };
108
121
  }
109
- } catch {
110
- // JSON parsing failed, fall back to verification
122
+ return {
123
+ success: false,
124
+ error: interpreted.error,
125
+ warnings: interpreted.warnings,
126
+ details: interpreted.details
127
+ };
111
128
  }
112
129
 
113
- // Fallback: Verify creation using EditorAssetLibrary
114
- let verify: any = {};
115
- try {
116
- verify = await this.bridge.call({
117
- objectPath: '/Script/EditorScriptingUtilities.Default__EditorAssetLibrary',
118
- functionName: 'DoesAssetExist',
119
- parameters: {
120
- AssetPath: materialPath
121
- }
122
- });
123
- } catch {}
124
-
125
- const exists = verify?.ReturnValue === true || verify?.Result === true;
130
+ const exists = await this.assetExists(materialPath);
126
131
  if (exists) {
127
- return { success: true, path: materialPath, message: `Material ${name} created at ${path}` };
128
- } else {
129
- return { success: false, error: 'Material creation may have failed. Check Output Log for details.', debug: responseStr };
132
+ return {
133
+ success: true,
134
+ path: materialPath,
135
+ message: `Material ${name} created at ${materialPath}`,
136
+ warnings: interpreted.warnings,
137
+ details: interpreted.details
138
+ };
130
139
  }
140
+
141
+ return {
142
+ success: false,
143
+ error: interpreted.message,
144
+ warnings: interpreted.warnings,
145
+ details: interpreted.details
146
+ };
131
147
  } catch (err) {
132
148
  return { success: false, error: `Failed to create material: ${err}` };
133
149
  }
@@ -145,4 +161,19 @@ except Exception as e:
145
161
  return { success: false, error: `Failed to apply material: ${err}` };
146
162
  }
147
163
  }
164
+
165
+ private async assetExists(assetPath: string): Promise<boolean> {
166
+ try {
167
+ const response = await this.bridge.call({
168
+ objectPath: '/Script/EditorScriptingUtilities.Default__EditorAssetLibrary',
169
+ functionName: 'DoesAssetExist',
170
+ parameters: {
171
+ AssetPath: assetPath
172
+ }
173
+ });
174
+ return coerceBoolean(response?.ReturnValue ?? response?.Result ?? response, false) === true;
175
+ } catch {
176
+ return false;
177
+ }
178
+ }
148
179
  }
@@ -1,5 +1,6 @@
1
1
  import { UnrealBridge } from '../unreal-bridge.js';
2
2
  import { sanitizeAssetName, validateAssetParams } from '../utils/validation.js';
3
+ import { interpretStandardResult, coerceString } from '../utils/result-helpers.js';
3
4
 
4
5
  export class NiagaraTools {
5
6
  constructor(private bridge: UnrealBridge) {}
@@ -21,54 +22,50 @@ export class NiagaraTools {
21
22
  }) {
22
23
  try {
23
24
  const path = params.savePath || '/Game/Effects/Niagara';
24
- // const fullPath = `${path}/${params.name}`; // Currently unused
25
- const python = `
25
+ const python = `
26
26
  import unreal
27
+ import json
28
+
27
29
  path = r"${path}"
28
30
  name = r"${params.name}"
29
31
  full_path = f"{path}/{name}"
30
- # If already exists, just report success
32
+
31
33
  if unreal.EditorAssetLibrary.does_asset_exist(full_path):
32
- print("RESULT:{'success': True, 'path': '" + full_path + "', 'existing': True}")
34
+ print('RESULT:' + json.dumps({'success': True, 'path': full_path, 'existing': True}))
33
35
  else:
34
36
  asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
35
37
  factory = None
36
38
  try:
37
39
  factory = unreal.NiagaraSystemFactoryNew()
38
- except Exception as e:
40
+ except Exception:
39
41
  factory = None
42
+
40
43
  if factory is None:
41
- print("RESULT:{'success': False, 'error': 'NiagaraSystemFactoryNew unavailable'}")
44
+ print('RESULT:' + json.dumps({'success': False, 'error': 'NiagaraSystemFactoryNew unavailable'}))
42
45
  else:
43
46
  asset = asset_tools.create_asset(asset_name=name, package_path=path, asset_class=unreal.NiagaraSystem, factory=factory)
44
47
  if asset:
45
48
  unreal.EditorAssetLibrary.save_asset(full_path)
46
- print("RESULT:{'success': True, 'path': '" + full_path + "'}")
49
+ print('RESULT:' + json.dumps({'success': True, 'path': full_path}))
47
50
  else:
48
- print("RESULT:{'success': False, 'error': 'AssetTools create_asset failed'}")
51
+ print('RESULT:' + json.dumps({'success': False, 'error': 'AssetTools create_asset failed'}))
49
52
  `.trim();
50
53
  const resp = await this.bridge.executePython(python);
51
- let output = '';
52
- if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
53
- output = resp.LogOutput.map((l: any) => l.Output || '').join('');
54
- } else if (typeof resp === 'string') {
55
- output = resp;
56
- } else {
57
- output = JSON.stringify(resp);
58
- }
59
- const m = output.match(/RESULT:({.*})/);
60
- if (m) {
61
- try {
62
- const parsed = JSON.parse(m[1].replace(/'/g, '"'));
63
- if (parsed.success) {
64
- return { success: true, path: parsed.path, message: `Niagara system ${params.name} created` };
65
- }
66
- return { success: false, error: parsed.error || 'Unknown error creating Niagara system' };
67
- } catch {
68
- // fallthrough
69
- }
54
+ const interpreted = interpretStandardResult(resp, {
55
+ successMessage: `Niagara system ${params.name} created`,
56
+ failureMessage: `Failed to create Niagara system ${params.name}`
57
+ });
58
+
59
+ if (!interpreted.success) {
60
+ return { success: false, error: interpreted.error ?? interpreted.message };
70
61
  }
71
- return { success: false, error: 'No RESULT from Python when creating Niagara system' };
62
+
63
+ const pathFromPayload = coerceString(interpreted.payload.path) ?? `${path}/${params.name}`;
64
+ return {
65
+ success: true,
66
+ path: pathFromPayload,
67
+ message: interpreted.message
68
+ };
72
69
  } catch (err) {
73
70
  return { success: false, error: `Failed to create Niagara system: ${err}` };
74
71
  }
@@ -122,9 +119,7 @@ else:
122
119
  commands.push(`SetEmitterMesh ${params.systemName} ${params.emitterName} ${props.mesh}`);
123
120
  }
124
121
  }
125
- for (const cmd of commands) {
126
- await this.bridge.executeConsoleCommand(cmd);
127
- }
122
+ await this.bridge.executeConsoleCommands(commands);
128
123
  return { success: true, message: `Emitter ${params.emitterName} added to ${params.systemName}` };
129
124
  } catch (err) {
130
125
  return { success: false, error: `Failed to add emitter: ${err}` };
@@ -192,21 +187,24 @@ else:
192
187
  // Verify existence via Python to avoid RC EditorAssetLibrary issues
193
188
  const verifyPy = `
194
189
  import unreal
190
+ import json
191
+
195
192
  p = r"${fullPath}"
196
- print("RESULT:{'success': True, 'exists': %s}" % ('True' if unreal.EditorAssetLibrary.does_asset_exist(p) else 'False'))
193
+ exists = bool(unreal.EditorAssetLibrary.does_asset_exist(p))
194
+ print('RESULT:' + json.dumps({'success': True, 'exists': exists}))
197
195
  `.trim();
198
196
  const verifyResp = await this.bridge.executePython(verifyPy);
199
- let vout = '';
200
- if (verifyResp?.LogOutput && Array.isArray(verifyResp.LogOutput)) vout = verifyResp.LogOutput.map((l: any) => l.Output || '').join('');
201
- else if (typeof verifyResp === 'string') vout = verifyResp; else vout = JSON.stringify(verifyResp);
202
- const m = vout.match(/RESULT:({.*})/);
203
- if (m) {
204
- try {
205
- const parsed = JSON.parse(m[1].replace(/'/g, '"'));
206
- if (!parsed.exists) {
207
- return { success: false, error: `Asset not found after creation: ${fullPath}` };
208
- }
209
- } catch {}
197
+ const verifyResult = interpretStandardResult(verifyResp, {
198
+ successMessage: 'Niagara asset verification complete',
199
+ failureMessage: `Failed to verify Niagara asset at ${fullPath}`
200
+ });
201
+
202
+ if (!verifyResult.success) {
203
+ return { success: false, error: verifyResult.error ?? verifyResult.message };
204
+ }
205
+
206
+ if (verifyResult.payload.exists === false) {
207
+ return { success: false, error: `Asset not found after creation: ${fullPath}` };
210
208
  }
211
209
 
212
210
  return { success: true, message: `${params.effectType} effect ${safeName} created`, path: fullPath };
@@ -237,7 +235,7 @@ print("RESULT:{'success': True, 'exists': %s}" % ('True' if unreal.EditorAssetLi
237
235
  if (s.gridResolution) { const r = s.gridResolution; commands.push(`SetGPUGridResolution ${params.name} ${r[0]} ${r[1]} ${r[2]}`); }
238
236
  if (s.iterations !== undefined) commands.push(`SetGPUIterations ${params.name} ${s.iterations}`);
239
237
  }
240
- for (const cmd of commands) await this.bridge.executeConsoleCommand(cmd);
238
+ await this.bridge.executeConsoleCommands(commands);
241
239
  return { success: true, message: `GPU simulation ${params.name} created`, path: `${path}/${params.name}` };
242
240
  } catch (err) {
243
241
  return { success: false, error: `Failed to create GPU simulation: ${err}` };
@@ -259,54 +257,62 @@ print("RESULT:{'success': True, 'exists': %s}" % ('True' if unreal.EditorAssetLi
259
257
  const loc = Array.isArray(params.location) ? { x: params.location[0], y: params.location[1], z: params.location[2] } : params.location;
260
258
  const rot = params.rotation || [0, 0, 0];
261
259
  const scl = Array.isArray(params.scale) ? params.scale : (typeof params.scale === 'number' ? [params.scale, params.scale, params.scale] : [1, 1, 1]);
262
- const py = `
260
+ const py = `
263
261
  import unreal
262
+ import json
263
+
264
264
  loc = unreal.Vector(${loc.x || 0}, ${loc.y || 0}, ${loc.z || 0})
265
265
  rot = unreal.Rotator(${rot[0]}, ${rot[1]}, ${rot[2]})
266
266
  scale = unreal.Vector(${scl[0]}, ${scl[1]}, ${scl[2]})
267
267
  sys_path = r"${params.systemPath}"
268
+
268
269
  if unreal.EditorAssetLibrary.does_asset_exist(sys_path):
269
- sys = unreal.EditorAssetLibrary.load_asset(sys_path)
270
- actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
271
- actor = actor_subsystem.spawn_actor_from_class(unreal.NiagaraActor, loc, rot)
272
- if actor:
273
- comp = actor.get_niagara_component()
274
- try:
275
- comp.set_asset(sys)
276
- except Exception:
277
- try:
278
- comp.set_editor_property('asset', sys)
279
- except Exception:
280
- pass
281
- comp.set_world_scale3d(scale)
282
- comp.activate(True)
283
- actor.set_actor_label(f"Niagara_{unreal.SystemLibrary.get_game_time_in_seconds(actor.get_world()):.0f}")
284
- print("RESULT:{'success': True, 'actor': '" + actor.get_actor_label() + "'}")
285
- else:
286
- print("RESULT:{'success': False, 'error': 'Failed to spawn NiagaraActor'}")
270
+ sys = unreal.EditorAssetLibrary.load_asset(sys_path)
271
+ actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
272
+ actor = actor_subsystem.spawn_actor_from_class(unreal.NiagaraActor, loc, rot)
273
+ if actor:
274
+ comp = actor.get_niagara_component()
275
+ try:
276
+ comp.set_asset(sys)
277
+ except Exception:
278
+ try:
279
+ comp.set_editor_property('asset', sys)
280
+ except Exception:
281
+ pass
282
+ comp.set_world_scale3d(scale)
283
+ comp.activate(True)
284
+ actor.set_actor_label(f"Niagara_{unreal.SystemLibrary.get_game_time_in_seconds(actor.get_world()):.0f}")
285
+ print('RESULT:' + json.dumps({'success': True, 'actor': actor.get_actor_label()}))
286
+ else:
287
+ print('RESULT:' + json.dumps({'success': False, 'error': 'Failed to spawn NiagaraActor'}))
287
288
  else:
288
- print("RESULT:{'success': False, 'error': 'System asset not found'}")
289
+ print('RESULT:' + json.dumps({'success': False, 'error': 'System asset not found'}))
289
290
  `.trim();
290
- const resp = await this.bridge.executePython(py);
291
- let output = '';
292
- if (resp?.LogOutput && Array.isArray(resp.LogOutput)) output = resp.LogOutput.map((l: any) => l.Output || '').join('');
293
- else if (typeof resp === 'string') output = resp; else output = JSON.stringify(resp);
294
- const m = output.match(/RESULT:({.*})/);
295
- if (m) {
296
- try { const parsed = JSON.parse(m[1].replace(/'/g, '"')); return parsed.success ? { success: true, message: 'Niagara effect spawned' } : { success: false, error: parsed.error || 'Spawn failed' }; } catch {}
297
- }
298
- return { success: true, message: 'Niagara effect spawn attempted' };
291
+ const resp = await this.bridge.executePython(py);
292
+ const interpreted = interpretStandardResult(resp, {
293
+ successMessage: 'Niagara effect spawned',
294
+ failureMessage: 'Failed to spawn Niagara effect'
295
+ });
296
+
297
+ const actorLabel = coerceString(interpreted.payload.actor);
298
+
299
+ if (!interpreted.success) {
300
+ return { success: false, error: interpreted.error ?? interpreted.message };
301
+ }
302
+
303
+ const outcome: { success: true; message: string; actor?: string } = {
304
+ success: true,
305
+ message: interpreted.message
306
+ };
307
+
308
+ if (actorLabel) {
309
+ outcome.actor = actorLabel;
310
+ }
311
+
312
+ return outcome;
299
313
  } catch (err) {
300
314
  return { success: false, error: `Failed to spawn effect: ${err}` };
301
315
  }
302
316
  }
303
317
 
304
- private async _executeCommand(command: string) {
305
- return this.bridge.httpCall('/remote/object/call', 'PUT', {
306
- objectPath: '/Script/Engine.Default__KismetSystemLibrary',
307
- functionName: 'ExecuteConsoleCommand',
308
- parameters: { WorldContextObject: null, Command: command, SpecificPlayer: null },
309
- generateTransaction: false
310
- });
311
- }
312
318
  }