2025-03-31 03:58:01 +08:00
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 ---
2025-03-31 22:49:35 +08:00
// Define the list of valid actions
private static readonly List < string > ValidActions = new List < string >
{
"import" , "create" , "modify" , "delete" , "duplicate" ,
"move" , "rename" , "search" , "get_info" , "create_folder" ,
"get_components"
} ;
2025-03-31 03:58:01 +08:00
public static object HandleCommand ( JObject @params )
{
string action = @params [ "action" ] ? . ToString ( ) . ToLower ( ) ;
if ( string . IsNullOrEmpty ( action ) )
{
return Response . Error ( "Action parameter is required." ) ;
}
2025-03-31 22:49:35 +08:00
// 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}" ) ;
}
2025-03-31 03:58:01 +08:00
// 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 ) ;
2025-03-31 22:49:35 +08:00
case "get_components" :
return GetComponentsFromAsset ( path ) ;
2025-03-31 03:58:01 +08:00
default :
2025-03-31 22:49:35 +08:00
// 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}" ) ;
2025-03-31 03:58:01 +08:00
}
}
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}" ) ;
2025-03-31 22:49:35 +08:00
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) ---
2025-03-31 03:58:01 +08:00
// Example: Modifying a Material
2025-03-31 22:49:35 +08:00
else if ( asset is Material material )
2025-03-31 03:58:01 +08:00
{
2025-03-31 22:49:35 +08:00
// 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 ) ;
2025-03-31 03:58:01 +08:00
}
2025-03-31 22:49:35 +08:00
// Example: Modifying a ScriptableObject
2025-03-31 03:58:01 +08:00
else if ( asset is ScriptableObject so )
{
2025-03-31 22:49:35 +08:00
// Apply properties directly to the ScriptableObject.
modified | = ApplyObjectProperties ( so , properties ) ; // General helper
2025-03-31 03:58:01 +08:00
}
// Example: Modifying TextureImporter settings
else if ( asset is Texture ) {
AssetImporter importer = AssetImporter . GetAtPath ( fullPath ) ;
if ( importer is TextureImporter textureImporter )
{
2025-03-31 22:49:35 +08:00
bool importerModified = ApplyObjectProperties ( textureImporter , properties ) ;
if ( importerModified ) {
// Importer settings need saving and reimporting
2025-03-31 03:58:01 +08:00
AssetDatabase . WriteImportSettingsIfDirty ( fullPath ) ;
AssetDatabase . ImportAsset ( fullPath , ImportAssetOptions . ForceUpdate ) ; // Reimport to apply changes
2025-03-31 22:49:35 +08:00
modified = true ; // Mark overall operation as modified
2025-03-31 03:58:01 +08:00
}
}
else {
Debug . LogWarning ( $"Could not get TextureImporter for {fullPath}." ) ;
}
}
// TODO: Add modification logic for other common asset types (Models, AudioClips importers, etc.)
2025-03-31 22:49:35 +08:00
else // Fallback for other asset types OR direct properties on non-GameObject assets
2025-03-31 03:58:01 +08:00
{
2025-03-31 22:49:35 +08:00
// 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 ) ;
2025-03-31 03:58:01 +08:00
}
2025-03-31 22:49:35 +08:00
// --- End Existing Logic ---
2025-03-31 03:58:01 +08:00
2025-03-31 22:49:35 +08:00
// Check if any modification happened (either component or direct asset modification)
2025-03-31 03:58:01 +08:00
if ( modified )
2025-03-31 22:49:35 +08:00
{
// 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();
2025-03-31 03:58:01 +08:00
return Response . Success ( $"Asset '{fullPath}' modified successfully." , GetAssetData ( fullPath ) ) ;
} else {
2025-03-31 22:49:35 +08:00
// 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));
2025-03-31 03:58:01 +08:00
}
}
catch ( Exception e )
{
2025-03-31 22:49:35 +08:00
// 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}" ) ;
2025-03-31 03:58:01 +08:00
}
}
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}" ) ;
}
}
2025-03-31 22:49:35 +08:00
/// <summary>
/// Retrieves components attached to a GameObject asset (like a Prefab).
/// </summary>
/// <param name="path">The asset path of the GameObject or Prefab.</param>
/// <returns>A response object containing a list of component type names or an error.</returns>
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 < UnityEngine . Object > ( 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 < Component > ( ) ;
// 6. Format component data
List < object > 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 < object > ( ) ; // 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}" ) ;
}
}
2025-03-31 03:58:01 +08:00
// --- 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?
} ;
}
}
}