Skip to main content
Glama
materials.ts6.43 kB
import { UnrealBridge } from '../unreal-bridge.js'; import { coerceBoolean, interpretStandardResult } from '../utils/result-helpers.js'; import { escapePythonString } from '../utils/python.js'; export class MaterialTools { constructor(private bridge: UnrealBridge) {} async createMaterial(name: string, path: string) { try { if (!name || name.trim() === '') { return { success: false, error: 'Material name cannot be empty' }; } if (name.length > 100) { return { success: false, error: `Material name too long (${name.length} chars). Maximum is 100 characters.` }; } const invalidChars = /[\s./<>|{}[\]()@#\\]/; if (invalidChars.test(name)) { const foundChars = name.match(invalidChars); return { success: false, error: `Material name contains invalid characters: '${foundChars?.[0]}'. Avoid spaces, dots, slashes, backslashes, brackets, and special symbols.` }; } if (typeof path !== 'string') { return { success: false, error: `Invalid path type: expected string, got ${typeof path}` }; } const trimmedPath = path.trim(); const effectivePath = trimmedPath.length === 0 ? '/Game' : trimmedPath; const cleanPath = effectivePath.replace(/\/$/, ''); if (!cleanPath.startsWith('/Game') && !cleanPath.startsWith('/Engine')) { return { success: false, error: `Invalid path: must start with /Game or /Engine, got ${cleanPath}` }; } const normalizedPath = cleanPath.toLowerCase(); const restrictedPrefixes = ['/engine/restricted', '/engine/generated', '/engine/transient']; if (restrictedPrefixes.some(prefix => normalizedPath.startsWith(prefix))) { const errorMessage = `Destination path is read-only and cannot be used for material creation: ${cleanPath}`; return { success: false, error: errorMessage, message: errorMessage }; } const materialPath = `${cleanPath}/${name}`; const payload = { name, cleanPath, materialPath }; const escapedName = escapePythonString(name); const pythonCode = ` import unreal, json payload = json.loads(r'''${JSON.stringify(payload)}''') result = { 'success': False, 'message': '', 'error': '', 'warnings': [], 'details': [], 'name': payload.get('name') or "${escapedName}", 'path': payload.get('materialPath') } material_path = result['path'] clean_path = payload.get('cleanPath') or '/Game' try: if unreal.EditorAssetLibrary.does_asset_exist(material_path): result['success'] = True result['exists'] = True result['message'] = f"Material already exists at {material_path}" else: asset_tools = unreal.AssetToolsHelpers.get_asset_tools() factory = unreal.MaterialFactoryNew() asset = asset_tools.create_asset( asset_name=payload.get('name'), package_path=clean_path, asset_class=unreal.Material, factory=factory ) if asset: unreal.EditorAssetLibrary.save_asset(material_path) result['success'] = True result['created'] = True result['message'] = f"Material created at {material_path}" else: result['error'] = 'Failed to create material' result['message'] = result['error'] except Exception as exc: result['error'] = str(exc) if not result['message']: result['message'] = result['error'] print('RESULT:' + json.dumps(result)) `.trim(); const pyResult = await this.bridge.executePython(pythonCode); const interpreted = interpretStandardResult(pyResult, { successMessage: `Material ${name} processed`, failureMessage: 'Failed to create material' }); if (interpreted.success) { const exists = coerceBoolean(interpreted.payload.exists, false) === true; const created = coerceBoolean(interpreted.payload.created, false) === true; if (exists) { return { success: true, path: materialPath, message: `Material ${name} already exists at ${materialPath}` }; } if (created) { return { success: true, path: materialPath, message: `Material ${name} created at ${materialPath}` }; } return { success: true, path: materialPath, message: interpreted.message }; } if (interpreted.error) { const exists = await this.assetExists(materialPath); if (exists) { return { success: true, path: materialPath, message: `Material ${name} created at ${materialPath}`, warnings: interpreted.warnings, details: interpreted.details }; } return { success: false, error: interpreted.error, warnings: interpreted.warnings, details: interpreted.details }; } const exists = await this.assetExists(materialPath); if (exists) { return { success: true, path: materialPath, message: `Material ${name} created at ${materialPath}`, warnings: interpreted.warnings, details: interpreted.details }; } return { success: false, error: interpreted.message, warnings: interpreted.warnings, details: interpreted.details }; } catch (err) { return { success: false, error: `Failed to create material: ${err}` }; } } async applyMaterialToActor(actorPath: string, materialPath: string, slotIndex = 0) { try { await this.bridge.httpCall('/remote/object/property', 'PUT', { objectPath: actorPath, propertyName: `StaticMeshComponent.Materials[${slotIndex}]`, propertyValue: materialPath }); return { success: true, message: 'Material applied' }; } catch (err) { return { success: false, error: `Failed to apply material: ${err}` }; } } private async assetExists(assetPath: string): Promise<boolean> { try { const response = await this.bridge.call({ objectPath: '/Script/EditorScriptingUtilities.Default__EditorAssetLibrary', functionName: 'DoesAssetExist', parameters: { AssetPath: assetPath } }); return coerceBoolean(response?.ReturnValue ?? response?.Result ?? response, false) === true; } catch { return false; } } }

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