using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; #if UNITY_VFX_GRAPH using UnityEngine.VFX; #endif namespace MCPForUnity.Editor.Tools.Vfx { /// /// Asset management operations for VFX Graph. /// Handles creating, assigning, and listing VFX assets. /// Requires com.unity.visualeffectgraph package and UNITY_VFX_GRAPH symbol. /// internal static class VfxGraphAssets { #if !UNITY_VFX_GRAPH public static object CreateAsset(JObject @params) { return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; } public static object AssignAsset(JObject @params) { return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; } public static object ListTemplates(JObject @params) { return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; } public static object ListAssets(JObject @params) { return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; } #else private static readonly string[] SupportedVfxGraphVersions = { "12.1" }; /// /// Creates a new VFX Graph asset file from a template. /// public static object CreateAsset(JObject @params) { string assetName = @params["assetName"]?.ToString(); string folderPath = @params["folderPath"]?.ToString() ?? "Assets/VFX"; string template = @params["template"]?.ToString() ?? "empty"; if (string.IsNullOrEmpty(assetName)) { return new { success = false, message = "assetName is required" }; } string versionError = ValidateVfxGraphVersion(); if (!string.IsNullOrEmpty(versionError)) { return new { success = false, message = versionError }; } // Ensure folder exists if (!AssetDatabase.IsValidFolder(folderPath)) { string[] folders = folderPath.Split('/'); string currentPath = folders[0]; for (int i = 1; i < folders.Length; i++) { string newPath = currentPath + "/" + folders[i]; if (!AssetDatabase.IsValidFolder(newPath)) { AssetDatabase.CreateFolder(currentPath, folders[i]); } currentPath = newPath; } } string assetPath = $"{folderPath}/{assetName}.vfx"; // Check if asset already exists if (AssetDatabase.LoadAssetAtPath(assetPath) != null) { bool overwrite = @params["overwrite"]?.ToObject() ?? false; if (!overwrite) { return new { success = false, message = $"Asset already exists at {assetPath}. Set overwrite=true to replace." }; } AssetDatabase.DeleteAsset(assetPath); } // Find template asset and copy it string templatePath = FindTemplate(template); string templateAssetPath = TryGetAssetPathFromFileSystem(templatePath); VisualEffectAsset newAsset = null; if (!string.IsNullOrEmpty(templateAssetPath)) { // Copy the asset to create a new VFX Graph asset if (!AssetDatabase.CopyAsset(templateAssetPath, assetPath)) { return new { success = false, message = $"Failed to copy VFX template from {templateAssetPath}" }; } AssetDatabase.Refresh(); newAsset = AssetDatabase.LoadAssetAtPath(assetPath); } else { return new { success = false, message = "VFX template not found. Add a .vfx template asset or install VFX Graph templates." }; } if (newAsset == null) { return new { success = false, message = "Failed to create VFX asset. Try using a template from list_templates." }; } return new { success = true, message = $"Created VFX asset: {assetPath}", data = new { assetPath = assetPath, assetName = newAsset.name, template = template } }; } /// /// Finds VFX template path by name. /// private static string FindTemplate(string templateName) { // Get the actual filesystem path for the VFX Graph package using PackageManager API var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.visualeffectgraph"); var searchPaths = new List(); if (packageInfo != null) { // Use the resolved path from PackageManager (handles Library/PackageCache paths) searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Editor/Templates")); searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Samples")); } // Also search project-local paths searchPaths.Add("Assets/VFX/Templates"); string[] templatePatterns = new[] { $"{templateName}.vfx", $"VFX{templateName}.vfx", $"Simple{templateName}.vfx", $"{templateName}VFX.vfx" }; foreach (string basePath in searchPaths) { string searchRoot = basePath; if (basePath.StartsWith("Assets/")) { searchRoot = System.IO.Path.Combine(UnityEngine.Application.dataPath, basePath.Substring("Assets/".Length)); } if (!System.IO.Directory.Exists(searchRoot)) { continue; } foreach (string pattern in templatePatterns) { string[] files = System.IO.Directory.GetFiles(searchRoot, pattern, System.IO.SearchOption.AllDirectories); if (files.Length > 0) { return files[0]; } } // Also search by partial match try { string[] allVfxFiles = System.IO.Directory.GetFiles(searchRoot, "*.vfx", System.IO.SearchOption.AllDirectories); foreach (string file in allVfxFiles) { if (System.IO.Path.GetFileNameWithoutExtension(file).ToLower().Contains(templateName.ToLower())) { return file; } } } catch (Exception ex) { Debug.LogWarning($"Failed to search VFX templates under '{searchRoot}': {ex.Message}"); } } // Search in project assets string[] guids = AssetDatabase.FindAssets("t:VisualEffectAsset " + templateName); if (guids.Length > 0) { string assetPath = AssetDatabase.GUIDToAssetPath(guids[0]); // Convert asset path (e.g., "Assets/...") to absolute filesystem path if (!string.IsNullOrEmpty(assetPath) && assetPath.StartsWith("Assets/")) { return System.IO.Path.Combine(UnityEngine.Application.dataPath, assetPath.Substring("Assets/".Length)); } if (!string.IsNullOrEmpty(assetPath) && assetPath.StartsWith("Packages/")) { var info = UnityEditor.PackageManager.PackageInfo.FindForAssetPath(assetPath); if (info != null) { string relPath = assetPath.Substring(("Packages/" + info.name + "/").Length); return System.IO.Path.Combine(info.resolvedPath, relPath); } } return null; } return null; } /// /// Assigns a VFX asset to a VisualEffect component. /// public static object AssignAsset(JObject @params) { VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params); if (vfx == null) { return new { success = false, message = "VisualEffect component not found" }; } string assetPath = @params["assetPath"]?.ToString(); if (string.IsNullOrEmpty(assetPath)) { return new { success = false, message = "assetPath is required" }; } // Validate and normalize path // Reject absolute paths, parent directory traversal, and backslashes if (assetPath.Contains("\\") || assetPath.Contains("..") || System.IO.Path.IsPathRooted(assetPath)) { return new { success = false, message = "Invalid assetPath: traversal and absolute paths are not allowed" }; } if (assetPath.StartsWith("Packages/")) { return new { success = false, message = "Invalid assetPath: VFX assets must live under Assets/." }; } if (!assetPath.StartsWith("Assets/")) { assetPath = "Assets/" + assetPath; } if (!assetPath.EndsWith(".vfx")) { assetPath += ".vfx"; } // Verify the normalized path doesn't escape the project string fullPath = System.IO.Path.Combine(UnityEngine.Application.dataPath, assetPath.Substring("Assets/".Length)); string canonicalProjectRoot = System.IO.Path.GetFullPath(UnityEngine.Application.dataPath); string canonicalAssetPath = System.IO.Path.GetFullPath(fullPath); if (!canonicalAssetPath.StartsWith(canonicalProjectRoot + System.IO.Path.DirectorySeparatorChar) && canonicalAssetPath != canonicalProjectRoot) { return new { success = false, message = "Invalid assetPath: would escape project directory" }; } var asset = AssetDatabase.LoadAssetAtPath(assetPath); if (asset == null) { // Try searching by name string searchName = System.IO.Path.GetFileNameWithoutExtension(assetPath); string[] guids = AssetDatabase.FindAssets($"t:VisualEffectAsset {searchName}"); if (guids.Length > 0) { assetPath = AssetDatabase.GUIDToAssetPath(guids[0]); asset = AssetDatabase.LoadAssetAtPath(assetPath); } } if (asset == null) { return new { success = false, message = $"VFX asset not found: {assetPath}" }; } Undo.RecordObject(vfx, "Assign VFX Asset"); vfx.visualEffectAsset = asset; EditorUtility.SetDirty(vfx); return new { success = true, message = $"Assigned VFX asset '{asset.name}' to {vfx.gameObject.name}", data = new { gameObject = vfx.gameObject.name, assetName = asset.name, assetPath = assetPath } }; } /// /// Lists available VFX templates. /// public static object ListTemplates(JObject @params) { var templates = new List(); var seenPaths = new HashSet(StringComparer.OrdinalIgnoreCase); // Get the actual filesystem path for the VFX Graph package using PackageManager API var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.visualeffectgraph"); var searchPaths = new List(); if (packageInfo != null) { // Use the resolved path from PackageManager (handles Library/PackageCache paths) searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Editor/Templates")); searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Samples")); } // Also search project-local paths searchPaths.Add("Assets/VFX/Templates"); searchPaths.Add("Assets/VFX"); // Precompute normalized package path for comparison string normalizedPackagePath = null; if (packageInfo != null) { normalizedPackagePath = packageInfo.resolvedPath.Replace("\\", "/"); } // Precompute the Assets base path for converting absolute paths to project-relative string assetsBasePath = Application.dataPath.Replace("\\", "/"); foreach (string basePath in searchPaths) { if (!System.IO.Directory.Exists(basePath)) { continue; } try { string[] vfxFiles = System.IO.Directory.GetFiles(basePath, "*.vfx", System.IO.SearchOption.AllDirectories); foreach (string file in vfxFiles) { string absolutePath = file.Replace("\\", "/"); string name = System.IO.Path.GetFileNameWithoutExtension(file); bool isPackage = normalizedPackagePath != null && absolutePath.StartsWith(normalizedPackagePath); // Convert absolute path to project-relative path string projectRelativePath; if (isPackage) { // For package paths, convert to Packages/... format projectRelativePath = "Packages/" + packageInfo.name + absolutePath.Substring(normalizedPackagePath.Length); } else if (absolutePath.StartsWith(assetsBasePath)) { // For project assets, convert to Assets/... format projectRelativePath = "Assets" + absolutePath.Substring(assetsBasePath.Length); } else { // Fallback: use the absolute path if we can't determine the relative path projectRelativePath = absolutePath; } string normalizedPath = projectRelativePath.Replace("\\", "/"); if (seenPaths.Add(normalizedPath)) { templates.Add(new { name = name, path = projectRelativePath, source = isPackage ? "package" : "project" }); } } } catch (Exception ex) { Debug.LogWarning($"Failed to list VFX templates under '{basePath}': {ex.Message}"); } } // Also search project assets string[] guids = AssetDatabase.FindAssets("t:VisualEffectAsset"); foreach (string guid in guids) { string path = AssetDatabase.GUIDToAssetPath(guid); string normalizedPath = path.Replace("\\", "/"); if (seenPaths.Add(normalizedPath)) { string name = System.IO.Path.GetFileNameWithoutExtension(path); templates.Add(new { name = name, path = path, source = "project" }); } } return new { success = true, data = new { count = templates.Count, templates = templates } }; } /// /// Lists all VFX assets in the project. /// public static object ListAssets(JObject @params) { string searchFolder = @params["folder"]?.ToString(); string searchPattern = @params["search"]?.ToString(); string filter = "t:VisualEffectAsset"; if (!string.IsNullOrEmpty(searchPattern)) { filter += " " + searchPattern; } string[] guids; if (!string.IsNullOrEmpty(searchFolder)) { if (searchFolder.Contains("\\") || searchFolder.Contains("..") || System.IO.Path.IsPathRooted(searchFolder)) { return new { success = false, message = "Invalid folder: traversal and absolute paths are not allowed" }; } if (searchFolder.StartsWith("Packages/")) { return new { success = false, message = "Invalid folder: VFX assets must live under Assets/." }; } if (!searchFolder.StartsWith("Assets/")) { searchFolder = "Assets/" + searchFolder; } string fullPath = System.IO.Path.Combine(UnityEngine.Application.dataPath, searchFolder.Substring("Assets/".Length)); string canonicalProjectRoot = System.IO.Path.GetFullPath(UnityEngine.Application.dataPath); string canonicalSearchFolder = System.IO.Path.GetFullPath(fullPath); if (!canonicalSearchFolder.StartsWith(canonicalProjectRoot + System.IO.Path.DirectorySeparatorChar) && canonicalSearchFolder != canonicalProjectRoot) { return new { success = false, message = "Invalid folder: would escape project directory" }; } guids = AssetDatabase.FindAssets(filter, new[] { searchFolder }); } else { guids = AssetDatabase.FindAssets(filter); } var assets = new List(); foreach (string guid in guids) { string path = AssetDatabase.GUIDToAssetPath(guid); var asset = AssetDatabase.LoadAssetAtPath(path); if (asset != null) { assets.Add(new { name = asset.name, path = path, guid = guid }); } } return new { success = true, data = new { count = assets.Count, assets = assets } }; } private static string ValidateVfxGraphVersion() { var info = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.visualeffectgraph"); if (info == null) { return "VFX Graph package (com.unity.visualeffectgraph) not installed"; } if (IsVersionSupported(info.version)) { return null; } string supported = string.Join(", ", SupportedVfxGraphVersions.Select(version => $"{version}.x")); return $"Unsupported VFX Graph version {info.version}. Supported versions: {supported}."; } private static bool IsVersionSupported(string installedVersion) { if (string.IsNullOrEmpty(installedVersion)) { return false; } string normalized = installedVersion; int suffixIndex = normalized.IndexOfAny(new[] { '-', '+' }); if (suffixIndex >= 0) { normalized = normalized.Substring(0, suffixIndex); } if (!Version.TryParse(normalized, out Version installed)) { return false; } foreach (string supported in SupportedVfxGraphVersions) { if (!Version.TryParse(supported, out Version target)) { continue; } if (installed.Major == target.Major && installed.Minor == target.Minor) { return true; } } return false; } private static string TryGetAssetPathFromFileSystem(string templatePath) { if (string.IsNullOrEmpty(templatePath)) { return null; } string normalized = templatePath.Replace("\\", "/"); string assetsRoot = Application.dataPath.Replace("\\", "/"); if (normalized.StartsWith(assetsRoot + "/")) { return "Assets/" + normalized.Substring(assetsRoot.Length + 1); } var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.visualeffectgraph"); if (packageInfo != null) { string packageRoot = packageInfo.resolvedPath.Replace("\\", "/"); if (normalized.StartsWith(packageRoot + "/")) { return "Packages/" + packageInfo.name + "/" + normalized.Substring(packageRoot.Length + 1); } } return null; } #endif } }