Skip to main content
Glama
niagara.ts12.6 kB
import { UnrealBridge } from '../unreal-bridge.js'; import { sanitizeAssetName, validateAssetParams } from '../utils/validation.js'; import { interpretStandardResult, coerceString } from '../utils/result-helpers.js'; export class NiagaraTools { constructor(private bridge: UnrealBridge) {} /** * Create Niagara System (real asset via Python) */ async createSystem(params: { name: string; savePath?: string; template?: 'Empty' | 'Fountain' | 'Ambient' | 'Projectile' | 'Custom'; emitters?: Array<{ name: string; spawnRate?: number; lifetime?: number; shape?: 'Point' | 'Sphere' | 'Box' | 'Cylinder' | 'Cone'; shapeSize?: [number, number, number]; }>; }) { try { const path = params.savePath || '/Game/Effects/Niagara'; const python = ` import unreal import json path = r"${path}" name = r"${params.name}" full_path = f"{path}/{name}" if unreal.EditorAssetLibrary.does_asset_exist(full_path): print('RESULT:' + json.dumps({'success': True, 'path': full_path, 'existing': True})) else: asset_tools = unreal.AssetToolsHelpers.get_asset_tools() factory = None try: factory = unreal.NiagaraSystemFactoryNew() except Exception: factory = None if factory is None: print('RESULT:' + json.dumps({'success': False, 'error': 'NiagaraSystemFactoryNew unavailable'})) else: asset = asset_tools.create_asset(asset_name=name, package_path=path, asset_class=unreal.NiagaraSystem, factory=factory) if asset: unreal.EditorAssetLibrary.save_asset(full_path) print('RESULT:' + json.dumps({'success': True, 'path': full_path})) else: print('RESULT:' + json.dumps({'success': False, 'error': 'AssetTools create_asset failed'})) `.trim(); const resp = await this.bridge.executePython(python); const interpreted = interpretStandardResult(resp, { successMessage: `Niagara system ${params.name} created`, failureMessage: `Failed to create Niagara system ${params.name}` }); if (!interpreted.success) { return { success: false, error: interpreted.error ?? interpreted.message }; } const pathFromPayload = coerceString(interpreted.payload.path) ?? `${path}/${params.name}`; return { success: true, path: pathFromPayload, message: interpreted.message }; } catch (err) { return { success: false, error: `Failed to create Niagara system: ${err}` }; } } /** * Add Emitter to System (left as-is; console commands may be placeholders) */ async addEmitter(params: { systemName: string; emitterName: string; emitterType: 'Sprite' | 'Mesh' | 'Ribbon' | 'Beam' | 'GPU'; properties?: { spawnRate?: number; lifetime?: number; velocityMin?: [number, number, number]; velocityMax?: [number, number, number]; size?: number; color?: [number, number, number, number]; material?: string; mesh?: string; }; }) { try { const commands = [ `AddNiagaraEmitter ${params.systemName} ${params.emitterName} ${params.emitterType}` ]; if (params.properties) { const props = params.properties; if (props.spawnRate !== undefined) { commands.push(`SetEmitterSpawnRate ${params.systemName} ${params.emitterName} ${props.spawnRate}`); } if (props.lifetime !== undefined) { commands.push(`SetEmitterLifetime ${params.systemName} ${params.emitterName} ${props.lifetime}`); } if (props.velocityMin && props.velocityMax) { const min = props.velocityMin; const max = props.velocityMax; commands.push(`SetEmitterVelocity ${params.systemName} ${params.emitterName} ${min[0]} ${min[1]} ${min[2]} ${max[0]} ${max[1]} ${max[2]}`); } if (props.size !== undefined) { commands.push(`SetEmitterSize ${params.systemName} ${params.emitterName} ${props.size}`); } if (props.color) { const color = props.color; commands.push(`SetEmitterColor ${params.systemName} ${params.emitterName} ${color[0]} ${color[1]} ${color[2]} ${color[3]}`); } if (props.material) { commands.push(`SetEmitterMaterial ${params.systemName} ${params.emitterName} ${props.material}`); } if (props.mesh && params.emitterType === 'Mesh') { commands.push(`SetEmitterMesh ${params.systemName} ${params.emitterName} ${props.mesh}`); } } await this.bridge.executeConsoleCommands(commands); return { success: true, message: `Emitter ${params.emitterName} added to ${params.systemName}` }; } catch (err) { return { success: false, error: `Failed to add emitter: ${err}` }; } } async setParameter(params: { systemName: string; parameterName: string; parameterType: 'Float' | 'Vector' | 'Color' | 'Bool' | 'Int'; value: any; isUserParameter?: boolean; }) { try { const paramType = params.isUserParameter ? 'User' : 'System'; let valueStr = ''; switch (params.parameterType) { case 'Float': case 'Int': case 'Bool': valueStr = String(params.value); break; case 'Vector': { const v = params.value as number[]; valueStr = `${v[0]} ${v[1]} ${v[2]}`; break; } case 'Color': { const c = params.value as number[]; valueStr = `${c[0]} ${c[1]} ${c[2]} ${c[3] || 1}`; break; } } const command = `SetNiagara${paramType}Parameter ${params.systemName} ${params.parameterName} ${params.parameterType} ${valueStr}`; await this.bridge.executeConsoleCommand(command); return { success: true, message: `Parameter ${params.parameterName} set on ${params.systemName}` }; } catch (err) { return { success: false, error: `Failed to set parameter: ${err}` }; } } /** * Create Preset Effect (now creates a real Niagara system asset) */ async createEffect(params: { effectType: 'Fire' | 'Smoke' | 'Explosion' | 'Water' | 'Rain' | 'Snow' | 'Magic' | 'Lightning' | 'Dust' | 'Steam'; name: string; location: [number, number, number] | { x: number, y: number, z: number }; scale?: number; intensity?: number; customParameters?: { [key: string]: any }; }) { try { // Validate effect type at runtime (inputs can come from JSON) const allowedTypes = ['Fire','Smoke','Explosion','Water','Rain','Snow','Magic','Lightning','Dust','Steam']; if (!params || !allowedTypes.includes(String(params.effectType))) { return { success: false, error: `Invalid effectType: ${String(params?.effectType)}` }; } // Sanitize and validate name and path const defaultPath = '/Game/Effects/Niagara'; const nameToUse = sanitizeAssetName(params.name); const validation = validateAssetParams({ name: nameToUse, savePath: defaultPath }); if (!validation.valid) { return { success: false, error: validation.error || 'Invalid asset parameters' }; } const safeName = validation.sanitized.name; const savePath = validation.sanitized.savePath || defaultPath; const fullPath = `${savePath}/${safeName}`; // Create or ensure the Niagara system asset exists const createRes = await this.createSystem({ name: safeName, savePath, template: 'Empty' }); if (!createRes.success) { return { success: false, error: createRes.error || 'Failed creating Niagara system' }; } // Verify existence via Python to avoid RC EditorAssetLibrary issues const verifyPy = ` import unreal import json p = r"${fullPath}" exists = bool(unreal.EditorAssetLibrary.does_asset_exist(p)) print('RESULT:' + json.dumps({'success': True, 'exists': exists})) `.trim(); const verifyResp = await this.bridge.executePython(verifyPy); const verifyResult = interpretStandardResult(verifyResp, { successMessage: 'Niagara asset verification complete', failureMessage: `Failed to verify Niagara asset at ${fullPath}` }); if (!verifyResult.success) { return { success: false, error: verifyResult.error ?? verifyResult.message }; } if (verifyResult.payload.exists === false) { return { success: false, error: `Asset not found after creation: ${fullPath}` }; } return { success: true, message: `${params.effectType} effect ${safeName} created`, path: fullPath }; } catch (err) { return { success: false, error: `Failed to create effect: ${err}` }; } } async createGPUSimulation(params: { name: string; simulationType: 'Fluid' | 'Hair' | 'Cloth' | 'Debris' | 'Crowd'; particleCount: number; savePath?: string; gpuSettings?: { computeShader?: string; textureFormat?: 'RGBA8' | 'RGBA16F' | 'RGBA32F'; gridResolution?: [number, number, number]; iterations?: number; }; }) { try { const path = params.savePath || '/Game/Effects/GPUSimulations'; const commands = [`CreateGPUSimulation ${params.name} ${params.simulationType} ${params.particleCount} ${path}`]; if (params.gpuSettings) { const s = params.gpuSettings; if (s.computeShader) commands.push(`SetGPUComputeShader ${params.name} ${s.computeShader}`); if (s.textureFormat) commands.push(`SetGPUTextureFormat ${params.name} ${s.textureFormat}`); if (s.gridResolution) { const r = s.gridResolution; commands.push(`SetGPUGridResolution ${params.name} ${r[0]} ${r[1]} ${r[2]}`); } if (s.iterations !== undefined) commands.push(`SetGPUIterations ${params.name} ${s.iterations}`); } await this.bridge.executeConsoleCommands(commands); return { success: true, message: `GPU simulation ${params.name} created`, path: `${path}/${params.name}` }; } catch (err) { return { success: false, error: `Failed to create GPU simulation: ${err}` }; } } /** * Spawn Niagara Effect in Level using Python (NiagaraActor) */ async spawnEffect(params: { systemPath: string; location: [number, number, number] | { x: number, y: number, z: number }; rotation?: [number, number, number]; scale?: [number, number, number] | number; autoDestroy?: boolean; attachToActor?: string; }) { try { const loc = Array.isArray(params.location) ? { x: params.location[0], y: params.location[1], z: params.location[2] } : params.location; const rot = params.rotation || [0, 0, 0]; const scl = Array.isArray(params.scale) ? params.scale : (typeof params.scale === 'number' ? [params.scale, params.scale, params.scale] : [1, 1, 1]); const py = ` import unreal import json loc = unreal.Vector(${loc.x || 0}, ${loc.y || 0}, ${loc.z || 0}) rot = unreal.Rotator(${rot[0]}, ${rot[1]}, ${rot[2]}) scale = unreal.Vector(${scl[0]}, ${scl[1]}, ${scl[2]}) sys_path = r"${params.systemPath}" if unreal.EditorAssetLibrary.does_asset_exist(sys_path): sys = unreal.EditorAssetLibrary.load_asset(sys_path) actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) actor = actor_subsystem.spawn_actor_from_class(unreal.NiagaraActor, loc, rot) if actor: comp = actor.get_niagara_component() try: comp.set_asset(sys) except Exception: try: comp.set_editor_property('asset', sys) except Exception: pass comp.set_world_scale3d(scale) comp.activate(True) actor.set_actor_label(f"Niagara_{unreal.SystemLibrary.get_game_time_in_seconds(actor.get_world()):.0f}") print('RESULT:' + json.dumps({'success': True, 'actor': actor.get_actor_label()})) else: print('RESULT:' + json.dumps({'success': False, 'error': 'Failed to spawn NiagaraActor'})) else: print('RESULT:' + json.dumps({'success': False, 'error': 'System asset not found'})) `.trim(); const resp = await this.bridge.executePython(py); const interpreted = interpretStandardResult(resp, { successMessage: 'Niagara effect spawned', failureMessage: 'Failed to spawn Niagara effect' }); const actorLabel = coerceString(interpreted.payload.actor); if (!interpreted.success) { return { success: false, error: interpreted.error ?? interpreted.message }; } const outcome: { success: true; message: string; actor?: string } = { success: true, message: interpreted.message }; if (actorLabel) { outcome.actor = actorLabel; } return outcome; } catch (err) { return { success: false, error: `Failed to spawn effect: ${err}` }; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ChiR24/Unreal_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server