using UnityEngine; using UnityEditor; using Newtonsoft.Json.Linq; using System; using System.IO; using System.Linq; using System.Collections.Generic; using UnityMCP.Editor.Helpers; // For Response class using System.Globalization; namespace UnityMCP.Editor.Tools { /// /// Handles asset management operations within the Unity project. /// public static class ManageAsset { // --- Main Handler --- // Define the list of valid actions private static readonly List ValidActions = new List { "import", "create", "modify", "delete", "duplicate", "move", "rename", "search", "get_info", "create_folder", "get_components" }; public static object HandleCommand(JObject @params) { string action = @params["action"]?.ToString().ToLower(); if (string.IsNullOrEmpty(action)) { return Response.Error("Action parameter is required."); } // Check if the action is valid before switching if (!ValidActions.Contains(action)) { string validActionsList = string.Join(", ", ValidActions); return Response.Error($"Unknown action: '{action}'. Valid actions are: {validActionsList}"); } // Common parameters string path = @params["path"]?.ToString(); try { switch (action) { case "import": // Note: Unity typically auto-imports. This might re-import or configure import settings. return ReimportAsset(path, @params["properties"] as JObject); case "create": return CreateAsset(@params); case "modify": return ModifyAsset(path, @params["properties"] as JObject); case "delete": return DeleteAsset(path); case "duplicate": return DuplicateAsset(path, @params["destination"]?.ToString()); case "move": // Often same as rename if within Assets/ case "rename": return MoveOrRenameAsset(path, @params["destination"]?.ToString()); case "search": return SearchAssets(@params); case "get_info": return GetAssetInfo(path, @params["generatePreview"]?.ToObject() ?? false); case "create_folder": // Added specific action for clarity return CreateFolder(path); case "get_components": return GetComponentsFromAsset(path); default: // This error message is less likely to be hit now, but kept here as a fallback or for potential future modifications. string validActionsListDefault = string.Join(", ", ValidActions); return Response.Error($"Unknown action: '{action}'. Valid actions are: {validActionsListDefault}"); } } catch (Exception e) { Debug.LogError($"[ManageAsset] Action '{action}' failed for path '{path}': {e}"); return Response.Error($"Internal error processing action '{action}' on '{path}': {e.Message}"); } } // --- Action Implementations --- private static object ReimportAsset(string path, JObject properties) { if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for reimport."); string fullPath = SanitizeAssetPath(path); if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}"); try { // TODO: Apply importer properties before reimporting? // This is complex as it requires getting the AssetImporter, casting it, // applying properties via reflection or specific methods, saving, then reimporting. if (properties != null && properties.HasValues) { Debug.LogWarning("[ManageAsset.Reimport] Modifying importer properties before reimport is not fully implemented yet."); // AssetImporter importer = AssetImporter.GetAtPath(fullPath); // if (importer != null) { /* Apply properties */ AssetDatabase.WriteImportSettingsIfDirty(fullPath); } } AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); // AssetDatabase.Refresh(); // Usually ImportAsset handles refresh return Response.Success($"Asset '{fullPath}' reimported.", GetAssetData(fullPath)); } catch (Exception e) { return Response.Error($"Failed to reimport asset '{fullPath}': {e.Message}"); } } private static object CreateAsset(JObject @params) { string path = @params["path"]?.ToString(); string assetType = @params["assetType"]?.ToString(); JObject properties = @params["properties"] as JObject; if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for create."); if (string.IsNullOrEmpty(assetType)) return Response.Error("'assetType' is required for create."); string fullPath = SanitizeAssetPath(path); string directory = Path.GetDirectoryName(fullPath); // Ensure directory exists if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), directory))) { Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), directory)); AssetDatabase.Refresh(); // Make sure Unity knows about the new folder } if (AssetExists(fullPath)) return Response.Error($"Asset already exists at path: {fullPath}"); try { UnityEngine.Object newAsset = null; string lowerAssetType = assetType.ToLowerInvariant(); // Handle common asset types if (lowerAssetType == "folder") { return CreateFolder(path); // Use dedicated method } else if (lowerAssetType == "material") { Material mat = new Material(Shader.Find("Standard")); // Default shader // TODO: Apply properties from JObject (e.g., shader name, color, texture assignments) if(properties != null) ApplyMaterialProperties(mat, properties); AssetDatabase.CreateAsset(mat, fullPath); newAsset = mat; } else if (lowerAssetType == "scriptableobject") { string scriptClassName = properties?["scriptClass"]?.ToString(); if (string.IsNullOrEmpty(scriptClassName)) return Response.Error("'scriptClass' property required when creating ScriptableObject asset."); Type scriptType = FindType(scriptClassName); if (scriptType == null || !typeof(ScriptableObject).IsAssignableFrom(scriptType)) { return Response.Error($"Script class '{scriptClassName}' not found or does not inherit from ScriptableObject."); } ScriptableObject so = ScriptableObject.CreateInstance(scriptType); // TODO: Apply properties from JObject to the ScriptableObject instance? AssetDatabase.CreateAsset(so, fullPath); newAsset = so; } else if (lowerAssetType == "prefab") { // Creating prefabs usually involves saving an existing GameObject hierarchy. // A common pattern is to create an empty GameObject, configure it, and then save it. return Response.Error("Creating prefabs programmatically usually requires a source GameObject. Use manage_gameobject to create/configure, then save as prefab via a separate mechanism or future enhancement."); // Example (conceptual): // GameObject source = GameObject.Find(properties["sourceGameObject"].ToString()); // if(source != null) PrefabUtility.SaveAsPrefabAsset(source, fullPath); } // TODO: Add more asset types (Animation Controller, Scene, etc.) else { // Generic creation attempt (might fail or create empty files) // For some types, just creating the file might be enough if Unity imports it. // File.Create(Path.Combine(Directory.GetCurrentDirectory(), fullPath)).Close(); // AssetDatabase.ImportAsset(fullPath); // Let Unity try to import it // newAsset = AssetDatabase.LoadAssetAtPath(fullPath); return Response.Error($"Creation for asset type '{assetType}' is not explicitly supported yet. Supported: Folder, Material, ScriptableObject."); } if (newAsset == null && !Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), fullPath))) // Check if it wasn't a folder and asset wasn't created { return Response.Error($"Failed to create asset '{assetType}' at '{fullPath}'. See logs for details."); } AssetDatabase.SaveAssets(); // AssetDatabase.Refresh(); // CreateAsset often handles refresh return Response.Success($"Asset '{fullPath}' created successfully.", GetAssetData(fullPath)); } catch (Exception e) { return Response.Error($"Failed to create asset at '{fullPath}': {e.Message}"); } } private static object CreateFolder(string path) { if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for create_folder."); string fullPath = SanitizeAssetPath(path); string parentDir = Path.GetDirectoryName(fullPath); string folderName = Path.GetFileName(fullPath); if (AssetExists(fullPath)) { // Check if it's actually a folder already if (AssetDatabase.IsValidFolder(fullPath)) { return Response.Success($"Folder already exists at path: {fullPath}", GetAssetData(fullPath)); } else { return Response.Error($"An asset (not a folder) already exists at path: {fullPath}"); } } try { // Ensure parent exists if (!string.IsNullOrEmpty(parentDir) && !AssetDatabase.IsValidFolder(parentDir)) { // Recursively create parent folders if needed (AssetDatabase handles this internally) // Or we can do it manually: Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), parentDir)); AssetDatabase.Refresh(); } string guid = AssetDatabase.CreateFolder(parentDir, folderName); if (string.IsNullOrEmpty(guid)) { return Response.Error($"Failed to create folder '{fullPath}'. Check logs and permissions."); } // AssetDatabase.Refresh(); // CreateFolder usually handles refresh return Response.Success($"Folder '{fullPath}' created successfully.", GetAssetData(fullPath)); } catch (Exception e) { return Response.Error($"Failed to create folder '{fullPath}': {e.Message}"); } } private static object ModifyAsset(string path, JObject properties) { if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for modify."); if (properties == null || !properties.HasValues) return Response.Error("'properties' are required for modify."); string fullPath = SanitizeAssetPath(path); if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}"); try { UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath(fullPath); if (asset == null) return Response.Error($"Failed to load asset at path: {fullPath}"); bool modified = false; // Flag to track if any changes were made // --- NEW: Handle GameObject / Prefab Component Modification --- if (asset is GameObject gameObject) { // Iterate through the properties JSON: keys are component names, values are properties objects for that component foreach (var prop in properties.Properties()) { string componentName = prop.Name; // e.g., "Collectible" // Check if the value associated with the component name is actually an object containing properties if (prop.Value is JObject componentProperties && componentProperties.HasValues) // e.g., {"bobSpeed": 2.0} { // Find the component on the GameObject using the name from the JSON key // Using GetComponent(string) is convenient but might require exact type name or be ambiguous. // Consider using FindType helper if needed for more complex scenarios. Component targetComponent = gameObject.GetComponent(componentName); if (targetComponent != null) { // Apply the nested properties (e.g., bobSpeed) to the found component instance // Use |= to ensure 'modified' becomes true if any component is successfully modified modified |= ApplyObjectProperties(targetComponent, componentProperties); } else { // Log a warning if a specified component couldn't be found Debug.LogWarning($"[ManageAsset.ModifyAsset] Component '{componentName}' not found on GameObject '{gameObject.name}' in asset '{fullPath}'. Skipping modification for this component."); } } else { // Log a warning if the structure isn't {"ComponentName": {"prop": value}} // We could potentially try to apply this property directly to the GameObject here if needed, // but the primary goal is component modification. Debug.LogWarning($"[ManageAsset.ModifyAsset] Property '{prop.Name}' for GameObject modification should have a JSON object value containing component properties. Value was: {prop.Value.Type}. Skipping."); } } // Note: 'modified' is now true if ANY component property was successfully changed. } // --- End NEW --- // --- Existing logic for other asset types (now as else-if) --- // Example: Modifying a Material else if (asset is Material material) { // Apply properties directly to the material. If this modifies, it sets modified=true. // Use |= in case the asset was already marked modified by previous logic (though unlikely here) modified |= ApplyMaterialProperties(material, properties); } // Example: Modifying a ScriptableObject else if (asset is ScriptableObject so) { // Apply properties directly to the ScriptableObject. modified |= ApplyObjectProperties(so, properties); // General helper } // Example: Modifying TextureImporter settings else if (asset is Texture) { AssetImporter importer = AssetImporter.GetAtPath(fullPath); if (importer is TextureImporter textureImporter) { bool importerModified = ApplyObjectProperties(textureImporter, properties); if (importerModified) { // Importer settings need saving and reimporting AssetDatabase.WriteImportSettingsIfDirty(fullPath); AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); // Reimport to apply changes modified = true; // Mark overall operation as modified } } else { Debug.LogWarning($"Could not get TextureImporter for {fullPath}."); } } // TODO: Add modification logic for other common asset types (Models, AudioClips importers, etc.) else // Fallback for other asset types OR direct properties on non-GameObject assets { // This block handles non-GameObject/Material/ScriptableObject/Texture assets. // Attempts to apply properties directly to the asset itself. Debug.LogWarning($"[ManageAsset.ModifyAsset] Asset type '{asset.GetType().Name}' at '{fullPath}' is not explicitly handled for component modification. Attempting generic property setting on the asset itself."); modified |= ApplyObjectProperties(asset, properties); } // --- End Existing Logic --- // Check if any modification happened (either component or direct asset modification) if (modified) { // Mark the asset as dirty (important for prefabs/SOs) so Unity knows to save it. EditorUtility.SetDirty(asset); // Save all modified assets to disk. AssetDatabase.SaveAssets(); // Refresh might be needed in some edge cases, but SaveAssets usually covers it. // AssetDatabase.Refresh(); return Response.Success($"Asset '{fullPath}' modified successfully.", GetAssetData(fullPath)); } else { // If no changes were made (e.g., component not found, property names incorrect, value unchanged), return a success message indicating nothing changed. return Response.Success($"No applicable or modifiable properties found for asset '{fullPath}'. Check component names, property names, and values.", GetAssetData(fullPath)); // Previous message: return Response.Success($"No applicable properties found to modify for asset '{fullPath}'.", GetAssetData(fullPath)); } } catch (Exception e) { // Log the detailed error internally Debug.LogError($"[ManageAsset] Action 'modify' failed for path '{path}': {e}"); // Return a user-friendly error message return Response.Error($"Failed to modify asset '{fullPath}': {e.Message}"); } } private static object DeleteAsset(string path) { if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for delete."); string fullPath = SanitizeAssetPath(path); if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}"); try { bool success = AssetDatabase.DeleteAsset(fullPath); if (success) { // AssetDatabase.Refresh(); // DeleteAsset usually handles refresh return Response.Success($"Asset '{fullPath}' deleted successfully."); } else { // This might happen if the file couldn't be deleted (e.g., locked) return Response.Error($"Failed to delete asset '{fullPath}'. Check logs or if the file is locked."); } } catch (Exception e) { return Response.Error($"Error deleting asset '{fullPath}': {e.Message}"); } } private static object DuplicateAsset(string path, string destinationPath) { if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for duplicate."); string sourcePath = SanitizeAssetPath(path); if (!AssetExists(sourcePath)) return Response.Error($"Source asset not found at path: {sourcePath}"); string destPath; if (string.IsNullOrEmpty(destinationPath)) { // Generate a unique path if destination is not provided destPath = AssetDatabase.GenerateUniqueAssetPath(sourcePath); } else { destPath = SanitizeAssetPath(destinationPath); if (AssetExists(destPath)) return Response.Error($"Asset already exists at destination path: {destPath}"); // Ensure destination directory exists EnsureDirectoryExists(Path.GetDirectoryName(destPath)); } try { bool success = AssetDatabase.CopyAsset(sourcePath, destPath); if (success) { // AssetDatabase.Refresh(); return Response.Success($"Asset '{sourcePath}' duplicated to '{destPath}'.", GetAssetData(destPath)); } else { return Response.Error($"Failed to duplicate asset from '{sourcePath}' to '{destPath}'."); } } catch (Exception e) { return Response.Error($"Error duplicating asset '{sourcePath}': {e.Message}"); } } private static object MoveOrRenameAsset(string path, string destinationPath) { if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for move/rename."); if (string.IsNullOrEmpty(destinationPath)) return Response.Error("'destination' path is required for move/rename."); string sourcePath = SanitizeAssetPath(path); string destPath = SanitizeAssetPath(destinationPath); if (!AssetExists(sourcePath)) return Response.Error($"Source asset not found at path: {sourcePath}"); if (AssetExists(destPath)) return Response.Error($"An asset already exists at the destination path: {destPath}"); // Ensure destination directory exists EnsureDirectoryExists(Path.GetDirectoryName(destPath)); try { // Validate will return an error string if failed, null if successful string error = AssetDatabase.ValidateMoveAsset(sourcePath, destPath); if (!string.IsNullOrEmpty(error)) { return Response.Error($"Failed to move/rename asset from '{sourcePath}' to '{destPath}': {error}"); } string guid = AssetDatabase.MoveAsset(sourcePath, destPath); if (!string.IsNullOrEmpty(guid)) // MoveAsset returns the new GUID on success { // AssetDatabase.Refresh(); // MoveAsset usually handles refresh return Response.Success($"Asset moved/renamed from '{sourcePath}' to '{destPath}'.", GetAssetData(destPath)); } else { // This case might not be reachable if ValidateMoveAsset passes, but good to have return Response.Error($"MoveAsset call failed unexpectedly for '{sourcePath}' to '{destPath}'."); } } catch (Exception e) { return Response.Error($"Error moving/renaming asset '{sourcePath}': {e.Message}"); } } private static object SearchAssets(JObject @params) { string searchPattern = @params["searchPattern"]?.ToString(); string filterType = @params["filterType"]?.ToString(); string pathScope = @params["path"]?.ToString(); // Use path as folder scope string filterDateAfterStr = @params["filterDateAfter"]?.ToString(); int pageSize = @params["pageSize"]?.ToObject() ?? 50; // Default page size int pageNumber = @params["pageNumber"]?.ToObject() ?? 1; // Default page number (1-based) bool generatePreview = @params["generatePreview"]?.ToObject() ?? false; List searchFilters = new List(); if (!string.IsNullOrEmpty(searchPattern)) searchFilters.Add(searchPattern); if (!string.IsNullOrEmpty(filterType)) searchFilters.Add($"t:{filterType}"); string[] folderScope = null; if (!string.IsNullOrEmpty(pathScope)) { folderScope = new string[] { SanitizeAssetPath(pathScope) }; if (!AssetDatabase.IsValidFolder(folderScope[0])) { // Maybe the user provided a file path instead of a folder? // We could search in the containing folder, or return an error. Debug.LogWarning($"Search path '{folderScope[0]}' is not a valid folder. Searching entire project."); folderScope = null; // Search everywhere if path isn't a folder } } DateTime? filterDateAfter = null; if (!string.IsNullOrEmpty(filterDateAfterStr)) { if (DateTime.TryParse(filterDateAfterStr, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime parsedDate)) { filterDateAfter = parsedDate; } else { Debug.LogWarning($"Could not parse filterDateAfter: '{filterDateAfterStr}'. Expected ISO 8601 format."); } } try { string[] guids = AssetDatabase.FindAssets(string.Join(" ", searchFilters), folderScope); List results = new List(); int totalFound = 0; foreach (string guid in guids) { string assetPath = AssetDatabase.GUIDToAssetPath(guid); if (string.IsNullOrEmpty(assetPath)) continue; // Apply date filter if present if (filterDateAfter.HasValue) { DateTime lastWriteTime = File.GetLastWriteTimeUtc(Path.Combine(Directory.GetCurrentDirectory(), assetPath)); if (lastWriteTime <= filterDateAfter.Value) { continue; // Skip assets older than or equal to the filter date } } totalFound++; // Count matching assets before pagination results.Add(GetAssetData(assetPath, generatePreview)); } // Apply pagination int startIndex = (pageNumber - 1) * pageSize; var pagedResults = results.Skip(startIndex).Take(pageSize).ToList(); return Response.Success($"Found {totalFound} asset(s). Returning page {pageNumber} ({pagedResults.Count} assets).", new { totalAssets = totalFound, pageSize = pageSize, pageNumber = pageNumber, assets = pagedResults }); } catch (Exception e) { return Response.Error($"Error searching assets: {e.Message}"); } } private static object GetAssetInfo(string path, bool generatePreview) { if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for get_info."); string fullPath = SanitizeAssetPath(path); if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}"); try { return Response.Success("Asset info retrieved.", GetAssetData(fullPath, generatePreview)); } catch (Exception e) { return Response.Error($"Error getting info for asset '{fullPath}': {e.Message}"); } } /// /// Retrieves components attached to a GameObject asset (like a Prefab). /// /// The asset path of the GameObject or Prefab. /// A response object containing a list of component type names or an error. private static object GetComponentsFromAsset(string path) { // 1. Validate input path if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for get_components."); // 2. Sanitize and check existence string fullPath = SanitizeAssetPath(path); if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}"); try { // 3. Load the asset UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath(fullPath); if (asset == null) return Response.Error($"Failed to load asset at path: {fullPath}"); // 4. Check if it's a GameObject (Prefabs load as GameObjects) GameObject gameObject = asset as GameObject; if (gameObject == null) { // Also check if it's *directly* a Component type (less common for primary assets) Component componentAsset = asset as Component; if (componentAsset != null) { // If the asset itself *is* a component, maybe return just its info? // This is an edge case. Let's stick to GameObjects for now. return Response.Error($"Asset at '{fullPath}' is a Component ({asset.GetType().FullName}), not a GameObject. Components are typically retrieved *from* a GameObject."); } return Response.Error($"Asset at '{fullPath}' is not a GameObject (Type: {asset.GetType().FullName}). Cannot get components from this asset type."); } // 5. Get components Component[] components = gameObject.GetComponents(); // 6. Format component data List componentList = components.Select(comp => new { typeName = comp.GetType().FullName, instanceID = comp.GetInstanceID(), // TODO: Add more component-specific details here if needed in the future? // Requires reflection or specific handling per component type. }).ToList(); // Explicit cast for clarity if needed // 7. Return success response return Response.Success($"Found {componentList.Count} component(s) on asset '{fullPath}'.", componentList); } catch (Exception e) { Debug.LogError($"[ManageAsset.GetComponentsFromAsset] Error getting components for '{fullPath}': {e}"); return Response.Error($"Error getting components for asset '{fullPath}': {e.Message}"); } } // --- Internal Helpers --- /// /// Ensures the asset path starts with "Assets/". /// private static string SanitizeAssetPath(string path) { if (string.IsNullOrEmpty(path)) return path; path = path.Replace('\\', '/'); // Normalize separators if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) { return "Assets/" + path.TrimStart('/'); } return path; } /// /// Checks if an asset exists at the given path (file or folder). /// private static bool AssetExists(string sanitizedPath) { // AssetDatabase APIs are generally preferred over raw File/Directory checks for assets. // Check if it's a known asset GUID. if (!string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(sanitizedPath))) { return true; } // AssetPathToGUID might not work for newly created folders not yet refreshed. // Check directory explicitly for folders. if(Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), sanitizedPath))) { // Check if it's considered a *valid* folder by Unity return AssetDatabase.IsValidFolder(sanitizedPath); } // Check file existence for non-folder assets. if(File.Exists(Path.Combine(Directory.GetCurrentDirectory(), sanitizedPath))){ return true; // Assume if file exists, it's an asset or will be imported } return false; // Alternative: return !string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(sanitizedPath)); } /// /// Ensures the directory for a given asset path exists, creating it if necessary. /// private static void EnsureDirectoryExists(string directoryPath) { if (string.IsNullOrEmpty(directoryPath)) return; string fullDirPath = Path.Combine(Directory.GetCurrentDirectory(), directoryPath); if (!Directory.Exists(fullDirPath)) { Directory.CreateDirectory(fullDirPath); AssetDatabase.Refresh(); // Let Unity know about the new folder } } /// /// Applies properties from JObject to a Material. /// private static bool ApplyMaterialProperties(Material mat, JObject properties) { if (mat == null || properties == null) return false; bool modified = false; // Example: Set shader if (properties["shader"]?.Type == JTokenType.String) { Shader newShader = Shader.Find(properties["shader"].ToString()); if (newShader != null && mat.shader != newShader) { mat.shader = newShader; modified = true; } } // Example: Set color property if (properties["color"] is JObject colorProps) { string propName = colorProps["name"]?.ToString() ?? "_Color"; // Default main color if (colorProps["value"] is JArray colArr && colArr.Count >= 3) { try { Color newColor = new Color( colArr[0].ToObject(), colArr[1].ToObject(), colArr[2].ToObject(), colArr.Count > 3 ? colArr[3].ToObject() : 1.0f ); if(mat.HasProperty(propName) && mat.GetColor(propName) != newColor) { mat.SetColor(propName, newColor); modified = true; } } catch (Exception ex) { Debug.LogWarning($"Error parsing color property '{propName}': {ex.Message}"); } } } // Example: Set float property if (properties["float"] is JObject floatProps) { string propName = floatProps["name"]?.ToString(); if (!string.IsNullOrEmpty(propName) && floatProps["value"]?.Type == JTokenType.Float || floatProps["value"]?.Type == JTokenType.Integer) { try { float newVal = floatProps["value"].ToObject(); if (mat.HasProperty(propName) && mat.GetFloat(propName) != newVal) { mat.SetFloat(propName, newVal); modified = true; } } catch (Exception ex) { Debug.LogWarning($"Error parsing float property '{propName}': {ex.Message}"); } } } // Example: Set texture property if (properties["texture"] is JObject texProps) { string propName = texProps["name"]?.ToString() ?? "_MainTex"; // Default main texture string texPath = texProps["path"]?.ToString(); if (!string.IsNullOrEmpty(texPath)) { Texture newTex = AssetDatabase.LoadAssetAtPath(SanitizeAssetPath(texPath)); if (newTex != null && mat.HasProperty(propName) && mat.GetTexture(propName) != newTex) { mat.SetTexture(propName, newTex); modified = true; } else if(newTex == null) { Debug.LogWarning($"Texture not found at path: {texPath}"); } } } // TODO: Add handlers for other property types (Vectors, Ints, Keywords, RenderQueue, etc.) return modified; } /// /// Generic helper to set properties on any UnityEngine.Object using reflection. /// private static bool ApplyObjectProperties(UnityEngine.Object target, JObject properties) { if (target == null || properties == null) return false; bool modified = false; Type type = target.GetType(); foreach (var prop in properties.Properties()) { string propName = prop.Name; JToken propValue = prop.Value; if (SetPropertyOrField(target, propName, propValue, type)) { modified = true; } } return modified; } /// /// Helper to set a property or field via reflection, handling basic types and Unity objects. /// private static bool SetPropertyOrField(object target, string memberName, JToken value, Type type = null) { type = type ?? target.GetType(); System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase; try { System.Reflection.PropertyInfo propInfo = type.GetProperty(memberName, flags); if (propInfo != null && propInfo.CanWrite) { object convertedValue = ConvertJTokenToType(value, propInfo.PropertyType); if (convertedValue != null && !object.Equals(propInfo.GetValue(target), convertedValue)) { propInfo.SetValue(target, convertedValue); return true; } } else { System.Reflection.FieldInfo fieldInfo = type.GetField(memberName, flags); if (fieldInfo != null) { object convertedValue = ConvertJTokenToType(value, fieldInfo.FieldType); if (convertedValue != null && !object.Equals(fieldInfo.GetValue(target), convertedValue)) { fieldInfo.SetValue(target, convertedValue); return true; } } } } catch (Exception ex) { Debug.LogWarning($"[SetPropertyOrField] Failed to set '{memberName}' on {type.Name}: {ex.Message}"); } return false; } /// /// Simple JToken to Type conversion for common Unity types and primitives. /// private static object ConvertJTokenToType(JToken token, Type targetType) { try { if (token == null || token.Type == JTokenType.Null) return null; if (targetType == typeof(string)) return token.ToObject(); if (targetType == typeof(int)) return token.ToObject(); if (targetType == typeof(float)) return token.ToObject(); if (targetType == typeof(bool)) return token.ToObject(); if (targetType == typeof(Vector2) && token is JArray arrV2 && arrV2.Count == 2) return new Vector2(arrV2[0].ToObject(), arrV2[1].ToObject()); if (targetType == typeof(Vector3) && token is JArray arrV3 && arrV3.Count == 3) return new Vector3(arrV3[0].ToObject(), arrV3[1].ToObject(), arrV3[2].ToObject()); if (targetType == typeof(Vector4) && token is JArray arrV4 && arrV4.Count == 4) return new Vector4(arrV4[0].ToObject(), arrV4[1].ToObject(), arrV4[2].ToObject(), arrV4[3].ToObject()); if (targetType == typeof(Quaternion) && token is JArray arrQ && arrQ.Count == 4) return new Quaternion(arrQ[0].ToObject(), arrQ[1].ToObject(), arrQ[2].ToObject(), arrQ[3].ToObject()); if (targetType == typeof(Color) && token is JArray arrC && arrC.Count >= 3) // Allow RGB or RGBA return new Color(arrC[0].ToObject(), arrC[1].ToObject(), arrC[2].ToObject(), arrC.Count > 3 ? arrC[3].ToObject() : 1.0f); if (targetType.IsEnum) return Enum.Parse(targetType, token.ToString(), true); // Case-insensitive enum parsing // Handle loading Unity Objects (Materials, Textures, etc.) by path if (typeof(UnityEngine.Object).IsAssignableFrom(targetType) && token.Type == JTokenType.String) { string assetPath = SanitizeAssetPath(token.ToString()); UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath(assetPath, targetType); if (loadedAsset == null) { Debug.LogWarning($"[ConvertJTokenToType] Could not load asset of type {targetType.Name} from path: {assetPath}"); } return loadedAsset; } // Fallback: Try direct conversion (might work for other simple value types) return token.ToObject(targetType); } catch (Exception ex) { Debug.LogWarning($"[ConvertJTokenToType] Could not convert JToken '{token}' (type {token.Type}) to type '{targetType.Name}': {ex.Message}"); return null; } } /// /// Helper to find a Type by name, searching relevant assemblies. /// Needed for creating ScriptableObjects or finding component types by name. /// private static Type FindType(string typeName) { if (string.IsNullOrEmpty(typeName)) return null; // Try direct lookup first (common Unity types often don't need assembly qualified name) var type = Type.GetType(typeName) ?? Type.GetType($"UnityEngine.{typeName}, UnityEngine.CoreModule") ?? Type.GetType($"UnityEngine.UI.{typeName}, UnityEngine.UI") ?? Type.GetType($"UnityEditor.{typeName}, UnityEditor.CoreModule"); if (type != null) return type; // If not found, search loaded assemblies (slower but more robust for user scripts) foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { // Look for non-namespaced first type = assembly.GetType(typeName, false, true); // throwOnError=false, ignoreCase=true if (type != null) return type; // Check common namespaces if simple name given type = assembly.GetType("UnityEngine." + typeName, false, true); if (type != null) return type; type = assembly.GetType("UnityEditor." + typeName, false, true); if (type != null) return type; // Add other likely namespaces if needed (e.g., specific plugins) } Debug.LogWarning($"[FindType] Type '{typeName}' not found in any loaded assembly."); return null; // Not found } // --- Data Serialization --- /// /// Creates a serializable representation of an asset. /// private static object GetAssetData(string path, bool generatePreview = false) { if (string.IsNullOrEmpty(path) || !AssetExists(path)) return null; string guid = AssetDatabase.AssetPathToGUID(path); Type assetType = AssetDatabase.GetMainAssetTypeAtPath(path); UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath(path); string previewBase64 = null; int previewWidth = 0; int previewHeight = 0; if (generatePreview && asset != null) { Texture2D preview = AssetPreview.GetAssetPreview(asset); if (preview != null) { try { // Ensure texture is readable for EncodeToPNG // Creating a temporary readable copy is safer RenderTexture rt = RenderTexture.GetTemporary(preview.width, preview.height); Graphics.Blit(preview, rt); RenderTexture previous = RenderTexture.active; RenderTexture.active = rt; Texture2D readablePreview = new Texture2D(preview.width, preview.height); readablePreview.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0); readablePreview.Apply(); RenderTexture.active = previous; RenderTexture.ReleaseTemporary(rt); byte[] pngData = readablePreview.EncodeToPNG(); previewBase64 = Convert.ToBase64String(pngData); previewWidth = readablePreview.width; previewHeight = readablePreview.height; UnityEngine.Object.DestroyImmediate(readablePreview); // Clean up temp texture } catch (Exception ex) { Debug.LogWarning($"Failed to generate readable preview for '{path}': {ex.Message}. Preview might not be readable."); // Fallback: Try getting static preview if available? // Texture2D staticPreview = AssetPreview.GetMiniThumbnail(asset); } } else { Debug.LogWarning($"Could not get asset preview for {path} (Type: {assetType?.Name}). Is it supported?"); } } return new { path = path, guid = guid, assetType = assetType?.FullName ?? "Unknown", name = Path.GetFileNameWithoutExtension(path), fileName = Path.GetFileName(path), isFolder = AssetDatabase.IsValidFolder(path), instanceID = asset?.GetInstanceID() ?? 0, lastWriteTimeUtc = File.GetLastWriteTimeUtc(Path.Combine(Directory.GetCurrentDirectory(), path)).ToString("o"), // ISO 8601 // --- Preview Data --- previewBase64 = previewBase64, // PNG data as Base64 string previewWidth = previewWidth, previewHeight = previewHeight // TODO: Add more metadata? Importer settings? Dependencies? }; } } }