unreal-engine-mcp-server 0.2.1

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 (155) hide show
  1. package/.dockerignore +57 -0
  2. package/.env.production +25 -0
  3. package/.eslintrc.json +54 -0
  4. package/.github/workflows/publish-mcp.yml +75 -0
  5. package/Dockerfile +54 -0
  6. package/LICENSE +21 -0
  7. package/Public/icon.png +0 -0
  8. package/README.md +209 -0
  9. package/claude_desktop_config_example.json +13 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.js +7 -0
  12. package/dist/index.d.ts +31 -0
  13. package/dist/index.js +484 -0
  14. package/dist/prompts/index.d.ts +14 -0
  15. package/dist/prompts/index.js +38 -0
  16. package/dist/python-utils.d.ts +29 -0
  17. package/dist/python-utils.js +54 -0
  18. package/dist/resources/actors.d.ts +13 -0
  19. package/dist/resources/actors.js +83 -0
  20. package/dist/resources/assets.d.ts +23 -0
  21. package/dist/resources/assets.js +245 -0
  22. package/dist/resources/levels.d.ts +17 -0
  23. package/dist/resources/levels.js +94 -0
  24. package/dist/tools/actors.d.ts +51 -0
  25. package/dist/tools/actors.js +459 -0
  26. package/dist/tools/animation.d.ts +196 -0
  27. package/dist/tools/animation.js +579 -0
  28. package/dist/tools/assets.d.ts +21 -0
  29. package/dist/tools/assets.js +304 -0
  30. package/dist/tools/audio.d.ts +170 -0
  31. package/dist/tools/audio.js +416 -0
  32. package/dist/tools/blueprint.d.ts +144 -0
  33. package/dist/tools/blueprint.js +652 -0
  34. package/dist/tools/build_environment_advanced.d.ts +66 -0
  35. package/dist/tools/build_environment_advanced.js +484 -0
  36. package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
  37. package/dist/tools/consolidated-tool-definitions.js +607 -0
  38. package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
  39. package/dist/tools/consolidated-tool-handlers.js +1050 -0
  40. package/dist/tools/debug.d.ts +185 -0
  41. package/dist/tools/debug.js +265 -0
  42. package/dist/tools/editor.d.ts +88 -0
  43. package/dist/tools/editor.js +365 -0
  44. package/dist/tools/engine.d.ts +30 -0
  45. package/dist/tools/engine.js +36 -0
  46. package/dist/tools/foliage.d.ts +155 -0
  47. package/dist/tools/foliage.js +525 -0
  48. package/dist/tools/introspection.d.ts +98 -0
  49. package/dist/tools/introspection.js +683 -0
  50. package/dist/tools/landscape.d.ts +158 -0
  51. package/dist/tools/landscape.js +375 -0
  52. package/dist/tools/level.d.ts +110 -0
  53. package/dist/tools/level.js +362 -0
  54. package/dist/tools/lighting.d.ts +159 -0
  55. package/dist/tools/lighting.js +1179 -0
  56. package/dist/tools/materials.d.ts +34 -0
  57. package/dist/tools/materials.js +146 -0
  58. package/dist/tools/niagara.d.ts +145 -0
  59. package/dist/tools/niagara.js +289 -0
  60. package/dist/tools/performance.d.ts +163 -0
  61. package/dist/tools/performance.js +412 -0
  62. package/dist/tools/physics.d.ts +189 -0
  63. package/dist/tools/physics.js +784 -0
  64. package/dist/tools/rc.d.ts +110 -0
  65. package/dist/tools/rc.js +363 -0
  66. package/dist/tools/sequence.d.ts +112 -0
  67. package/dist/tools/sequence.js +675 -0
  68. package/dist/tools/tool-definitions.d.ts +4919 -0
  69. package/dist/tools/tool-definitions.js +891 -0
  70. package/dist/tools/tool-handlers.d.ts +47 -0
  71. package/dist/tools/tool-handlers.js +830 -0
  72. package/dist/tools/ui.d.ts +171 -0
  73. package/dist/tools/ui.js +337 -0
  74. package/dist/tools/visual.d.ts +29 -0
  75. package/dist/tools/visual.js +67 -0
  76. package/dist/types/env.d.ts +10 -0
  77. package/dist/types/env.js +18 -0
  78. package/dist/types/index.d.ts +323 -0
  79. package/dist/types/index.js +28 -0
  80. package/dist/types/tool-types.d.ts +274 -0
  81. package/dist/types/tool-types.js +13 -0
  82. package/dist/unreal-bridge.d.ts +126 -0
  83. package/dist/unreal-bridge.js +992 -0
  84. package/dist/utils/cache-manager.d.ts +64 -0
  85. package/dist/utils/cache-manager.js +176 -0
  86. package/dist/utils/error-handler.d.ts +66 -0
  87. package/dist/utils/error-handler.js +243 -0
  88. package/dist/utils/errors.d.ts +133 -0
  89. package/dist/utils/errors.js +256 -0
  90. package/dist/utils/http.d.ts +26 -0
  91. package/dist/utils/http.js +135 -0
  92. package/dist/utils/logger.d.ts +12 -0
  93. package/dist/utils/logger.js +32 -0
  94. package/dist/utils/normalize.d.ts +17 -0
  95. package/dist/utils/normalize.js +49 -0
  96. package/dist/utils/response-validator.d.ts +34 -0
  97. package/dist/utils/response-validator.js +121 -0
  98. package/dist/utils/safe-json.d.ts +4 -0
  99. package/dist/utils/safe-json.js +97 -0
  100. package/dist/utils/stdio-redirect.d.ts +2 -0
  101. package/dist/utils/stdio-redirect.js +20 -0
  102. package/dist/utils/validation.d.ts +50 -0
  103. package/dist/utils/validation.js +173 -0
  104. package/mcp-config-example.json +14 -0
  105. package/package.json +63 -0
  106. package/server.json +60 -0
  107. package/src/cli.ts +7 -0
  108. package/src/index.ts +543 -0
  109. package/src/prompts/index.ts +51 -0
  110. package/src/python/editor_compat.py +181 -0
  111. package/src/python-utils.ts +57 -0
  112. package/src/resources/actors.ts +92 -0
  113. package/src/resources/assets.ts +251 -0
  114. package/src/resources/levels.ts +83 -0
  115. package/src/tools/actors.ts +480 -0
  116. package/src/tools/animation.ts +713 -0
  117. package/src/tools/assets.ts +305 -0
  118. package/src/tools/audio.ts +548 -0
  119. package/src/tools/blueprint.ts +736 -0
  120. package/src/tools/build_environment_advanced.ts +526 -0
  121. package/src/tools/consolidated-tool-definitions.ts +619 -0
  122. package/src/tools/consolidated-tool-handlers.ts +1093 -0
  123. package/src/tools/debug.ts +368 -0
  124. package/src/tools/editor.ts +360 -0
  125. package/src/tools/engine.ts +32 -0
  126. package/src/tools/foliage.ts +652 -0
  127. package/src/tools/introspection.ts +778 -0
  128. package/src/tools/landscape.ts +523 -0
  129. package/src/tools/level.ts +410 -0
  130. package/src/tools/lighting.ts +1316 -0
  131. package/src/tools/materials.ts +148 -0
  132. package/src/tools/niagara.ts +312 -0
  133. package/src/tools/performance.ts +549 -0
  134. package/src/tools/physics.ts +924 -0
  135. package/src/tools/rc.ts +437 -0
  136. package/src/tools/sequence.ts +791 -0
  137. package/src/tools/tool-definitions.ts +907 -0
  138. package/src/tools/tool-handlers.ts +941 -0
  139. package/src/tools/ui.ts +499 -0
  140. package/src/tools/visual.ts +60 -0
  141. package/src/types/env.ts +27 -0
  142. package/src/types/index.ts +414 -0
  143. package/src/types/tool-types.ts +343 -0
  144. package/src/unreal-bridge.ts +1118 -0
  145. package/src/utils/cache-manager.ts +213 -0
  146. package/src/utils/error-handler.ts +320 -0
  147. package/src/utils/errors.ts +312 -0
  148. package/src/utils/http.ts +184 -0
  149. package/src/utils/logger.ts +30 -0
  150. package/src/utils/normalize.ts +54 -0
  151. package/src/utils/response-validator.ts +145 -0
  152. package/src/utils/safe-json.ts +112 -0
  153. package/src/utils/stdio-redirect.ts +18 -0
  154. package/src/utils/validation.ts +212 -0
  155. package/tsconfig.json +33 -0
@@ -0,0 +1,148 @@
1
+ import { UnrealBridge } from '../unreal-bridge.js';
2
+
3
+ export class MaterialTools {
4
+ constructor(private bridge: UnrealBridge) {}
5
+
6
+ async createMaterial(name: string, path: string) {
7
+ try {
8
+ // Validate name
9
+ if (!name || name.trim() === '') {
10
+ return { success: false, error: 'Material name cannot be empty' };
11
+ }
12
+
13
+ // Check name length (Unreal has 260 char path limit)
14
+ if (name.length > 100) {
15
+ return { success: false, error: `Material name too long (${name.length} chars). Maximum is 100 characters.` };
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.
21
+ const invalidChars = /[\s./<>|{}[\]()@#\\]/;
22
+ if (invalidChars.test(name)) {
23
+ const foundChars = name.match(invalidChars);
24
+ return { success: false, error: `Material name contains invalid characters: '${foundChars?.[0]}'. Avoid spaces, dots, slashes, backslashes, brackets, and special symbols.` };
25
+ }
26
+
27
+ // Validate path type
28
+ if (typeof path !== 'string') {
29
+ return { success: false, error: `Invalid path type: expected string, got ${typeof path}` };
30
+ }
31
+
32
+ // Clean up path - remove trailing slashes
33
+ const cleanPath = path.replace(/\/$/, '');
34
+
35
+ // Validate path starts with /Game or /Engine
36
+ if (!cleanPath.startsWith('/Game') && !cleanPath.startsWith('/Engine')) {
37
+ return { success: false, error: `Invalid path: must start with /Game or /Engine, got ${cleanPath}` };
38
+ }
39
+
40
+ // Use Python API to create material
41
+ const materialPath = `${cleanPath}/${name}`;
42
+ // Use the correct Unreal Engine 5 Python API
43
+ const pythonCode = `
44
+ import unreal
45
+ import json
46
+
47
+ try:
48
+ # Check if material already exists
49
+ material_path = '${materialPath}'
50
+ if unreal.EditorAssetLibrary.does_asset_exist(material_path):
51
+ print(json.dumps({"success": True, "exists": True, "path": material_path}))
52
+ else:
53
+ # Get the AssetTools
54
+ asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
55
+
56
+ # Create a MaterialFactoryNew
57
+ 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
+ asset = asset_tools.create_asset(
65
+ asset_name='${name}',
66
+ package_path=clean_path,
67
+ asset_class=unreal.Material,
68
+ factory=factory
69
+ )
70
+
71
+ if asset:
72
+ # Save the package
73
+ unreal.EditorAssetLibrary.save_asset(material_path)
74
+ print(json.dumps({"success": True, "created": True, "path": material_path}))
75
+ 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)}))
79
+ `.trim();
80
+
81
+ 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);
91
+ }
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
+ }
108
+ }
109
+ } catch {
110
+ // JSON parsing failed, fall back to verification
111
+ }
112
+
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;
126
+ 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 };
130
+ }
131
+ } catch (err) {
132
+ return { success: false, error: `Failed to create material: ${err}` };
133
+ }
134
+ }
135
+
136
+ async applyMaterialToActor(actorPath: string, materialPath: string, slotIndex = 0) {
137
+ try {
138
+ await this.bridge.httpCall('/remote/object/property', 'PUT', {
139
+ objectPath: actorPath,
140
+ propertyName: `StaticMeshComponent.Materials[${slotIndex}]`,
141
+ propertyValue: materialPath
142
+ });
143
+ return { success: true, message: 'Material applied' };
144
+ } catch (err) {
145
+ return { success: false, error: `Failed to apply material: ${err}` };
146
+ }
147
+ }
148
+ }
@@ -0,0 +1,312 @@
1
+ import { UnrealBridge } from '../unreal-bridge.js';
2
+ import { sanitizeAssetName, validateAssetParams } from '../utils/validation.js';
3
+
4
+ export class NiagaraTools {
5
+ constructor(private bridge: UnrealBridge) {}
6
+
7
+ /**
8
+ * Create Niagara System (real asset via Python)
9
+ */
10
+ async createSystem(params: {
11
+ name: string;
12
+ savePath?: string;
13
+ template?: 'Empty' | 'Fountain' | 'Ambient' | 'Projectile' | 'Custom';
14
+ emitters?: Array<{
15
+ name: string;
16
+ spawnRate?: number;
17
+ lifetime?: number;
18
+ shape?: 'Point' | 'Sphere' | 'Box' | 'Cylinder' | 'Cone';
19
+ shapeSize?: [number, number, number];
20
+ }>;
21
+ }) {
22
+ try {
23
+ const path = params.savePath || '/Game/Effects/Niagara';
24
+ // const fullPath = `${path}/${params.name}`; // Currently unused
25
+ const python = `
26
+ import unreal
27
+ path = r"${path}"
28
+ name = r"${params.name}"
29
+ full_path = f"{path}/{name}"
30
+ # If already exists, just report success
31
+ if unreal.EditorAssetLibrary.does_asset_exist(full_path):
32
+ print("RESULT:{'success': True, 'path': '" + full_path + "', 'existing': True}")
33
+ else:
34
+ asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
35
+ factory = None
36
+ try:
37
+ factory = unreal.NiagaraSystemFactoryNew()
38
+ except Exception as e:
39
+ factory = None
40
+ if factory is None:
41
+ print("RESULT:{'success': False, 'error': 'NiagaraSystemFactoryNew unavailable'}")
42
+ else:
43
+ asset = asset_tools.create_asset(asset_name=name, package_path=path, asset_class=unreal.NiagaraSystem, factory=factory)
44
+ if asset:
45
+ unreal.EditorAssetLibrary.save_asset(full_path)
46
+ print("RESULT:{'success': True, 'path': '" + full_path + "'}")
47
+ else:
48
+ print("RESULT:{'success': False, 'error': 'AssetTools create_asset failed'}")
49
+ `.trim();
50
+ 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
+ }
70
+ }
71
+ return { success: false, error: 'No RESULT from Python when creating Niagara system' };
72
+ } catch (err) {
73
+ return { success: false, error: `Failed to create Niagara system: ${err}` };
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Add Emitter to System (left as-is; console commands may be placeholders)
79
+ */
80
+ async addEmitter(params: {
81
+ systemName: string;
82
+ emitterName: string;
83
+ emitterType: 'Sprite' | 'Mesh' | 'Ribbon' | 'Beam' | 'GPU';
84
+ properties?: {
85
+ spawnRate?: number;
86
+ lifetime?: number;
87
+ velocityMin?: [number, number, number];
88
+ velocityMax?: [number, number, number];
89
+ size?: number;
90
+ color?: [number, number, number, number];
91
+ material?: string;
92
+ mesh?: string;
93
+ };
94
+ }) {
95
+ try {
96
+ const commands = [
97
+ `AddNiagaraEmitter ${params.systemName} ${params.emitterName} ${params.emitterType}`
98
+ ];
99
+ if (params.properties) {
100
+ const props = params.properties;
101
+ if (props.spawnRate !== undefined) {
102
+ commands.push(`SetEmitterSpawnRate ${params.systemName} ${params.emitterName} ${props.spawnRate}`);
103
+ }
104
+ if (props.lifetime !== undefined) {
105
+ commands.push(`SetEmitterLifetime ${params.systemName} ${params.emitterName} ${props.lifetime}`);
106
+ }
107
+ if (props.velocityMin && props.velocityMax) {
108
+ const min = props.velocityMin; const max = props.velocityMax;
109
+ commands.push(`SetEmitterVelocity ${params.systemName} ${params.emitterName} ${min[0]} ${min[1]} ${min[2]} ${max[0]} ${max[1]} ${max[2]}`);
110
+ }
111
+ if (props.size !== undefined) {
112
+ commands.push(`SetEmitterSize ${params.systemName} ${params.emitterName} ${props.size}`);
113
+ }
114
+ if (props.color) {
115
+ const color = props.color;
116
+ commands.push(`SetEmitterColor ${params.systemName} ${params.emitterName} ${color[0]} ${color[1]} ${color[2]} ${color[3]}`);
117
+ }
118
+ if (props.material) {
119
+ commands.push(`SetEmitterMaterial ${params.systemName} ${params.emitterName} ${props.material}`);
120
+ }
121
+ if (props.mesh && params.emitterType === 'Mesh') {
122
+ commands.push(`SetEmitterMesh ${params.systemName} ${params.emitterName} ${props.mesh}`);
123
+ }
124
+ }
125
+ for (const cmd of commands) {
126
+ await this.bridge.executeConsoleCommand(cmd);
127
+ }
128
+ return { success: true, message: `Emitter ${params.emitterName} added to ${params.systemName}` };
129
+ } catch (err) {
130
+ return { success: false, error: `Failed to add emitter: ${err}` };
131
+ }
132
+ }
133
+
134
+ async setParameter(params: {
135
+ systemName: string;
136
+ parameterName: string;
137
+ parameterType: 'Float' | 'Vector' | 'Color' | 'Bool' | 'Int';
138
+ value: any;
139
+ isUserParameter?: boolean;
140
+ }) {
141
+ try {
142
+ const paramType = params.isUserParameter ? 'User' : 'System';
143
+ let valueStr = '';
144
+ switch (params.parameterType) {
145
+ case 'Float': case 'Int': case 'Bool': valueStr = String(params.value); break;
146
+ case 'Vector': { const v = params.value as number[]; valueStr = `${v[0]} ${v[1]} ${v[2]}`; break; }
147
+ case 'Color': { const c = params.value as number[]; valueStr = `${c[0]} ${c[1]} ${c[2]} ${c[3] || 1}`; break; }
148
+ }
149
+ const command = `SetNiagara${paramType}Parameter ${params.systemName} ${params.parameterName} ${params.parameterType} ${valueStr}`;
150
+ await this.bridge.executeConsoleCommand(command);
151
+ return { success: true, message: `Parameter ${params.parameterName} set on ${params.systemName}` };
152
+ } catch (err) {
153
+ return { success: false, error: `Failed to set parameter: ${err}` };
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Create Preset Effect (now creates a real Niagara system asset)
159
+ */
160
+ async createEffect(params: {
161
+ effectType: 'Fire' | 'Smoke' | 'Explosion' | 'Water' | 'Rain' | 'Snow' | 'Magic' | 'Lightning' | 'Dust' | 'Steam';
162
+ name: string;
163
+ location: [number, number, number] | { x: number, y: number, z: number };
164
+ scale?: number;
165
+ intensity?: number;
166
+ customParameters?: { [key: string]: any };
167
+ }) {
168
+ try {
169
+ // Validate effect type at runtime (inputs can come from JSON)
170
+ const allowedTypes = ['Fire','Smoke','Explosion','Water','Rain','Snow','Magic','Lightning','Dust','Steam'];
171
+ if (!params || !allowedTypes.includes(String(params.effectType))) {
172
+ return { success: false, error: `Invalid effectType: ${String(params?.effectType)}` };
173
+ }
174
+
175
+ // Sanitize and validate name and path
176
+ const defaultPath = '/Game/Effects/Niagara';
177
+ const nameToUse = sanitizeAssetName(params.name);
178
+ const validation = validateAssetParams({ name: nameToUse, savePath: defaultPath });
179
+ if (!validation.valid) {
180
+ return { success: false, error: validation.error || 'Invalid asset parameters' };
181
+ }
182
+ const safeName = validation.sanitized.name;
183
+ const savePath = validation.sanitized.savePath || defaultPath;
184
+ const fullPath = `${savePath}/${safeName}`;
185
+
186
+ // Create or ensure the Niagara system asset exists
187
+ const createRes = await this.createSystem({ name: safeName, savePath, template: 'Empty' });
188
+ if (!createRes.success) {
189
+ return { success: false, error: createRes.error || 'Failed creating Niagara system' };
190
+ }
191
+
192
+ // Verify existence via Python to avoid RC EditorAssetLibrary issues
193
+ const verifyPy = `
194
+ import unreal
195
+ p = r"${fullPath}"
196
+ print("RESULT:{'success': True, 'exists': %s}" % ('True' if unreal.EditorAssetLibrary.does_asset_exist(p) else 'False'))
197
+ `.trim();
198
+ 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 {}
210
+ }
211
+
212
+ return { success: true, message: `${params.effectType} effect ${safeName} created`, path: fullPath };
213
+ } catch (err) {
214
+ return { success: false, error: `Failed to create effect: ${err}` };
215
+ }
216
+ }
217
+
218
+ async createGPUSimulation(params: {
219
+ name: string;
220
+ simulationType: 'Fluid' | 'Hair' | 'Cloth' | 'Debris' | 'Crowd';
221
+ particleCount: number;
222
+ savePath?: string;
223
+ gpuSettings?: {
224
+ computeShader?: string;
225
+ textureFormat?: 'RGBA8' | 'RGBA16F' | 'RGBA32F';
226
+ gridResolution?: [number, number, number];
227
+ iterations?: number;
228
+ };
229
+ }) {
230
+ try {
231
+ const path = params.savePath || '/Game/Effects/GPUSimulations';
232
+ const commands = [`CreateGPUSimulation ${params.name} ${params.simulationType} ${params.particleCount} ${path}`];
233
+ if (params.gpuSettings) {
234
+ const s = params.gpuSettings;
235
+ if (s.computeShader) commands.push(`SetGPUComputeShader ${params.name} ${s.computeShader}`);
236
+ if (s.textureFormat) commands.push(`SetGPUTextureFormat ${params.name} ${s.textureFormat}`);
237
+ if (s.gridResolution) { const r = s.gridResolution; commands.push(`SetGPUGridResolution ${params.name} ${r[0]} ${r[1]} ${r[2]}`); }
238
+ if (s.iterations !== undefined) commands.push(`SetGPUIterations ${params.name} ${s.iterations}`);
239
+ }
240
+ for (const cmd of commands) await this.bridge.executeConsoleCommand(cmd);
241
+ return { success: true, message: `GPU simulation ${params.name} created`, path: `${path}/${params.name}` };
242
+ } catch (err) {
243
+ return { success: false, error: `Failed to create GPU simulation: ${err}` };
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Spawn Niagara Effect in Level using Python (NiagaraActor)
249
+ */
250
+ async spawnEffect(params: {
251
+ systemPath: string;
252
+ location: [number, number, number] | { x: number, y: number, z: number };
253
+ rotation?: [number, number, number];
254
+ scale?: [number, number, number] | number;
255
+ autoDestroy?: boolean;
256
+ attachToActor?: string;
257
+ }) {
258
+ try {
259
+ const loc = Array.isArray(params.location) ? { x: params.location[0], y: params.location[1], z: params.location[2] } : params.location;
260
+ const rot = params.rotation || [0, 0, 0];
261
+ const scl = Array.isArray(params.scale) ? params.scale : (typeof params.scale === 'number' ? [params.scale, params.scale, params.scale] : [1, 1, 1]);
262
+ const py = `
263
+ import unreal
264
+ loc = unreal.Vector(${loc.x || 0}, ${loc.y || 0}, ${loc.z || 0})
265
+ rot = unreal.Rotator(${rot[0]}, ${rot[1]}, ${rot[2]})
266
+ scale = unreal.Vector(${scl[0]}, ${scl[1]}, ${scl[2]})
267
+ sys_path = r"${params.systemPath}"
268
+ 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'}")
287
+ else:
288
+ print("RESULT:{'success': False, 'error': 'System asset not found'}")
289
+ `.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' };
299
+ } catch (err) {
300
+ return { success: false, error: `Failed to spawn effect: ${err}` };
301
+ }
302
+ }
303
+
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
+ }