2025-04-08 18:14:13 +08:00
using System ;
using System.Collections.Generic ;
using System.Globalization ;
using System.IO ;
using System.Linq ;
using Newtonsoft.Json.Linq ;
using UnityEditor ;
using UnityEngine ;
2025-08-21 03:59:49 +08:00
using MCPForUnity.Editor.Helpers ; // For Response class
2025-09-04 00:12:44 +08:00
using static MCPForUnity . Editor . Tools . ManageGameObject ;
2025-04-08 18:14:13 +08:00
2025-07-14 02:33:06 +08:00
#if UNITY_6000_0_OR_NEWER
using PhysicsMaterialType = UnityEngine . PhysicsMaterial ;
using PhysicsMaterialCombine = UnityEngine . PhysicsMaterialCombine ;
# else
using PhysicsMaterialType = UnityEngine . PhysicMaterial ;
using PhysicsMaterialCombine = UnityEngine . PhysicMaterialCombine ;
# endif
2025-08-21 03:59:49 +08:00
namespace MCPForUnity.Editor.Tools
2025-04-08 18:14:13 +08:00
{
/// <summary>
/// Handles asset management operations within the Unity project.
/// </summary>
2025-10-04 06:53:09 +08:00
[McpForUnityTool("manage_asset")]
2025-04-08 18:14:13 +08:00
public static class ManageAsset
{
// --- Main Handler ---
// 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" ,
} ;
public static object HandleCommand ( JObject @params )
{
string action = @params [ "action" ] ? . ToString ( ) . ToLower ( ) ;
if ( string . IsNullOrEmpty ( action ) )
{
return Response . Error ( "Action parameter is required." ) ;
}
// Check if the action is valid before switching
if ( ! ValidActions . Contains ( action ) )
{
string validActionsList = string . Join ( ", " , ValidActions ) ;
return Response . Error (
$"Unknown action: '{action}'. Valid actions are: {validActionsList}"
) ;
}
// Common parameters
string path = @params [ "path" ] ? . ToString ( ) ;
try
{
switch ( action )
{
case "import" :
// Note: Unity typically auto-imports. This might re-import or configure import settings.
return ReimportAsset ( path , @params [ "properties" ] as JObject ) ;
case "create" :
return CreateAsset ( @params ) ;
case "modify" :
return ModifyAsset ( path , @params [ "properties" ] as JObject ) ;
case "delete" :
return DeleteAsset ( path ) ;
case "duplicate" :
return DuplicateAsset ( path , @params [ "destination" ] ? . ToString ( ) ) ;
case "move" : // Often same as rename if within Assets/
case "rename" :
return MoveOrRenameAsset ( path , @params [ "destination" ] ? . ToString ( ) ) ;
case "search" :
return SearchAssets ( @params ) ;
case "get_info" :
return GetAssetInfo (
path ,
@params [ "generatePreview" ] ? . ToObject < bool > ( ) ? ? 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." ) ;
2025-09-27 07:28:56 +08:00
string fullPath = AssetPathUtility . SanitizeAssetPath ( path ) ;
2025-04-08 18:14:13 +08:00
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." ) ;
2025-09-27 07:28:56 +08:00
string fullPath = AssetPathUtility . SanitizeAssetPath ( path ) ;
2025-04-08 18:14:13 +08:00
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" )
{
2025-09-04 08:11:30 +08:00
// 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 ) ;
2025-04-08 18:14:13 +08:00
if ( properties ! = null )
ApplyMaterialProperties ( mat , properties ) ;
AssetDatabase . CreateAsset ( mat , fullPath ) ;
newAsset = mat ;
}
2025-05-22 17:23:12 +08:00
else if ( lowerAssetType = = "physicsmaterial" )
{
2025-07-14 02:33:06 +08:00
PhysicsMaterialType pmat = new PhysicsMaterialType ( ) ;
2025-05-22 17:23:12 +08:00
if ( properties ! = null )
ApplyPhysicsMaterialProperties ( pmat , properties ) ;
AssetDatabase . CreateAsset ( pmat , fullPath ) ;
newAsset = pmat ;
}
2025-04-08 18:14:13 +08:00
else if ( lowerAssetType = = "scriptableobject" )
{
string scriptClassName = properties ? [ "scriptClass" ] ? . ToString ( ) ;
if ( string . IsNullOrEmpty ( scriptClassName ) )
return Response . Error (
"'scriptClass' property required when creating ScriptableObject asset."
) ;
2025-09-03 09:45:30 +08:00
Type scriptType = ComponentResolver . TryResolve ( scriptClassName , out var resolvedType , out var error ) ? resolvedType : null ;
2025-04-08 18:14:13 +08:00
if (
scriptType = = null
| | ! typeof ( ScriptableObject ) . IsAssignableFrom ( scriptType )
)
{
2025-09-03 23:27:34 +08:00
var reason = scriptType = = null
? ( string . IsNullOrEmpty ( error ) ? "Type not found." : error )
: "Type found but does not inherit from ScriptableObject." ;
return Response . Error ( $"Script class '{scriptClassName}' invalid: {reason}" ) ;
2025-04-08 18:14:13 +08:00
}
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." ) ;
2025-09-27 07:28:56 +08:00
string fullPath = AssetPathUtility . SanitizeAssetPath ( path ) ;
2025-04-08 18:14:13 +08:00
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." ) ;
2025-09-27 07:28:56 +08:00
string fullPath = AssetPathUtility . SanitizeAssetPath ( path ) ;
2025-04-08 18:14:13 +08:00
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 ; // 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}
{
2025-09-03 23:27:34 +08:00
// Resolve component type via ComponentResolver, then fetch by Type
Component targetComponent = null ;
2025-09-04 00:12:44 +08:00
bool resolved = ComponentResolver . TryResolve ( componentName , out var compType , out var compError ) ;
if ( resolved )
2025-09-03 23:27:34 +08:00
{
targetComponent = gameObject . GetComponent ( compType ) ;
}
2025-09-27 07:28:56 +08:00
2025-09-04 00:12:44 +08:00
// Only warn about resolution failure if component also not found
if ( targetComponent = = null & & ! resolved )
2025-09-03 23:27:34 +08:00
{
Debug . LogWarning (
$"[ManageAsset.ModifyAsset] Failed to resolve component '{componentName}' on '{gameObject.name}': {compError}"
) ;
}
2025-04-08 18:14:13 +08:00
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." ) ;
2025-09-27 07:28:56 +08:00
string fullPath = AssetPathUtility . SanitizeAssetPath ( path ) ;
2025-04-08 18:14:13 +08:00
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." ) ;
2025-09-27 07:28:56 +08:00
string sourcePath = AssetPathUtility . SanitizeAssetPath ( path ) ;
2025-04-08 18:14:13 +08:00
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
{
2025-09-27 07:28:56 +08:00
destPath = AssetPathUtility . SanitizeAssetPath ( destinationPath ) ;
2025-04-08 18:14:13 +08:00
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." ) ;
2025-09-27 07:28:56 +08:00
string sourcePath = AssetPathUtility . SanitizeAssetPath ( path ) ;
string destPath = AssetPathUtility . SanitizeAssetPath ( destinationPath ) ;
2025-04-08 18:14:13 +08:00
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 ) )
{
2025-09-27 07:28:56 +08:00
folderScope = new string [ ] { AssetPathUtility . SanitizeAssetPath ( pathScope ) } ;
2025-04-08 18:14:13 +08:00
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." ) ;
2025-09-27 07:28:56 +08:00
string fullPath = AssetPathUtility . SanitizeAssetPath ( path ) ;
2025-04-08 18:14:13 +08:00
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}" ) ;
}
}
/// <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
2025-09-27 07:28:56 +08:00
string fullPath = AssetPathUtility . SanitizeAssetPath ( path ) ;
2025-04-08 18:14:13 +08:00
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}"
) ;
}
}
// --- Internal Helpers ---
/// <summary>
/// Ensures the asset path starts with "Assets/".
/// </summary>
/// <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}"
) ;
}
}
2025-09-27 07:28:56 +08:00
}
else if ( properties [ "color" ] is JArray colorArr ) //Use color now with examples set in manage_asset.py
2025-07-07 07:51:12 +08:00
{
2025-09-27 07:28:56 +08:00
string propName = "_Color" ;
try
{
2025-07-07 07:51:12 +08:00
if ( colorArr . Count > = 3 )
{
Color newColor = new Color (
colorArr [ 0 ] . ToObject < float > ( ) ,
2025-09-27 07:28:56 +08:00
colorArr [ 1 ] . ToObject < float > ( ) ,
colorArr [ 2 ] . ToObject < float > ( ) ,
2025-07-07 07:51:12 +08:00
colorArr . Count > 3 ? colorArr [ 3 ] . ToObject < float > ( ) : 1.0f
) ;
if ( mat . HasProperty ( propName ) & & mat . GetColor ( propName ) ! = newColor )
{
mat . SetColor ( propName , newColor ) ;
modified = true ;
}
}
2025-09-27 07:28:56 +08:00
}
catch ( Exception ex )
{
2025-07-07 07:51:12 +08:00
Debug . LogWarning (
$"Error parsing color property '{propName}': {ex.Message}"
) ;
}
2025-04-08 18:14:13 +08:00
}
// Example: Set float property
if ( properties [ "float" ] is JObject floatProps )
{
string propName = floatProps [ "name" ] ? . ToString ( ) ;
if (
2025-09-03 23:27:34 +08:00
! string . IsNullOrEmpty ( propName ) & &
( floatProps [ "value" ] ? . Type = = JTokenType . Float | | floatProps [ "value" ] ? . Type = = JTokenType . Integer )
2025-04-08 18:14:13 +08:00
)
{
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 > (
2025-09-27 07:28:56 +08:00
AssetPathUtility . SanitizeAssetPath ( texPath )
2025-04-08 18:14:13 +08:00
) ;
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 ;
}
2025-05-22 19:48:16 +08:00
/// <summary>
/// Applies properties from JObject to a PhysicsMaterial.
/// </summary>
2025-07-14 02:33:06 +08:00
private static bool ApplyPhysicsMaterialProperties ( PhysicsMaterialType pmat , JObject properties )
2025-05-22 17:23:12 +08:00
{
if ( pmat = = null | | properties = = null )
return false ;
bool modified = false ;
// Example: Set dynamic friction
2025-07-14 02:33:06 +08:00
if ( properties [ "dynamicFriction" ] ? . Type = = JTokenType . Float )
2025-05-22 17:23:12 +08:00
{
2025-07-14 02:33:06 +08:00
float dynamicFriction = properties [ "dynamicFriction" ] . ToObject < float > ( ) ;
2025-05-22 17:23:12 +08:00
pmat . dynamicFriction = dynamicFriction ;
modified = true ;
}
// Example: Set static friction
2025-07-14 02:33:06 +08:00
if ( properties [ "staticFriction" ] ? . Type = = JTokenType . Float )
2025-05-22 17:23:12 +08:00
{
2025-07-14 02:33:06 +08:00
float staticFriction = properties [ "staticFriction" ] . ToObject < float > ( ) ;
2025-05-22 17:23:12 +08:00
pmat . staticFriction = staticFriction ;
modified = true ;
}
// Example: Set bounciness
2025-07-14 02:33:06 +08:00
if ( properties [ "bounciness" ] ? . Type = = JTokenType . Float )
2025-05-22 17:23:12 +08:00
{
2025-07-14 02:33:06 +08:00
float bounciness = properties [ "bounciness" ] . ToObject < float > ( ) ;
2025-05-22 17:23:12 +08:00
pmat . bounciness = bounciness ;
modified = true ;
}
2025-07-14 02:33:06 +08:00
List < String > averageList = new List < String > { "ave" , "Ave" , "average" , "Average" } ;
List < String > multiplyList = new List < String > { "mul" , "Mul" , "mult" , "Mult" , "multiply" , "Multiply" } ;
List < String > minimumList = new List < String > { "min" , "Min" , "minimum" , "Minimum" } ;
List < String > maximumList = new List < String > { "max" , "Max" , "maximum" , "Maximum" } ;
2025-05-22 17:23:12 +08:00
// Example: Set friction combine
2025-07-14 02:33:06 +08:00
if ( properties [ "frictionCombine" ] ? . Type = = JTokenType . String )
2025-05-22 17:23:12 +08:00
{
2025-07-14 02:33:06 +08:00
string frictionCombine = properties [ "frictionCombine" ] . ToString ( ) ;
2025-05-22 17:23:12 +08:00
if ( averageList . Contains ( frictionCombine ) )
pmat . frictionCombine = PhysicsMaterialCombine . Average ;
else if ( multiplyList . Contains ( frictionCombine ) )
pmat . frictionCombine = PhysicsMaterialCombine . Multiply ;
else if ( minimumList . Contains ( frictionCombine ) )
pmat . frictionCombine = PhysicsMaterialCombine . Minimum ;
else if ( maximumList . Contains ( frictionCombine ) )
pmat . frictionCombine = PhysicsMaterialCombine . Maximum ;
modified = true ;
}
// Example: Set bounce combine
2025-07-14 02:33:06 +08:00
if ( properties [ "bounceCombine" ] ? . Type = = JTokenType . String )
2025-05-22 17:23:12 +08:00
{
2025-07-14 02:33:06 +08:00
string bounceCombine = properties [ "bounceCombine" ] . ToString ( ) ;
2025-05-22 17:23:12 +08:00
if ( averageList . Contains ( bounceCombine ) )
pmat . bounceCombine = PhysicsMaterialCombine . Average ;
else if ( multiplyList . Contains ( bounceCombine ) )
pmat . bounceCombine = PhysicsMaterialCombine . Multiply ;
else if ( minimumList . Contains ( bounceCombine ) )
pmat . bounceCombine = PhysicsMaterialCombine . Minimum ;
else if ( maximumList . Contains ( bounceCombine ) )
pmat . bounceCombine = PhysicsMaterialCombine . Maximum ;
modified = true ;
}
return modified ;
}
2025-04-08 18:14:13 +08:00
/// <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
)
{
2025-09-27 07:28:56 +08:00
string assetPath = AssetPathUtility . SanitizeAssetPath ( token . ToString ( ) ) ;
2025-04-08 18:14:13 +08:00
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 ;
}
}
// --- 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
2025-09-04 08:11:30 +08:00
RenderTexture rt = null ;
Texture2D readablePreview = null ;
2025-04-08 18:14:13 +08:00
RenderTexture previous = RenderTexture . active ;
2025-09-04 08:11:30 +08:00
try
{
rt = RenderTexture . GetTemporary ( preview . width , preview . height ) ;
Graphics . Blit ( preview , rt ) ;
RenderTexture . active = rt ;
2025-09-04 08:58:05 +08:00
readablePreview = new Texture2D ( preview . width , preview . height , TextureFormat . RGB24 , false ) ;
2025-09-04 08:11:30 +08:00
readablePreview . ReadPixels ( new Rect ( 0 , 0 , rt . width , rt . height ) , 0 , 0 ) ;
readablePreview . Apply ( ) ;
2025-09-04 08:58:05 +08:00
var pngData = readablePreview . EncodeToPNG ( ) ;
if ( pngData ! = null & & pngData . Length > 0 )
{
previewBase64 = Convert . ToBase64String ( pngData ) ;
previewWidth = readablePreview . width ;
previewHeight = readablePreview . height ;
}
2025-09-04 08:11:30 +08:00
}
finally
{
RenderTexture . active = previous ;
if ( rt ! = null ) RenderTexture . ReleaseTemporary ( rt ) ;
2025-09-04 08:58:05 +08:00
if ( readablePreview ! = null ) UnityEngine . Object . DestroyImmediate ( readablePreview ) ;
2025-09-04 08:11:30 +08:00
}
2025-04-08 18:14:13 +08:00
}
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?
} ;
}
}
}