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