using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Codely.Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using UnityTcp.Editor.Helpers; // For Response class
using static UnityTcp.Editor.Tools.ManageGameObject;
#if UNITY_6000_0_OR_NEWER
using PhysicsMaterialType = UnityEngine.PhysicsMaterial;
using PhysicsMaterialCombine = UnityEngine.PhysicsMaterialCombine;
#else
using PhysicsMaterialType = UnityEngine.PhysicMaterial;
using PhysicsMaterialCombine = UnityEngine.PhysicMaterialCombine;
#endif
namespace UnityTcp.Editor.Tools
{
///
/// Handles asset management operations within the Unity project.
///
public static class ManageAsset
{
private const int MaxBatchOps = 10;
// --- Main Handler ---
// Define the list of valid actions
private static readonly List ValidActions = new List
{
"create_batch",
"edit_batch",
"ensure_has_meta",
"ensure_meta_integrity",
"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.");
}
// Normalize public aliases to canonical server actions (keep parity with TS client)
if (action == "import_asset")
{
action = "import";
@params["action"] = "import";
}
// Back-compat alias: assetType (camelCase) -> asset_type (snake_case)
if (@params["asset_type"] == null && @params["assetType"] != null)
{
@params["asset_type"] = @params["assetType"];
}
// Back-compat aliases: snake_case -> camelCase (keep parity with TS client schema)
if (@params["searchPattern"] == null && @params["search_pattern"] != null)
@params["searchPattern"] = @params["search_pattern"];
if (@params["filterType"] == null && @params["filter_type"] != null)
@params["filterType"] = @params["filter_type"];
if (@params["filterDateAfter"] == null && @params["filter_date_after"] != null)
@params["filterDateAfter"] = @params["filter_date_after"];
if (@params["pageSize"] == null && @params["page_size"] != null)
@params["pageSize"] = @params["page_size"];
if (@params["pageNumber"] == null && @params["page_number"] != null)
@params["pageNumber"] = @params["page_number"];
if (@params["generatePreview"] == null && @params["generate_preview"] != null)
@params["generatePreview"] = @params["generate_preview"];
// 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}"
);
}
// --- Validate client_state_rev for write operations ---
var writeActions = new[] { "create_batch", "edit_batch", "ensure_has_meta", "ensure_meta_integrity", "import", "create", "modify", "delete", "duplicate", "move", "rename", "create_folder" };
if (writeActions.Contains(action))
{
var revConflict = StateComposer.ValidateClientRevisionFromParams(@params);
if (revConflict != null) return revConflict;
}
// Common parameters
string path = @params["path"]?.ToString();
try
{
switch (action)
{
// Batch operations (strict)
case "create_batch":
return HandleCreateBatch(@params);
case "edit_batch":
return HandleEditBatch(@params);
// Ensure operations (idempotent)
case "ensure_has_meta":
return EnsureHasMeta(path);
case "ensure_meta_integrity":
return EnsureMetaIntegrity(path);
// Regular operations
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);
bool ghostDesync;
if (!AssetExists(fullPath, out ghostDesync))
return BuildAssetNotFoundResponse($"Asset not found at path: {fullPath}", fullPath, ghostDesync);
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();
// Support both 'assetType' (camelCase) and 'asset_type' (snake_case) for compatibility
string assetType = @params["assetType"]?.ToString() ?? @params["asset_type"]?.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")
{
// Prefer provided shader; fall back to common pipelines
var requested = properties?["shader"]?.ToString();
Shader shader =
(!string.IsNullOrEmpty(requested) ? Shader.Find(requested) : null)
?? Shader.Find("Universal Render Pipeline/Lit")
?? Shader.Find("HDRP/Lit")
?? Shader.Find("Standard")
?? Shader.Find("Unlit/Color");
if (shader == null)
return Response.Error($"Could not find a suitable shader (requested: '{requested ?? "none"}').");
var mat = new Material(shader);
if (properties != null)
ApplyMaterialProperties(mat, properties);
AssetDatabase.CreateAsset(mat, fullPath);
newAsset = mat;
}
else if (lowerAssetType == "physicsmaterial")
{
PhysicsMaterialType pmat = new PhysicsMaterialType();
if (properties != null)
ApplyPhysicsMaterialProperties(pmat, properties);
AssetDatabase.CreateAsset(pmat, fullPath);
newAsset = pmat;
}
else if (lowerAssetType == "scriptableobject")
{
string scriptClassName = properties?["scriptClass"]?.ToString();
if (string.IsNullOrEmpty(scriptClassName))
return Response.Error(
"'scriptClass' property required when creating ScriptableObject asset."
);
// NOTE:
// Previously this used ComponentResolver.TryResolve, which is intentionally limited to
// Component/MonoBehaviour types. That meant any ScriptableObject type (including custom ones)
// would always fail to resolve, even after a successful compilation / domain reload.
//
// Here we use a dedicated resolver that searches for ScriptableObject-derived types instead.
string resolveError;
Type scriptType = ResolveScriptableObjectType(scriptClassName, out resolveError);
if (scriptType == null)
{
var reason = string.IsNullOrEmpty(resolveError)
? "Type not found."
: resolveError;
return Response.Error(
$"Script class '{scriptClassName}' invalid: {reason}"
);
}
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}");
}
}
///
/// Resolve a ScriptableObject type by short or fully-qualified name.
/// Searches loaded assemblies and ensures the type derives from ScriptableObject.
/// Does NOT rely on ComponentResolver (which is Component/MonoBehaviour-specific).
///
private static Type ResolveScriptableObjectType(string nameOrFullName, out string error)
{
error = string.Empty;
if (string.IsNullOrWhiteSpace(nameOrFullName))
{
error = "scriptClass cannot be null or empty.";
return null;
}
// 1) Direct Type.GetType lookup (works for fully-qualified names with assembly, or some common cases)
Type type = Type.GetType(nameOrFullName, throwOnError: false);
if (IsValidScriptableObject(type))
{
return type;
}
// 2) Search all loaded assemblies, preferring Player (runtime) assemblies when available
var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
#if UNITY_EDITOR
var playerAsmNames = new HashSet(
UnityEditor.Compilation.CompilationPipeline
.GetAssemblies(UnityEditor.Compilation.AssembliesType.Player)
.Select(a => a.name),
StringComparer.Ordinal
);
IEnumerable playerAsms =
loadedAssemblies.Where(a => playerAsmNames.Contains(a.GetName().Name));
IEnumerable editorAsms =
loadedAssemblies.Except(playerAsms);
#else
IEnumerable playerAsms = loadedAssemblies;
IEnumerable editorAsms =
Array.Empty();
#endif
static IEnumerable SafeGetTypes(System.Reflection.Assembly a)
{
try
{
return a.GetTypes();
}
catch (System.Reflection.ReflectionTypeLoadException rtle)
{
return rtle.Types.Where(t => t != null)!;
}
}
bool isShortName = !nameOrFullName.Contains(".");
Func match = isShortName
? t => t.Name.Equals(nameOrFullName, StringComparison.Ordinal)
: t => t.FullName != null
&& t.FullName.Equals(nameOrFullName, StringComparison.Ordinal);
var fromPlayer = playerAsms
.SelectMany(SafeGetTypes)
.Where(IsValidScriptableObject)
.Where(match);
var fromEditor = editorAsms
.SelectMany(SafeGetTypes)
.Where(IsValidScriptableObject)
.Where(match);
var candidates = new List(fromPlayer);
if (candidates.Count == 0)
{
candidates.AddRange(fromEditor);
}
if (candidates.Count == 1)
{
return candidates[0];
}
if (candidates.Count > 1)
{
var lines = candidates.Select(
t => $"{t.FullName} (assembly {t.Assembly.GetName().Name})"
);
error =
$"Multiple ScriptableObject types matched '{nameOrFullName}':\n - "
+ string.Join("\n - ", lines)
+ "\nProvide a fully qualified type name (Namespace.TypeName) to disambiguate.";
return null;
}
error =
$"ScriptableObject type '{nameOrFullName}' not found in loaded assemblies. "
+ "Use a fully-qualified name (Namespace.TypeName) and ensure the script compiled.";
return null;
}
private static bool IsValidScriptableObject(Type t) =>
t != null && typeof(ScriptableObject).IsAssignableFrom(t);
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);
bool ghostDesync;
if (!AssetExists(fullPath, out ghostDesync))
return BuildAssetNotFoundResponse($"Asset not found at path: {fullPath}", fullPath, ghostDesync);
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}
{
// Resolve component type via ComponentResolver, then fetch by Type
Component targetComponent = null;
bool resolved = ComponentResolver.TryResolve(componentName, out var compType, out var compError);
if (resolved)
{
targetComponent = gameObject.GetComponent(compType);
}
// Only warn about resolution failure if component also not found
if (targetComponent == null && !resolved)
{
Debug.LogWarning(
$"[ManageAsset.ModifyAsset] Failed to resolve component '{componentName}' on '{gameObject.name}': {compError}"
);
}
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);
bool ghostDesync;
if (!AssetExists(fullPath, out ghostDesync))
return BuildAssetNotFoundResponse($"Asset not found at path: {fullPath}", fullPath, ghostDesync);
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);
bool ghostDesync;
if (!AssetExists(sourcePath, out ghostDesync))
return BuildAssetNotFoundResponse($"Source asset not found at path: {sourcePath}", sourcePath, ghostDesync);
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);
bool ghostDesync;
if (!AssetExists(sourcePath, out ghostDesync))
return BuildAssetNotFoundResponse($"Source asset not found at path: {sourcePath}", sourcePath, ghostDesync);
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, empty string if successful
string validateError = AssetDatabase.ValidateMoveAsset(sourcePath, destPath);
if (!string.IsNullOrEmpty(validateError))
{
return Response.Error(
$"Failed to move/rename asset from '{sourcePath}' to '{destPath}': {validateError}"
);
}
// MoveAsset returns an empty string on success, or an error message on failure
string moveError = AssetDatabase.MoveAsset(sourcePath, destPath);
if (string.IsNullOrEmpty(moveError))
{
// AssetDatabase.Refresh(); // MoveAsset usually handles refresh
return Response.Success(
$"Asset moved/renamed from '{sourcePath}' to '{destPath}'.",
GetAssetData(destPath)
);
}
else
{
return Response.Error(
$"Failed to move/rename asset from '{sourcePath}' to '{destPath}': {moveError}"
);
}
}
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