828 lines
40 KiB
C#
828 lines
40 KiB
C#
|
|
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
|
||
|
|
{
|
||
|
|
/// <summary>
|
||
|
|
/// Handles asset management operations within the Unity project.
|
||
|
|
/// </summary>
|
||
|
|
public static class ManageAsset
|
||
|
|
{
|
||
|
|
// --- Main Handler ---
|
||
|
|
|
||
|
|
public static object HandleCommand(JObject @params)
|
||
|
|
{
|
||
|
|
string action = @params["action"]?.ToString().ToLower();
|
||
|
|
if (string.IsNullOrEmpty(action))
|
||
|
|
{
|
||
|
|
return Response.Error("Action parameter is required.");
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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<bool>() ?? false);
|
||
|
|
case "create_folder": // Added specific action for clarity
|
||
|
|
return CreateFolder(path);
|
||
|
|
|
||
|
|
default:
|
||
|
|
return Response.Error($"Unknown action: '{action}'.");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
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<UnityEngine.Object>(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<UnityEngine.Object>(fullPath);
|
||
|
|
if (asset == null) return Response.Error($"Failed to load asset at path: {fullPath}");
|
||
|
|
|
||
|
|
bool modified = false;
|
||
|
|
// Example: Modifying a Material
|
||
|
|
if (asset is Material material)
|
||
|
|
{
|
||
|
|
modified = ApplyMaterialProperties(material, properties);
|
||
|
|
}
|
||
|
|
// Example: Modifying a ScriptableObject (more complex, needs reflection or specific interface)
|
||
|
|
else if (asset is ScriptableObject so)
|
||
|
|
{
|
||
|
|
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)
|
||
|
|
{
|
||
|
|
modified = ApplyObjectProperties(textureImporter, properties);
|
||
|
|
if (modified) {
|
||
|
|
// Importer settings need saving
|
||
|
|
AssetDatabase.WriteImportSettingsIfDirty(fullPath);
|
||
|
|
AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); // Reimport to apply changes
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
Debug.LogWarning($"Could not get TextureImporter for {fullPath}.");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// TODO: Add modification logic for other common asset types (Models, AudioClips importers, etc.)
|
||
|
|
else
|
||
|
|
{
|
||
|
|
Debug.LogWarning($"Modification for asset type '{asset.GetType().Name}' at '{fullPath}' is not fully implemented. Attempting generic property setting.");
|
||
|
|
modified = ApplyObjectProperties(asset, properties);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (modified)
|
||
|
|
{
|
||
|
|
EditorUtility.SetDirty(asset); // Mark the asset itself as dirty
|
||
|
|
AssetDatabase.SaveAssets(); // Save changes to disk
|
||
|
|
// AssetDatabase.Refresh(); // SaveAssets usually handles refresh
|
||
|
|
return Response.Success($"Asset '{fullPath}' modified successfully.", GetAssetData(fullPath));
|
||
|
|
} else {
|
||
|
|
return Response.Success($"No applicable properties found to modify for asset '{fullPath}'.", GetAssetData(fullPath));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (Exception e)
|
||
|
|
{
|
||
|
|
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<int?>() ?? 50; // Default page size
|
||
|
|
int pageNumber = @params["pageNumber"]?.ToObject<int?>() ?? 1; // Default page number (1-based)
|
||
|
|
bool generatePreview = @params["generatePreview"]?.ToObject<bool>() ?? false;
|
||
|
|
|
||
|
|
List<string> searchFilters = new List<string>();
|
||
|
|
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<object> results = new List<object>();
|
||
|
|
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}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Internal Helpers ---
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Ensures the asset path starts with "Assets/".
|
||
|
|
/// </summary>
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Checks if an asset exists at the given path (file or folder).
|
||
|
|
/// </summary>
|
||
|
|
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));
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Ensures the directory for a given asset path exists, creating it if necessary.
|
||
|
|
/// </summary>
|
||
|
|
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
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Applies properties from JObject to a Material.
|
||
|
|
/// </summary>
|
||
|
|
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<float>(),
|
||
|
|
colArr[1].ToObject<float>(),
|
||
|
|
colArr[2].ToObject<float>(),
|
||
|
|
colArr.Count > 3 ? colArr[3].ToObject<float>() : 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<float>();
|
||
|
|
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<Texture>(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;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Generic helper to set properties on any UnityEngine.Object using reflection.
|
||
|
|
/// </summary>
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Helper to set a property or field via reflection, handling basic types and Unity objects.
|
||
|
|
/// </summary>
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Simple JToken to Type conversion for common Unity types and primitives.
|
||
|
|
/// </summary>
|
||
|
|
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<string>();
|
||
|
|
if (targetType == typeof(int)) return token.ToObject<int>();
|
||
|
|
if (targetType == typeof(float)) return token.ToObject<float>();
|
||
|
|
if (targetType == typeof(bool)) return token.ToObject<bool>();
|
||
|
|
if (targetType == typeof(Vector2) && token is JArray arrV2 && arrV2.Count == 2)
|
||
|
|
return new Vector2(arrV2[0].ToObject<float>(), arrV2[1].ToObject<float>());
|
||
|
|
if (targetType == typeof(Vector3) && token is JArray arrV3 && arrV3.Count == 3)
|
||
|
|
return new Vector3(arrV3[0].ToObject<float>(), arrV3[1].ToObject<float>(), arrV3[2].ToObject<float>());
|
||
|
|
if (targetType == typeof(Vector4) && token is JArray arrV4 && arrV4.Count == 4)
|
||
|
|
return new Vector4(arrV4[0].ToObject<float>(), arrV4[1].ToObject<float>(), arrV4[2].ToObject<float>(), arrV4[3].ToObject<float>());
|
||
|
|
if (targetType == typeof(Quaternion) && token is JArray arrQ && arrQ.Count == 4)
|
||
|
|
return new Quaternion(arrQ[0].ToObject<float>(), arrQ[1].ToObject<float>(), arrQ[2].ToObject<float>(), arrQ[3].ToObject<float>());
|
||
|
|
if (targetType == typeof(Color) && token is JArray arrC && arrC.Count >= 3) // Allow RGB or RGBA
|
||
|
|
return new Color(arrC[0].ToObject<float>(), arrC[1].ToObject<float>(), arrC[2].ToObject<float>(), arrC.Count > 3 ? arrC[3].ToObject<float>() : 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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Helper to find a Type by name, searching relevant assemblies.
|
||
|
|
/// Needed for creating ScriptableObjects or finding component types by name.
|
||
|
|
/// </summary>
|
||
|
|
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 ---
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Creates a serializable representation of an asset.
|
||
|
|
/// </summary>
|
||
|
|
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<UnityEngine.Object>(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?
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|