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.
- package/.dockerignore +57 -0
- package/.env.production +25 -0
- package/.eslintrc.json +54 -0
- package/.github/workflows/publish-mcp.yml +75 -0
- package/Dockerfile +54 -0
- package/LICENSE +21 -0
- package/Public/icon.png +0 -0
- package/README.md +209 -0
- package/claude_desktop_config_example.json +13 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +7 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +484 -0
- package/dist/prompts/index.d.ts +14 -0
- package/dist/prompts/index.js +38 -0
- package/dist/python-utils.d.ts +29 -0
- package/dist/python-utils.js +54 -0
- package/dist/resources/actors.d.ts +13 -0
- package/dist/resources/actors.js +83 -0
- package/dist/resources/assets.d.ts +23 -0
- package/dist/resources/assets.js +245 -0
- package/dist/resources/levels.d.ts +17 -0
- package/dist/resources/levels.js +94 -0
- package/dist/tools/actors.d.ts +51 -0
- package/dist/tools/actors.js +459 -0
- package/dist/tools/animation.d.ts +196 -0
- package/dist/tools/animation.js +579 -0
- package/dist/tools/assets.d.ts +21 -0
- package/dist/tools/assets.js +304 -0
- package/dist/tools/audio.d.ts +170 -0
- package/dist/tools/audio.js +416 -0
- package/dist/tools/blueprint.d.ts +144 -0
- package/dist/tools/blueprint.js +652 -0
- package/dist/tools/build_environment_advanced.d.ts +66 -0
- package/dist/tools/build_environment_advanced.js +484 -0
- package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
- package/dist/tools/consolidated-tool-definitions.js +607 -0
- package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
- package/dist/tools/consolidated-tool-handlers.js +1050 -0
- package/dist/tools/debug.d.ts +185 -0
- package/dist/tools/debug.js +265 -0
- package/dist/tools/editor.d.ts +88 -0
- package/dist/tools/editor.js +365 -0
- package/dist/tools/engine.d.ts +30 -0
- package/dist/tools/engine.js +36 -0
- package/dist/tools/foliage.d.ts +155 -0
- package/dist/tools/foliage.js +525 -0
- package/dist/tools/introspection.d.ts +98 -0
- package/dist/tools/introspection.js +683 -0
- package/dist/tools/landscape.d.ts +158 -0
- package/dist/tools/landscape.js +375 -0
- package/dist/tools/level.d.ts +110 -0
- package/dist/tools/level.js +362 -0
- package/dist/tools/lighting.d.ts +159 -0
- package/dist/tools/lighting.js +1179 -0
- package/dist/tools/materials.d.ts +34 -0
- package/dist/tools/materials.js +146 -0
- package/dist/tools/niagara.d.ts +145 -0
- package/dist/tools/niagara.js +289 -0
- package/dist/tools/performance.d.ts +163 -0
- package/dist/tools/performance.js +412 -0
- package/dist/tools/physics.d.ts +189 -0
- package/dist/tools/physics.js +784 -0
- package/dist/tools/rc.d.ts +110 -0
- package/dist/tools/rc.js +363 -0
- package/dist/tools/sequence.d.ts +112 -0
- package/dist/tools/sequence.js +675 -0
- package/dist/tools/tool-definitions.d.ts +4919 -0
- package/dist/tools/tool-definitions.js +891 -0
- package/dist/tools/tool-handlers.d.ts +47 -0
- package/dist/tools/tool-handlers.js +830 -0
- package/dist/tools/ui.d.ts +171 -0
- package/dist/tools/ui.js +337 -0
- package/dist/tools/visual.d.ts +29 -0
- package/dist/tools/visual.js +67 -0
- package/dist/types/env.d.ts +10 -0
- package/dist/types/env.js +18 -0
- package/dist/types/index.d.ts +323 -0
- package/dist/types/index.js +28 -0
- package/dist/types/tool-types.d.ts +274 -0
- package/dist/types/tool-types.js +13 -0
- package/dist/unreal-bridge.d.ts +126 -0
- package/dist/unreal-bridge.js +992 -0
- package/dist/utils/cache-manager.d.ts +64 -0
- package/dist/utils/cache-manager.js +176 -0
- package/dist/utils/error-handler.d.ts +66 -0
- package/dist/utils/error-handler.js +243 -0
- package/dist/utils/errors.d.ts +133 -0
- package/dist/utils/errors.js +256 -0
- package/dist/utils/http.d.ts +26 -0
- package/dist/utils/http.js +135 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +32 -0
- package/dist/utils/normalize.d.ts +17 -0
- package/dist/utils/normalize.js +49 -0
- package/dist/utils/response-validator.d.ts +34 -0
- package/dist/utils/response-validator.js +121 -0
- package/dist/utils/safe-json.d.ts +4 -0
- package/dist/utils/safe-json.js +97 -0
- package/dist/utils/stdio-redirect.d.ts +2 -0
- package/dist/utils/stdio-redirect.js +20 -0
- package/dist/utils/validation.d.ts +50 -0
- package/dist/utils/validation.js +173 -0
- package/mcp-config-example.json +14 -0
- package/package.json +63 -0
- package/server.json +60 -0
- package/src/cli.ts +7 -0
- package/src/index.ts +543 -0
- package/src/prompts/index.ts +51 -0
- package/src/python/editor_compat.py +181 -0
- package/src/python-utils.ts +57 -0
- package/src/resources/actors.ts +92 -0
- package/src/resources/assets.ts +251 -0
- package/src/resources/levels.ts +83 -0
- package/src/tools/actors.ts +480 -0
- package/src/tools/animation.ts +713 -0
- package/src/tools/assets.ts +305 -0
- package/src/tools/audio.ts +548 -0
- package/src/tools/blueprint.ts +736 -0
- package/src/tools/build_environment_advanced.ts +526 -0
- package/src/tools/consolidated-tool-definitions.ts +619 -0
- package/src/tools/consolidated-tool-handlers.ts +1093 -0
- package/src/tools/debug.ts +368 -0
- package/src/tools/editor.ts +360 -0
- package/src/tools/engine.ts +32 -0
- package/src/tools/foliage.ts +652 -0
- package/src/tools/introspection.ts +778 -0
- package/src/tools/landscape.ts +523 -0
- package/src/tools/level.ts +410 -0
- package/src/tools/lighting.ts +1316 -0
- package/src/tools/materials.ts +148 -0
- package/src/tools/niagara.ts +312 -0
- package/src/tools/performance.ts +549 -0
- package/src/tools/physics.ts +924 -0
- package/src/tools/rc.ts +437 -0
- package/src/tools/sequence.ts +791 -0
- package/src/tools/tool-definitions.ts +907 -0
- package/src/tools/tool-handlers.ts +941 -0
- package/src/tools/ui.ts +499 -0
- package/src/tools/visual.ts +60 -0
- package/src/types/env.ts +27 -0
- package/src/types/index.ts +414 -0
- package/src/types/tool-types.ts +343 -0
- package/src/unreal-bridge.ts +1118 -0
- package/src/utils/cache-manager.ts +213 -0
- package/src/utils/error-handler.ts +320 -0
- package/src/utils/errors.ts +312 -0
- package/src/utils/http.ts +184 -0
- package/src/utils/logger.ts +30 -0
- package/src/utils/normalize.ts +54 -0
- package/src/utils/response-validator.ts +145 -0
- package/src/utils/safe-json.ts +112 -0
- package/src/utils/stdio-redirect.ts +18 -0
- package/src/utils/validation.ts +212 -0
- 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
|
+
}
|