This is page 12 of 18. Use http://codebase.md/justinpbarnett/unity-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .claude │ ├── prompts │ │ ├── nl-unity-suite-nl.md │ │ └── nl-unity-suite-t.md │ └── settings.json ├── .github │ ├── scripts │ │ └── mark_skipped.py │ └── workflows │ ├── bump-version.yml │ ├── claude-nl-suite.yml │ ├── github-repo-stats.yml │ └── unity-tests.yml ├── .gitignore ├── deploy-dev.bat ├── docs │ ├── CURSOR_HELP.md │ ├── CUSTOM_TOOLS.md │ ├── README-DEV-zh.md │ ├── README-DEV.md │ ├── screenshots │ │ ├── v5_01_uninstall.png │ │ ├── v5_02_install.png │ │ ├── v5_03_open_mcp_window.png │ │ ├── v5_04_rebuild_mcp_server.png │ │ ├── v5_05_rebuild_success.png │ │ ├── v6_2_create_python_tools_asset.png │ │ ├── v6_2_python_tools_asset.png │ │ ├── v6_new_ui_asset_store_version.png │ │ ├── v6_new_ui_dark.png │ │ └── v6_new_ui_light.png │ ├── TELEMETRY.md │ ├── v5_MIGRATION.md │ └── v6_NEW_UI_CHANGES.md ├── LICENSE ├── logo.png ├── mcp_source.py ├── MCPForUnity │ ├── Editor │ │ ├── AssemblyInfo.cs │ │ ├── AssemblyInfo.cs.meta │ │ ├── Data │ │ │ ├── DefaultServerConfig.cs │ │ │ ├── DefaultServerConfig.cs.meta │ │ │ ├── McpClients.cs │ │ │ ├── McpClients.cs.meta │ │ │ ├── PythonToolsAsset.cs │ │ │ └── PythonToolsAsset.cs.meta │ │ ├── Data.meta │ │ ├── Dependencies │ │ │ ├── DependencyManager.cs │ │ │ ├── DependencyManager.cs.meta │ │ │ ├── Models │ │ │ │ ├── DependencyCheckResult.cs │ │ │ │ ├── DependencyCheckResult.cs.meta │ │ │ │ ├── DependencyStatus.cs │ │ │ │ └── DependencyStatus.cs.meta │ │ │ ├── Models.meta │ │ │ ├── PlatformDetectors │ │ │ │ ├── IPlatformDetector.cs │ │ │ │ ├── IPlatformDetector.cs.meta │ │ │ │ ├── LinuxPlatformDetector.cs │ │ │ │ ├── LinuxPlatformDetector.cs.meta │ │ │ │ ├── MacOSPlatformDetector.cs │ │ │ │ ├── MacOSPlatformDetector.cs.meta │ │ │ │ ├── PlatformDetectorBase.cs │ │ │ │ ├── PlatformDetectorBase.cs.meta │ │ │ │ ├── WindowsPlatformDetector.cs │ │ │ │ └── WindowsPlatformDetector.cs.meta │ │ │ └── PlatformDetectors.meta │ │ ├── Dependencies.meta │ │ ├── External │ │ │ ├── Tommy.cs │ │ │ └── Tommy.cs.meta │ │ ├── External.meta │ │ ├── Helpers │ │ │ ├── AssetPathUtility.cs │ │ │ ├── AssetPathUtility.cs.meta │ │ │ ├── CodexConfigHelper.cs │ │ │ ├── CodexConfigHelper.cs.meta │ │ │ ├── ConfigJsonBuilder.cs │ │ │ ├── ConfigJsonBuilder.cs.meta │ │ │ ├── ExecPath.cs │ │ │ ├── ExecPath.cs.meta │ │ │ ├── GameObjectSerializer.cs │ │ │ ├── GameObjectSerializer.cs.meta │ │ │ ├── McpConfigFileHelper.cs │ │ │ ├── McpConfigFileHelper.cs.meta │ │ │ ├── McpConfigurationHelper.cs │ │ │ ├── McpConfigurationHelper.cs.meta │ │ │ ├── McpLog.cs │ │ │ ├── McpLog.cs.meta │ │ │ ├── McpPathResolver.cs │ │ │ ├── McpPathResolver.cs.meta │ │ │ ├── PackageDetector.cs │ │ │ ├── PackageDetector.cs.meta │ │ │ ├── PackageInstaller.cs │ │ │ ├── PackageInstaller.cs.meta │ │ │ ├── PortManager.cs │ │ │ ├── PortManager.cs.meta │ │ │ ├── PythonToolSyncProcessor.cs │ │ │ ├── PythonToolSyncProcessor.cs.meta │ │ │ ├── Response.cs │ │ │ ├── Response.cs.meta │ │ │ ├── ServerInstaller.cs │ │ │ ├── ServerInstaller.cs.meta │ │ │ ├── ServerPathResolver.cs │ │ │ ├── ServerPathResolver.cs.meta │ │ │ ├── TelemetryHelper.cs │ │ │ ├── TelemetryHelper.cs.meta │ │ │ ├── Vector3Helper.cs │ │ │ └── Vector3Helper.cs.meta │ │ ├── Helpers.meta │ │ ├── Importers │ │ │ ├── PythonFileImporter.cs │ │ │ └── PythonFileImporter.cs.meta │ │ ├── Importers.meta │ │ ├── MCPForUnity.Editor.asmdef │ │ ├── MCPForUnity.Editor.asmdef.meta │ │ ├── MCPForUnityBridge.cs │ │ ├── MCPForUnityBridge.cs.meta │ │ ├── Models │ │ │ ├── Command.cs │ │ │ ├── Command.cs.meta │ │ │ ├── McpClient.cs │ │ │ ├── McpClient.cs.meta │ │ │ ├── McpConfig.cs │ │ │ ├── McpConfig.cs.meta │ │ │ ├── MCPConfigServer.cs │ │ │ ├── MCPConfigServer.cs.meta │ │ │ ├── MCPConfigServers.cs │ │ │ ├── MCPConfigServers.cs.meta │ │ │ ├── McpStatus.cs │ │ │ ├── McpStatus.cs.meta │ │ │ ├── McpTypes.cs │ │ │ ├── McpTypes.cs.meta │ │ │ ├── ServerConfig.cs │ │ │ └── ServerConfig.cs.meta │ │ ├── Models.meta │ │ ├── Resources │ │ │ ├── McpForUnityResourceAttribute.cs │ │ │ ├── McpForUnityResourceAttribute.cs.meta │ │ │ ├── MenuItems │ │ │ │ ├── GetMenuItems.cs │ │ │ │ └── GetMenuItems.cs.meta │ │ │ ├── MenuItems.meta │ │ │ ├── Tests │ │ │ │ ├── GetTests.cs │ │ │ │ └── GetTests.cs.meta │ │ │ └── Tests.meta │ │ ├── Resources.meta │ │ ├── Services │ │ │ ├── BridgeControlService.cs │ │ │ ├── BridgeControlService.cs.meta │ │ │ ├── ClientConfigurationService.cs │ │ │ ├── ClientConfigurationService.cs.meta │ │ │ ├── IBridgeControlService.cs │ │ │ ├── IBridgeControlService.cs.meta │ │ │ ├── IClientConfigurationService.cs │ │ │ ├── IClientConfigurationService.cs.meta │ │ │ ├── IPackageUpdateService.cs │ │ │ ├── IPackageUpdateService.cs.meta │ │ │ ├── IPathResolverService.cs │ │ │ ├── IPathResolverService.cs.meta │ │ │ ├── IPythonToolRegistryService.cs │ │ │ ├── IPythonToolRegistryService.cs.meta │ │ │ ├── ITestRunnerService.cs │ │ │ ├── ITestRunnerService.cs.meta │ │ │ ├── IToolSyncService.cs │ │ │ ├── IToolSyncService.cs.meta │ │ │ ├── MCPServiceLocator.cs │ │ │ ├── MCPServiceLocator.cs.meta │ │ │ ├── PackageUpdateService.cs │ │ │ ├── PackageUpdateService.cs.meta │ │ │ ├── PathResolverService.cs │ │ │ ├── PathResolverService.cs.meta │ │ │ ├── PythonToolRegistryService.cs │ │ │ ├── PythonToolRegistryService.cs.meta │ │ │ ├── TestRunnerService.cs │ │ │ ├── TestRunnerService.cs.meta │ │ │ ├── ToolSyncService.cs │ │ │ └── ToolSyncService.cs.meta │ │ ├── Services.meta │ │ ├── Setup │ │ │ ├── SetupWizard.cs │ │ │ ├── SetupWizard.cs.meta │ │ │ ├── SetupWizardWindow.cs │ │ │ └── SetupWizardWindow.cs.meta │ │ ├── Setup.meta │ │ ├── Tools │ │ │ ├── CommandRegistry.cs │ │ │ ├── CommandRegistry.cs.meta │ │ │ ├── ExecuteMenuItem.cs │ │ │ ├── ExecuteMenuItem.cs.meta │ │ │ ├── ManageAsset.cs │ │ │ ├── ManageAsset.cs.meta │ │ │ ├── ManageEditor.cs │ │ │ ├── ManageEditor.cs.meta │ │ │ ├── ManageGameObject.cs │ │ │ ├── ManageGameObject.cs.meta │ │ │ ├── ManageScene.cs │ │ │ ├── ManageScene.cs.meta │ │ │ ├── ManageScript.cs │ │ │ ├── ManageScript.cs.meta │ │ │ ├── ManageShader.cs │ │ │ ├── ManageShader.cs.meta │ │ │ ├── McpForUnityToolAttribute.cs │ │ │ ├── McpForUnityToolAttribute.cs.meta │ │ │ ├── Prefabs │ │ │ │ ├── ManagePrefabs.cs │ │ │ │ └── ManagePrefabs.cs.meta │ │ │ ├── Prefabs.meta │ │ │ ├── ReadConsole.cs │ │ │ ├── ReadConsole.cs.meta │ │ │ ├── RunTests.cs │ │ │ └── RunTests.cs.meta │ │ ├── Tools.meta │ │ ├── Windows │ │ │ ├── ManualConfigEditorWindow.cs │ │ │ ├── ManualConfigEditorWindow.cs.meta │ │ │ ├── MCPForUnityEditorWindow.cs │ │ │ ├── MCPForUnityEditorWindow.cs.meta │ │ │ ├── MCPForUnityEditorWindowNew.cs │ │ │ ├── MCPForUnityEditorWindowNew.cs.meta │ │ │ ├── MCPForUnityEditorWindowNew.uss │ │ │ ├── MCPForUnityEditorWindowNew.uss.meta │ │ │ ├── MCPForUnityEditorWindowNew.uxml │ │ │ ├── MCPForUnityEditorWindowNew.uxml.meta │ │ │ ├── VSCodeManualSetupWindow.cs │ │ │ └── VSCodeManualSetupWindow.cs.meta │ │ └── Windows.meta │ ├── Editor.meta │ ├── package.json │ ├── package.json.meta │ ├── README.md │ ├── README.md.meta │ ├── Runtime │ │ ├── MCPForUnity.Runtime.asmdef │ │ ├── MCPForUnity.Runtime.asmdef.meta │ │ ├── Serialization │ │ │ ├── UnityTypeConverters.cs │ │ │ └── UnityTypeConverters.cs.meta │ │ └── Serialization.meta │ ├── Runtime.meta │ └── UnityMcpServer~ │ └── src │ ├── __init__.py │ ├── config.py │ ├── Dockerfile │ ├── models.py │ ├── module_discovery.py │ ├── port_discovery.py │ ├── pyproject.toml │ ├── pyrightconfig.json │ ├── registry │ │ ├── __init__.py │ │ ├── resource_registry.py │ │ └── tool_registry.py │ ├── reload_sentinel.py │ ├── resources │ │ ├── __init__.py │ │ ├── menu_items.py │ │ └── tests.py │ ├── server_version.txt │ ├── server.py │ ├── telemetry_decorator.py │ ├── telemetry.py │ ├── test_telemetry.py │ ├── tools │ │ ├── __init__.py │ │ ├── execute_menu_item.py │ │ ├── manage_asset.py │ │ ├── manage_editor.py │ │ ├── manage_gameobject.py │ │ ├── manage_prefabs.py │ │ ├── manage_scene.py │ │ ├── manage_script.py │ │ ├── manage_shader.py │ │ ├── read_console.py │ │ ├── resource_tools.py │ │ ├── run_tests.py │ │ └── script_apply_edits.py │ ├── unity_connection.py │ └── uv.lock ├── prune_tool_results.py ├── README-zh.md ├── README.md ├── restore-dev.bat ├── scripts │ └── validate-nlt-coverage.sh ├── test_unity_socket_framing.py ├── TestProjects │ └── UnityMCPTests │ ├── .gitignore │ ├── Assets │ │ ├── Editor.meta │ │ ├── Scenes │ │ │ ├── SampleScene.unity │ │ │ └── SampleScene.unity.meta │ │ ├── Scenes.meta │ │ ├── Scripts │ │ │ ├── Hello.cs │ │ │ ├── Hello.cs.meta │ │ │ ├── LongUnityScriptClaudeTest.cs │ │ │ ├── LongUnityScriptClaudeTest.cs.meta │ │ │ ├── TestAsmdef │ │ │ │ ├── CustomComponent.cs │ │ │ │ ├── CustomComponent.cs.meta │ │ │ │ ├── TestAsmdef.asmdef │ │ │ │ └── TestAsmdef.asmdef.meta │ │ │ └── TestAsmdef.meta │ │ ├── Scripts.meta │ │ ├── Tests │ │ │ ├── EditMode │ │ │ │ ├── Data │ │ │ │ │ ├── PythonToolsAssetTests.cs │ │ │ │ │ └── PythonToolsAssetTests.cs.meta │ │ │ │ ├── Data.meta │ │ │ │ ├── Helpers │ │ │ │ │ ├── CodexConfigHelperTests.cs │ │ │ │ │ ├── CodexConfigHelperTests.cs.meta │ │ │ │ │ ├── WriteToConfigTests.cs │ │ │ │ │ └── WriteToConfigTests.cs.meta │ │ │ │ ├── Helpers.meta │ │ │ │ ├── MCPForUnityTests.Editor.asmdef │ │ │ │ ├── MCPForUnityTests.Editor.asmdef.meta │ │ │ │ ├── Resources │ │ │ │ │ ├── GetMenuItemsTests.cs │ │ │ │ │ └── GetMenuItemsTests.cs.meta │ │ │ │ ├── Resources.meta │ │ │ │ ├── Services │ │ │ │ │ ├── PackageUpdateServiceTests.cs │ │ │ │ │ ├── PackageUpdateServiceTests.cs.meta │ │ │ │ │ ├── PythonToolRegistryServiceTests.cs │ │ │ │ │ ├── PythonToolRegistryServiceTests.cs.meta │ │ │ │ │ ├── ToolSyncServiceTests.cs │ │ │ │ │ └── ToolSyncServiceTests.cs.meta │ │ │ │ ├── Services.meta │ │ │ │ ├── Tools │ │ │ │ │ ├── AIPropertyMatchingTests.cs │ │ │ │ │ ├── AIPropertyMatchingTests.cs.meta │ │ │ │ │ ├── CommandRegistryTests.cs │ │ │ │ │ ├── CommandRegistryTests.cs.meta │ │ │ │ │ ├── ComponentResolverTests.cs │ │ │ │ │ ├── ComponentResolverTests.cs.meta │ │ │ │ │ ├── ExecuteMenuItemTests.cs │ │ │ │ │ ├── ExecuteMenuItemTests.cs.meta │ │ │ │ │ ├── ManageGameObjectTests.cs │ │ │ │ │ ├── ManageGameObjectTests.cs.meta │ │ │ │ │ ├── ManagePrefabsTests.cs │ │ │ │ │ ├── ManagePrefabsTests.cs.meta │ │ │ │ │ ├── ManageScriptValidationTests.cs │ │ │ │ │ └── ManageScriptValidationTests.cs.meta │ │ │ │ ├── Tools.meta │ │ │ │ ├── Windows │ │ │ │ │ ├── ManualConfigJsonBuilderTests.cs │ │ │ │ │ └── ManualConfigJsonBuilderTests.cs.meta │ │ │ │ └── Windows.meta │ │ │ └── EditMode.meta │ │ └── Tests.meta │ ├── Packages │ │ └── manifest.json │ └── ProjectSettings │ ├── Packages │ │ └── com.unity.testtools.codecoverage │ │ └── Settings.json │ └── ProjectVersion.txt ├── tests │ ├── conftest.py │ ├── test_edit_normalization_and_noop.py │ ├── test_edit_strict_and_warnings.py │ ├── test_find_in_file_minimal.py │ ├── test_get_sha.py │ ├── test_improved_anchor_matching.py │ ├── test_logging_stdout.py │ ├── test_manage_script_uri.py │ ├── test_read_console_truncate.py │ ├── test_read_resource_minimal.py │ ├── test_resources_api.py │ ├── test_script_editing.py │ ├── test_script_tools.py │ ├── test_telemetry_endpoint_validation.py │ ├── test_telemetry_queue_worker.py │ ├── test_telemetry_subaction.py │ ├── test_transport_framing.py │ └── test_validate_script_summary.py ├── tools │ └── stress_mcp.py └── UnityMcpBridge ├── Editor │ ├── AssemblyInfo.cs │ ├── AssemblyInfo.cs.meta │ ├── Data │ │ ├── DefaultServerConfig.cs │ │ ├── DefaultServerConfig.cs.meta │ │ ├── McpClients.cs │ │ └── McpClients.cs.meta │ ├── Data.meta │ ├── Dependencies │ │ ├── DependencyManager.cs │ │ ├── DependencyManager.cs.meta │ │ ├── Models │ │ │ ├── DependencyCheckResult.cs │ │ │ ├── DependencyCheckResult.cs.meta │ │ │ ├── DependencyStatus.cs │ │ │ └── DependencyStatus.cs.meta │ │ ├── Models.meta │ │ ├── PlatformDetectors │ │ │ ├── IPlatformDetector.cs │ │ │ ├── IPlatformDetector.cs.meta │ │ │ ├── LinuxPlatformDetector.cs │ │ │ ├── LinuxPlatformDetector.cs.meta │ │ │ ├── MacOSPlatformDetector.cs │ │ │ ├── MacOSPlatformDetector.cs.meta │ │ │ ├── PlatformDetectorBase.cs │ │ │ ├── PlatformDetectorBase.cs.meta │ │ │ ├── WindowsPlatformDetector.cs │ │ │ └── WindowsPlatformDetector.cs.meta │ │ └── PlatformDetectors.meta │ ├── Dependencies.meta │ ├── External │ │ ├── Tommy.cs │ │ └── Tommy.cs.meta │ ├── External.meta │ ├── Helpers │ │ ├── AssetPathUtility.cs │ │ ├── AssetPathUtility.cs.meta │ │ ├── CodexConfigHelper.cs │ │ ├── CodexConfigHelper.cs.meta │ │ ├── ConfigJsonBuilder.cs │ │ ├── ConfigJsonBuilder.cs.meta │ │ ├── ExecPath.cs │ │ ├── ExecPath.cs.meta │ │ ├── GameObjectSerializer.cs │ │ ├── GameObjectSerializer.cs.meta │ │ ├── McpConfigFileHelper.cs │ │ ├── McpConfigFileHelper.cs.meta │ │ ├── McpConfigurationHelper.cs │ │ ├── McpConfigurationHelper.cs.meta │ │ ├── McpLog.cs │ │ ├── McpLog.cs.meta │ │ ├── McpPathResolver.cs │ │ ├── McpPathResolver.cs.meta │ │ ├── PackageDetector.cs │ │ ├── PackageDetector.cs.meta │ │ ├── PackageInstaller.cs │ │ ├── PackageInstaller.cs.meta │ │ ├── PortManager.cs │ │ ├── PortManager.cs.meta │ │ ├── Response.cs │ │ ├── Response.cs.meta │ │ ├── ServerInstaller.cs │ │ ├── ServerInstaller.cs.meta │ │ ├── ServerPathResolver.cs │ │ ├── ServerPathResolver.cs.meta │ │ ├── TelemetryHelper.cs │ │ ├── TelemetryHelper.cs.meta │ │ ├── Vector3Helper.cs │ │ └── Vector3Helper.cs.meta │ ├── Helpers.meta │ ├── MCPForUnity.Editor.asmdef │ ├── MCPForUnity.Editor.asmdef.meta │ ├── MCPForUnityBridge.cs │ ├── MCPForUnityBridge.cs.meta │ ├── Models │ │ ├── Command.cs │ │ ├── Command.cs.meta │ │ ├── McpClient.cs │ │ ├── McpClient.cs.meta │ │ ├── McpConfig.cs │ │ ├── McpConfig.cs.meta │ │ ├── MCPConfigServer.cs │ │ ├── MCPConfigServer.cs.meta │ │ ├── MCPConfigServers.cs │ │ ├── MCPConfigServers.cs.meta │ │ ├── McpStatus.cs │ │ ├── McpStatus.cs.meta │ │ ├── McpTypes.cs │ │ ├── McpTypes.cs.meta │ │ ├── ServerConfig.cs │ │ └── ServerConfig.cs.meta │ ├── Models.meta │ ├── Setup │ │ ├── SetupWizard.cs │ │ ├── SetupWizard.cs.meta │ │ ├── SetupWizardWindow.cs │ │ └── SetupWizardWindow.cs.meta │ ├── Setup.meta │ ├── Tools │ │ ├── CommandRegistry.cs │ │ ├── CommandRegistry.cs.meta │ │ ├── ManageAsset.cs │ │ ├── ManageAsset.cs.meta │ │ ├── ManageEditor.cs │ │ ├── ManageEditor.cs.meta │ │ ├── ManageGameObject.cs │ │ ├── ManageGameObject.cs.meta │ │ ├── ManageScene.cs │ │ ├── ManageScene.cs.meta │ │ ├── ManageScript.cs │ │ ├── ManageScript.cs.meta │ │ ├── ManageShader.cs │ │ ├── ManageShader.cs.meta │ │ ├── McpForUnityToolAttribute.cs │ │ ├── McpForUnityToolAttribute.cs.meta │ │ ├── MenuItems │ │ │ ├── ManageMenuItem.cs │ │ │ ├── ManageMenuItem.cs.meta │ │ │ ├── MenuItemExecutor.cs │ │ │ ├── MenuItemExecutor.cs.meta │ │ │ ├── MenuItemsReader.cs │ │ │ └── MenuItemsReader.cs.meta │ │ ├── MenuItems.meta │ │ ├── Prefabs │ │ │ ├── ManagePrefabs.cs │ │ │ └── ManagePrefabs.cs.meta │ │ ├── Prefabs.meta │ │ ├── ReadConsole.cs │ │ └── ReadConsole.cs.meta │ ├── Tools.meta │ ├── Windows │ │ ├── ManualConfigEditorWindow.cs │ │ ├── ManualConfigEditorWindow.cs.meta │ │ ├── MCPForUnityEditorWindow.cs │ │ ├── MCPForUnityEditorWindow.cs.meta │ │ ├── VSCodeManualSetupWindow.cs │ │ └── VSCodeManualSetupWindow.cs.meta │ └── Windows.meta ├── Editor.meta ├── package.json ├── package.json.meta ├── README.md ├── README.md.meta ├── Runtime │ ├── MCPForUnity.Runtime.asmdef │ ├── MCPForUnity.Runtime.asmdef.meta │ ├── Serialization │ │ ├── UnityTypeConverters.cs │ │ └── UnityTypeConverters.cs.meta │ └── Serialization.meta ├── Runtime.meta └── UnityMcpServer~ └── src ├── __init__.py ├── config.py ├── Dockerfile ├── port_discovery.py ├── pyproject.toml ├── pyrightconfig.json ├── registry │ ├── __init__.py │ └── tool_registry.py ├── reload_sentinel.py ├── server_version.txt ├── server.py ├── telemetry_decorator.py ├── telemetry.py ├── test_telemetry.py ├── tools │ ├── __init__.py │ ├── manage_asset.py │ ├── manage_editor.py │ ├── manage_gameobject.py │ ├── manage_menu_item.py │ ├── manage_prefabs.py │ ├── manage_scene.py │ ├── manage_script.py │ ├── manage_shader.py │ ├── read_console.py │ ├── resource_tools.py │ └── script_apply_edits.py ├── unity_connection.py └── uv.lock ``` # Files -------------------------------------------------------------------------------- /MCPForUnity/Editor/Tools/ManageAsset.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.Linq; 6 | using Newtonsoft.Json.Linq; 7 | using UnityEditor; 8 | using UnityEngine; 9 | using MCPForUnity.Editor.Helpers; // For Response class 10 | using static MCPForUnity.Editor.Tools.ManageGameObject; 11 | 12 | #if UNITY_6000_0_OR_NEWER 13 | using PhysicsMaterialType = UnityEngine.PhysicsMaterial; 14 | using PhysicsMaterialCombine = UnityEngine.PhysicsMaterialCombine; 15 | #else 16 | using PhysicsMaterialType = UnityEngine.PhysicMaterial; 17 | using PhysicsMaterialCombine = UnityEngine.PhysicMaterialCombine; 18 | #endif 19 | 20 | namespace MCPForUnity.Editor.Tools 21 | { 22 | /// <summary> 23 | /// Handles asset management operations within the Unity project. 24 | /// </summary> 25 | [McpForUnityTool("manage_asset")] 26 | public static class ManageAsset 27 | { 28 | // --- Main Handler --- 29 | 30 | // Define the list of valid actions 31 | private static readonly List<string> ValidActions = new List<string> 32 | { 33 | "import", 34 | "create", 35 | "modify", 36 | "delete", 37 | "duplicate", 38 | "move", 39 | "rename", 40 | "search", 41 | "get_info", 42 | "create_folder", 43 | "get_components", 44 | }; 45 | 46 | public static object HandleCommand(JObject @params) 47 | { 48 | string action = @params["action"]?.ToString().ToLower(); 49 | if (string.IsNullOrEmpty(action)) 50 | { 51 | return Response.Error("Action parameter is required."); 52 | } 53 | 54 | // Check if the action is valid before switching 55 | if (!ValidActions.Contains(action)) 56 | { 57 | string validActionsList = string.Join(", ", ValidActions); 58 | return Response.Error( 59 | $"Unknown action: '{action}'. Valid actions are: {validActionsList}" 60 | ); 61 | } 62 | 63 | // Common parameters 64 | string path = @params["path"]?.ToString(); 65 | 66 | try 67 | { 68 | switch (action) 69 | { 70 | case "import": 71 | // Note: Unity typically auto-imports. This might re-import or configure import settings. 72 | return ReimportAsset(path, @params["properties"] as JObject); 73 | case "create": 74 | return CreateAsset(@params); 75 | case "modify": 76 | return ModifyAsset(path, @params["properties"] as JObject); 77 | case "delete": 78 | return DeleteAsset(path); 79 | case "duplicate": 80 | return DuplicateAsset(path, @params["destination"]?.ToString()); 81 | case "move": // Often same as rename if within Assets/ 82 | case "rename": 83 | return MoveOrRenameAsset(path, @params["destination"]?.ToString()); 84 | case "search": 85 | return SearchAssets(@params); 86 | case "get_info": 87 | return GetAssetInfo( 88 | path, 89 | @params["generatePreview"]?.ToObject<bool>() ?? false 90 | ); 91 | case "create_folder": // Added specific action for clarity 92 | return CreateFolder(path); 93 | case "get_components": 94 | return GetComponentsFromAsset(path); 95 | 96 | default: 97 | // This error message is less likely to be hit now, but kept here as a fallback or for potential future modifications. 98 | string validActionsListDefault = string.Join(", ", ValidActions); 99 | return Response.Error( 100 | $"Unknown action: '{action}'. Valid actions are: {validActionsListDefault}" 101 | ); 102 | } 103 | } 104 | catch (Exception e) 105 | { 106 | Debug.LogError($"[ManageAsset] Action '{action}' failed for path '{path}': {e}"); 107 | return Response.Error( 108 | $"Internal error processing action '{action}' on '{path}': {e.Message}" 109 | ); 110 | } 111 | } 112 | 113 | // --- Action Implementations --- 114 | 115 | private static object ReimportAsset(string path, JObject properties) 116 | { 117 | if (string.IsNullOrEmpty(path)) 118 | return Response.Error("'path' is required for reimport."); 119 | string fullPath = AssetPathUtility.SanitizeAssetPath(path); 120 | if (!AssetExists(fullPath)) 121 | return Response.Error($"Asset not found at path: {fullPath}"); 122 | 123 | try 124 | { 125 | // TODO: Apply importer properties before reimporting? 126 | // This is complex as it requires getting the AssetImporter, casting it, 127 | // applying properties via reflection or specific methods, saving, then reimporting. 128 | if (properties != null && properties.HasValues) 129 | { 130 | Debug.LogWarning( 131 | "[ManageAsset.Reimport] Modifying importer properties before reimport is not fully implemented yet." 132 | ); 133 | // AssetImporter importer = AssetImporter.GetAtPath(fullPath); 134 | // if (importer != null) { /* Apply properties */ AssetDatabase.WriteImportSettingsIfDirty(fullPath); } 135 | } 136 | 137 | AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); 138 | // AssetDatabase.Refresh(); // Usually ImportAsset handles refresh 139 | return Response.Success($"Asset '{fullPath}' reimported.", GetAssetData(fullPath)); 140 | } 141 | catch (Exception e) 142 | { 143 | return Response.Error($"Failed to reimport asset '{fullPath}': {e.Message}"); 144 | } 145 | } 146 | 147 | private static object CreateAsset(JObject @params) 148 | { 149 | string path = @params["path"]?.ToString(); 150 | string assetType = @params["assetType"]?.ToString(); 151 | JObject properties = @params["properties"] as JObject; 152 | 153 | if (string.IsNullOrEmpty(path)) 154 | return Response.Error("'path' is required for create."); 155 | if (string.IsNullOrEmpty(assetType)) 156 | return Response.Error("'assetType' is required for create."); 157 | 158 | string fullPath = AssetPathUtility.SanitizeAssetPath(path); 159 | string directory = Path.GetDirectoryName(fullPath); 160 | 161 | // Ensure directory exists 162 | if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), directory))) 163 | { 164 | Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), directory)); 165 | AssetDatabase.Refresh(); // Make sure Unity knows about the new folder 166 | } 167 | 168 | if (AssetExists(fullPath)) 169 | return Response.Error($"Asset already exists at path: {fullPath}"); 170 | 171 | try 172 | { 173 | UnityEngine.Object newAsset = null; 174 | string lowerAssetType = assetType.ToLowerInvariant(); 175 | 176 | // Handle common asset types 177 | if (lowerAssetType == "folder") 178 | { 179 | return CreateFolder(path); // Use dedicated method 180 | } 181 | else if (lowerAssetType == "material") 182 | { 183 | // Prefer provided shader; fall back to common pipelines 184 | var requested = properties?["shader"]?.ToString(); 185 | Shader shader = 186 | (!string.IsNullOrEmpty(requested) ? Shader.Find(requested) : null) 187 | ?? Shader.Find("Universal Render Pipeline/Lit") 188 | ?? Shader.Find("HDRP/Lit") 189 | ?? Shader.Find("Standard") 190 | ?? Shader.Find("Unlit/Color"); 191 | if (shader == null) 192 | return Response.Error($"Could not find a suitable shader (requested: '{requested ?? "none"}')."); 193 | 194 | var mat = new Material(shader); 195 | if (properties != null) 196 | ApplyMaterialProperties(mat, properties); 197 | AssetDatabase.CreateAsset(mat, fullPath); 198 | newAsset = mat; 199 | } 200 | else if (lowerAssetType == "physicsmaterial") 201 | { 202 | PhysicsMaterialType pmat = new PhysicsMaterialType(); 203 | if (properties != null) 204 | ApplyPhysicsMaterialProperties(pmat, properties); 205 | AssetDatabase.CreateAsset(pmat, fullPath); 206 | newAsset = pmat; 207 | } 208 | else if (lowerAssetType == "scriptableobject") 209 | { 210 | string scriptClassName = properties?["scriptClass"]?.ToString(); 211 | if (string.IsNullOrEmpty(scriptClassName)) 212 | return Response.Error( 213 | "'scriptClass' property required when creating ScriptableObject asset." 214 | ); 215 | 216 | Type scriptType = ComponentResolver.TryResolve(scriptClassName, out var resolvedType, out var error) ? resolvedType : null; 217 | if ( 218 | scriptType == null 219 | || !typeof(ScriptableObject).IsAssignableFrom(scriptType) 220 | ) 221 | { 222 | var reason = scriptType == null 223 | ? (string.IsNullOrEmpty(error) ? "Type not found." : error) 224 | : "Type found but does not inherit from ScriptableObject."; 225 | return Response.Error($"Script class '{scriptClassName}' invalid: {reason}"); 226 | } 227 | 228 | ScriptableObject so = ScriptableObject.CreateInstance(scriptType); 229 | // TODO: Apply properties from JObject to the ScriptableObject instance? 230 | AssetDatabase.CreateAsset(so, fullPath); 231 | newAsset = so; 232 | } 233 | else if (lowerAssetType == "prefab") 234 | { 235 | // Creating prefabs usually involves saving an existing GameObject hierarchy. 236 | // A common pattern is to create an empty GameObject, configure it, and then save it. 237 | return Response.Error( 238 | "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." 239 | ); 240 | // Example (conceptual): 241 | // GameObject source = GameObject.Find(properties["sourceGameObject"].ToString()); 242 | // if(source != null) PrefabUtility.SaveAsPrefabAsset(source, fullPath); 243 | } 244 | // TODO: Add more asset types (Animation Controller, Scene, etc.) 245 | else 246 | { 247 | // Generic creation attempt (might fail or create empty files) 248 | // For some types, just creating the file might be enough if Unity imports it. 249 | // File.Create(Path.Combine(Directory.GetCurrentDirectory(), fullPath)).Close(); 250 | // AssetDatabase.ImportAsset(fullPath); // Let Unity try to import it 251 | // newAsset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(fullPath); 252 | return Response.Error( 253 | $"Creation for asset type '{assetType}' is not explicitly supported yet. Supported: Folder, Material, ScriptableObject." 254 | ); 255 | } 256 | 257 | if ( 258 | newAsset == null 259 | && !Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), fullPath)) 260 | ) // Check if it wasn't a folder and asset wasn't created 261 | { 262 | return Response.Error( 263 | $"Failed to create asset '{assetType}' at '{fullPath}'. See logs for details." 264 | ); 265 | } 266 | 267 | AssetDatabase.SaveAssets(); 268 | // AssetDatabase.Refresh(); // CreateAsset often handles refresh 269 | return Response.Success( 270 | $"Asset '{fullPath}' created successfully.", 271 | GetAssetData(fullPath) 272 | ); 273 | } 274 | catch (Exception e) 275 | { 276 | return Response.Error($"Failed to create asset at '{fullPath}': {e.Message}"); 277 | } 278 | } 279 | 280 | private static object CreateFolder(string path) 281 | { 282 | if (string.IsNullOrEmpty(path)) 283 | return Response.Error("'path' is required for create_folder."); 284 | string fullPath = AssetPathUtility.SanitizeAssetPath(path); 285 | string parentDir = Path.GetDirectoryName(fullPath); 286 | string folderName = Path.GetFileName(fullPath); 287 | 288 | if (AssetExists(fullPath)) 289 | { 290 | // Check if it's actually a folder already 291 | if (AssetDatabase.IsValidFolder(fullPath)) 292 | { 293 | return Response.Success( 294 | $"Folder already exists at path: {fullPath}", 295 | GetAssetData(fullPath) 296 | ); 297 | } 298 | else 299 | { 300 | return Response.Error( 301 | $"An asset (not a folder) already exists at path: {fullPath}" 302 | ); 303 | } 304 | } 305 | 306 | try 307 | { 308 | // Ensure parent exists 309 | if (!string.IsNullOrEmpty(parentDir) && !AssetDatabase.IsValidFolder(parentDir)) 310 | { 311 | // Recursively create parent folders if needed (AssetDatabase handles this internally) 312 | // Or we can do it manually: Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), parentDir)); AssetDatabase.Refresh(); 313 | } 314 | 315 | string guid = AssetDatabase.CreateFolder(parentDir, folderName); 316 | if (string.IsNullOrEmpty(guid)) 317 | { 318 | return Response.Error( 319 | $"Failed to create folder '{fullPath}'. Check logs and permissions." 320 | ); 321 | } 322 | 323 | // AssetDatabase.Refresh(); // CreateFolder usually handles refresh 324 | return Response.Success( 325 | $"Folder '{fullPath}' created successfully.", 326 | GetAssetData(fullPath) 327 | ); 328 | } 329 | catch (Exception e) 330 | { 331 | return Response.Error($"Failed to create folder '{fullPath}': {e.Message}"); 332 | } 333 | } 334 | 335 | private static object ModifyAsset(string path, JObject properties) 336 | { 337 | if (string.IsNullOrEmpty(path)) 338 | return Response.Error("'path' is required for modify."); 339 | if (properties == null || !properties.HasValues) 340 | return Response.Error("'properties' are required for modify."); 341 | 342 | string fullPath = AssetPathUtility.SanitizeAssetPath(path); 343 | if (!AssetExists(fullPath)) 344 | return Response.Error($"Asset not found at path: {fullPath}"); 345 | 346 | try 347 | { 348 | UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>( 349 | fullPath 350 | ); 351 | if (asset == null) 352 | return Response.Error($"Failed to load asset at path: {fullPath}"); 353 | 354 | bool modified = false; // Flag to track if any changes were made 355 | 356 | // --- NEW: Handle GameObject / Prefab Component Modification --- 357 | if (asset is GameObject gameObject) 358 | { 359 | // Iterate through the properties JSON: keys are component names, values are properties objects for that component 360 | foreach (var prop in properties.Properties()) 361 | { 362 | string componentName = prop.Name; // e.g., "Collectible" 363 | // Check if the value associated with the component name is actually an object containing properties 364 | if ( 365 | prop.Value is JObject componentProperties 366 | && componentProperties.HasValues 367 | ) // e.g., {"bobSpeed": 2.0} 368 | { 369 | // Resolve component type via ComponentResolver, then fetch by Type 370 | Component targetComponent = null; 371 | bool resolved = ComponentResolver.TryResolve(componentName, out var compType, out var compError); 372 | if (resolved) 373 | { 374 | targetComponent = gameObject.GetComponent(compType); 375 | } 376 | 377 | // Only warn about resolution failure if component also not found 378 | if (targetComponent == null && !resolved) 379 | { 380 | Debug.LogWarning( 381 | $"[ManageAsset.ModifyAsset] Failed to resolve component '{componentName}' on '{gameObject.name}': {compError}" 382 | ); 383 | } 384 | 385 | if (targetComponent != null) 386 | { 387 | // Apply the nested properties (e.g., bobSpeed) to the found component instance 388 | // Use |= to ensure 'modified' becomes true if any component is successfully modified 389 | modified |= ApplyObjectProperties( 390 | targetComponent, 391 | componentProperties 392 | ); 393 | } 394 | else 395 | { 396 | // Log a warning if a specified component couldn't be found 397 | Debug.LogWarning( 398 | $"[ManageAsset.ModifyAsset] Component '{componentName}' not found on GameObject '{gameObject.name}' in asset '{fullPath}'. Skipping modification for this component." 399 | ); 400 | } 401 | } 402 | else 403 | { 404 | // Log a warning if the structure isn't {"ComponentName": {"prop": value}} 405 | // We could potentially try to apply this property directly to the GameObject here if needed, 406 | // but the primary goal is component modification. 407 | Debug.LogWarning( 408 | $"[ManageAsset.ModifyAsset] Property '{prop.Name}' for GameObject modification should have a JSON object value containing component properties. Value was: {prop.Value.Type}. Skipping." 409 | ); 410 | } 411 | } 412 | // Note: 'modified' is now true if ANY component property was successfully changed. 413 | } 414 | // --- End NEW --- 415 | 416 | // --- Existing logic for other asset types (now as else-if) --- 417 | // Example: Modifying a Material 418 | else if (asset is Material material) 419 | { 420 | // Apply properties directly to the material. If this modifies, it sets modified=true. 421 | // Use |= in case the asset was already marked modified by previous logic (though unlikely here) 422 | modified |= ApplyMaterialProperties(material, properties); 423 | } 424 | // Example: Modifying a ScriptableObject 425 | else if (asset is ScriptableObject so) 426 | { 427 | // Apply properties directly to the ScriptableObject. 428 | modified |= ApplyObjectProperties(so, properties); // General helper 429 | } 430 | // Example: Modifying TextureImporter settings 431 | else if (asset is Texture) 432 | { 433 | AssetImporter importer = AssetImporter.GetAtPath(fullPath); 434 | if (importer is TextureImporter textureImporter) 435 | { 436 | bool importerModified = ApplyObjectProperties(textureImporter, properties); 437 | if (importerModified) 438 | { 439 | // Importer settings need saving and reimporting 440 | AssetDatabase.WriteImportSettingsIfDirty(fullPath); 441 | AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); // Reimport to apply changes 442 | modified = true; // Mark overall operation as modified 443 | } 444 | } 445 | else 446 | { 447 | Debug.LogWarning($"Could not get TextureImporter for {fullPath}."); 448 | } 449 | } 450 | // TODO: Add modification logic for other common asset types (Models, AudioClips importers, etc.) 451 | else // Fallback for other asset types OR direct properties on non-GameObject assets 452 | { 453 | // This block handles non-GameObject/Material/ScriptableObject/Texture assets. 454 | // Attempts to apply properties directly to the asset itself. 455 | Debug.LogWarning( 456 | $"[ManageAsset.ModifyAsset] Asset type '{asset.GetType().Name}' at '{fullPath}' is not explicitly handled for component modification. Attempting generic property setting on the asset itself." 457 | ); 458 | modified |= ApplyObjectProperties(asset, properties); 459 | } 460 | // --- End Existing Logic --- 461 | 462 | // Check if any modification happened (either component or direct asset modification) 463 | if (modified) 464 | { 465 | // Mark the asset as dirty (important for prefabs/SOs) so Unity knows to save it. 466 | EditorUtility.SetDirty(asset); 467 | // Save all modified assets to disk. 468 | AssetDatabase.SaveAssets(); 469 | // Refresh might be needed in some edge cases, but SaveAssets usually covers it. 470 | // AssetDatabase.Refresh(); 471 | return Response.Success( 472 | $"Asset '{fullPath}' modified successfully.", 473 | GetAssetData(fullPath) 474 | ); 475 | } 476 | else 477 | { 478 | // If no changes were made (e.g., component not found, property names incorrect, value unchanged), return a success message indicating nothing changed. 479 | return Response.Success( 480 | $"No applicable or modifiable properties found for asset '{fullPath}'. Check component names, property names, and values.", 481 | GetAssetData(fullPath) 482 | ); 483 | // Previous message: return Response.Success($"No applicable properties found to modify for asset '{fullPath}'.", GetAssetData(fullPath)); 484 | } 485 | } 486 | catch (Exception e) 487 | { 488 | // Log the detailed error internally 489 | Debug.LogError($"[ManageAsset] Action 'modify' failed for path '{path}': {e}"); 490 | // Return a user-friendly error message 491 | return Response.Error($"Failed to modify asset '{fullPath}': {e.Message}"); 492 | } 493 | } 494 | 495 | private static object DeleteAsset(string path) 496 | { 497 | if (string.IsNullOrEmpty(path)) 498 | return Response.Error("'path' is required for delete."); 499 | string fullPath = AssetPathUtility.SanitizeAssetPath(path); 500 | if (!AssetExists(fullPath)) 501 | return Response.Error($"Asset not found at path: {fullPath}"); 502 | 503 | try 504 | { 505 | bool success = AssetDatabase.DeleteAsset(fullPath); 506 | if (success) 507 | { 508 | // AssetDatabase.Refresh(); // DeleteAsset usually handles refresh 509 | return Response.Success($"Asset '{fullPath}' deleted successfully."); 510 | } 511 | else 512 | { 513 | // This might happen if the file couldn't be deleted (e.g., locked) 514 | return Response.Error( 515 | $"Failed to delete asset '{fullPath}'. Check logs or if the file is locked." 516 | ); 517 | } 518 | } 519 | catch (Exception e) 520 | { 521 | return Response.Error($"Error deleting asset '{fullPath}': {e.Message}"); 522 | } 523 | } 524 | 525 | private static object DuplicateAsset(string path, string destinationPath) 526 | { 527 | if (string.IsNullOrEmpty(path)) 528 | return Response.Error("'path' is required for duplicate."); 529 | 530 | string sourcePath = AssetPathUtility.SanitizeAssetPath(path); 531 | if (!AssetExists(sourcePath)) 532 | return Response.Error($"Source asset not found at path: {sourcePath}"); 533 | 534 | string destPath; 535 | if (string.IsNullOrEmpty(destinationPath)) 536 | { 537 | // Generate a unique path if destination is not provided 538 | destPath = AssetDatabase.GenerateUniqueAssetPath(sourcePath); 539 | } 540 | else 541 | { 542 | destPath = AssetPathUtility.SanitizeAssetPath(destinationPath); 543 | if (AssetExists(destPath)) 544 | return Response.Error($"Asset already exists at destination path: {destPath}"); 545 | // Ensure destination directory exists 546 | EnsureDirectoryExists(Path.GetDirectoryName(destPath)); 547 | } 548 | 549 | try 550 | { 551 | bool success = AssetDatabase.CopyAsset(sourcePath, destPath); 552 | if (success) 553 | { 554 | // AssetDatabase.Refresh(); 555 | return Response.Success( 556 | $"Asset '{sourcePath}' duplicated to '{destPath}'.", 557 | GetAssetData(destPath) 558 | ); 559 | } 560 | else 561 | { 562 | return Response.Error( 563 | $"Failed to duplicate asset from '{sourcePath}' to '{destPath}'." 564 | ); 565 | } 566 | } 567 | catch (Exception e) 568 | { 569 | return Response.Error($"Error duplicating asset '{sourcePath}': {e.Message}"); 570 | } 571 | } 572 | 573 | private static object MoveOrRenameAsset(string path, string destinationPath) 574 | { 575 | if (string.IsNullOrEmpty(path)) 576 | return Response.Error("'path' is required for move/rename."); 577 | if (string.IsNullOrEmpty(destinationPath)) 578 | return Response.Error("'destination' path is required for move/rename."); 579 | 580 | string sourcePath = AssetPathUtility.SanitizeAssetPath(path); 581 | string destPath = AssetPathUtility.SanitizeAssetPath(destinationPath); 582 | 583 | if (!AssetExists(sourcePath)) 584 | return Response.Error($"Source asset not found at path: {sourcePath}"); 585 | if (AssetExists(destPath)) 586 | return Response.Error( 587 | $"An asset already exists at the destination path: {destPath}" 588 | ); 589 | 590 | // Ensure destination directory exists 591 | EnsureDirectoryExists(Path.GetDirectoryName(destPath)); 592 | 593 | try 594 | { 595 | // Validate will return an error string if failed, null if successful 596 | string error = AssetDatabase.ValidateMoveAsset(sourcePath, destPath); 597 | if (!string.IsNullOrEmpty(error)) 598 | { 599 | return Response.Error( 600 | $"Failed to move/rename asset from '{sourcePath}' to '{destPath}': {error}" 601 | ); 602 | } 603 | 604 | string guid = AssetDatabase.MoveAsset(sourcePath, destPath); 605 | if (!string.IsNullOrEmpty(guid)) // MoveAsset returns the new GUID on success 606 | { 607 | // AssetDatabase.Refresh(); // MoveAsset usually handles refresh 608 | return Response.Success( 609 | $"Asset moved/renamed from '{sourcePath}' to '{destPath}'.", 610 | GetAssetData(destPath) 611 | ); 612 | } 613 | else 614 | { 615 | // This case might not be reachable if ValidateMoveAsset passes, but good to have 616 | return Response.Error( 617 | $"MoveAsset call failed unexpectedly for '{sourcePath}' to '{destPath}'." 618 | ); 619 | } 620 | } 621 | catch (Exception e) 622 | { 623 | return Response.Error($"Error moving/renaming asset '{sourcePath}': {e.Message}"); 624 | } 625 | } 626 | 627 | private static object SearchAssets(JObject @params) 628 | { 629 | string searchPattern = @params["searchPattern"]?.ToString(); 630 | string filterType = @params["filterType"]?.ToString(); 631 | string pathScope = @params["path"]?.ToString(); // Use path as folder scope 632 | string filterDateAfterStr = @params["filterDateAfter"]?.ToString(); 633 | int pageSize = @params["pageSize"]?.ToObject<int?>() ?? 50; // Default page size 634 | int pageNumber = @params["pageNumber"]?.ToObject<int?>() ?? 1; // Default page number (1-based) 635 | bool generatePreview = @params["generatePreview"]?.ToObject<bool>() ?? false; 636 | 637 | List<string> searchFilters = new List<string>(); 638 | if (!string.IsNullOrEmpty(searchPattern)) 639 | searchFilters.Add(searchPattern); 640 | if (!string.IsNullOrEmpty(filterType)) 641 | searchFilters.Add($"t:{filterType}"); 642 | 643 | string[] folderScope = null; 644 | if (!string.IsNullOrEmpty(pathScope)) 645 | { 646 | folderScope = new string[] { AssetPathUtility.SanitizeAssetPath(pathScope) }; 647 | if (!AssetDatabase.IsValidFolder(folderScope[0])) 648 | { 649 | // Maybe the user provided a file path instead of a folder? 650 | // We could search in the containing folder, or return an error. 651 | Debug.LogWarning( 652 | $"Search path '{folderScope[0]}' is not a valid folder. Searching entire project." 653 | ); 654 | folderScope = null; // Search everywhere if path isn't a folder 655 | } 656 | } 657 | 658 | DateTime? filterDateAfter = null; 659 | if (!string.IsNullOrEmpty(filterDateAfterStr)) 660 | { 661 | if ( 662 | DateTime.TryParse( 663 | filterDateAfterStr, 664 | CultureInfo.InvariantCulture, 665 | DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, 666 | out DateTime parsedDate 667 | ) 668 | ) 669 | { 670 | filterDateAfter = parsedDate; 671 | } 672 | else 673 | { 674 | Debug.LogWarning( 675 | $"Could not parse filterDateAfter: '{filterDateAfterStr}'. Expected ISO 8601 format." 676 | ); 677 | } 678 | } 679 | 680 | try 681 | { 682 | string[] guids = AssetDatabase.FindAssets( 683 | string.Join(" ", searchFilters), 684 | folderScope 685 | ); 686 | List<object> results = new List<object>(); 687 | int totalFound = 0; 688 | 689 | foreach (string guid in guids) 690 | { 691 | string assetPath = AssetDatabase.GUIDToAssetPath(guid); 692 | if (string.IsNullOrEmpty(assetPath)) 693 | continue; 694 | 695 | // Apply date filter if present 696 | if (filterDateAfter.HasValue) 697 | { 698 | DateTime lastWriteTime = File.GetLastWriteTimeUtc( 699 | Path.Combine(Directory.GetCurrentDirectory(), assetPath) 700 | ); 701 | if (lastWriteTime <= filterDateAfter.Value) 702 | { 703 | continue; // Skip assets older than or equal to the filter date 704 | } 705 | } 706 | 707 | totalFound++; // Count matching assets before pagination 708 | results.Add(GetAssetData(assetPath, generatePreview)); 709 | } 710 | 711 | // Apply pagination 712 | int startIndex = (pageNumber - 1) * pageSize; 713 | var pagedResults = results.Skip(startIndex).Take(pageSize).ToList(); 714 | 715 | return Response.Success( 716 | $"Found {totalFound} asset(s). Returning page {pageNumber} ({pagedResults.Count} assets).", 717 | new 718 | { 719 | totalAssets = totalFound, 720 | pageSize = pageSize, 721 | pageNumber = pageNumber, 722 | assets = pagedResults, 723 | } 724 | ); 725 | } 726 | catch (Exception e) 727 | { 728 | return Response.Error($"Error searching assets: {e.Message}"); 729 | } 730 | } 731 | 732 | private static object GetAssetInfo(string path, bool generatePreview) 733 | { 734 | if (string.IsNullOrEmpty(path)) 735 | return Response.Error("'path' is required for get_info."); 736 | string fullPath = AssetPathUtility.SanitizeAssetPath(path); 737 | if (!AssetExists(fullPath)) 738 | return Response.Error($"Asset not found at path: {fullPath}"); 739 | 740 | try 741 | { 742 | return Response.Success( 743 | "Asset info retrieved.", 744 | GetAssetData(fullPath, generatePreview) 745 | ); 746 | } 747 | catch (Exception e) 748 | { 749 | return Response.Error($"Error getting info for asset '{fullPath}': {e.Message}"); 750 | } 751 | } 752 | 753 | /// <summary> 754 | /// Retrieves components attached to a GameObject asset (like a Prefab). 755 | /// </summary> 756 | /// <param name="path">The asset path of the GameObject or Prefab.</param> 757 | /// <returns>A response object containing a list of component type names or an error.</returns> 758 | private static object GetComponentsFromAsset(string path) 759 | { 760 | // 1. Validate input path 761 | if (string.IsNullOrEmpty(path)) 762 | return Response.Error("'path' is required for get_components."); 763 | 764 | // 2. Sanitize and check existence 765 | string fullPath = AssetPathUtility.SanitizeAssetPath(path); 766 | if (!AssetExists(fullPath)) 767 | return Response.Error($"Asset not found at path: {fullPath}"); 768 | 769 | try 770 | { 771 | // 3. Load the asset 772 | UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>( 773 | fullPath 774 | ); 775 | if (asset == null) 776 | return Response.Error($"Failed to load asset at path: {fullPath}"); 777 | 778 | // 4. Check if it's a GameObject (Prefabs load as GameObjects) 779 | GameObject gameObject = asset as GameObject; 780 | if (gameObject == null) 781 | { 782 | // Also check if it's *directly* a Component type (less common for primary assets) 783 | Component componentAsset = asset as Component; 784 | if (componentAsset != null) 785 | { 786 | // If the asset itself *is* a component, maybe return just its info? 787 | // This is an edge case. Let's stick to GameObjects for now. 788 | return Response.Error( 789 | $"Asset at '{fullPath}' is a Component ({asset.GetType().FullName}), not a GameObject. Components are typically retrieved *from* a GameObject." 790 | ); 791 | } 792 | return Response.Error( 793 | $"Asset at '{fullPath}' is not a GameObject (Type: {asset.GetType().FullName}). Cannot get components from this asset type." 794 | ); 795 | } 796 | 797 | // 5. Get components 798 | Component[] components = gameObject.GetComponents<Component>(); 799 | 800 | // 6. Format component data 801 | List<object> componentList = components 802 | .Select(comp => new 803 | { 804 | typeName = comp.GetType().FullName, 805 | instanceID = comp.GetInstanceID(), 806 | // TODO: Add more component-specific details here if needed in the future? 807 | // Requires reflection or specific handling per component type. 808 | }) 809 | .ToList<object>(); // Explicit cast for clarity if needed 810 | 811 | // 7. Return success response 812 | return Response.Success( 813 | $"Found {componentList.Count} component(s) on asset '{fullPath}'.", 814 | componentList 815 | ); 816 | } 817 | catch (Exception e) 818 | { 819 | Debug.LogError( 820 | $"[ManageAsset.GetComponentsFromAsset] Error getting components for '{fullPath}': {e}" 821 | ); 822 | return Response.Error( 823 | $"Error getting components for asset '{fullPath}': {e.Message}" 824 | ); 825 | } 826 | } 827 | 828 | // --- Internal Helpers --- 829 | 830 | /// <summary> 831 | /// Ensures the asset path starts with "Assets/". 832 | /// </summary> 833 | /// <summary> 834 | /// Checks if an asset exists at the given path (file or folder). 835 | /// </summary> 836 | private static bool AssetExists(string sanitizedPath) 837 | { 838 | // AssetDatabase APIs are generally preferred over raw File/Directory checks for assets. 839 | // Check if it's a known asset GUID. 840 | if (!string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(sanitizedPath))) 841 | { 842 | return true; 843 | } 844 | // AssetPathToGUID might not work for newly created folders not yet refreshed. 845 | // Check directory explicitly for folders. 846 | if (Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), sanitizedPath))) 847 | { 848 | // Check if it's considered a *valid* folder by Unity 849 | return AssetDatabase.IsValidFolder(sanitizedPath); 850 | } 851 | // Check file existence for non-folder assets. 852 | if (File.Exists(Path.Combine(Directory.GetCurrentDirectory(), sanitizedPath))) 853 | { 854 | return true; // Assume if file exists, it's an asset or will be imported 855 | } 856 | 857 | return false; 858 | // Alternative: return !string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(sanitizedPath)); 859 | } 860 | 861 | /// <summary> 862 | /// Ensures the directory for a given asset path exists, creating it if necessary. 863 | /// </summary> 864 | private static void EnsureDirectoryExists(string directoryPath) 865 | { 866 | if (string.IsNullOrEmpty(directoryPath)) 867 | return; 868 | string fullDirPath = Path.Combine(Directory.GetCurrentDirectory(), directoryPath); 869 | if (!Directory.Exists(fullDirPath)) 870 | { 871 | Directory.CreateDirectory(fullDirPath); 872 | AssetDatabase.Refresh(); // Let Unity know about the new folder 873 | } 874 | } 875 | 876 | /// <summary> 877 | /// Applies properties from JObject to a Material. 878 | /// </summary> 879 | private static bool ApplyMaterialProperties(Material mat, JObject properties) 880 | { 881 | if (mat == null || properties == null) 882 | return false; 883 | bool modified = false; 884 | 885 | // Example: Set shader 886 | if (properties["shader"]?.Type == JTokenType.String) 887 | { 888 | Shader newShader = Shader.Find(properties["shader"].ToString()); 889 | if (newShader != null && mat.shader != newShader) 890 | { 891 | mat.shader = newShader; 892 | modified = true; 893 | } 894 | } 895 | // Example: Set color property 896 | if (properties["color"] is JObject colorProps) 897 | { 898 | string propName = colorProps["name"]?.ToString() ?? "_Color"; // Default main color 899 | if (colorProps["value"] is JArray colArr && colArr.Count >= 3) 900 | { 901 | try 902 | { 903 | Color newColor = new Color( 904 | colArr[0].ToObject<float>(), 905 | colArr[1].ToObject<float>(), 906 | colArr[2].ToObject<float>(), 907 | colArr.Count > 3 ? colArr[3].ToObject<float>() : 1.0f 908 | ); 909 | if (mat.HasProperty(propName) && mat.GetColor(propName) != newColor) 910 | { 911 | mat.SetColor(propName, newColor); 912 | modified = true; 913 | } 914 | } 915 | catch (Exception ex) 916 | { 917 | Debug.LogWarning( 918 | $"Error parsing color property '{propName}': {ex.Message}" 919 | ); 920 | } 921 | } 922 | } 923 | else if (properties["color"] is JArray colorArr) //Use color now with examples set in manage_asset.py 924 | { 925 | string propName = "_Color"; 926 | try 927 | { 928 | if (colorArr.Count >= 3) 929 | { 930 | Color newColor = new Color( 931 | colorArr[0].ToObject<float>(), 932 | colorArr[1].ToObject<float>(), 933 | colorArr[2].ToObject<float>(), 934 | colorArr.Count > 3 ? colorArr[3].ToObject<float>() : 1.0f 935 | ); 936 | if (mat.HasProperty(propName) && mat.GetColor(propName) != newColor) 937 | { 938 | mat.SetColor(propName, newColor); 939 | modified = true; 940 | } 941 | } 942 | } 943 | catch (Exception ex) 944 | { 945 | Debug.LogWarning( 946 | $"Error parsing color property '{propName}': {ex.Message}" 947 | ); 948 | } 949 | } 950 | // Example: Set float property 951 | if (properties["float"] is JObject floatProps) 952 | { 953 | string propName = floatProps["name"]?.ToString(); 954 | if ( 955 | !string.IsNullOrEmpty(propName) && 956 | (floatProps["value"]?.Type == JTokenType.Float || floatProps["value"]?.Type == JTokenType.Integer) 957 | ) 958 | { 959 | try 960 | { 961 | float newVal = floatProps["value"].ToObject<float>(); 962 | if (mat.HasProperty(propName) && mat.GetFloat(propName) != newVal) 963 | { 964 | mat.SetFloat(propName, newVal); 965 | modified = true; 966 | } 967 | } 968 | catch (Exception ex) 969 | { 970 | Debug.LogWarning( 971 | $"Error parsing float property '{propName}': {ex.Message}" 972 | ); 973 | } 974 | } 975 | } 976 | // Example: Set texture property 977 | if (properties["texture"] is JObject texProps) 978 | { 979 | string propName = texProps["name"]?.ToString() ?? "_MainTex"; // Default main texture 980 | string texPath = texProps["path"]?.ToString(); 981 | if (!string.IsNullOrEmpty(texPath)) 982 | { 983 | Texture newTex = AssetDatabase.LoadAssetAtPath<Texture>( 984 | AssetPathUtility.SanitizeAssetPath(texPath) 985 | ); 986 | if ( 987 | newTex != null 988 | && mat.HasProperty(propName) 989 | && mat.GetTexture(propName) != newTex 990 | ) 991 | { 992 | mat.SetTexture(propName, newTex); 993 | modified = true; 994 | } 995 | else if (newTex == null) 996 | { 997 | Debug.LogWarning($"Texture not found at path: {texPath}"); 998 | } 999 | } 1000 | } 1001 | 1002 | // TODO: Add handlers for other property types (Vectors, Ints, Keywords, RenderQueue, etc.) 1003 | return modified; 1004 | } 1005 | 1006 | /// <summary> 1007 | /// Applies properties from JObject to a PhysicsMaterial. 1008 | /// </summary> 1009 | private static bool ApplyPhysicsMaterialProperties(PhysicsMaterialType pmat, JObject properties) 1010 | { 1011 | if (pmat == null || properties == null) 1012 | return false; 1013 | bool modified = false; 1014 | 1015 | // Example: Set dynamic friction 1016 | if (properties["dynamicFriction"]?.Type == JTokenType.Float) 1017 | { 1018 | float dynamicFriction = properties["dynamicFriction"].ToObject<float>(); 1019 | pmat.dynamicFriction = dynamicFriction; 1020 | modified = true; 1021 | } 1022 | 1023 | // Example: Set static friction 1024 | if (properties["staticFriction"]?.Type == JTokenType.Float) 1025 | { 1026 | float staticFriction = properties["staticFriction"].ToObject<float>(); 1027 | pmat.staticFriction = staticFriction; 1028 | modified = true; 1029 | } 1030 | 1031 | // Example: Set bounciness 1032 | if (properties["bounciness"]?.Type == JTokenType.Float) 1033 | { 1034 | float bounciness = properties["bounciness"].ToObject<float>(); 1035 | pmat.bounciness = bounciness; 1036 | modified = true; 1037 | } 1038 | 1039 | List<String> averageList = new List<String> { "ave", "Ave", "average", "Average" }; 1040 | List<String> multiplyList = new List<String> { "mul", "Mul", "mult", "Mult", "multiply", "Multiply" }; 1041 | List<String> minimumList = new List<String> { "min", "Min", "minimum", "Minimum" }; 1042 | List<String> maximumList = new List<String> { "max", "Max", "maximum", "Maximum" }; 1043 | 1044 | // Example: Set friction combine 1045 | if (properties["frictionCombine"]?.Type == JTokenType.String) 1046 | { 1047 | string frictionCombine = properties["frictionCombine"].ToString(); 1048 | if (averageList.Contains(frictionCombine)) 1049 | pmat.frictionCombine = PhysicsMaterialCombine.Average; 1050 | else if (multiplyList.Contains(frictionCombine)) 1051 | pmat.frictionCombine = PhysicsMaterialCombine.Multiply; 1052 | else if (minimumList.Contains(frictionCombine)) 1053 | pmat.frictionCombine = PhysicsMaterialCombine.Minimum; 1054 | else if (maximumList.Contains(frictionCombine)) 1055 | pmat.frictionCombine = PhysicsMaterialCombine.Maximum; 1056 | modified = true; 1057 | } 1058 | 1059 | // Example: Set bounce combine 1060 | if (properties["bounceCombine"]?.Type == JTokenType.String) 1061 | { 1062 | string bounceCombine = properties["bounceCombine"].ToString(); 1063 | if (averageList.Contains(bounceCombine)) 1064 | pmat.bounceCombine = PhysicsMaterialCombine.Average; 1065 | else if (multiplyList.Contains(bounceCombine)) 1066 | pmat.bounceCombine = PhysicsMaterialCombine.Multiply; 1067 | else if (minimumList.Contains(bounceCombine)) 1068 | pmat.bounceCombine = PhysicsMaterialCombine.Minimum; 1069 | else if (maximumList.Contains(bounceCombine)) 1070 | pmat.bounceCombine = PhysicsMaterialCombine.Maximum; 1071 | modified = true; 1072 | } 1073 | 1074 | return modified; 1075 | } 1076 | 1077 | /// <summary> 1078 | /// Generic helper to set properties on any UnityEngine.Object using reflection. 1079 | /// </summary> 1080 | private static bool ApplyObjectProperties(UnityEngine.Object target, JObject properties) 1081 | { 1082 | if (target == null || properties == null) 1083 | return false; 1084 | bool modified = false; 1085 | Type type = target.GetType(); 1086 | 1087 | foreach (var prop in properties.Properties()) 1088 | { 1089 | string propName = prop.Name; 1090 | JToken propValue = prop.Value; 1091 | if (SetPropertyOrField(target, propName, propValue, type)) 1092 | { 1093 | modified = true; 1094 | } 1095 | } 1096 | return modified; 1097 | } 1098 | 1099 | /// <summary> 1100 | /// Helper to set a property or field via reflection, handling basic types and Unity objects. 1101 | /// </summary> 1102 | private static bool SetPropertyOrField( 1103 | object target, 1104 | string memberName, 1105 | JToken value, 1106 | Type type = null 1107 | ) 1108 | { 1109 | type = type ?? target.GetType(); 1110 | System.Reflection.BindingFlags flags = 1111 | System.Reflection.BindingFlags.Public 1112 | | System.Reflection.BindingFlags.Instance 1113 | | System.Reflection.BindingFlags.IgnoreCase; 1114 | 1115 | try 1116 | { 1117 | System.Reflection.PropertyInfo propInfo = type.GetProperty(memberName, flags); 1118 | if (propInfo != null && propInfo.CanWrite) 1119 | { 1120 | object convertedValue = ConvertJTokenToType(value, propInfo.PropertyType); 1121 | if ( 1122 | convertedValue != null 1123 | && !object.Equals(propInfo.GetValue(target), convertedValue) 1124 | ) 1125 | { 1126 | propInfo.SetValue(target, convertedValue); 1127 | return true; 1128 | } 1129 | } 1130 | else 1131 | { 1132 | System.Reflection.FieldInfo fieldInfo = type.GetField(memberName, flags); 1133 | if (fieldInfo != null) 1134 | { 1135 | object convertedValue = ConvertJTokenToType(value, fieldInfo.FieldType); 1136 | if ( 1137 | convertedValue != null 1138 | && !object.Equals(fieldInfo.GetValue(target), convertedValue) 1139 | ) 1140 | { 1141 | fieldInfo.SetValue(target, convertedValue); 1142 | return true; 1143 | } 1144 | } 1145 | } 1146 | } 1147 | catch (Exception ex) 1148 | { 1149 | Debug.LogWarning( 1150 | $"[SetPropertyOrField] Failed to set '{memberName}' on {type.Name}: {ex.Message}" 1151 | ); 1152 | } 1153 | return false; 1154 | } 1155 | 1156 | /// <summary> 1157 | /// Simple JToken to Type conversion for common Unity types and primitives. 1158 | /// </summary> 1159 | private static object ConvertJTokenToType(JToken token, Type targetType) 1160 | { 1161 | try 1162 | { 1163 | if (token == null || token.Type == JTokenType.Null) 1164 | return null; 1165 | 1166 | if (targetType == typeof(string)) 1167 | return token.ToObject<string>(); 1168 | if (targetType == typeof(int)) 1169 | return token.ToObject<int>(); 1170 | if (targetType == typeof(float)) 1171 | return token.ToObject<float>(); 1172 | if (targetType == typeof(bool)) 1173 | return token.ToObject<bool>(); 1174 | if (targetType == typeof(Vector2) && token is JArray arrV2 && arrV2.Count == 2) 1175 | return new Vector2(arrV2[0].ToObject<float>(), arrV2[1].ToObject<float>()); 1176 | if (targetType == typeof(Vector3) && token is JArray arrV3 && arrV3.Count == 3) 1177 | return new Vector3( 1178 | arrV3[0].ToObject<float>(), 1179 | arrV3[1].ToObject<float>(), 1180 | arrV3[2].ToObject<float>() 1181 | ); 1182 | if (targetType == typeof(Vector4) && token is JArray arrV4 && arrV4.Count == 4) 1183 | return new Vector4( 1184 | arrV4[0].ToObject<float>(), 1185 | arrV4[1].ToObject<float>(), 1186 | arrV4[2].ToObject<float>(), 1187 | arrV4[3].ToObject<float>() 1188 | ); 1189 | if (targetType == typeof(Quaternion) && token is JArray arrQ && arrQ.Count == 4) 1190 | return new Quaternion( 1191 | arrQ[0].ToObject<float>(), 1192 | arrQ[1].ToObject<float>(), 1193 | arrQ[2].ToObject<float>(), 1194 | arrQ[3].ToObject<float>() 1195 | ); 1196 | if (targetType == typeof(Color) && token is JArray arrC && arrC.Count >= 3) // Allow RGB or RGBA 1197 | return new Color( 1198 | arrC[0].ToObject<float>(), 1199 | arrC[1].ToObject<float>(), 1200 | arrC[2].ToObject<float>(), 1201 | arrC.Count > 3 ? arrC[3].ToObject<float>() : 1.0f 1202 | ); 1203 | if (targetType.IsEnum) 1204 | return Enum.Parse(targetType, token.ToString(), true); // Case-insensitive enum parsing 1205 | 1206 | // Handle loading Unity Objects (Materials, Textures, etc.) by path 1207 | if ( 1208 | typeof(UnityEngine.Object).IsAssignableFrom(targetType) 1209 | && token.Type == JTokenType.String 1210 | ) 1211 | { 1212 | string assetPath = AssetPathUtility.SanitizeAssetPath(token.ToString()); 1213 | UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath( 1214 | assetPath, 1215 | targetType 1216 | ); 1217 | if (loadedAsset == null) 1218 | { 1219 | Debug.LogWarning( 1220 | $"[ConvertJTokenToType] Could not load asset of type {targetType.Name} from path: {assetPath}" 1221 | ); 1222 | } 1223 | return loadedAsset; 1224 | } 1225 | 1226 | // Fallback: Try direct conversion (might work for other simple value types) 1227 | return token.ToObject(targetType); 1228 | } 1229 | catch (Exception ex) 1230 | { 1231 | Debug.LogWarning( 1232 | $"[ConvertJTokenToType] Could not convert JToken '{token}' (type {token.Type}) to type '{targetType.Name}': {ex.Message}" 1233 | ); 1234 | return null; 1235 | } 1236 | } 1237 | 1238 | 1239 | // --- Data Serialization --- 1240 | 1241 | /// <summary> 1242 | /// Creates a serializable representation of an asset. 1243 | /// </summary> 1244 | private static object GetAssetData(string path, bool generatePreview = false) 1245 | { 1246 | if (string.IsNullOrEmpty(path) || !AssetExists(path)) 1247 | return null; 1248 | 1249 | string guid = AssetDatabase.AssetPathToGUID(path); 1250 | Type assetType = AssetDatabase.GetMainAssetTypeAtPath(path); 1251 | UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path); 1252 | string previewBase64 = null; 1253 | int previewWidth = 0; 1254 | int previewHeight = 0; 1255 | 1256 | if (generatePreview && asset != null) 1257 | { 1258 | Texture2D preview = AssetPreview.GetAssetPreview(asset); 1259 | 1260 | if (preview != null) 1261 | { 1262 | try 1263 | { 1264 | // Ensure texture is readable for EncodeToPNG 1265 | // Creating a temporary readable copy is safer 1266 | RenderTexture rt = null; 1267 | Texture2D readablePreview = null; 1268 | RenderTexture previous = RenderTexture.active; 1269 | try 1270 | { 1271 | rt = RenderTexture.GetTemporary(preview.width, preview.height); 1272 | Graphics.Blit(preview, rt); 1273 | RenderTexture.active = rt; 1274 | readablePreview = new Texture2D(preview.width, preview.height, TextureFormat.RGB24, false); 1275 | readablePreview.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0); 1276 | readablePreview.Apply(); 1277 | 1278 | var pngData = readablePreview.EncodeToPNG(); 1279 | if (pngData != null && pngData.Length > 0) 1280 | { 1281 | previewBase64 = Convert.ToBase64String(pngData); 1282 | previewWidth = readablePreview.width; 1283 | previewHeight = readablePreview.height; 1284 | } 1285 | } 1286 | finally 1287 | { 1288 | RenderTexture.active = previous; 1289 | if (rt != null) RenderTexture.ReleaseTemporary(rt); 1290 | if (readablePreview != null) UnityEngine.Object.DestroyImmediate(readablePreview); 1291 | } 1292 | } 1293 | catch (Exception ex) 1294 | { 1295 | Debug.LogWarning( 1296 | $"Failed to generate readable preview for '{path}': {ex.Message}. Preview might not be readable." 1297 | ); 1298 | // Fallback: Try getting static preview if available? 1299 | // Texture2D staticPreview = AssetPreview.GetMiniThumbnail(asset); 1300 | } 1301 | } 1302 | else 1303 | { 1304 | Debug.LogWarning( 1305 | $"Could not get asset preview for {path} (Type: {assetType?.Name}). Is it supported?" 1306 | ); 1307 | } 1308 | } 1309 | 1310 | return new 1311 | { 1312 | path = path, 1313 | guid = guid, 1314 | assetType = assetType?.FullName ?? "Unknown", 1315 | name = Path.GetFileNameWithoutExtension(path), 1316 | fileName = Path.GetFileName(path), 1317 | isFolder = AssetDatabase.IsValidFolder(path), 1318 | instanceID = asset?.GetInstanceID() ?? 0, 1319 | lastWriteTimeUtc = File.GetLastWriteTimeUtc( 1320 | Path.Combine(Directory.GetCurrentDirectory(), path) 1321 | ) 1322 | .ToString("o"), // ISO 8601 1323 | // --- Preview Data --- 1324 | previewBase64 = previewBase64, // PNG data as Base64 string 1325 | previewWidth = previewWidth, 1326 | previewHeight = previewHeight, 1327 | // TODO: Add more metadata? Importer settings? Dependencies? 1328 | }; 1329 | } 1330 | } 1331 | } 1332 | ``` -------------------------------------------------------------------------------- /UnityMcpBridge/Editor/Tools/ManageAsset.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.Linq; 6 | using Newtonsoft.Json.Linq; 7 | using UnityEditor; 8 | using UnityEngine; 9 | using MCPForUnity.Editor.Helpers; // For Response class 10 | using static MCPForUnity.Editor.Tools.ManageGameObject; 11 | 12 | #if UNITY_6000_0_OR_NEWER 13 | using PhysicsMaterialType = UnityEngine.PhysicsMaterial; 14 | using PhysicsMaterialCombine = UnityEngine.PhysicsMaterialCombine; 15 | #else 16 | using PhysicsMaterialType = UnityEngine.PhysicMaterial; 17 | using PhysicsMaterialCombine = UnityEngine.PhysicMaterialCombine; 18 | #endif 19 | 20 | namespace MCPForUnity.Editor.Tools 21 | { 22 | /// <summary> 23 | /// Handles asset management operations within the Unity project. 24 | /// </summary> 25 | [McpForUnityTool("manage_asset")] 26 | public static class ManageAsset 27 | { 28 | // --- Main Handler --- 29 | 30 | // Define the list of valid actions 31 | private static readonly List<string> ValidActions = new List<string> 32 | { 33 | "import", 34 | "create", 35 | "modify", 36 | "delete", 37 | "duplicate", 38 | "move", 39 | "rename", 40 | "search", 41 | "get_info", 42 | "create_folder", 43 | "get_components", 44 | }; 45 | 46 | public static object HandleCommand(JObject @params) 47 | { 48 | string action = @params["action"]?.ToString().ToLower(); 49 | if (string.IsNullOrEmpty(action)) 50 | { 51 | return Response.Error("Action parameter is required."); 52 | } 53 | 54 | // Check if the action is valid before switching 55 | if (!ValidActions.Contains(action)) 56 | { 57 | string validActionsList = string.Join(", ", ValidActions); 58 | return Response.Error( 59 | $"Unknown action: '{action}'. Valid actions are: {validActionsList}" 60 | ); 61 | } 62 | 63 | // Common parameters 64 | string path = @params["path"]?.ToString(); 65 | 66 | try 67 | { 68 | switch (action) 69 | { 70 | case "import": 71 | // Note: Unity typically auto-imports. This might re-import or configure import settings. 72 | return ReimportAsset(path, @params["properties"] as JObject); 73 | case "create": 74 | return CreateAsset(@params); 75 | case "modify": 76 | return ModifyAsset(path, @params["properties"] as JObject); 77 | case "delete": 78 | return DeleteAsset(path); 79 | case "duplicate": 80 | return DuplicateAsset(path, @params["destination"]?.ToString()); 81 | case "move": // Often same as rename if within Assets/ 82 | case "rename": 83 | return MoveOrRenameAsset(path, @params["destination"]?.ToString()); 84 | case "search": 85 | return SearchAssets(@params); 86 | case "get_info": 87 | return GetAssetInfo( 88 | path, 89 | @params["generatePreview"]?.ToObject<bool>() ?? false 90 | ); 91 | case "create_folder": // Added specific action for clarity 92 | return CreateFolder(path); 93 | case "get_components": 94 | return GetComponentsFromAsset(path); 95 | 96 | default: 97 | // This error message is less likely to be hit now, but kept here as a fallback or for potential future modifications. 98 | string validActionsListDefault = string.Join(", ", ValidActions); 99 | return Response.Error( 100 | $"Unknown action: '{action}'. Valid actions are: {validActionsListDefault}" 101 | ); 102 | } 103 | } 104 | catch (Exception e) 105 | { 106 | Debug.LogError($"[ManageAsset] Action '{action}' failed for path '{path}': {e}"); 107 | return Response.Error( 108 | $"Internal error processing action '{action}' on '{path}': {e.Message}" 109 | ); 110 | } 111 | } 112 | 113 | // --- Action Implementations --- 114 | 115 | private static object ReimportAsset(string path, JObject properties) 116 | { 117 | if (string.IsNullOrEmpty(path)) 118 | return Response.Error("'path' is required for reimport."); 119 | string fullPath = AssetPathUtility.SanitizeAssetPath(path); 120 | if (!AssetExists(fullPath)) 121 | return Response.Error($"Asset not found at path: {fullPath}"); 122 | 123 | try 124 | { 125 | // TODO: Apply importer properties before reimporting? 126 | // This is complex as it requires getting the AssetImporter, casting it, 127 | // applying properties via reflection or specific methods, saving, then reimporting. 128 | if (properties != null && properties.HasValues) 129 | { 130 | Debug.LogWarning( 131 | "[ManageAsset.Reimport] Modifying importer properties before reimport is not fully implemented yet." 132 | ); 133 | // AssetImporter importer = AssetImporter.GetAtPath(fullPath); 134 | // if (importer != null) { /* Apply properties */ AssetDatabase.WriteImportSettingsIfDirty(fullPath); } 135 | } 136 | 137 | AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); 138 | // AssetDatabase.Refresh(); // Usually ImportAsset handles refresh 139 | return Response.Success($"Asset '{fullPath}' reimported.", GetAssetData(fullPath)); 140 | } 141 | catch (Exception e) 142 | { 143 | return Response.Error($"Failed to reimport asset '{fullPath}': {e.Message}"); 144 | } 145 | } 146 | 147 | private static object CreateAsset(JObject @params) 148 | { 149 | string path = @params["path"]?.ToString(); 150 | string assetType = @params["assetType"]?.ToString(); 151 | JObject properties = @params["properties"] as JObject; 152 | 153 | if (string.IsNullOrEmpty(path)) 154 | return Response.Error("'path' is required for create."); 155 | if (string.IsNullOrEmpty(assetType)) 156 | return Response.Error("'assetType' is required for create."); 157 | 158 | string fullPath = AssetPathUtility.SanitizeAssetPath(path); 159 | string directory = Path.GetDirectoryName(fullPath); 160 | 161 | // Ensure directory exists 162 | if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), directory))) 163 | { 164 | Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), directory)); 165 | AssetDatabase.Refresh(); // Make sure Unity knows about the new folder 166 | } 167 | 168 | if (AssetExists(fullPath)) 169 | return Response.Error($"Asset already exists at path: {fullPath}"); 170 | 171 | try 172 | { 173 | UnityEngine.Object newAsset = null; 174 | string lowerAssetType = assetType.ToLowerInvariant(); 175 | 176 | // Handle common asset types 177 | if (lowerAssetType == "folder") 178 | { 179 | return CreateFolder(path); // Use dedicated method 180 | } 181 | else if (lowerAssetType == "material") 182 | { 183 | // Prefer provided shader; fall back to common pipelines 184 | var requested = properties?["shader"]?.ToString(); 185 | Shader shader = 186 | (!string.IsNullOrEmpty(requested) ? Shader.Find(requested) : null) 187 | ?? Shader.Find("Universal Render Pipeline/Lit") 188 | ?? Shader.Find("HDRP/Lit") 189 | ?? Shader.Find("Standard") 190 | ?? Shader.Find("Unlit/Color"); 191 | if (shader == null) 192 | return Response.Error($"Could not find a suitable shader (requested: '{requested ?? "none"}')."); 193 | 194 | var mat = new Material(shader); 195 | if (properties != null) 196 | ApplyMaterialProperties(mat, properties); 197 | AssetDatabase.CreateAsset(mat, fullPath); 198 | newAsset = mat; 199 | } 200 | else if (lowerAssetType == "physicsmaterial") 201 | { 202 | PhysicsMaterialType pmat = new PhysicsMaterialType(); 203 | if (properties != null) 204 | ApplyPhysicsMaterialProperties(pmat, properties); 205 | AssetDatabase.CreateAsset(pmat, fullPath); 206 | newAsset = pmat; 207 | } 208 | else if (lowerAssetType == "scriptableobject") 209 | { 210 | string scriptClassName = properties?["scriptClass"]?.ToString(); 211 | if (string.IsNullOrEmpty(scriptClassName)) 212 | return Response.Error( 213 | "'scriptClass' property required when creating ScriptableObject asset." 214 | ); 215 | 216 | Type scriptType = ComponentResolver.TryResolve(scriptClassName, out var resolvedType, out var error) ? resolvedType : null; 217 | if ( 218 | scriptType == null 219 | || !typeof(ScriptableObject).IsAssignableFrom(scriptType) 220 | ) 221 | { 222 | var reason = scriptType == null 223 | ? (string.IsNullOrEmpty(error) ? "Type not found." : error) 224 | : "Type found but does not inherit from ScriptableObject."; 225 | return Response.Error($"Script class '{scriptClassName}' invalid: {reason}"); 226 | } 227 | 228 | ScriptableObject so = ScriptableObject.CreateInstance(scriptType); 229 | // TODO: Apply properties from JObject to the ScriptableObject instance? 230 | AssetDatabase.CreateAsset(so, fullPath); 231 | newAsset = so; 232 | } 233 | else if (lowerAssetType == "prefab") 234 | { 235 | // Creating prefabs usually involves saving an existing GameObject hierarchy. 236 | // A common pattern is to create an empty GameObject, configure it, and then save it. 237 | return Response.Error( 238 | "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." 239 | ); 240 | // Example (conceptual): 241 | // GameObject source = GameObject.Find(properties["sourceGameObject"].ToString()); 242 | // if(source != null) PrefabUtility.SaveAsPrefabAsset(source, fullPath); 243 | } 244 | // TODO: Add more asset types (Animation Controller, Scene, etc.) 245 | else 246 | { 247 | // Generic creation attempt (might fail or create empty files) 248 | // For some types, just creating the file might be enough if Unity imports it. 249 | // File.Create(Path.Combine(Directory.GetCurrentDirectory(), fullPath)).Close(); 250 | // AssetDatabase.ImportAsset(fullPath); // Let Unity try to import it 251 | // newAsset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(fullPath); 252 | return Response.Error( 253 | $"Creation for asset type '{assetType}' is not explicitly supported yet. Supported: Folder, Material, ScriptableObject." 254 | ); 255 | } 256 | 257 | if ( 258 | newAsset == null 259 | && !Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), fullPath)) 260 | ) // Check if it wasn't a folder and asset wasn't created 261 | { 262 | return Response.Error( 263 | $"Failed to create asset '{assetType}' at '{fullPath}'. See logs for details." 264 | ); 265 | } 266 | 267 | AssetDatabase.SaveAssets(); 268 | // AssetDatabase.Refresh(); // CreateAsset often handles refresh 269 | return Response.Success( 270 | $"Asset '{fullPath}' created successfully.", 271 | GetAssetData(fullPath) 272 | ); 273 | } 274 | catch (Exception e) 275 | { 276 | return Response.Error($"Failed to create asset at '{fullPath}': {e.Message}"); 277 | } 278 | } 279 | 280 | private static object CreateFolder(string path) 281 | { 282 | if (string.IsNullOrEmpty(path)) 283 | return Response.Error("'path' is required for create_folder."); 284 | string fullPath = AssetPathUtility.SanitizeAssetPath(path); 285 | string parentDir = Path.GetDirectoryName(fullPath); 286 | string folderName = Path.GetFileName(fullPath); 287 | 288 | if (AssetExists(fullPath)) 289 | { 290 | // Check if it's actually a folder already 291 | if (AssetDatabase.IsValidFolder(fullPath)) 292 | { 293 | return Response.Success( 294 | $"Folder already exists at path: {fullPath}", 295 | GetAssetData(fullPath) 296 | ); 297 | } 298 | else 299 | { 300 | return Response.Error( 301 | $"An asset (not a folder) already exists at path: {fullPath}" 302 | ); 303 | } 304 | } 305 | 306 | try 307 | { 308 | // Ensure parent exists 309 | if (!string.IsNullOrEmpty(parentDir) && !AssetDatabase.IsValidFolder(parentDir)) 310 | { 311 | // Recursively create parent folders if needed (AssetDatabase handles this internally) 312 | // Or we can do it manually: Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), parentDir)); AssetDatabase.Refresh(); 313 | } 314 | 315 | string guid = AssetDatabase.CreateFolder(parentDir, folderName); 316 | if (string.IsNullOrEmpty(guid)) 317 | { 318 | return Response.Error( 319 | $"Failed to create folder '{fullPath}'. Check logs and permissions." 320 | ); 321 | } 322 | 323 | // AssetDatabase.Refresh(); // CreateFolder usually handles refresh 324 | return Response.Success( 325 | $"Folder '{fullPath}' created successfully.", 326 | GetAssetData(fullPath) 327 | ); 328 | } 329 | catch (Exception e) 330 | { 331 | return Response.Error($"Failed to create folder '{fullPath}': {e.Message}"); 332 | } 333 | } 334 | 335 | private static object ModifyAsset(string path, JObject properties) 336 | { 337 | if (string.IsNullOrEmpty(path)) 338 | return Response.Error("'path' is required for modify."); 339 | if (properties == null || !properties.HasValues) 340 | return Response.Error("'properties' are required for modify."); 341 | 342 | string fullPath = AssetPathUtility.SanitizeAssetPath(path); 343 | if (!AssetExists(fullPath)) 344 | return Response.Error($"Asset not found at path: {fullPath}"); 345 | 346 | try 347 | { 348 | UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>( 349 | fullPath 350 | ); 351 | if (asset == null) 352 | return Response.Error($"Failed to load asset at path: {fullPath}"); 353 | 354 | bool modified = false; // Flag to track if any changes were made 355 | 356 | // --- NEW: Handle GameObject / Prefab Component Modification --- 357 | if (asset is GameObject gameObject) 358 | { 359 | // Iterate through the properties JSON: keys are component names, values are properties objects for that component 360 | foreach (var prop in properties.Properties()) 361 | { 362 | string componentName = prop.Name; // e.g., "Collectible" 363 | // Check if the value associated with the component name is actually an object containing properties 364 | if ( 365 | prop.Value is JObject componentProperties 366 | && componentProperties.HasValues 367 | ) // e.g., {"bobSpeed": 2.0} 368 | { 369 | // Resolve component type via ComponentResolver, then fetch by Type 370 | Component targetComponent = null; 371 | bool resolved = ComponentResolver.TryResolve(componentName, out var compType, out var compError); 372 | if (resolved) 373 | { 374 | targetComponent = gameObject.GetComponent(compType); 375 | } 376 | 377 | // Only warn about resolution failure if component also not found 378 | if (targetComponent == null && !resolved) 379 | { 380 | Debug.LogWarning( 381 | $"[ManageAsset.ModifyAsset] Failed to resolve component '{componentName}' on '{gameObject.name}': {compError}" 382 | ); 383 | } 384 | 385 | if (targetComponent != null) 386 | { 387 | // Apply the nested properties (e.g., bobSpeed) to the found component instance 388 | // Use |= to ensure 'modified' becomes true if any component is successfully modified 389 | modified |= ApplyObjectProperties( 390 | targetComponent, 391 | componentProperties 392 | ); 393 | } 394 | else 395 | { 396 | // Log a warning if a specified component couldn't be found 397 | Debug.LogWarning( 398 | $"[ManageAsset.ModifyAsset] Component '{componentName}' not found on GameObject '{gameObject.name}' in asset '{fullPath}'. Skipping modification for this component." 399 | ); 400 | } 401 | } 402 | else 403 | { 404 | // Log a warning if the structure isn't {"ComponentName": {"prop": value}} 405 | // We could potentially try to apply this property directly to the GameObject here if needed, 406 | // but the primary goal is component modification. 407 | Debug.LogWarning( 408 | $"[ManageAsset.ModifyAsset] Property '{prop.Name}' for GameObject modification should have a JSON object value containing component properties. Value was: {prop.Value.Type}. Skipping." 409 | ); 410 | } 411 | } 412 | // Note: 'modified' is now true if ANY component property was successfully changed. 413 | } 414 | // --- End NEW --- 415 | 416 | // --- Existing logic for other asset types (now as else-if) --- 417 | // Example: Modifying a Material 418 | else if (asset is Material material) 419 | { 420 | // Apply properties directly to the material. If this modifies, it sets modified=true. 421 | // Use |= in case the asset was already marked modified by previous logic (though unlikely here) 422 | modified |= ApplyMaterialProperties(material, properties); 423 | } 424 | // Example: Modifying a ScriptableObject 425 | else if (asset is ScriptableObject so) 426 | { 427 | // Apply properties directly to the ScriptableObject. 428 | modified |= ApplyObjectProperties(so, properties); // General helper 429 | } 430 | // Example: Modifying TextureImporter settings 431 | else if (asset is Texture) 432 | { 433 | AssetImporter importer = AssetImporter.GetAtPath(fullPath); 434 | if (importer is TextureImporter textureImporter) 435 | { 436 | bool importerModified = ApplyObjectProperties(textureImporter, properties); 437 | if (importerModified) 438 | { 439 | // Importer settings need saving and reimporting 440 | AssetDatabase.WriteImportSettingsIfDirty(fullPath); 441 | AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); // Reimport to apply changes 442 | modified = true; // Mark overall operation as modified 443 | } 444 | } 445 | else 446 | { 447 | Debug.LogWarning($"Could not get TextureImporter for {fullPath}."); 448 | } 449 | } 450 | // TODO: Add modification logic for other common asset types (Models, AudioClips importers, etc.) 451 | else // Fallback for other asset types OR direct properties on non-GameObject assets 452 | { 453 | // This block handles non-GameObject/Material/ScriptableObject/Texture assets. 454 | // Attempts to apply properties directly to the asset itself. 455 | Debug.LogWarning( 456 | $"[ManageAsset.ModifyAsset] Asset type '{asset.GetType().Name}' at '{fullPath}' is not explicitly handled for component modification. Attempting generic property setting on the asset itself." 457 | ); 458 | modified |= ApplyObjectProperties(asset, properties); 459 | } 460 | // --- End Existing Logic --- 461 | 462 | // Check if any modification happened (either component or direct asset modification) 463 | if (modified) 464 | { 465 | // Mark the asset as dirty (important for prefabs/SOs) so Unity knows to save it. 466 | EditorUtility.SetDirty(asset); 467 | // Save all modified assets to disk. 468 | AssetDatabase.SaveAssets(); 469 | // Refresh might be needed in some edge cases, but SaveAssets usually covers it. 470 | // AssetDatabase.Refresh(); 471 | return Response.Success( 472 | $"Asset '{fullPath}' modified successfully.", 473 | GetAssetData(fullPath) 474 | ); 475 | } 476 | else 477 | { 478 | // If no changes were made (e.g., component not found, property names incorrect, value unchanged), return a success message indicating nothing changed. 479 | return Response.Success( 480 | $"No applicable or modifiable properties found for asset '{fullPath}'. Check component names, property names, and values.", 481 | GetAssetData(fullPath) 482 | ); 483 | // Previous message: return Response.Success($"No applicable properties found to modify for asset '{fullPath}'.", GetAssetData(fullPath)); 484 | } 485 | } 486 | catch (Exception e) 487 | { 488 | // Log the detailed error internally 489 | Debug.LogError($"[ManageAsset] Action 'modify' failed for path '{path}': {e}"); 490 | // Return a user-friendly error message 491 | return Response.Error($"Failed to modify asset '{fullPath}': {e.Message}"); 492 | } 493 | } 494 | 495 | private static object DeleteAsset(string path) 496 | { 497 | if (string.IsNullOrEmpty(path)) 498 | return Response.Error("'path' is required for delete."); 499 | string fullPath = AssetPathUtility.SanitizeAssetPath(path); 500 | if (!AssetExists(fullPath)) 501 | return Response.Error($"Asset not found at path: {fullPath}"); 502 | 503 | try 504 | { 505 | bool success = AssetDatabase.DeleteAsset(fullPath); 506 | if (success) 507 | { 508 | // AssetDatabase.Refresh(); // DeleteAsset usually handles refresh 509 | return Response.Success($"Asset '{fullPath}' deleted successfully."); 510 | } 511 | else 512 | { 513 | // This might happen if the file couldn't be deleted (e.g., locked) 514 | return Response.Error( 515 | $"Failed to delete asset '{fullPath}'. Check logs or if the file is locked." 516 | ); 517 | } 518 | } 519 | catch (Exception e) 520 | { 521 | return Response.Error($"Error deleting asset '{fullPath}': {e.Message}"); 522 | } 523 | } 524 | 525 | private static object DuplicateAsset(string path, string destinationPath) 526 | { 527 | if (string.IsNullOrEmpty(path)) 528 | return Response.Error("'path' is required for duplicate."); 529 | 530 | string sourcePath = AssetPathUtility.SanitizeAssetPath(path); 531 | if (!AssetExists(sourcePath)) 532 | return Response.Error($"Source asset not found at path: {sourcePath}"); 533 | 534 | string destPath; 535 | if (string.IsNullOrEmpty(destinationPath)) 536 | { 537 | // Generate a unique path if destination is not provided 538 | destPath = AssetDatabase.GenerateUniqueAssetPath(sourcePath); 539 | } 540 | else 541 | { 542 | destPath = AssetPathUtility.SanitizeAssetPath(destinationPath); 543 | if (AssetExists(destPath)) 544 | return Response.Error($"Asset already exists at destination path: {destPath}"); 545 | // Ensure destination directory exists 546 | EnsureDirectoryExists(Path.GetDirectoryName(destPath)); 547 | } 548 | 549 | try 550 | { 551 | bool success = AssetDatabase.CopyAsset(sourcePath, destPath); 552 | if (success) 553 | { 554 | // AssetDatabase.Refresh(); 555 | return Response.Success( 556 | $"Asset '{sourcePath}' duplicated to '{destPath}'.", 557 | GetAssetData(destPath) 558 | ); 559 | } 560 | else 561 | { 562 | return Response.Error( 563 | $"Failed to duplicate asset from '{sourcePath}' to '{destPath}'." 564 | ); 565 | } 566 | } 567 | catch (Exception e) 568 | { 569 | return Response.Error($"Error duplicating asset '{sourcePath}': {e.Message}"); 570 | } 571 | } 572 | 573 | private static object MoveOrRenameAsset(string path, string destinationPath) 574 | { 575 | if (string.IsNullOrEmpty(path)) 576 | return Response.Error("'path' is required for move/rename."); 577 | if (string.IsNullOrEmpty(destinationPath)) 578 | return Response.Error("'destination' path is required for move/rename."); 579 | 580 | string sourcePath = AssetPathUtility.SanitizeAssetPath(path); 581 | string destPath = AssetPathUtility.SanitizeAssetPath(destinationPath); 582 | 583 | if (!AssetExists(sourcePath)) 584 | return Response.Error($"Source asset not found at path: {sourcePath}"); 585 | if (AssetExists(destPath)) 586 | return Response.Error( 587 | $"An asset already exists at the destination path: {destPath}" 588 | ); 589 | 590 | // Ensure destination directory exists 591 | EnsureDirectoryExists(Path.GetDirectoryName(destPath)); 592 | 593 | try 594 | { 595 | // Validate will return an error string if failed, null if successful 596 | string error = AssetDatabase.ValidateMoveAsset(sourcePath, destPath); 597 | if (!string.IsNullOrEmpty(error)) 598 | { 599 | return Response.Error( 600 | $"Failed to move/rename asset from '{sourcePath}' to '{destPath}': {error}" 601 | ); 602 | } 603 | 604 | string guid = AssetDatabase.MoveAsset(sourcePath, destPath); 605 | if (!string.IsNullOrEmpty(guid)) // MoveAsset returns the new GUID on success 606 | { 607 | // AssetDatabase.Refresh(); // MoveAsset usually handles refresh 608 | return Response.Success( 609 | $"Asset moved/renamed from '{sourcePath}' to '{destPath}'.", 610 | GetAssetData(destPath) 611 | ); 612 | } 613 | else 614 | { 615 | // This case might not be reachable if ValidateMoveAsset passes, but good to have 616 | return Response.Error( 617 | $"MoveAsset call failed unexpectedly for '{sourcePath}' to '{destPath}'." 618 | ); 619 | } 620 | } 621 | catch (Exception e) 622 | { 623 | return Response.Error($"Error moving/renaming asset '{sourcePath}': {e.Message}"); 624 | } 625 | } 626 | 627 | private static object SearchAssets(JObject @params) 628 | { 629 | string searchPattern = @params["searchPattern"]?.ToString(); 630 | string filterType = @params["filterType"]?.ToString(); 631 | string pathScope = @params["path"]?.ToString(); // Use path as folder scope 632 | string filterDateAfterStr = @params["filterDateAfter"]?.ToString(); 633 | int pageSize = @params["pageSize"]?.ToObject<int?>() ?? 50; // Default page size 634 | int pageNumber = @params["pageNumber"]?.ToObject<int?>() ?? 1; // Default page number (1-based) 635 | bool generatePreview = @params["generatePreview"]?.ToObject<bool>() ?? false; 636 | 637 | List<string> searchFilters = new List<string>(); 638 | if (!string.IsNullOrEmpty(searchPattern)) 639 | searchFilters.Add(searchPattern); 640 | if (!string.IsNullOrEmpty(filterType)) 641 | searchFilters.Add($"t:{filterType}"); 642 | 643 | string[] folderScope = null; 644 | if (!string.IsNullOrEmpty(pathScope)) 645 | { 646 | folderScope = new string[] { AssetPathUtility.SanitizeAssetPath(pathScope) }; 647 | if (!AssetDatabase.IsValidFolder(folderScope[0])) 648 | { 649 | // Maybe the user provided a file path instead of a folder? 650 | // We could search in the containing folder, or return an error. 651 | Debug.LogWarning( 652 | $"Search path '{folderScope[0]}' is not a valid folder. Searching entire project." 653 | ); 654 | folderScope = null; // Search everywhere if path isn't a folder 655 | } 656 | } 657 | 658 | DateTime? filterDateAfter = null; 659 | if (!string.IsNullOrEmpty(filterDateAfterStr)) 660 | { 661 | if ( 662 | DateTime.TryParse( 663 | filterDateAfterStr, 664 | CultureInfo.InvariantCulture, 665 | DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, 666 | out DateTime parsedDate 667 | ) 668 | ) 669 | { 670 | filterDateAfter = parsedDate; 671 | } 672 | else 673 | { 674 | Debug.LogWarning( 675 | $"Could not parse filterDateAfter: '{filterDateAfterStr}'. Expected ISO 8601 format." 676 | ); 677 | } 678 | } 679 | 680 | try 681 | { 682 | string[] guids = AssetDatabase.FindAssets( 683 | string.Join(" ", searchFilters), 684 | folderScope 685 | ); 686 | List<object> results = new List<object>(); 687 | int totalFound = 0; 688 | 689 | foreach (string guid in guids) 690 | { 691 | string assetPath = AssetDatabase.GUIDToAssetPath(guid); 692 | if (string.IsNullOrEmpty(assetPath)) 693 | continue; 694 | 695 | // Apply date filter if present 696 | if (filterDateAfter.HasValue) 697 | { 698 | DateTime lastWriteTime = File.GetLastWriteTimeUtc( 699 | Path.Combine(Directory.GetCurrentDirectory(), assetPath) 700 | ); 701 | if (lastWriteTime <= filterDateAfter.Value) 702 | { 703 | continue; // Skip assets older than or equal to the filter date 704 | } 705 | } 706 | 707 | totalFound++; // Count matching assets before pagination 708 | results.Add(GetAssetData(assetPath, generatePreview)); 709 | } 710 | 711 | // Apply pagination 712 | int startIndex = (pageNumber - 1) * pageSize; 713 | var pagedResults = results.Skip(startIndex).Take(pageSize).ToList(); 714 | 715 | return Response.Success( 716 | $"Found {totalFound} asset(s). Returning page {pageNumber} ({pagedResults.Count} assets).", 717 | new 718 | { 719 | totalAssets = totalFound, 720 | pageSize = pageSize, 721 | pageNumber = pageNumber, 722 | assets = pagedResults, 723 | } 724 | ); 725 | } 726 | catch (Exception e) 727 | { 728 | return Response.Error($"Error searching assets: {e.Message}"); 729 | } 730 | } 731 | 732 | private static object GetAssetInfo(string path, bool generatePreview) 733 | { 734 | if (string.IsNullOrEmpty(path)) 735 | return Response.Error("'path' is required for get_info."); 736 | string fullPath = AssetPathUtility.SanitizeAssetPath(path); 737 | if (!AssetExists(fullPath)) 738 | return Response.Error($"Asset not found at path: {fullPath}"); 739 | 740 | try 741 | { 742 | return Response.Success( 743 | "Asset info retrieved.", 744 | GetAssetData(fullPath, generatePreview) 745 | ); 746 | } 747 | catch (Exception e) 748 | { 749 | return Response.Error($"Error getting info for asset '{fullPath}': {e.Message}"); 750 | } 751 | } 752 | 753 | /// <summary> 754 | /// Retrieves components attached to a GameObject asset (like a Prefab). 755 | /// </summary> 756 | /// <param name="path">The asset path of the GameObject or Prefab.</param> 757 | /// <returns>A response object containing a list of component type names or an error.</returns> 758 | private static object GetComponentsFromAsset(string path) 759 | { 760 | // 1. Validate input path 761 | if (string.IsNullOrEmpty(path)) 762 | return Response.Error("'path' is required for get_components."); 763 | 764 | // 2. Sanitize and check existence 765 | string fullPath = AssetPathUtility.SanitizeAssetPath(path); 766 | if (!AssetExists(fullPath)) 767 | return Response.Error($"Asset not found at path: {fullPath}"); 768 | 769 | try 770 | { 771 | // 3. Load the asset 772 | UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>( 773 | fullPath 774 | ); 775 | if (asset == null) 776 | return Response.Error($"Failed to load asset at path: {fullPath}"); 777 | 778 | // 4. Check if it's a GameObject (Prefabs load as GameObjects) 779 | GameObject gameObject = asset as GameObject; 780 | if (gameObject == null) 781 | { 782 | // Also check if it's *directly* a Component type (less common for primary assets) 783 | Component componentAsset = asset as Component; 784 | if (componentAsset != null) 785 | { 786 | // If the asset itself *is* a component, maybe return just its info? 787 | // This is an edge case. Let's stick to GameObjects for now. 788 | return Response.Error( 789 | $"Asset at '{fullPath}' is a Component ({asset.GetType().FullName}), not a GameObject. Components are typically retrieved *from* a GameObject." 790 | ); 791 | } 792 | return Response.Error( 793 | $"Asset at '{fullPath}' is not a GameObject (Type: {asset.GetType().FullName}). Cannot get components from this asset type." 794 | ); 795 | } 796 | 797 | // 5. Get components 798 | Component[] components = gameObject.GetComponents<Component>(); 799 | 800 | // 6. Format component data 801 | List<object> componentList = components 802 | .Select(comp => new 803 | { 804 | typeName = comp.GetType().FullName, 805 | instanceID = comp.GetInstanceID(), 806 | // TODO: Add more component-specific details here if needed in the future? 807 | // Requires reflection or specific handling per component type. 808 | }) 809 | .ToList<object>(); // Explicit cast for clarity if needed 810 | 811 | // 7. Return success response 812 | return Response.Success( 813 | $"Found {componentList.Count} component(s) on asset '{fullPath}'.", 814 | componentList 815 | ); 816 | } 817 | catch (Exception e) 818 | { 819 | Debug.LogError( 820 | $"[ManageAsset.GetComponentsFromAsset] Error getting components for '{fullPath}': {e}" 821 | ); 822 | return Response.Error( 823 | $"Error getting components for asset '{fullPath}': {e.Message}" 824 | ); 825 | } 826 | } 827 | 828 | // --- Internal Helpers --- 829 | 830 | /// <summary> 831 | /// Ensures the asset path starts with "Assets/". 832 | /// </summary> 833 | /// <summary> 834 | /// Checks if an asset exists at the given path (file or folder). 835 | /// </summary> 836 | private static bool AssetExists(string sanitizedPath) 837 | { 838 | // AssetDatabase APIs are generally preferred over raw File/Directory checks for assets. 839 | // Check if it's a known asset GUID. 840 | if (!string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(sanitizedPath))) 841 | { 842 | return true; 843 | } 844 | // AssetPathToGUID might not work for newly created folders not yet refreshed. 845 | // Check directory explicitly for folders. 846 | if (Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), sanitizedPath))) 847 | { 848 | // Check if it's considered a *valid* folder by Unity 849 | return AssetDatabase.IsValidFolder(sanitizedPath); 850 | } 851 | // Check file existence for non-folder assets. 852 | if (File.Exists(Path.Combine(Directory.GetCurrentDirectory(), sanitizedPath))) 853 | { 854 | return true; // Assume if file exists, it's an asset or will be imported 855 | } 856 | 857 | return false; 858 | // Alternative: return !string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(sanitizedPath)); 859 | } 860 | 861 | /// <summary> 862 | /// Ensures the directory for a given asset path exists, creating it if necessary. 863 | /// </summary> 864 | private static void EnsureDirectoryExists(string directoryPath) 865 | { 866 | if (string.IsNullOrEmpty(directoryPath)) 867 | return; 868 | string fullDirPath = Path.Combine(Directory.GetCurrentDirectory(), directoryPath); 869 | if (!Directory.Exists(fullDirPath)) 870 | { 871 | Directory.CreateDirectory(fullDirPath); 872 | AssetDatabase.Refresh(); // Let Unity know about the new folder 873 | } 874 | } 875 | 876 | /// <summary> 877 | /// Applies properties from JObject to a Material. 878 | /// </summary> 879 | private static bool ApplyMaterialProperties(Material mat, JObject properties) 880 | { 881 | if (mat == null || properties == null) 882 | return false; 883 | bool modified = false; 884 | 885 | // Example: Set shader 886 | if (properties["shader"]?.Type == JTokenType.String) 887 | { 888 | Shader newShader = Shader.Find(properties["shader"].ToString()); 889 | if (newShader != null && mat.shader != newShader) 890 | { 891 | mat.shader = newShader; 892 | modified = true; 893 | } 894 | } 895 | // Example: Set color property 896 | if (properties["color"] is JObject colorProps) 897 | { 898 | string propName = colorProps["name"]?.ToString() ?? "_Color"; // Default main color 899 | if (colorProps["value"] is JArray colArr && colArr.Count >= 3) 900 | { 901 | try 902 | { 903 | Color newColor = new Color( 904 | colArr[0].ToObject<float>(), 905 | colArr[1].ToObject<float>(), 906 | colArr[2].ToObject<float>(), 907 | colArr.Count > 3 ? colArr[3].ToObject<float>() : 1.0f 908 | ); 909 | if (mat.HasProperty(propName) && mat.GetColor(propName) != newColor) 910 | { 911 | mat.SetColor(propName, newColor); 912 | modified = true; 913 | } 914 | } 915 | catch (Exception ex) 916 | { 917 | Debug.LogWarning( 918 | $"Error parsing color property '{propName}': {ex.Message}" 919 | ); 920 | } 921 | } 922 | } 923 | else if (properties["color"] is JArray colorArr) //Use color now with examples set in manage_asset.py 924 | { 925 | string propName = "_Color"; 926 | try 927 | { 928 | if (colorArr.Count >= 3) 929 | { 930 | Color newColor = new Color( 931 | colorArr[0].ToObject<float>(), 932 | colorArr[1].ToObject<float>(), 933 | colorArr[2].ToObject<float>(), 934 | colorArr.Count > 3 ? colorArr[3].ToObject<float>() : 1.0f 935 | ); 936 | if (mat.HasProperty(propName) && mat.GetColor(propName) != newColor) 937 | { 938 | mat.SetColor(propName, newColor); 939 | modified = true; 940 | } 941 | } 942 | } 943 | catch (Exception ex) 944 | { 945 | Debug.LogWarning( 946 | $"Error parsing color property '{propName}': {ex.Message}" 947 | ); 948 | } 949 | } 950 | // Example: Set float property 951 | if (properties["float"] is JObject floatProps) 952 | { 953 | string propName = floatProps["name"]?.ToString(); 954 | if ( 955 | !string.IsNullOrEmpty(propName) && 956 | (floatProps["value"]?.Type == JTokenType.Float || floatProps["value"]?.Type == JTokenType.Integer) 957 | ) 958 | { 959 | try 960 | { 961 | float newVal = floatProps["value"].ToObject<float>(); 962 | if (mat.HasProperty(propName) && mat.GetFloat(propName) != newVal) 963 | { 964 | mat.SetFloat(propName, newVal); 965 | modified = true; 966 | } 967 | } 968 | catch (Exception ex) 969 | { 970 | Debug.LogWarning( 971 | $"Error parsing float property '{propName}': {ex.Message}" 972 | ); 973 | } 974 | } 975 | } 976 | // Example: Set texture property 977 | if (properties["texture"] is JObject texProps) 978 | { 979 | string propName = texProps["name"]?.ToString() ?? "_MainTex"; // Default main texture 980 | string texPath = texProps["path"]?.ToString(); 981 | if (!string.IsNullOrEmpty(texPath)) 982 | { 983 | Texture newTex = AssetDatabase.LoadAssetAtPath<Texture>( 984 | AssetPathUtility.SanitizeAssetPath(texPath) 985 | ); 986 | if ( 987 | newTex != null 988 | && mat.HasProperty(propName) 989 | && mat.GetTexture(propName) != newTex 990 | ) 991 | { 992 | mat.SetTexture(propName, newTex); 993 | modified = true; 994 | } 995 | else if (newTex == null) 996 | { 997 | Debug.LogWarning($"Texture not found at path: {texPath}"); 998 | } 999 | } 1000 | } 1001 | 1002 | // TODO: Add handlers for other property types (Vectors, Ints, Keywords, RenderQueue, etc.) 1003 | return modified; 1004 | } 1005 | 1006 | /// <summary> 1007 | /// Applies properties from JObject to a PhysicsMaterial. 1008 | /// </summary> 1009 | private static bool ApplyPhysicsMaterialProperties(PhysicsMaterialType pmat, JObject properties) 1010 | { 1011 | if (pmat == null || properties == null) 1012 | return false; 1013 | bool modified = false; 1014 | 1015 | // Example: Set dynamic friction 1016 | if (properties["dynamicFriction"]?.Type == JTokenType.Float) 1017 | { 1018 | float dynamicFriction = properties["dynamicFriction"].ToObject<float>(); 1019 | pmat.dynamicFriction = dynamicFriction; 1020 | modified = true; 1021 | } 1022 | 1023 | // Example: Set static friction 1024 | if (properties["staticFriction"]?.Type == JTokenType.Float) 1025 | { 1026 | float staticFriction = properties["staticFriction"].ToObject<float>(); 1027 | pmat.staticFriction = staticFriction; 1028 | modified = true; 1029 | } 1030 | 1031 | // Example: Set bounciness 1032 | if (properties["bounciness"]?.Type == JTokenType.Float) 1033 | { 1034 | float bounciness = properties["bounciness"].ToObject<float>(); 1035 | pmat.bounciness = bounciness; 1036 | modified = true; 1037 | } 1038 | 1039 | List<String> averageList = new List<String> { "ave", "Ave", "average", "Average" }; 1040 | List<String> multiplyList = new List<String> { "mul", "Mul", "mult", "Mult", "multiply", "Multiply" }; 1041 | List<String> minimumList = new List<String> { "min", "Min", "minimum", "Minimum" }; 1042 | List<String> maximumList = new List<String> { "max", "Max", "maximum", "Maximum" }; 1043 | 1044 | // Example: Set friction combine 1045 | if (properties["frictionCombine"]?.Type == JTokenType.String) 1046 | { 1047 | string frictionCombine = properties["frictionCombine"].ToString(); 1048 | if (averageList.Contains(frictionCombine)) 1049 | pmat.frictionCombine = PhysicsMaterialCombine.Average; 1050 | else if (multiplyList.Contains(frictionCombine)) 1051 | pmat.frictionCombine = PhysicsMaterialCombine.Multiply; 1052 | else if (minimumList.Contains(frictionCombine)) 1053 | pmat.frictionCombine = PhysicsMaterialCombine.Minimum; 1054 | else if (maximumList.Contains(frictionCombine)) 1055 | pmat.frictionCombine = PhysicsMaterialCombine.Maximum; 1056 | modified = true; 1057 | } 1058 | 1059 | // Example: Set bounce combine 1060 | if (properties["bounceCombine"]?.Type == JTokenType.String) 1061 | { 1062 | string bounceCombine = properties["bounceCombine"].ToString(); 1063 | if (averageList.Contains(bounceCombine)) 1064 | pmat.bounceCombine = PhysicsMaterialCombine.Average; 1065 | else if (multiplyList.Contains(bounceCombine)) 1066 | pmat.bounceCombine = PhysicsMaterialCombine.Multiply; 1067 | else if (minimumList.Contains(bounceCombine)) 1068 | pmat.bounceCombine = PhysicsMaterialCombine.Minimum; 1069 | else if (maximumList.Contains(bounceCombine)) 1070 | pmat.bounceCombine = PhysicsMaterialCombine.Maximum; 1071 | modified = true; 1072 | } 1073 | 1074 | return modified; 1075 | } 1076 | 1077 | /// <summary> 1078 | /// Generic helper to set properties on any UnityEngine.Object using reflection. 1079 | /// </summary> 1080 | private static bool ApplyObjectProperties(UnityEngine.Object target, JObject properties) 1081 | { 1082 | if (target == null || properties == null) 1083 | return false; 1084 | bool modified = false; 1085 | Type type = target.GetType(); 1086 | 1087 | foreach (var prop in properties.Properties()) 1088 | { 1089 | string propName = prop.Name; 1090 | JToken propValue = prop.Value; 1091 | if (SetPropertyOrField(target, propName, propValue, type)) 1092 | { 1093 | modified = true; 1094 | } 1095 | } 1096 | return modified; 1097 | } 1098 | 1099 | /// <summary> 1100 | /// Helper to set a property or field via reflection, handling basic types and Unity objects. 1101 | /// </summary> 1102 | private static bool SetPropertyOrField( 1103 | object target, 1104 | string memberName, 1105 | JToken value, 1106 | Type type = null 1107 | ) 1108 | { 1109 | type = type ?? target.GetType(); 1110 | System.Reflection.BindingFlags flags = 1111 | System.Reflection.BindingFlags.Public 1112 | | System.Reflection.BindingFlags.Instance 1113 | | System.Reflection.BindingFlags.IgnoreCase; 1114 | 1115 | try 1116 | { 1117 | System.Reflection.PropertyInfo propInfo = type.GetProperty(memberName, flags); 1118 | if (propInfo != null && propInfo.CanWrite) 1119 | { 1120 | object convertedValue = ConvertJTokenToType(value, propInfo.PropertyType); 1121 | if ( 1122 | convertedValue != null 1123 | && !object.Equals(propInfo.GetValue(target), convertedValue) 1124 | ) 1125 | { 1126 | propInfo.SetValue(target, convertedValue); 1127 | return true; 1128 | } 1129 | } 1130 | else 1131 | { 1132 | System.Reflection.FieldInfo fieldInfo = type.GetField(memberName, flags); 1133 | if (fieldInfo != null) 1134 | { 1135 | object convertedValue = ConvertJTokenToType(value, fieldInfo.FieldType); 1136 | if ( 1137 | convertedValue != null 1138 | && !object.Equals(fieldInfo.GetValue(target), convertedValue) 1139 | ) 1140 | { 1141 | fieldInfo.SetValue(target, convertedValue); 1142 | return true; 1143 | } 1144 | } 1145 | } 1146 | } 1147 | catch (Exception ex) 1148 | { 1149 | Debug.LogWarning( 1150 | $"[SetPropertyOrField] Failed to set '{memberName}' on {type.Name}: {ex.Message}" 1151 | ); 1152 | } 1153 | return false; 1154 | } 1155 | 1156 | /// <summary> 1157 | /// Simple JToken to Type conversion for common Unity types and primitives. 1158 | /// </summary> 1159 | private static object ConvertJTokenToType(JToken token, Type targetType) 1160 | { 1161 | try 1162 | { 1163 | if (token == null || token.Type == JTokenType.Null) 1164 | return null; 1165 | 1166 | if (targetType == typeof(string)) 1167 | return token.ToObject<string>(); 1168 | if (targetType == typeof(int)) 1169 | return token.ToObject<int>(); 1170 | if (targetType == typeof(float)) 1171 | return token.ToObject<float>(); 1172 | if (targetType == typeof(bool)) 1173 | return token.ToObject<bool>(); 1174 | if (targetType == typeof(Vector2) && token is JArray arrV2 && arrV2.Count == 2) 1175 | return new Vector2(arrV2[0].ToObject<float>(), arrV2[1].ToObject<float>()); 1176 | if (targetType == typeof(Vector3) && token is JArray arrV3 && arrV3.Count == 3) 1177 | return new Vector3( 1178 | arrV3[0].ToObject<float>(), 1179 | arrV3[1].ToObject<float>(), 1180 | arrV3[2].ToObject<float>() 1181 | ); 1182 | if (targetType == typeof(Vector4) && token is JArray arrV4 && arrV4.Count == 4) 1183 | return new Vector4( 1184 | arrV4[0].ToObject<float>(), 1185 | arrV4[1].ToObject<float>(), 1186 | arrV4[2].ToObject<float>(), 1187 | arrV4[3].ToObject<float>() 1188 | ); 1189 | if (targetType == typeof(Quaternion) && token is JArray arrQ && arrQ.Count == 4) 1190 | return new Quaternion( 1191 | arrQ[0].ToObject<float>(), 1192 | arrQ[1].ToObject<float>(), 1193 | arrQ[2].ToObject<float>(), 1194 | arrQ[3].ToObject<float>() 1195 | ); 1196 | if (targetType == typeof(Color) && token is JArray arrC && arrC.Count >= 3) // Allow RGB or RGBA 1197 | return new Color( 1198 | arrC[0].ToObject<float>(), 1199 | arrC[1].ToObject<float>(), 1200 | arrC[2].ToObject<float>(), 1201 | arrC.Count > 3 ? arrC[3].ToObject<float>() : 1.0f 1202 | ); 1203 | if (targetType.IsEnum) 1204 | return Enum.Parse(targetType, token.ToString(), true); // Case-insensitive enum parsing 1205 | 1206 | // Handle loading Unity Objects (Materials, Textures, etc.) by path 1207 | if ( 1208 | typeof(UnityEngine.Object).IsAssignableFrom(targetType) 1209 | && token.Type == JTokenType.String 1210 | ) 1211 | { 1212 | string assetPath = AssetPathUtility.SanitizeAssetPath(token.ToString()); 1213 | UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath( 1214 | assetPath, 1215 | targetType 1216 | ); 1217 | if (loadedAsset == null) 1218 | { 1219 | Debug.LogWarning( 1220 | $"[ConvertJTokenToType] Could not load asset of type {targetType.Name} from path: {assetPath}" 1221 | ); 1222 | } 1223 | return loadedAsset; 1224 | } 1225 | 1226 | // Fallback: Try direct conversion (might work for other simple value types) 1227 | return token.ToObject(targetType); 1228 | } 1229 | catch (Exception ex) 1230 | { 1231 | Debug.LogWarning( 1232 | $"[ConvertJTokenToType] Could not convert JToken '{token}' (type {token.Type}) to type '{targetType.Name}': {ex.Message}" 1233 | ); 1234 | return null; 1235 | } 1236 | } 1237 | 1238 | 1239 | // --- Data Serialization --- 1240 | 1241 | /// <summary> 1242 | /// Creates a serializable representation of an asset. 1243 | /// </summary> 1244 | private static object GetAssetData(string path, bool generatePreview = false) 1245 | { 1246 | if (string.IsNullOrEmpty(path) || !AssetExists(path)) 1247 | return null; 1248 | 1249 | string guid = AssetDatabase.AssetPathToGUID(path); 1250 | Type assetType = AssetDatabase.GetMainAssetTypeAtPath(path); 1251 | UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path); 1252 | string previewBase64 = null; 1253 | int previewWidth = 0; 1254 | int previewHeight = 0; 1255 | 1256 | if (generatePreview && asset != null) 1257 | { 1258 | Texture2D preview = AssetPreview.GetAssetPreview(asset); 1259 | 1260 | if (preview != null) 1261 | { 1262 | try 1263 | { 1264 | // Ensure texture is readable for EncodeToPNG 1265 | // Creating a temporary readable copy is safer 1266 | RenderTexture rt = null; 1267 | Texture2D readablePreview = null; 1268 | RenderTexture previous = RenderTexture.active; 1269 | try 1270 | { 1271 | rt = RenderTexture.GetTemporary(preview.width, preview.height); 1272 | Graphics.Blit(preview, rt); 1273 | RenderTexture.active = rt; 1274 | readablePreview = new Texture2D(preview.width, preview.height, TextureFormat.RGB24, false); 1275 | readablePreview.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0); 1276 | readablePreview.Apply(); 1277 | 1278 | var pngData = readablePreview.EncodeToPNG(); 1279 | if (pngData != null && pngData.Length > 0) 1280 | { 1281 | previewBase64 = Convert.ToBase64String(pngData); 1282 | previewWidth = readablePreview.width; 1283 | previewHeight = readablePreview.height; 1284 | } 1285 | } 1286 | finally 1287 | { 1288 | RenderTexture.active = previous; 1289 | if (rt != null) RenderTexture.ReleaseTemporary(rt); 1290 | if (readablePreview != null) UnityEngine.Object.DestroyImmediate(readablePreview); 1291 | } 1292 | } 1293 | catch (Exception ex) 1294 | { 1295 | Debug.LogWarning( 1296 | $"Failed to generate readable preview for '{path}': {ex.Message}. Preview might not be readable." 1297 | ); 1298 | // Fallback: Try getting static preview if available? 1299 | // Texture2D staticPreview = AssetPreview.GetMiniThumbnail(asset); 1300 | } 1301 | } 1302 | else 1303 | { 1304 | Debug.LogWarning( 1305 | $"Could not get asset preview for {path} (Type: {assetType?.Name}). Is it supported?" 1306 | ); 1307 | } 1308 | } 1309 | 1310 | return new 1311 | { 1312 | path = path, 1313 | guid = guid, 1314 | assetType = assetType?.FullName ?? "Unknown", 1315 | name = Path.GetFileNameWithoutExtension(path), 1316 | fileName = Path.GetFileName(path), 1317 | isFolder = AssetDatabase.IsValidFolder(path), 1318 | instanceID = asset?.GetInstanceID() ?? 0, 1319 | lastWriteTimeUtc = File.GetLastWriteTimeUtc( 1320 | Path.Combine(Directory.GetCurrentDirectory(), path) 1321 | ) 1322 | .ToString("o"), // ISO 8601 1323 | // --- Preview Data --- 1324 | previewBase64 = previewBase64, // PNG data as Base64 string 1325 | previewWidth = previewWidth, 1326 | previewHeight = previewHeight, 1327 | // TODO: Add more metadata? Importer settings? Dependencies? 1328 | }; 1329 | } 1330 | } 1331 | } 1332 | ```