This is page 15 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/ManageGameObject.cs: -------------------------------------------------------------------------------- ```csharp 1 | #nullable disable 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using Newtonsoft.Json; // Added for JsonSerializationException 7 | using Newtonsoft.Json.Linq; 8 | using UnityEditor; 9 | using UnityEditor.Compilation; // For CompilationPipeline 10 | using UnityEditor.SceneManagement; 11 | using UnityEditorInternal; 12 | using UnityEngine; 13 | using UnityEngine.SceneManagement; 14 | using MCPForUnity.Editor.Helpers; // For Response class 15 | using MCPForUnity.Runtime.Serialization; 16 | 17 | namespace MCPForUnity.Editor.Tools 18 | { 19 | /// <summary> 20 | /// Handles GameObject manipulation within the current scene (CRUD, find, components). 21 | /// </summary> 22 | [McpForUnityTool("manage_gameobject")] 23 | public static class ManageGameObject 24 | { 25 | // Shared JsonSerializer to avoid per-call allocation overhead 26 | private static readonly JsonSerializer InputSerializer = JsonSerializer.Create(new JsonSerializerSettings 27 | { 28 | Converters = new List<JsonConverter> 29 | { 30 | new Vector3Converter(), 31 | new Vector2Converter(), 32 | new QuaternionConverter(), 33 | new ColorConverter(), 34 | new RectConverter(), 35 | new BoundsConverter(), 36 | new UnityEngineObjectConverter() 37 | } 38 | }); 39 | 40 | // --- Main Handler --- 41 | 42 | public static object HandleCommand(JObject @params) 43 | { 44 | if (@params == null) 45 | { 46 | return Response.Error("Parameters cannot be null."); 47 | } 48 | 49 | string action = @params["action"]?.ToString().ToLower(); 50 | if (string.IsNullOrEmpty(action)) 51 | { 52 | return Response.Error("Action parameter is required."); 53 | } 54 | 55 | // Parameters used by various actions 56 | JToken targetToken = @params["target"]; // Can be string (name/path) or int (instanceID) 57 | string searchMethod = @params["searchMethod"]?.ToString().ToLower(); 58 | 59 | // Get common parameters (consolidated) 60 | string name = @params["name"]?.ToString(); 61 | string tag = @params["tag"]?.ToString(); 62 | string layer = @params["layer"]?.ToString(); 63 | JToken parentToken = @params["parent"]; 64 | 65 | // --- Add parameter for controlling non-public field inclusion --- 66 | bool includeNonPublicSerialized = @params["includeNonPublicSerialized"]?.ToObject<bool>() ?? true; // Default to true 67 | // --- End add parameter --- 68 | 69 | // --- Prefab Redirection Check --- 70 | string targetPath = 71 | targetToken?.Type == JTokenType.String ? targetToken.ToString() : null; 72 | if ( 73 | !string.IsNullOrEmpty(targetPath) 74 | && targetPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase) 75 | ) 76 | { 77 | // Allow 'create' (instantiate), 'find' (?), 'get_components' (?) 78 | if (action == "modify" || action == "set_component_property") 79 | { 80 | Debug.Log( 81 | $"[ManageGameObject->ManageAsset] Redirecting action '{action}' for prefab '{targetPath}' to ManageAsset." 82 | ); 83 | // Prepare params for ManageAsset.ModifyAsset 84 | JObject assetParams = new JObject(); 85 | assetParams["action"] = "modify"; // ManageAsset uses "modify" 86 | assetParams["path"] = targetPath; 87 | 88 | // Extract properties. 89 | // For 'set_component_property', combine componentName and componentProperties. 90 | // For 'modify', directly use componentProperties. 91 | JObject properties = null; 92 | if (action == "set_component_property") 93 | { 94 | string compName = @params["componentName"]?.ToString(); 95 | JObject compProps = @params["componentProperties"]?[compName] as JObject; // Handle potential nesting 96 | if (string.IsNullOrEmpty(compName)) 97 | return Response.Error( 98 | "Missing 'componentName' for 'set_component_property' on prefab." 99 | ); 100 | if (compProps == null) 101 | return Response.Error( 102 | $"Missing or invalid 'componentProperties' for component '{compName}' for 'set_component_property' on prefab." 103 | ); 104 | 105 | properties = new JObject(); 106 | properties[compName] = compProps; 107 | } 108 | else // action == "modify" 109 | { 110 | properties = @params["componentProperties"] as JObject; 111 | if (properties == null) 112 | return Response.Error( 113 | "Missing 'componentProperties' for 'modify' action on prefab." 114 | ); 115 | } 116 | 117 | assetParams["properties"] = properties; 118 | 119 | // Call ManageAsset handler 120 | return ManageAsset.HandleCommand(assetParams); 121 | } 122 | else if ( 123 | action == "delete" 124 | || action == "add_component" 125 | || action == "remove_component" 126 | || action == "get_components" 127 | ) // Added get_components here too 128 | { 129 | // Explicitly block other modifications on the prefab asset itself via manage_gameobject 130 | return Response.Error( 131 | $"Action '{action}' on a prefab asset ('{targetPath}') should be performed using the 'manage_asset' command." 132 | ); 133 | } 134 | // Allow 'create' (instantiation) and 'find' to proceed, although finding a prefab asset by path might be less common via manage_gameobject. 135 | // No specific handling needed here, the code below will run. 136 | } 137 | // --- End Prefab Redirection Check --- 138 | 139 | try 140 | { 141 | switch (action) 142 | { 143 | case "create": 144 | return CreateGameObject(@params); 145 | case "modify": 146 | return ModifyGameObject(@params, targetToken, searchMethod); 147 | case "delete": 148 | return DeleteGameObject(targetToken, searchMethod); 149 | case "find": 150 | return FindGameObjects(@params, targetToken, searchMethod); 151 | case "get_components": 152 | string getCompTarget = targetToken?.ToString(); // Expect name, path, or ID string 153 | if (getCompTarget == null) 154 | return Response.Error( 155 | "'target' parameter required for get_components." 156 | ); 157 | // Pass the includeNonPublicSerialized flag here 158 | return GetComponentsFromTarget(getCompTarget, searchMethod, includeNonPublicSerialized); 159 | case "get_component": 160 | string getSingleCompTarget = targetToken?.ToString(); 161 | if (getSingleCompTarget == null) 162 | return Response.Error( 163 | "'target' parameter required for get_component." 164 | ); 165 | string componentName = @params["componentName"]?.ToString(); 166 | if (string.IsNullOrEmpty(componentName)) 167 | return Response.Error( 168 | "'componentName' parameter required for get_component." 169 | ); 170 | return GetSingleComponentFromTarget(getSingleCompTarget, searchMethod, componentName, includeNonPublicSerialized); 171 | case "add_component": 172 | return AddComponentToTarget(@params, targetToken, searchMethod); 173 | case "remove_component": 174 | return RemoveComponentFromTarget(@params, targetToken, searchMethod); 175 | case "set_component_property": 176 | return SetComponentPropertyOnTarget(@params, targetToken, searchMethod); 177 | 178 | default: 179 | return Response.Error($"Unknown action: '{action}'."); 180 | } 181 | } 182 | catch (Exception e) 183 | { 184 | Debug.LogError($"[ManageGameObject] Action '{action}' failed: {e}"); 185 | return Response.Error($"Internal error processing action '{action}': {e.Message}"); 186 | } 187 | } 188 | 189 | // --- Action Implementations --- 190 | 191 | private static object CreateGameObject(JObject @params) 192 | { 193 | string name = @params["name"]?.ToString(); 194 | if (string.IsNullOrEmpty(name)) 195 | { 196 | return Response.Error("'name' parameter is required for 'create' action."); 197 | } 198 | 199 | // Get prefab creation parameters 200 | bool saveAsPrefab = @params["saveAsPrefab"]?.ToObject<bool>() ?? false; 201 | string prefabPath = @params["prefabPath"]?.ToString(); 202 | string tag = @params["tag"]?.ToString(); // Get tag for creation 203 | string primitiveType = @params["primitiveType"]?.ToString(); // Keep primitiveType check 204 | GameObject newGo = null; // Initialize as null 205 | 206 | // --- Try Instantiating Prefab First --- 207 | string originalPrefabPath = prefabPath; // Keep original for messages 208 | if (!string.IsNullOrEmpty(prefabPath)) 209 | { 210 | // If no extension, search for the prefab by name 211 | if ( 212 | !prefabPath.Contains("/") 213 | && !prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase) 214 | ) 215 | { 216 | string prefabNameOnly = prefabPath; 217 | Debug.Log( 218 | $"[ManageGameObject.Create] Searching for prefab named: '{prefabNameOnly}'" 219 | ); 220 | string[] guids = AssetDatabase.FindAssets($"t:Prefab {prefabNameOnly}"); 221 | if (guids.Length == 0) 222 | { 223 | return Response.Error( 224 | $"Prefab named '{prefabNameOnly}' not found anywhere in the project." 225 | ); 226 | } 227 | else if (guids.Length > 1) 228 | { 229 | string foundPaths = string.Join( 230 | ", ", 231 | guids.Select(g => AssetDatabase.GUIDToAssetPath(g)) 232 | ); 233 | return Response.Error( 234 | $"Multiple prefabs found matching name '{prefabNameOnly}': {foundPaths}. Please provide a more specific path." 235 | ); 236 | } 237 | else // Exactly one found 238 | { 239 | prefabPath = AssetDatabase.GUIDToAssetPath(guids[0]); // Update prefabPath with the full path 240 | Debug.Log( 241 | $"[ManageGameObject.Create] Found unique prefab at path: '{prefabPath}'" 242 | ); 243 | } 244 | } 245 | else if (!prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) 246 | { 247 | // If it looks like a path but doesn't end with .prefab, assume user forgot it and append it. 248 | Debug.LogWarning( 249 | $"[ManageGameObject.Create] Provided prefabPath '{prefabPath}' does not end with .prefab. Assuming it's missing and appending." 250 | ); 251 | prefabPath += ".prefab"; 252 | // Note: This path might still not exist, AssetDatabase.LoadAssetAtPath will handle that. 253 | } 254 | // The logic above now handles finding or assuming the .prefab extension. 255 | 256 | GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath); 257 | if (prefabAsset != null) 258 | { 259 | try 260 | { 261 | // Instantiate the prefab, initially place it at the root 262 | // Parent will be set later if specified 263 | newGo = PrefabUtility.InstantiatePrefab(prefabAsset) as GameObject; 264 | 265 | if (newGo == null) 266 | { 267 | // This might happen if the asset exists but isn't a valid GameObject prefab somehow 268 | Debug.LogError( 269 | $"[ManageGameObject.Create] Failed to instantiate prefab at '{prefabPath}', asset might be corrupted or not a GameObject." 270 | ); 271 | return Response.Error( 272 | $"Failed to instantiate prefab at '{prefabPath}'." 273 | ); 274 | } 275 | // Name the instance based on the 'name' parameter, not the prefab's default name 276 | if (!string.IsNullOrEmpty(name)) 277 | { 278 | newGo.name = name; 279 | } 280 | // Register Undo for prefab instantiation 281 | Undo.RegisterCreatedObjectUndo( 282 | newGo, 283 | $"Instantiate Prefab '{prefabAsset.name}' as '{newGo.name}'" 284 | ); 285 | Debug.Log( 286 | $"[ManageGameObject.Create] Instantiated prefab '{prefabAsset.name}' from path '{prefabPath}' as '{newGo.name}'." 287 | ); 288 | } 289 | catch (Exception e) 290 | { 291 | return Response.Error( 292 | $"Error instantiating prefab '{prefabPath}': {e.Message}" 293 | ); 294 | } 295 | } 296 | else 297 | { 298 | // Only return error if prefabPath was specified but not found. 299 | // If prefabPath was empty/null, we proceed to create primitive/empty. 300 | Debug.LogWarning( 301 | $"[ManageGameObject.Create] Prefab asset not found at path: '{prefabPath}'. Will proceed to create new object if specified." 302 | ); 303 | // Do not return error here, allow fallback to primitive/empty creation 304 | } 305 | } 306 | 307 | // --- Fallback: Create Primitive or Empty GameObject --- 308 | bool createdNewObject = false; // Flag to track if we created (not instantiated) 309 | if (newGo == null) // Only proceed if prefab instantiation didn't happen 310 | { 311 | if (!string.IsNullOrEmpty(primitiveType)) 312 | { 313 | try 314 | { 315 | PrimitiveType type = (PrimitiveType) 316 | Enum.Parse(typeof(PrimitiveType), primitiveType, true); 317 | newGo = GameObject.CreatePrimitive(type); 318 | // Set name *after* creation for primitives 319 | if (!string.IsNullOrEmpty(name)) 320 | { 321 | newGo.name = name; 322 | } 323 | else 324 | { 325 | UnityEngine.Object.DestroyImmediate(newGo); // cleanup leak 326 | return Response.Error( 327 | "'name' parameter is required when creating a primitive." 328 | ); 329 | } 330 | createdNewObject = true; 331 | } 332 | catch (ArgumentException) 333 | { 334 | return Response.Error( 335 | $"Invalid primitive type: '{primitiveType}'. Valid types: {string.Join(", ", Enum.GetNames(typeof(PrimitiveType)))}" 336 | ); 337 | } 338 | catch (Exception e) 339 | { 340 | return Response.Error( 341 | $"Failed to create primitive '{primitiveType}': {e.Message}" 342 | ); 343 | } 344 | } 345 | else // Create empty GameObject 346 | { 347 | if (string.IsNullOrEmpty(name)) 348 | { 349 | return Response.Error( 350 | "'name' parameter is required for 'create' action when not instantiating a prefab or creating a primitive." 351 | ); 352 | } 353 | newGo = new GameObject(name); 354 | createdNewObject = true; 355 | } 356 | // Record creation for Undo *only* if we created a new object 357 | if (createdNewObject) 358 | { 359 | Undo.RegisterCreatedObjectUndo(newGo, $"Create GameObject '{newGo.name}'"); 360 | } 361 | } 362 | // --- Common Setup (Parent, Transform, Tag, Components) - Applied AFTER object exists --- 363 | if (newGo == null) 364 | { 365 | // Should theoretically not happen if logic above is correct, but safety check. 366 | return Response.Error("Failed to create or instantiate the GameObject."); 367 | } 368 | 369 | // Record potential changes to the existing prefab instance or the new GO 370 | // Record transform separately in case parent changes affect it 371 | Undo.RecordObject(newGo.transform, "Set GameObject Transform"); 372 | Undo.RecordObject(newGo, "Set GameObject Properties"); 373 | 374 | // Set Parent 375 | JToken parentToken = @params["parent"]; 376 | if (parentToken != null) 377 | { 378 | GameObject parentGo = FindObjectInternal(parentToken, "by_id_or_name_or_path"); // Flexible parent finding 379 | if (parentGo == null) 380 | { 381 | UnityEngine.Object.DestroyImmediate(newGo); // Clean up created object 382 | return Response.Error($"Parent specified ('{parentToken}') but not found."); 383 | } 384 | newGo.transform.SetParent(parentGo.transform, true); // worldPositionStays = true 385 | } 386 | 387 | // Set Transform 388 | Vector3? position = ParseVector3(@params["position"] as JArray); 389 | Vector3? rotation = ParseVector3(@params["rotation"] as JArray); 390 | Vector3? scale = ParseVector3(@params["scale"] as JArray); 391 | 392 | if (position.HasValue) 393 | newGo.transform.localPosition = position.Value; 394 | if (rotation.HasValue) 395 | newGo.transform.localEulerAngles = rotation.Value; 396 | if (scale.HasValue) 397 | newGo.transform.localScale = scale.Value; 398 | 399 | // Set Tag (added for create action) 400 | if (!string.IsNullOrEmpty(tag)) 401 | { 402 | // Similar logic as in ModifyGameObject for setting/creating tags 403 | string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag; 404 | try 405 | { 406 | newGo.tag = tagToSet; 407 | } 408 | catch (UnityException ex) 409 | { 410 | if (ex.Message.Contains("is not defined")) 411 | { 412 | Debug.LogWarning( 413 | $"[ManageGameObject.Create] Tag '{tagToSet}' not found. Attempting to create it." 414 | ); 415 | try 416 | { 417 | InternalEditorUtility.AddTag(tagToSet); 418 | newGo.tag = tagToSet; // Retry 419 | Debug.Log( 420 | $"[ManageGameObject.Create] Tag '{tagToSet}' created and assigned successfully." 421 | ); 422 | } 423 | catch (Exception innerEx) 424 | { 425 | UnityEngine.Object.DestroyImmediate(newGo); // Clean up 426 | return Response.Error( 427 | $"Failed to create or assign tag '{tagToSet}' during creation: {innerEx.Message}." 428 | ); 429 | } 430 | } 431 | else 432 | { 433 | UnityEngine.Object.DestroyImmediate(newGo); // Clean up 434 | return Response.Error( 435 | $"Failed to set tag to '{tagToSet}' during creation: {ex.Message}." 436 | ); 437 | } 438 | } 439 | } 440 | 441 | // Set Layer (new for create action) 442 | string layerName = @params["layer"]?.ToString(); 443 | if (!string.IsNullOrEmpty(layerName)) 444 | { 445 | int layerId = LayerMask.NameToLayer(layerName); 446 | if (layerId != -1) 447 | { 448 | newGo.layer = layerId; 449 | } 450 | else 451 | { 452 | Debug.LogWarning( 453 | $"[ManageGameObject.Create] Layer '{layerName}' not found. Using default layer." 454 | ); 455 | } 456 | } 457 | 458 | // Add Components 459 | if (@params["componentsToAdd"] is JArray componentsToAddArray) 460 | { 461 | foreach (var compToken in componentsToAddArray) 462 | { 463 | string typeName = null; 464 | JObject properties = null; 465 | 466 | if (compToken.Type == JTokenType.String) 467 | { 468 | typeName = compToken.ToString(); 469 | } 470 | else if (compToken is JObject compObj) 471 | { 472 | typeName = compObj["typeName"]?.ToString(); 473 | properties = compObj["properties"] as JObject; 474 | } 475 | 476 | if (!string.IsNullOrEmpty(typeName)) 477 | { 478 | var addResult = AddComponentInternal(newGo, typeName, properties); 479 | if (addResult != null) // Check if AddComponentInternal returned an error object 480 | { 481 | UnityEngine.Object.DestroyImmediate(newGo); // Clean up 482 | return addResult; // Return the error response 483 | } 484 | } 485 | else 486 | { 487 | Debug.LogWarning( 488 | $"[ManageGameObject] Invalid component format in componentsToAdd: {compToken}" 489 | ); 490 | } 491 | } 492 | } 493 | 494 | // Save as Prefab ONLY if we *created* a new object AND saveAsPrefab is true 495 | GameObject finalInstance = newGo; // Use this for selection and return data 496 | if (createdNewObject && saveAsPrefab) 497 | { 498 | string finalPrefabPath = prefabPath; // Use a separate variable for saving path 499 | // This check should now happen *before* attempting to save 500 | if (string.IsNullOrEmpty(finalPrefabPath)) 501 | { 502 | // Clean up the created object before returning error 503 | UnityEngine.Object.DestroyImmediate(newGo); 504 | return Response.Error( 505 | "'prefabPath' is required when 'saveAsPrefab' is true and creating a new object." 506 | ); 507 | } 508 | // Ensure the *saving* path ends with .prefab 509 | if (!finalPrefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) 510 | { 511 | Debug.Log( 512 | $"[ManageGameObject.Create] Appending .prefab extension to save path: '{finalPrefabPath}' -> '{finalPrefabPath}.prefab'" 513 | ); 514 | finalPrefabPath += ".prefab"; 515 | } 516 | 517 | try 518 | { 519 | // Ensure directory exists using the final saving path 520 | string directoryPath = System.IO.Path.GetDirectoryName(finalPrefabPath); 521 | if ( 522 | !string.IsNullOrEmpty(directoryPath) 523 | && !System.IO.Directory.Exists(directoryPath) 524 | ) 525 | { 526 | System.IO.Directory.CreateDirectory(directoryPath); 527 | AssetDatabase.Refresh(); // Refresh asset database to recognize the new folder 528 | Debug.Log( 529 | $"[ManageGameObject.Create] Created directory for prefab: {directoryPath}" 530 | ); 531 | } 532 | // Use SaveAsPrefabAssetAndConnect with the final saving path 533 | finalInstance = PrefabUtility.SaveAsPrefabAssetAndConnect( 534 | newGo, 535 | finalPrefabPath, 536 | InteractionMode.UserAction 537 | ); 538 | 539 | if (finalInstance == null) 540 | { 541 | // Destroy the original if saving failed somehow (shouldn't usually happen if path is valid) 542 | UnityEngine.Object.DestroyImmediate(newGo); 543 | return Response.Error( 544 | $"Failed to save GameObject '{name}' as prefab at '{finalPrefabPath}'. Check path and permissions." 545 | ); 546 | } 547 | Debug.Log( 548 | $"[ManageGameObject.Create] GameObject '{name}' saved as prefab to '{finalPrefabPath}' and instance connected." 549 | ); 550 | // Mark the new prefab asset as dirty? Not usually necessary, SaveAsPrefabAsset handles it. 551 | // EditorUtility.SetDirty(finalInstance); // Instance is handled by SaveAsPrefabAssetAndConnect 552 | } 553 | catch (Exception e) 554 | { 555 | // Clean up the instance if prefab saving fails 556 | UnityEngine.Object.DestroyImmediate(newGo); // Destroy the original attempt 557 | return Response.Error($"Error saving prefab '{finalPrefabPath}': {e.Message}"); 558 | } 559 | } 560 | 561 | // Select the instance in the scene (either prefab instance or newly created/saved one) 562 | Selection.activeGameObject = finalInstance; 563 | 564 | // Determine appropriate success message using the potentially updated or original path 565 | string messagePrefabPath = 566 | finalInstance == null 567 | ? originalPrefabPath 568 | : AssetDatabase.GetAssetPath( 569 | PrefabUtility.GetCorrespondingObjectFromSource(finalInstance) 570 | ?? (UnityEngine.Object)finalInstance 571 | ); 572 | string successMessage; 573 | if (!createdNewObject && !string.IsNullOrEmpty(messagePrefabPath)) // Instantiated existing prefab 574 | { 575 | successMessage = 576 | $"Prefab '{messagePrefabPath}' instantiated successfully as '{finalInstance.name}'."; 577 | } 578 | else if (createdNewObject && saveAsPrefab && !string.IsNullOrEmpty(messagePrefabPath)) // Created new and saved as prefab 579 | { 580 | successMessage = 581 | $"GameObject '{finalInstance.name}' created and saved as prefab to '{messagePrefabPath}'."; 582 | } 583 | else // Created new primitive or empty GO, didn't save as prefab 584 | { 585 | successMessage = 586 | $"GameObject '{finalInstance.name}' created successfully in scene."; 587 | } 588 | 589 | // Use the new serializer helper 590 | //return Response.Success(successMessage, GetGameObjectData(finalInstance)); 591 | return Response.Success(successMessage, Helpers.GameObjectSerializer.GetGameObjectData(finalInstance)); 592 | } 593 | 594 | private static object ModifyGameObject( 595 | JObject @params, 596 | JToken targetToken, 597 | string searchMethod 598 | ) 599 | { 600 | GameObject targetGo = FindObjectInternal(targetToken, searchMethod); 601 | if (targetGo == null) 602 | { 603 | return Response.Error( 604 | $"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'." 605 | ); 606 | } 607 | 608 | // Record state for Undo *before* modifications 609 | Undo.RecordObject(targetGo.transform, "Modify GameObject Transform"); 610 | Undo.RecordObject(targetGo, "Modify GameObject Properties"); 611 | 612 | bool modified = false; 613 | 614 | // Rename (using consolidated 'name' parameter) 615 | string name = @params["name"]?.ToString(); 616 | if (!string.IsNullOrEmpty(name) && targetGo.name != name) 617 | { 618 | targetGo.name = name; 619 | modified = true; 620 | } 621 | 622 | // Change Parent (using consolidated 'parent' parameter) 623 | JToken parentToken = @params["parent"]; 624 | if (parentToken != null) 625 | { 626 | GameObject newParentGo = FindObjectInternal(parentToken, "by_id_or_name_or_path"); 627 | // Check for hierarchy loops 628 | if ( 629 | newParentGo == null 630 | && !( 631 | parentToken.Type == JTokenType.Null 632 | || ( 633 | parentToken.Type == JTokenType.String 634 | && string.IsNullOrEmpty(parentToken.ToString()) 635 | ) 636 | ) 637 | ) 638 | { 639 | return Response.Error($"New parent ('{parentToken}') not found."); 640 | } 641 | if (newParentGo != null && newParentGo.transform.IsChildOf(targetGo.transform)) 642 | { 643 | return Response.Error( 644 | $"Cannot parent '{targetGo.name}' to '{newParentGo.name}', as it would create a hierarchy loop." 645 | ); 646 | } 647 | if (targetGo.transform.parent != (newParentGo?.transform)) 648 | { 649 | targetGo.transform.SetParent(newParentGo?.transform, true); // worldPositionStays = true 650 | modified = true; 651 | } 652 | } 653 | 654 | // Set Active State 655 | bool? setActive = @params["setActive"]?.ToObject<bool?>(); 656 | if (setActive.HasValue && targetGo.activeSelf != setActive.Value) 657 | { 658 | targetGo.SetActive(setActive.Value); 659 | modified = true; 660 | } 661 | 662 | // Change Tag (using consolidated 'tag' parameter) 663 | string tag = @params["tag"]?.ToString(); 664 | // Only attempt to change tag if a non-null tag is provided and it's different from the current one. 665 | // Allow setting an empty string to remove the tag (Unity uses "Untagged"). 666 | if (tag != null && targetGo.tag != tag) 667 | { 668 | // Ensure the tag is not empty, if empty, it means "Untagged" implicitly 669 | string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag; 670 | try 671 | { 672 | targetGo.tag = tagToSet; 673 | modified = true; 674 | } 675 | catch (UnityException ex) 676 | { 677 | // Check if the error is specifically because the tag doesn't exist 678 | if (ex.Message.Contains("is not defined")) 679 | { 680 | Debug.LogWarning( 681 | $"[ManageGameObject] Tag '{tagToSet}' not found. Attempting to create it." 682 | ); 683 | try 684 | { 685 | // Attempt to create the tag using internal utility 686 | InternalEditorUtility.AddTag(tagToSet); 687 | // Wait a frame maybe? Not strictly necessary but sometimes helps editor updates. 688 | // yield return null; // Cannot yield here, editor script limitation 689 | 690 | // Retry setting the tag immediately after creation 691 | targetGo.tag = tagToSet; 692 | modified = true; 693 | Debug.Log( 694 | $"[ManageGameObject] Tag '{tagToSet}' created and assigned successfully." 695 | ); 696 | } 697 | catch (Exception innerEx) 698 | { 699 | // Handle failure during tag creation or the second assignment attempt 700 | Debug.LogError( 701 | $"[ManageGameObject] Failed to create or assign tag '{tagToSet}' after attempting creation: {innerEx.Message}" 702 | ); 703 | return Response.Error( 704 | $"Failed to create or assign tag '{tagToSet}': {innerEx.Message}. Check Tag Manager and permissions." 705 | ); 706 | } 707 | } 708 | else 709 | { 710 | // If the exception was for a different reason, return the original error 711 | return Response.Error($"Failed to set tag to '{tagToSet}': {ex.Message}."); 712 | } 713 | } 714 | } 715 | 716 | // Change Layer (using consolidated 'layer' parameter) 717 | string layerName = @params["layer"]?.ToString(); 718 | if (!string.IsNullOrEmpty(layerName)) 719 | { 720 | int layerId = LayerMask.NameToLayer(layerName); 721 | if (layerId == -1 && layerName != "Default") 722 | { 723 | return Response.Error( 724 | $"Invalid layer specified: '{layerName}'. Use a valid layer name." 725 | ); 726 | } 727 | if (layerId != -1 && targetGo.layer != layerId) 728 | { 729 | targetGo.layer = layerId; 730 | modified = true; 731 | } 732 | } 733 | 734 | // Transform Modifications 735 | Vector3? position = ParseVector3(@params["position"] as JArray); 736 | Vector3? rotation = ParseVector3(@params["rotation"] as JArray); 737 | Vector3? scale = ParseVector3(@params["scale"] as JArray); 738 | 739 | if (position.HasValue && targetGo.transform.localPosition != position.Value) 740 | { 741 | targetGo.transform.localPosition = position.Value; 742 | modified = true; 743 | } 744 | if (rotation.HasValue && targetGo.transform.localEulerAngles != rotation.Value) 745 | { 746 | targetGo.transform.localEulerAngles = rotation.Value; 747 | modified = true; 748 | } 749 | if (scale.HasValue && targetGo.transform.localScale != scale.Value) 750 | { 751 | targetGo.transform.localScale = scale.Value; 752 | modified = true; 753 | } 754 | 755 | // --- Component Modifications --- 756 | // Note: These might need more specific Undo recording per component 757 | 758 | // Remove Components 759 | if (@params["componentsToRemove"] is JArray componentsToRemoveArray) 760 | { 761 | foreach (var compToken in componentsToRemoveArray) 762 | { 763 | // ... (parsing logic as in CreateGameObject) ... 764 | string typeName = compToken.ToString(); 765 | if (!string.IsNullOrEmpty(typeName)) 766 | { 767 | var removeResult = RemoveComponentInternal(targetGo, typeName); 768 | if (removeResult != null) 769 | return removeResult; // Return error if removal failed 770 | modified = true; 771 | } 772 | } 773 | } 774 | 775 | // Add Components (similar to create) 776 | if (@params["componentsToAdd"] is JArray componentsToAddArrayModify) 777 | { 778 | foreach (var compToken in componentsToAddArrayModify) 779 | { 780 | string typeName = null; 781 | JObject properties = null; 782 | if (compToken.Type == JTokenType.String) 783 | typeName = compToken.ToString(); 784 | else if (compToken is JObject compObj) 785 | { 786 | typeName = compObj["typeName"]?.ToString(); 787 | properties = compObj["properties"] as JObject; 788 | } 789 | 790 | if (!string.IsNullOrEmpty(typeName)) 791 | { 792 | var addResult = AddComponentInternal(targetGo, typeName, properties); 793 | if (addResult != null) 794 | return addResult; 795 | modified = true; 796 | } 797 | } 798 | } 799 | 800 | // Set Component Properties 801 | var componentErrors = new List<object>(); 802 | if (@params["componentProperties"] is JObject componentPropertiesObj) 803 | { 804 | foreach (var prop in componentPropertiesObj.Properties()) 805 | { 806 | string compName = prop.Name; 807 | JObject propertiesToSet = prop.Value as JObject; 808 | if (propertiesToSet != null) 809 | { 810 | var setResult = SetComponentPropertiesInternal( 811 | targetGo, 812 | compName, 813 | propertiesToSet 814 | ); 815 | if (setResult != null) 816 | { 817 | componentErrors.Add(setResult); 818 | } 819 | else 820 | { 821 | modified = true; 822 | } 823 | } 824 | } 825 | } 826 | 827 | // Return component errors if any occurred (after processing all components) 828 | if (componentErrors.Count > 0) 829 | { 830 | // Aggregate flattened error strings to make tests/API assertions simpler 831 | var aggregatedErrors = new System.Collections.Generic.List<string>(); 832 | foreach (var errorObj in componentErrors) 833 | { 834 | try 835 | { 836 | var dataProp = errorObj?.GetType().GetProperty("data"); 837 | var dataVal = dataProp?.GetValue(errorObj); 838 | if (dataVal != null) 839 | { 840 | var errorsProp = dataVal.GetType().GetProperty("errors"); 841 | var errorsEnum = errorsProp?.GetValue(dataVal) as System.Collections.IEnumerable; 842 | if (errorsEnum != null) 843 | { 844 | foreach (var item in errorsEnum) 845 | { 846 | var s = item?.ToString(); 847 | if (!string.IsNullOrEmpty(s)) aggregatedErrors.Add(s); 848 | } 849 | } 850 | } 851 | } 852 | catch { } 853 | } 854 | 855 | return Response.Error( 856 | $"One or more component property operations failed on '{targetGo.name}'.", 857 | new { componentErrors = componentErrors, errors = aggregatedErrors } 858 | ); 859 | } 860 | 861 | if (!modified) 862 | { 863 | // Use the new serializer helper 864 | // return Response.Success( 865 | // $"No modifications applied to GameObject '{targetGo.name}'.", 866 | // GetGameObjectData(targetGo)); 867 | 868 | return Response.Success( 869 | $"No modifications applied to GameObject '{targetGo.name}'.", 870 | Helpers.GameObjectSerializer.GetGameObjectData(targetGo) 871 | ); 872 | } 873 | 874 | EditorUtility.SetDirty(targetGo); // Mark scene as dirty 875 | // Use the new serializer helper 876 | return Response.Success( 877 | $"GameObject '{targetGo.name}' modified successfully.", 878 | Helpers.GameObjectSerializer.GetGameObjectData(targetGo) 879 | ); 880 | // return Response.Success( 881 | // $"GameObject '{targetGo.name}' modified successfully.", 882 | // GetGameObjectData(targetGo)); 883 | 884 | } 885 | 886 | private static object DeleteGameObject(JToken targetToken, string searchMethod) 887 | { 888 | // Find potentially multiple objects if name/tag search is used without find_all=false implicitly 889 | List<GameObject> targets = FindObjectsInternal(targetToken, searchMethod, true); // find_all=true for delete safety 890 | 891 | if (targets.Count == 0) 892 | { 893 | return Response.Error( 894 | $"Target GameObject(s) ('{targetToken}') not found using method '{searchMethod ?? "default"}'." 895 | ); 896 | } 897 | 898 | List<object> deletedObjects = new List<object>(); 899 | foreach (var targetGo in targets) 900 | { 901 | if (targetGo != null) 902 | { 903 | string goName = targetGo.name; 904 | int goId = targetGo.GetInstanceID(); 905 | // Use Undo.DestroyObjectImmediate for undo support 906 | Undo.DestroyObjectImmediate(targetGo); 907 | deletedObjects.Add(new { name = goName, instanceID = goId }); 908 | } 909 | } 910 | 911 | if (deletedObjects.Count > 0) 912 | { 913 | string message = 914 | targets.Count == 1 915 | ? $"GameObject '{deletedObjects[0].GetType().GetProperty("name").GetValue(deletedObjects[0])}' deleted successfully." 916 | : $"{deletedObjects.Count} GameObjects deleted successfully."; 917 | return Response.Success(message, deletedObjects); 918 | } 919 | else 920 | { 921 | // Should not happen if targets.Count > 0 initially, but defensive check 922 | return Response.Error("Failed to delete target GameObject(s)."); 923 | } 924 | } 925 | 926 | private static object FindGameObjects( 927 | JObject @params, 928 | JToken targetToken, 929 | string searchMethod 930 | ) 931 | { 932 | bool findAll = @params["findAll"]?.ToObject<bool>() ?? false; 933 | List<GameObject> foundObjects = FindObjectsInternal( 934 | targetToken, 935 | searchMethod, 936 | findAll, 937 | @params 938 | ); 939 | 940 | if (foundObjects.Count == 0) 941 | { 942 | return Response.Success("No matching GameObjects found.", new List<object>()); 943 | } 944 | 945 | // Use the new serializer helper 946 | //var results = foundObjects.Select(go => GetGameObjectData(go)).ToList(); 947 | var results = foundObjects.Select(go => Helpers.GameObjectSerializer.GetGameObjectData(go)).ToList(); 948 | return Response.Success($"Found {results.Count} GameObject(s).", results); 949 | } 950 | 951 | private static object GetComponentsFromTarget(string target, string searchMethod, bool includeNonPublicSerialized = true) 952 | { 953 | GameObject targetGo = FindObjectInternal(target, searchMethod); 954 | if (targetGo == null) 955 | { 956 | return Response.Error( 957 | $"Target GameObject ('{target}') not found using method '{searchMethod ?? "default"}'." 958 | ); 959 | } 960 | 961 | try 962 | { 963 | // --- Get components, immediately copy to list, and null original array --- 964 | Component[] originalComponents = targetGo.GetComponents<Component>(); 965 | List<Component> componentsToIterate = new List<Component>(originalComponents ?? Array.Empty<Component>()); // Copy immediately, handle null case 966 | int componentCount = componentsToIterate.Count; 967 | originalComponents = null; // Null the original reference 968 | // Debug.Log($"[GetComponentsFromTarget] Found {componentCount} components on {targetGo.name}. Copied to list, nulled original. Starting REVERSE for loop..."); 969 | // --- End Copy and Null --- 970 | 971 | var componentData = new List<object>(); 972 | 973 | for (int i = componentCount - 1; i >= 0; i--) // Iterate backwards over the COPY 974 | { 975 | Component c = componentsToIterate[i]; // Use the copy 976 | if (c == null) 977 | { 978 | // Debug.LogWarning($"[GetComponentsFromTarget REVERSE for] Encountered a null component at index {i} on {targetGo.name}. Skipping."); 979 | continue; // Safety check 980 | } 981 | // Debug.Log($"[GetComponentsFromTarget REVERSE for] Processing component: {c.GetType()?.FullName ?? "null"} (ID: {c.GetInstanceID()}) at index {i} on {targetGo.name}"); 982 | try 983 | { 984 | var data = Helpers.GameObjectSerializer.GetComponentData(c, includeNonPublicSerialized); 985 | if (data != null) // Ensure GetComponentData didn't return null 986 | { 987 | componentData.Insert(0, data); // Insert at beginning to maintain original order in final list 988 | } 989 | // else 990 | // { 991 | // Debug.LogWarning($"[GetComponentsFromTarget REVERSE for] GetComponentData returned null for component {c.GetType().FullName} (ID: {c.GetInstanceID()}) on {targetGo.name}. Skipping addition."); 992 | // } 993 | } 994 | catch (Exception ex) 995 | { 996 | Debug.LogError($"[GetComponentsFromTarget REVERSE for] Error processing component {c.GetType().FullName} (ID: {c.GetInstanceID()}) on {targetGo.name}: {ex.Message}\n{ex.StackTrace}"); 997 | // Optionally add placeholder data or just skip 998 | componentData.Insert(0, new JObject( // Insert error marker at beginning 999 | new JProperty("typeName", c.GetType().FullName + " (Serialization Error)"), 1000 | new JProperty("instanceID", c.GetInstanceID()), 1001 | new JProperty("error", ex.Message) 1002 | )); 1003 | } 1004 | } 1005 | // Debug.Log($"[GetComponentsFromTarget] Finished REVERSE for loop."); 1006 | 1007 | // Cleanup the list we created 1008 | componentsToIterate.Clear(); 1009 | componentsToIterate = null; 1010 | 1011 | return Response.Success( 1012 | $"Retrieved {componentData.Count} components from '{targetGo.name}'.", 1013 | componentData // List was built in original order 1014 | ); 1015 | } 1016 | catch (Exception e) 1017 | { 1018 | return Response.Error( 1019 | $"Error getting components from '{targetGo.name}': {e.Message}" 1020 | ); 1021 | } 1022 | } 1023 | 1024 | private static object GetSingleComponentFromTarget(string target, string searchMethod, string componentName, bool includeNonPublicSerialized = true) 1025 | { 1026 | GameObject targetGo = FindObjectInternal(target, searchMethod); 1027 | if (targetGo == null) 1028 | { 1029 | return Response.Error( 1030 | $"Target GameObject ('{target}') not found using method '{searchMethod ?? "default"}'." 1031 | ); 1032 | } 1033 | 1034 | try 1035 | { 1036 | // Try to find the component by name 1037 | Component targetComponent = targetGo.GetComponent(componentName); 1038 | 1039 | // If not found directly, try to find by type name (handle namespaces) 1040 | if (targetComponent == null) 1041 | { 1042 | Component[] allComponents = targetGo.GetComponents<Component>(); 1043 | foreach (Component comp in allComponents) 1044 | { 1045 | if (comp != null) 1046 | { 1047 | string typeName = comp.GetType().Name; 1048 | string fullTypeName = comp.GetType().FullName; 1049 | 1050 | if (typeName == componentName || fullTypeName == componentName) 1051 | { 1052 | targetComponent = comp; 1053 | break; 1054 | } 1055 | } 1056 | } 1057 | } 1058 | 1059 | if (targetComponent == null) 1060 | { 1061 | return Response.Error( 1062 | $"Component '{componentName}' not found on GameObject '{targetGo.name}'." 1063 | ); 1064 | } 1065 | 1066 | var componentData = Helpers.GameObjectSerializer.GetComponentData(targetComponent, includeNonPublicSerialized); 1067 | 1068 | if (componentData == null) 1069 | { 1070 | return Response.Error( 1071 | $"Failed to serialize component '{componentName}' on GameObject '{targetGo.name}'." 1072 | ); 1073 | } 1074 | 1075 | return Response.Success( 1076 | $"Retrieved component '{componentName}' from '{targetGo.name}'.", 1077 | componentData 1078 | ); 1079 | } 1080 | catch (Exception e) 1081 | { 1082 | return Response.Error( 1083 | $"Error getting component '{componentName}' from '{targetGo.name}': {e.Message}" 1084 | ); 1085 | } 1086 | } 1087 | 1088 | private static object AddComponentToTarget( 1089 | JObject @params, 1090 | JToken targetToken, 1091 | string searchMethod 1092 | ) 1093 | { 1094 | GameObject targetGo = FindObjectInternal(targetToken, searchMethod); 1095 | if (targetGo == null) 1096 | { 1097 | return Response.Error( 1098 | $"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'." 1099 | ); 1100 | } 1101 | 1102 | string typeName = null; 1103 | JObject properties = null; 1104 | 1105 | // Allow adding component specified directly or via componentsToAdd array (take first) 1106 | if (@params["componentName"] != null) 1107 | { 1108 | typeName = @params["componentName"]?.ToString(); 1109 | properties = @params["componentProperties"]?[typeName] as JObject; // Check if props are nested under name 1110 | } 1111 | else if ( 1112 | @params["componentsToAdd"] is JArray componentsToAddArray 1113 | && componentsToAddArray.Count > 0 1114 | ) 1115 | { 1116 | var compToken = componentsToAddArray.First; 1117 | if (compToken.Type == JTokenType.String) 1118 | typeName = compToken.ToString(); 1119 | else if (compToken is JObject compObj) 1120 | { 1121 | typeName = compObj["typeName"]?.ToString(); 1122 | properties = compObj["properties"] as JObject; 1123 | } 1124 | } 1125 | 1126 | if (string.IsNullOrEmpty(typeName)) 1127 | { 1128 | return Response.Error( 1129 | "Component type name ('componentName' or first element in 'componentsToAdd') is required." 1130 | ); 1131 | } 1132 | 1133 | var addResult = AddComponentInternal(targetGo, typeName, properties); 1134 | if (addResult != null) 1135 | return addResult; // Return error 1136 | 1137 | EditorUtility.SetDirty(targetGo); 1138 | // Use the new serializer helper 1139 | return Response.Success( 1140 | $"Component '{typeName}' added to '{targetGo.name}'.", 1141 | Helpers.GameObjectSerializer.GetGameObjectData(targetGo) 1142 | ); // Return updated GO data 1143 | } 1144 | 1145 | private static object RemoveComponentFromTarget( 1146 | JObject @params, 1147 | JToken targetToken, 1148 | string searchMethod 1149 | ) 1150 | { 1151 | GameObject targetGo = FindObjectInternal(targetToken, searchMethod); 1152 | if (targetGo == null) 1153 | { 1154 | return Response.Error( 1155 | $"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'." 1156 | ); 1157 | } 1158 | 1159 | string typeName = null; 1160 | // Allow removing component specified directly or via componentsToRemove array (take first) 1161 | if (@params["componentName"] != null) 1162 | { 1163 | typeName = @params["componentName"]?.ToString(); 1164 | } 1165 | else if ( 1166 | @params["componentsToRemove"] is JArray componentsToRemoveArray 1167 | && componentsToRemoveArray.Count > 0 1168 | ) 1169 | { 1170 | typeName = componentsToRemoveArray.First?.ToString(); 1171 | } 1172 | 1173 | if (string.IsNullOrEmpty(typeName)) 1174 | { 1175 | return Response.Error( 1176 | "Component type name ('componentName' or first element in 'componentsToRemove') is required." 1177 | ); 1178 | } 1179 | 1180 | var removeResult = RemoveComponentInternal(targetGo, typeName); 1181 | if (removeResult != null) 1182 | return removeResult; // Return error 1183 | 1184 | EditorUtility.SetDirty(targetGo); 1185 | // Use the new serializer helper 1186 | return Response.Success( 1187 | $"Component '{typeName}' removed from '{targetGo.name}'.", 1188 | Helpers.GameObjectSerializer.GetGameObjectData(targetGo) 1189 | ); 1190 | } 1191 | 1192 | private static object SetComponentPropertyOnTarget( 1193 | JObject @params, 1194 | JToken targetToken, 1195 | string searchMethod 1196 | ) 1197 | { 1198 | GameObject targetGo = FindObjectInternal(targetToken, searchMethod); 1199 | if (targetGo == null) 1200 | { 1201 | return Response.Error( 1202 | $"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'." 1203 | ); 1204 | } 1205 | 1206 | string compName = @params["componentName"]?.ToString(); 1207 | JObject propertiesToSet = null; 1208 | 1209 | if (!string.IsNullOrEmpty(compName)) 1210 | { 1211 | // Properties might be directly under componentProperties or nested under the component name 1212 | if (@params["componentProperties"] is JObject compProps) 1213 | { 1214 | propertiesToSet = compProps[compName] as JObject ?? compProps; // Allow flat or nested structure 1215 | } 1216 | } 1217 | else 1218 | { 1219 | return Response.Error("'componentName' parameter is required."); 1220 | } 1221 | 1222 | if (propertiesToSet == null || !propertiesToSet.HasValues) 1223 | { 1224 | return Response.Error( 1225 | "'componentProperties' dictionary for the specified component is required and cannot be empty." 1226 | ); 1227 | } 1228 | 1229 | var setResult = SetComponentPropertiesInternal(targetGo, compName, propertiesToSet); 1230 | if (setResult != null) 1231 | return setResult; // Return error 1232 | 1233 | EditorUtility.SetDirty(targetGo); 1234 | // Use the new serializer helper 1235 | return Response.Success( 1236 | $"Properties set for component '{compName}' on '{targetGo.name}'.", 1237 | Helpers.GameObjectSerializer.GetGameObjectData(targetGo) 1238 | ); 1239 | } 1240 | 1241 | // --- Internal Helpers --- 1242 | 1243 | /// <summary> 1244 | /// Parses a JArray like [x, y, z] into a Vector3. 1245 | /// </summary> 1246 | private static Vector3? ParseVector3(JArray array) 1247 | { 1248 | if (array != null && array.Count == 3) 1249 | { 1250 | try 1251 | { 1252 | return new Vector3( 1253 | array[0].ToObject<float>(), 1254 | array[1].ToObject<float>(), 1255 | array[2].ToObject<float>() 1256 | ); 1257 | } 1258 | catch (Exception ex) 1259 | { 1260 | Debug.LogWarning($"Failed to parse JArray as Vector3: {array}. Error: {ex.Message}"); 1261 | } 1262 | } 1263 | return null; 1264 | } 1265 | 1266 | /// <summary> 1267 | /// Finds a single GameObject based on token (ID, name, path) and search method. 1268 | /// </summary> 1269 | private static GameObject FindObjectInternal( 1270 | JToken targetToken, 1271 | string searchMethod, 1272 | JObject findParams = null 1273 | ) 1274 | { 1275 | // If find_all is not explicitly false, we still want only one for most single-target operations. 1276 | bool findAll = findParams?["findAll"]?.ToObject<bool>() ?? false; 1277 | // If a specific target ID is given, always find just that one. 1278 | if ( 1279 | targetToken?.Type == JTokenType.Integer 1280 | || (searchMethod == "by_id" && int.TryParse(targetToken?.ToString(), out _)) 1281 | ) 1282 | { 1283 | findAll = false; 1284 | } 1285 | List<GameObject> results = FindObjectsInternal( 1286 | targetToken, 1287 | searchMethod, 1288 | findAll, 1289 | findParams 1290 | ); 1291 | return results.Count > 0 ? results[0] : null; 1292 | } 1293 | 1294 | /// <summary> 1295 | /// Core logic for finding GameObjects based on various criteria. 1296 | /// </summary> 1297 | private static List<GameObject> FindObjectsInternal( 1298 | JToken targetToken, 1299 | string searchMethod, 1300 | bool findAll, 1301 | JObject findParams = null 1302 | ) 1303 | { 1304 | List<GameObject> results = new List<GameObject>(); 1305 | string searchTerm = findParams?["searchTerm"]?.ToString() ?? targetToken?.ToString(); // Use searchTerm if provided, else the target itself 1306 | bool searchInChildren = findParams?["searchInChildren"]?.ToObject<bool>() ?? false; 1307 | bool searchInactive = findParams?["searchInactive"]?.ToObject<bool>() ?? false; 1308 | 1309 | // Default search method if not specified 1310 | if (string.IsNullOrEmpty(searchMethod)) 1311 | { 1312 | if (targetToken?.Type == JTokenType.Integer) 1313 | searchMethod = "by_id"; 1314 | else if (!string.IsNullOrEmpty(searchTerm) && searchTerm.Contains('/')) 1315 | searchMethod = "by_path"; 1316 | else 1317 | searchMethod = "by_name"; // Default fallback 1318 | } 1319 | 1320 | GameObject rootSearchObject = null; 1321 | // If searching in children, find the initial target first 1322 | if (searchInChildren && targetToken != null) 1323 | { 1324 | rootSearchObject = FindObjectInternal(targetToken, "by_id_or_name_or_path"); // Find the root for child search 1325 | if (rootSearchObject == null) 1326 | { 1327 | Debug.LogWarning( 1328 | $"[ManageGameObject.Find] Root object '{targetToken}' for child search not found." 1329 | ); 1330 | return results; // Return empty if root not found 1331 | } 1332 | } 1333 | 1334 | switch (searchMethod) 1335 | { 1336 | case "by_id": 1337 | if (int.TryParse(searchTerm, out int instanceId)) 1338 | { 1339 | // EditorUtility.InstanceIDToObject is slow, iterate manually if possible 1340 | // GameObject obj = EditorUtility.InstanceIDToObject(instanceId) as GameObject; 1341 | var allObjects = GetAllSceneObjects(searchInactive); // More efficient 1342 | GameObject obj = allObjects.FirstOrDefault(go => 1343 | go.GetInstanceID() == instanceId 1344 | ); 1345 | if (obj != null) 1346 | results.Add(obj); 1347 | } 1348 | break; 1349 | case "by_name": 1350 | var searchPoolName = rootSearchObject 1351 | ? rootSearchObject 1352 | .GetComponentsInChildren<Transform>(searchInactive) 1353 | .Select(t => t.gameObject) 1354 | : GetAllSceneObjects(searchInactive); 1355 | results.AddRange(searchPoolName.Where(go => go.name == searchTerm)); 1356 | break; 1357 | case "by_path": 1358 | // Path is relative to scene root or rootSearchObject 1359 | Transform foundTransform = rootSearchObject 1360 | ? rootSearchObject.transform.Find(searchTerm) 1361 | : GameObject.Find(searchTerm)?.transform; 1362 | if (foundTransform != null) 1363 | results.Add(foundTransform.gameObject); 1364 | break; 1365 | case "by_tag": 1366 | var searchPoolTag = rootSearchObject 1367 | ? rootSearchObject 1368 | .GetComponentsInChildren<Transform>(searchInactive) 1369 | .Select(t => t.gameObject) 1370 | : GetAllSceneObjects(searchInactive); 1371 | results.AddRange(searchPoolTag.Where(go => go.CompareTag(searchTerm))); 1372 | break; 1373 | case "by_layer": 1374 | var searchPoolLayer = rootSearchObject 1375 | ? rootSearchObject 1376 | .GetComponentsInChildren<Transform>(searchInactive) 1377 | .Select(t => t.gameObject) 1378 | : GetAllSceneObjects(searchInactive); 1379 | if (int.TryParse(searchTerm, out int layerIndex)) 1380 | { 1381 | results.AddRange(searchPoolLayer.Where(go => go.layer == layerIndex)); 1382 | } 1383 | else 1384 | { 1385 | int namedLayer = LayerMask.NameToLayer(searchTerm); 1386 | if (namedLayer != -1) 1387 | results.AddRange(searchPoolLayer.Where(go => go.layer == namedLayer)); 1388 | } 1389 | break; 1390 | case "by_component": 1391 | Type componentType = FindType(searchTerm); 1392 | if (componentType != null) 1393 | { 1394 | // Determine FindObjectsInactive based on the searchInactive flag 1395 | FindObjectsInactive findInactive = searchInactive 1396 | ? FindObjectsInactive.Include 1397 | : FindObjectsInactive.Exclude; 1398 | // Replace FindObjectsOfType with FindObjectsByType, specifying the sorting mode and inactive state 1399 | var searchPoolComp = rootSearchObject 1400 | ? rootSearchObject 1401 | .GetComponentsInChildren(componentType, searchInactive) 1402 | .Select(c => (c as Component).gameObject) 1403 | : UnityEngine 1404 | .Object.FindObjectsByType( 1405 | componentType, 1406 | findInactive, 1407 | FindObjectsSortMode.None 1408 | ) 1409 | .Select(c => (c as Component).gameObject); 1410 | results.AddRange(searchPoolComp.Where(go => go != null)); // Ensure GO is valid 1411 | } 1412 | else 1413 | { 1414 | Debug.LogWarning( 1415 | $"[ManageGameObject.Find] Component type not found: {searchTerm}" 1416 | ); 1417 | } 1418 | break; 1419 | case "by_id_or_name_or_path": // Helper method used internally 1420 | if (int.TryParse(searchTerm, out int id)) 1421 | { 1422 | var allObjectsId = GetAllSceneObjects(true); // Search inactive for internal lookup 1423 | GameObject objById = allObjectsId.FirstOrDefault(go => 1424 | go.GetInstanceID() == id 1425 | ); 1426 | if (objById != null) 1427 | { 1428 | results.Add(objById); 1429 | break; 1430 | } 1431 | } 1432 | GameObject objByPath = GameObject.Find(searchTerm); 1433 | if (objByPath != null) 1434 | { 1435 | results.Add(objByPath); 1436 | break; 1437 | } 1438 | 1439 | var allObjectsName = GetAllSceneObjects(true); 1440 | results.AddRange(allObjectsName.Where(go => go.name == searchTerm)); 1441 | break; 1442 | default: 1443 | Debug.LogWarning( 1444 | $"[ManageGameObject.Find] Unknown search method: {searchMethod}" 1445 | ); 1446 | break; 1447 | } 1448 | 1449 | // If only one result is needed, return just the first one found. 1450 | if (!findAll && results.Count > 1) 1451 | { 1452 | return new List<GameObject> { results[0] }; 1453 | } 1454 | 1455 | return results.Distinct().ToList(); // Ensure uniqueness 1456 | } 1457 | 1458 | // Helper to get all scene objects efficiently 1459 | private static IEnumerable<GameObject> GetAllSceneObjects(bool includeInactive) 1460 | { 1461 | // SceneManager.GetActiveScene().GetRootGameObjects() is faster than FindObjectsOfType<GameObject>() 1462 | var rootObjects = SceneManager.GetActiveScene().GetRootGameObjects(); 1463 | var allObjects = new List<GameObject>(); 1464 | foreach (var root in rootObjects) 1465 | { 1466 | allObjects.AddRange( 1467 | root.GetComponentsInChildren<Transform>(includeInactive) 1468 | .Select(t => t.gameObject) 1469 | ); 1470 | } 1471 | return allObjects; 1472 | } 1473 | 1474 | /// <summary> 1475 | /// Adds a component by type name and optionally sets properties. 1476 | /// Returns null on success, or an error response object on failure. 1477 | /// </summary> 1478 | private static object AddComponentInternal( 1479 | GameObject targetGo, 1480 | string typeName, 1481 | JObject properties 1482 | ) 1483 | { 1484 | Type componentType = FindType(typeName); 1485 | if (componentType == null) 1486 | { 1487 | return Response.Error( 1488 | $"Component type '{typeName}' not found or is not a valid Component." 1489 | ); 1490 | } 1491 | if (!typeof(Component).IsAssignableFrom(componentType)) 1492 | { 1493 | return Response.Error($"Type '{typeName}' is not a Component."); 1494 | } 1495 | 1496 | // Prevent adding Transform again 1497 | if (componentType == typeof(Transform)) 1498 | { 1499 | return Response.Error("Cannot add another Transform component."); 1500 | } 1501 | 1502 | // Check for 2D/3D physics component conflicts 1503 | bool isAdding2DPhysics = 1504 | typeof(Rigidbody2D).IsAssignableFrom(componentType) 1505 | || typeof(Collider2D).IsAssignableFrom(componentType); 1506 | bool isAdding3DPhysics = 1507 | typeof(Rigidbody).IsAssignableFrom(componentType) 1508 | || typeof(Collider).IsAssignableFrom(componentType); 1509 | 1510 | if (isAdding2DPhysics) 1511 | { 1512 | // Check if the GameObject already has any 3D Rigidbody or Collider 1513 | if ( 1514 | targetGo.GetComponent<Rigidbody>() != null 1515 | || targetGo.GetComponent<Collider>() != null 1516 | ) 1517 | { 1518 | return Response.Error( 1519 | $"Cannot add 2D physics component '{typeName}' because the GameObject '{targetGo.name}' already has a 3D Rigidbody or Collider." 1520 | ); 1521 | } 1522 | } 1523 | else if (isAdding3DPhysics) 1524 | { 1525 | // Check if the GameObject already has any 2D Rigidbody or Collider 1526 | if ( 1527 | targetGo.GetComponent<Rigidbody2D>() != null 1528 | || targetGo.GetComponent<Collider2D>() != null 1529 | ) 1530 | { 1531 | return Response.Error( 1532 | $"Cannot add 3D physics component '{typeName}' because the GameObject '{targetGo.name}' already has a 2D Rigidbody or Collider." 1533 | ); 1534 | } 1535 | } 1536 | 1537 | try 1538 | { 1539 | // Use Undo.AddComponent for undo support 1540 | Component newComponent = Undo.AddComponent(targetGo, componentType); 1541 | if (newComponent == null) 1542 | { 1543 | return Response.Error( 1544 | $"Failed to add component '{typeName}' to '{targetGo.name}'. It might be disallowed (e.g., adding script twice)." 1545 | ); 1546 | } 1547 | 1548 | // Set default values for specific component types 1549 | if (newComponent is Light light) 1550 | { 1551 | // Default newly added lights to directional 1552 | light.type = LightType.Directional; 1553 | } 1554 | 1555 | // Set properties if provided 1556 | if (properties != null) 1557 | { 1558 | var setResult = SetComponentPropertiesInternal( 1559 | targetGo, 1560 | typeName, 1561 | properties, 1562 | newComponent 1563 | ); // Pass the new component instance 1564 | if (setResult != null) 1565 | { 1566 | // If setting properties failed, maybe remove the added component? 1567 | Undo.DestroyObjectImmediate(newComponent); 1568 | return setResult; // Return the error from setting properties 1569 | } 1570 | } 1571 | 1572 | return null; // Success 1573 | } 1574 | catch (Exception e) 1575 | { 1576 | return Response.Error( 1577 | $"Error adding component '{typeName}' to '{targetGo.name}': {e.Message}" 1578 | ); 1579 | } 1580 | } 1581 | 1582 | /// <summary> 1583 | /// Removes a component by type name. 1584 | /// Returns null on success, or an error response object on failure. 1585 | /// </summary> 1586 | private static object RemoveComponentInternal(GameObject targetGo, string typeName) 1587 | { 1588 | Type componentType = FindType(typeName); 1589 | if (componentType == null) 1590 | { 1591 | return Response.Error($"Component type '{typeName}' not found for removal."); 1592 | } 1593 | 1594 | // Prevent removing essential components 1595 | if (componentType == typeof(Transform)) 1596 | { 1597 | return Response.Error("Cannot remove the Transform component."); 1598 | } 1599 | 1600 | Component componentToRemove = targetGo.GetComponent(componentType); 1601 | if (componentToRemove == null) 1602 | { 1603 | return Response.Error( 1604 | $"Component '{typeName}' not found on '{targetGo.name}' to remove." 1605 | ); 1606 | } 1607 | 1608 | try 1609 | { 1610 | // Use Undo.DestroyObjectImmediate for undo support 1611 | Undo.DestroyObjectImmediate(componentToRemove); 1612 | return null; // Success 1613 | } 1614 | catch (Exception e) 1615 | { 1616 | return Response.Error( 1617 | $"Error removing component '{typeName}' from '{targetGo.name}': {e.Message}" 1618 | ); 1619 | } 1620 | } 1621 | 1622 | /// <summary> 1623 | /// Sets properties on a component. 1624 | /// Returns null on success, or an error response object on failure. 1625 | /// </summary> 1626 | private static object SetComponentPropertiesInternal( 1627 | GameObject targetGo, 1628 | string compName, 1629 | JObject propertiesToSet, 1630 | Component targetComponentInstance = null 1631 | ) 1632 | { 1633 | Component targetComponent = targetComponentInstance; 1634 | if (targetComponent == null) 1635 | { 1636 | if (ComponentResolver.TryResolve(compName, out var compType, out var compError)) 1637 | { 1638 | targetComponent = targetGo.GetComponent(compType); 1639 | } 1640 | else 1641 | { 1642 | targetComponent = targetGo.GetComponent(compName); // fallback to string-based lookup 1643 | } 1644 | } 1645 | if (targetComponent == null) 1646 | { 1647 | return Response.Error( 1648 | $"Component '{compName}' not found on '{targetGo.name}' to set properties." 1649 | ); 1650 | } 1651 | 1652 | Undo.RecordObject(targetComponent, "Set Component Properties"); 1653 | 1654 | var failures = new List<string>(); 1655 | foreach (var prop in propertiesToSet.Properties()) 1656 | { 1657 | string propName = prop.Name; 1658 | JToken propValue = prop.Value; 1659 | 1660 | try 1661 | { 1662 | bool setResult = SetProperty(targetComponent, propName, propValue); 1663 | if (!setResult) 1664 | { 1665 | var availableProperties = ComponentResolver.GetAllComponentProperties(targetComponent.GetType()); 1666 | var suggestions = ComponentResolver.GetAIPropertySuggestions(propName, availableProperties); 1667 | var msg = suggestions.Any() 1668 | ? $"Property '{propName}' not found. Did you mean: {string.Join(", ", suggestions)}? Available: [{string.Join(", ", availableProperties)}]" 1669 | : $"Property '{propName}' not found. Available: [{string.Join(", ", availableProperties)}]"; 1670 | Debug.LogWarning($"[ManageGameObject] {msg}"); 1671 | failures.Add(msg); 1672 | } 1673 | } 1674 | catch (Exception e) 1675 | { 1676 | Debug.LogError( 1677 | $"[ManageGameObject] Error setting property '{propName}' on '{compName}': {e.Message}" 1678 | ); 1679 | failures.Add($"Error setting '{propName}': {e.Message}"); 1680 | } 1681 | } 1682 | EditorUtility.SetDirty(targetComponent); 1683 | return failures.Count == 0 1684 | ? null 1685 | : Response.Error($"One or more properties failed on '{compName}'.", new { errors = failures }); 1686 | } 1687 | 1688 | /// <summary> 1689 | /// Helper to set a property or field via reflection, handling basic types. 1690 | /// </summary> 1691 | private static bool SetProperty(object target, string memberName, JToken value) 1692 | { 1693 | Type type = target.GetType(); 1694 | BindingFlags flags = 1695 | BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; 1696 | 1697 | // Use shared serializer to avoid per-call allocation 1698 | var inputSerializer = InputSerializer; 1699 | 1700 | try 1701 | { 1702 | // Handle special case for materials with dot notation (material.property) 1703 | // Examples: material.color, sharedMaterial.color, materials[0].color 1704 | if (memberName.Contains('.') || memberName.Contains('[')) 1705 | { 1706 | // Pass the inputSerializer down for nested conversions 1707 | return SetNestedProperty(target, memberName, value, inputSerializer); 1708 | } 1709 | 1710 | PropertyInfo propInfo = type.GetProperty(memberName, flags); 1711 | if (propInfo != null && propInfo.CanWrite) 1712 | { 1713 | // Use the inputSerializer for conversion 1714 | object convertedValue = ConvertJTokenToType(value, propInfo.PropertyType, inputSerializer); 1715 | if (convertedValue != null || value.Type == JTokenType.Null) // Allow setting null 1716 | { 1717 | propInfo.SetValue(target, convertedValue); 1718 | return true; 1719 | } 1720 | else 1721 | { 1722 | Debug.LogWarning($"[SetProperty] Conversion failed for property '{memberName}' (Type: {propInfo.PropertyType.Name}) from token: {value.ToString(Formatting.None)}"); 1723 | } 1724 | } 1725 | else 1726 | { 1727 | FieldInfo fieldInfo = type.GetField(memberName, flags); 1728 | if (fieldInfo != null) // Check if !IsLiteral? 1729 | { 1730 | // Use the inputSerializer for conversion 1731 | object convertedValue = ConvertJTokenToType(value, fieldInfo.FieldType, inputSerializer); 1732 | if (convertedValue != null || value.Type == JTokenType.Null) // Allow setting null 1733 | { 1734 | fieldInfo.SetValue(target, convertedValue); 1735 | return true; 1736 | } 1737 | else 1738 | { 1739 | Debug.LogWarning($"[SetProperty] Conversion failed for field '{memberName}' (Type: {fieldInfo.FieldType.Name}) from token: {value.ToString(Formatting.None)}"); 1740 | } 1741 | } 1742 | else 1743 | { 1744 | // Try NonPublic [SerializeField] fields 1745 | var npField = type.GetField(memberName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase); 1746 | if (npField != null && npField.GetCustomAttribute<SerializeField>() != null) 1747 | { 1748 | object convertedValue = ConvertJTokenToType(value, npField.FieldType, inputSerializer); 1749 | if (convertedValue != null || value.Type == JTokenType.Null) 1750 | { 1751 | npField.SetValue(target, convertedValue); 1752 | return true; 1753 | } 1754 | } 1755 | } 1756 | } 1757 | } 1758 | catch (Exception ex) 1759 | { 1760 | Debug.LogError( 1761 | $"[SetProperty] Failed to set '{memberName}' on {type.Name}: {ex.Message}\nToken: {value.ToString(Formatting.None)}" 1762 | ); 1763 | } 1764 | return false; 1765 | } 1766 | 1767 | /// <summary> 1768 | /// Sets a nested property using dot notation (e.g., "material.color") or array access (e.g., "materials[0]") 1769 | /// </summary> 1770 | // Pass the input serializer for conversions 1771 | //Using the serializer helper 1772 | private static bool SetNestedProperty(object target, string path, JToken value, JsonSerializer inputSerializer) 1773 | { 1774 | try 1775 | { 1776 | // Split the path into parts (handling both dot notation and array indexing) 1777 | string[] pathParts = SplitPropertyPath(path); 1778 | if (pathParts.Length == 0) 1779 | return false; 1780 | 1781 | object currentObject = target; 1782 | Type currentType = currentObject.GetType(); 1783 | BindingFlags flags = 1784 | BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; 1785 | 1786 | // Traverse the path until we reach the final property 1787 | for (int i = 0; i < pathParts.Length - 1; i++) 1788 | { 1789 | string part = pathParts[i]; 1790 | bool isArray = false; 1791 | int arrayIndex = -1; 1792 | 1793 | // Check if this part contains array indexing 1794 | if (part.Contains("[")) 1795 | { 1796 | int startBracket = part.IndexOf('['); 1797 | int endBracket = part.IndexOf(']'); 1798 | if (startBracket > 0 && endBracket > startBracket) 1799 | { 1800 | string indexStr = part.Substring( 1801 | startBracket + 1, 1802 | endBracket - startBracket - 1 1803 | ); 1804 | if (int.TryParse(indexStr, out arrayIndex)) 1805 | { 1806 | isArray = true; 1807 | part = part.Substring(0, startBracket); 1808 | } 1809 | } 1810 | } 1811 | // Get the property/field 1812 | PropertyInfo propInfo = currentType.GetProperty(part, flags); 1813 | FieldInfo fieldInfo = null; 1814 | if (propInfo == null) 1815 | { 1816 | fieldInfo = currentType.GetField(part, flags); 1817 | if (fieldInfo == null) 1818 | { 1819 | Debug.LogWarning( 1820 | $"[SetNestedProperty] Could not find property or field '{part}' on type '{currentType.Name}'" 1821 | ); 1822 | return false; 1823 | } 1824 | } 1825 | 1826 | // Get the value 1827 | currentObject = 1828 | propInfo != null 1829 | ? propInfo.GetValue(currentObject) 1830 | : fieldInfo.GetValue(currentObject); 1831 | //Need to stop if current property is null 1832 | if (currentObject == null) 1833 | { 1834 | Debug.LogWarning( 1835 | $"[SetNestedProperty] Property '{part}' is null, cannot access nested properties." 1836 | ); 1837 | return false; 1838 | } 1839 | // If this part was an array or list, access the specific index 1840 | if (isArray) 1841 | { 1842 | if (currentObject is Material[]) 1843 | { 1844 | var materials = currentObject as Material[]; 1845 | if (arrayIndex < 0 || arrayIndex >= materials.Length) 1846 | { 1847 | Debug.LogWarning( 1848 | $"[SetNestedProperty] Material index {arrayIndex} out of range (0-{materials.Length - 1})" 1849 | ); 1850 | return false; 1851 | } 1852 | currentObject = materials[arrayIndex]; 1853 | } 1854 | else if (currentObject is System.Collections.IList) 1855 | { 1856 | var list = currentObject as System.Collections.IList; 1857 | if (arrayIndex < 0 || arrayIndex >= list.Count) 1858 | { 1859 | Debug.LogWarning( 1860 | $"[SetNestedProperty] Index {arrayIndex} out of range (0-{list.Count - 1})" 1861 | ); 1862 | return false; 1863 | } 1864 | currentObject = list[arrayIndex]; 1865 | } 1866 | else 1867 | { 1868 | Debug.LogWarning( 1869 | $"[SetNestedProperty] Property '{part}' is not an array or list, cannot access by index." 1870 | ); 1871 | return false; 1872 | } 1873 | } 1874 | currentType = currentObject.GetType(); 1875 | } 1876 | 1877 | // Set the final property 1878 | string finalPart = pathParts[pathParts.Length - 1]; 1879 | 1880 | // Special handling for Material properties (shader properties) 1881 | if (currentObject is Material material && finalPart.StartsWith("_")) 1882 | { 1883 | // Use the serializer to convert the JToken value first 1884 | if (value is JArray jArray) 1885 | { 1886 | // Try converting to known types that SetColor/SetVector accept 1887 | if (jArray.Count == 4) 1888 | { 1889 | try { Color color = value.ToObject<Color>(inputSerializer); material.SetColor(finalPart, color); return true; } catch { } 1890 | try { Vector4 vec = value.ToObject<Vector4>(inputSerializer); material.SetVector(finalPart, vec); return true; } catch { } 1891 | } 1892 | else if (jArray.Count == 3) 1893 | { 1894 | try { Color color = value.ToObject<Color>(inputSerializer); material.SetColor(finalPart, color); return true; } catch { } // ToObject handles conversion to Color 1895 | } 1896 | else if (jArray.Count == 2) 1897 | { 1898 | try { Vector2 vec = value.ToObject<Vector2>(inputSerializer); material.SetVector(finalPart, vec); return true; } catch { } 1899 | } 1900 | } 1901 | else if (value.Type == JTokenType.Float || value.Type == JTokenType.Integer) 1902 | { 1903 | try { material.SetFloat(finalPart, value.ToObject<float>(inputSerializer)); return true; } catch { } 1904 | } 1905 | else if (value.Type == JTokenType.Boolean) 1906 | { 1907 | try { material.SetFloat(finalPart, value.ToObject<bool>(inputSerializer) ? 1f : 0f); return true; } catch { } 1908 | } 1909 | else if (value.Type == JTokenType.String) 1910 | { 1911 | // Try converting to Texture using the serializer/converter 1912 | try 1913 | { 1914 | Texture texture = value.ToObject<Texture>(inputSerializer); 1915 | if (texture != null) 1916 | { 1917 | material.SetTexture(finalPart, texture); 1918 | return true; 1919 | } 1920 | } 1921 | catch { } 1922 | } 1923 | 1924 | Debug.LogWarning( 1925 | $"[SetNestedProperty] Unsupported or failed conversion for material property '{finalPart}' from value: {value.ToString(Formatting.None)}" 1926 | ); 1927 | return false; 1928 | } 1929 | 1930 | // For standard properties (not shader specific) 1931 | PropertyInfo finalPropInfo = currentType.GetProperty(finalPart, flags); 1932 | if (finalPropInfo != null && finalPropInfo.CanWrite) 1933 | { 1934 | // Use the inputSerializer for conversion 1935 | object convertedValue = ConvertJTokenToType(value, finalPropInfo.PropertyType, inputSerializer); 1936 | if (convertedValue != null || value.Type == JTokenType.Null) 1937 | { 1938 | finalPropInfo.SetValue(currentObject, convertedValue); 1939 | return true; 1940 | } 1941 | else 1942 | { 1943 | Debug.LogWarning($"[SetNestedProperty] Final conversion failed for property '{finalPart}' (Type: {finalPropInfo.PropertyType.Name}) from token: {value.ToString(Formatting.None)}"); 1944 | } 1945 | } 1946 | else 1947 | { 1948 | FieldInfo finalFieldInfo = currentType.GetField(finalPart, flags); 1949 | if (finalFieldInfo != null) 1950 | { 1951 | // Use the inputSerializer for conversion 1952 | object convertedValue = ConvertJTokenToType(value, finalFieldInfo.FieldType, inputSerializer); 1953 | if (convertedValue != null || value.Type == JTokenType.Null) 1954 | { 1955 | finalFieldInfo.SetValue(currentObject, convertedValue); 1956 | return true; 1957 | } 1958 | else 1959 | { 1960 | Debug.LogWarning($"[SetNestedProperty] Final conversion failed for field '{finalPart}' (Type: {finalFieldInfo.FieldType.Name}) from token: {value.ToString(Formatting.None)}"); 1961 | } 1962 | } 1963 | else 1964 | { 1965 | Debug.LogWarning( 1966 | $"[SetNestedProperty] Could not find final writable property or field '{finalPart}' on type '{currentType.Name}'" 1967 | ); 1968 | } 1969 | } 1970 | } 1971 | catch (Exception ex) 1972 | { 1973 | Debug.LogError( 1974 | $"[SetNestedProperty] Error setting nested property '{path}': {ex.Message}\nToken: {value.ToString(Formatting.None)}" 1975 | ); 1976 | } 1977 | 1978 | return false; 1979 | } 1980 | 1981 | 1982 | /// <summary> 1983 | /// Split a property path into parts, handling both dot notation and array indexers 1984 | /// </summary> 1985 | private static string[] SplitPropertyPath(string path) 1986 | { 1987 | // Handle complex paths with both dots and array indexers 1988 | List<string> parts = new List<string>(); 1989 | int startIndex = 0; 1990 | bool inBrackets = false; 1991 | 1992 | for (int i = 0; i < path.Length; i++) 1993 | { 1994 | char c = path[i]; 1995 | 1996 | if (c == '[') 1997 | { 1998 | inBrackets = true; 1999 | } 2000 | else if (c == ']') 2001 | { 2002 | inBrackets = false; 2003 | } 2004 | else if (c == '.' && !inBrackets) 2005 | { 2006 | // Found a dot separator outside of brackets 2007 | parts.Add(path.Substring(startIndex, i - startIndex)); 2008 | startIndex = i + 1; 2009 | } 2010 | } 2011 | if (startIndex < path.Length) 2012 | { 2013 | parts.Add(path.Substring(startIndex)); 2014 | } 2015 | return parts.ToArray(); 2016 | } 2017 | 2018 | /// <summary> 2019 | /// Simple JToken to Type conversion for common Unity types, using JsonSerializer. 2020 | /// </summary> 2021 | // Pass the input serializer 2022 | private static object ConvertJTokenToType(JToken token, Type targetType, JsonSerializer inputSerializer) 2023 | { 2024 | if (token == null || token.Type == JTokenType.Null) 2025 | { 2026 | if (targetType.IsValueType && Nullable.GetUnderlyingType(targetType) == null) 2027 | { 2028 | Debug.LogWarning($"Cannot assign null to non-nullable value type {targetType.Name}. Returning default value."); 2029 | return Activator.CreateInstance(targetType); 2030 | } 2031 | return null; 2032 | } 2033 | 2034 | try 2035 | { 2036 | // Use the provided serializer instance which includes our custom converters 2037 | return token.ToObject(targetType, inputSerializer); 2038 | } 2039 | catch (JsonSerializationException jsonEx) 2040 | { 2041 | Debug.LogError($"JSON Deserialization Error converting token to {targetType.FullName}: {jsonEx.Message}\nToken: {token.ToString(Formatting.None)}"); 2042 | // Optionally re-throw or return null/default 2043 | // return targetType.IsValueType ? Activator.CreateInstance(targetType) : null; 2044 | throw; // Re-throw to indicate failure higher up 2045 | } 2046 | catch (ArgumentException argEx) 2047 | { 2048 | Debug.LogError($"Argument Error converting token to {targetType.FullName}: {argEx.Message}\nToken: {token.ToString(Formatting.None)}"); 2049 | throw; 2050 | } 2051 | catch (Exception ex) 2052 | { 2053 | Debug.LogError($"Unexpected error converting token to {targetType.FullName}: {ex}\nToken: {token.ToString(Formatting.None)}"); 2054 | throw; 2055 | } 2056 | // If ToObject succeeded, it would have returned. If it threw, we wouldn't reach here. 2057 | // This fallback logic is likely unreachable if ToObject covers all cases or throws on failure. 2058 | // Debug.LogWarning($"Conversion failed for token to {targetType.FullName}. Token: {token.ToString(Formatting.None)}"); 2059 | // return targetType.IsValueType ? Activator.CreateInstance(targetType) : null; 2060 | } 2061 | 2062 | // --- ParseJTokenTo... helpers are likely redundant now with the serializer approach --- 2063 | // Keep them temporarily for reference or if specific fallback logic is ever needed. 2064 | 2065 | private static Vector3 ParseJTokenToVector3(JToken token) 2066 | { 2067 | // ... (implementation - likely replaced by Vector3Converter) ... 2068 | // Consider removing these if the serializer handles them reliably. 2069 | if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y") && obj.ContainsKey("z")) 2070 | { 2071 | return new Vector3(obj["x"].ToObject<float>(), obj["y"].ToObject<float>(), obj["z"].ToObject<float>()); 2072 | } 2073 | if (token is JArray arr && arr.Count >= 3) 2074 | { 2075 | return new Vector3(arr[0].ToObject<float>(), arr[1].ToObject<float>(), arr[2].ToObject<float>()); 2076 | } 2077 | Debug.LogWarning($"Could not parse JToken '{token}' as Vector3 using fallback. Returning Vector3.zero."); 2078 | return Vector3.zero; 2079 | 2080 | } 2081 | private static Vector2 ParseJTokenToVector2(JToken token) 2082 | { 2083 | // ... (implementation - likely replaced by Vector2Converter) ... 2084 | if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y")) 2085 | { 2086 | return new Vector2(obj["x"].ToObject<float>(), obj["y"].ToObject<float>()); 2087 | } 2088 | if (token is JArray arr && arr.Count >= 2) 2089 | { 2090 | return new Vector2(arr[0].ToObject<float>(), arr[1].ToObject<float>()); 2091 | } 2092 | Debug.LogWarning($"Could not parse JToken '{token}' as Vector2 using fallback. Returning Vector2.zero."); 2093 | return Vector2.zero; 2094 | } 2095 | private static Quaternion ParseJTokenToQuaternion(JToken token) 2096 | { 2097 | // ... (implementation - likely replaced by QuaternionConverter) ... 2098 | if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y") && obj.ContainsKey("z") && obj.ContainsKey("w")) 2099 | { 2100 | return new Quaternion(obj["x"].ToObject<float>(), obj["y"].ToObject<float>(), obj["z"].ToObject<float>(), obj["w"].ToObject<float>()); 2101 | } 2102 | if (token is JArray arr && arr.Count >= 4) 2103 | { 2104 | return new Quaternion(arr[0].ToObject<float>(), arr[1].ToObject<float>(), arr[2].ToObject<float>(), arr[3].ToObject<float>()); 2105 | } 2106 | Debug.LogWarning($"Could not parse JToken '{token}' as Quaternion using fallback. Returning Quaternion.identity."); 2107 | return Quaternion.identity; 2108 | } 2109 | private static Color ParseJTokenToColor(JToken token) 2110 | { 2111 | // ... (implementation - likely replaced by ColorConverter) ... 2112 | if (token is JObject obj && obj.ContainsKey("r") && obj.ContainsKey("g") && obj.ContainsKey("b") && obj.ContainsKey("a")) 2113 | { 2114 | return new Color(obj["r"].ToObject<float>(), obj["g"].ToObject<float>(), obj["b"].ToObject<float>(), obj["a"].ToObject<float>()); 2115 | } 2116 | if (token is JArray arr && arr.Count >= 4) 2117 | { 2118 | return new Color(arr[0].ToObject<float>(), arr[1].ToObject<float>(), arr[2].ToObject<float>(), arr[3].ToObject<float>()); 2119 | } 2120 | Debug.LogWarning($"Could not parse JToken '{token}' as Color using fallback. Returning Color.white."); 2121 | return Color.white; 2122 | } 2123 | private static Rect ParseJTokenToRect(JToken token) 2124 | { 2125 | // ... (implementation - likely replaced by RectConverter) ... 2126 | if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y") && obj.ContainsKey("width") && obj.ContainsKey("height")) 2127 | { 2128 | return new Rect(obj["x"].ToObject<float>(), obj["y"].ToObject<float>(), obj["width"].ToObject<float>(), obj["height"].ToObject<float>()); 2129 | } 2130 | if (token is JArray arr && arr.Count >= 4) 2131 | { 2132 | return new Rect(arr[0].ToObject<float>(), arr[1].ToObject<float>(), arr[2].ToObject<float>(), arr[3].ToObject<float>()); 2133 | } 2134 | Debug.LogWarning($"Could not parse JToken '{token}' as Rect using fallback. Returning Rect.zero."); 2135 | return Rect.zero; 2136 | } 2137 | private static Bounds ParseJTokenToBounds(JToken token) 2138 | { 2139 | // ... (implementation - likely replaced by BoundsConverter) ... 2140 | if (token is JObject obj && obj.ContainsKey("center") && obj.ContainsKey("size")) 2141 | { 2142 | // Requires Vector3 conversion, which should ideally use the serializer too 2143 | Vector3 center = ParseJTokenToVector3(obj["center"]); // Or use obj["center"].ToObject<Vector3>(inputSerializer) 2144 | Vector3 size = ParseJTokenToVector3(obj["size"]); // Or use obj["size"].ToObject<Vector3>(inputSerializer) 2145 | return new Bounds(center, size); 2146 | } 2147 | // Array fallback for Bounds is less intuitive, maybe remove? 2148 | // if (token is JArray arr && arr.Count >= 6) 2149 | // { 2150 | // return new Bounds(new Vector3(arr[0].ToObject<float>(), arr[1].ToObject<float>(), arr[2].ToObject<float>()), new Vector3(arr[3].ToObject<float>(), arr[4].ToObject<float>(), arr[5].ToObject<float>())); 2151 | // } 2152 | Debug.LogWarning($"Could not parse JToken '{token}' as Bounds using fallback. Returning new Bounds(Vector3.zero, Vector3.zero)."); 2153 | return new Bounds(Vector3.zero, Vector3.zero); 2154 | } 2155 | // --- End Redundant Parse Helpers --- 2156 | 2157 | /// <summary> 2158 | /// Finds a specific UnityEngine.Object based on a find instruction JObject. 2159 | /// Primarily used by UnityEngineObjectConverter during deserialization. 2160 | /// </summary> 2161 | // Made public static so UnityEngineObjectConverter can call it. Moved from ConvertJTokenToType. 2162 | public static UnityEngine.Object FindObjectByInstruction(JObject instruction, Type targetType) 2163 | { 2164 | string findTerm = instruction["find"]?.ToString(); 2165 | string method = instruction["method"]?.ToString()?.ToLower(); 2166 | string componentName = instruction["component"]?.ToString(); // Specific component to get 2167 | 2168 | if (string.IsNullOrEmpty(findTerm)) 2169 | { 2170 | Debug.LogWarning("Find instruction missing 'find' term."); 2171 | return null; 2172 | } 2173 | 2174 | // Use a flexible default search method if none provided 2175 | string searchMethodToUse = string.IsNullOrEmpty(method) ? "by_id_or_name_or_path" : method; 2176 | 2177 | // If the target is an asset (Material, Texture, ScriptableObject etc.) try AssetDatabase first 2178 | if (typeof(Material).IsAssignableFrom(targetType) || 2179 | typeof(Texture).IsAssignableFrom(targetType) || 2180 | typeof(ScriptableObject).IsAssignableFrom(targetType) || 2181 | targetType.FullName.StartsWith("UnityEngine.U2D") || // Sprites etc. 2182 | typeof(AudioClip).IsAssignableFrom(targetType) || 2183 | typeof(AnimationClip).IsAssignableFrom(targetType) || 2184 | typeof(Font).IsAssignableFrom(targetType) || 2185 | typeof(Shader).IsAssignableFrom(targetType) || 2186 | typeof(ComputeShader).IsAssignableFrom(targetType) || 2187 | typeof(GameObject).IsAssignableFrom(targetType) && findTerm.StartsWith("Assets/")) // Prefab check 2188 | { 2189 | // Try loading directly by path/GUID first 2190 | UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath(findTerm, targetType); 2191 | if (asset != null) return asset; 2192 | asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(findTerm); // Try generic if type specific failed 2193 | if (asset != null && targetType.IsAssignableFrom(asset.GetType())) return asset; 2194 | 2195 | 2196 | // If direct path failed, try finding by name/type using FindAssets 2197 | string searchFilter = $"t:{targetType.Name} {System.IO.Path.GetFileNameWithoutExtension(findTerm)}"; // Search by type and name 2198 | string[] guids = AssetDatabase.FindAssets(searchFilter); 2199 | 2200 | if (guids.Length == 1) 2201 | { 2202 | asset = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guids[0]), targetType); 2203 | if (asset != null) return asset; 2204 | } 2205 | else if (guids.Length > 1) 2206 | { 2207 | Debug.LogWarning($"[FindObjectByInstruction] Ambiguous asset find: Found {guids.Length} assets matching filter '{searchFilter}'. Provide a full path or unique name."); 2208 | // Optionally return the first one? Or null? Returning null is safer. 2209 | return null; 2210 | } 2211 | // If still not found, fall through to scene search (though unlikely for assets) 2212 | } 2213 | 2214 | 2215 | // --- Scene Object Search --- 2216 | // Find the GameObject using the internal finder 2217 | GameObject foundGo = FindObjectInternal(new JValue(findTerm), searchMethodToUse); 2218 | 2219 | if (foundGo == null) 2220 | { 2221 | // Don't warn yet, could still be an asset not found above 2222 | // Debug.LogWarning($"Could not find GameObject using instruction: {instruction}"); 2223 | return null; 2224 | } 2225 | 2226 | // Now, get the target object/component from the found GameObject 2227 | if (targetType == typeof(GameObject)) 2228 | { 2229 | return foundGo; // We were looking for a GameObject 2230 | } 2231 | else if (typeof(Component).IsAssignableFrom(targetType)) 2232 | { 2233 | Type componentToGetType = targetType; 2234 | if (!string.IsNullOrEmpty(componentName)) 2235 | { 2236 | Type specificCompType = FindType(componentName); 2237 | if (specificCompType != null && typeof(Component).IsAssignableFrom(specificCompType)) 2238 | { 2239 | componentToGetType = specificCompType; 2240 | } 2241 | else 2242 | { 2243 | Debug.LogWarning($"Could not find component type '{componentName}' specified in find instruction. Falling back to target type '{targetType.Name}'."); 2244 | } 2245 | } 2246 | 2247 | Component foundComp = foundGo.GetComponent(componentToGetType); 2248 | if (foundComp == null) 2249 | { 2250 | Debug.LogWarning($"Found GameObject '{foundGo.name}' but could not find component of type '{componentToGetType.Name}'."); 2251 | } 2252 | return foundComp; 2253 | } 2254 | else 2255 | { 2256 | Debug.LogWarning($"Find instruction handling not implemented for target type: {targetType.Name}"); 2257 | return null; 2258 | } 2259 | } 2260 | 2261 | 2262 | /// <summary> 2263 | /// Robust component resolver that avoids Assembly.LoadFrom and works with asmdefs. 2264 | /// Searches already-loaded assemblies, prioritizing runtime script assemblies. 2265 | /// </summary> 2266 | private static Type FindType(string typeName) 2267 | { 2268 | if (ComponentResolver.TryResolve(typeName, out Type resolvedType, out string error)) 2269 | { 2270 | return resolvedType; 2271 | } 2272 | 2273 | // Log the resolver error if type wasn't found 2274 | if (!string.IsNullOrEmpty(error)) 2275 | { 2276 | Debug.LogWarning($"[FindType] {error}"); 2277 | } 2278 | 2279 | return null; 2280 | } 2281 | } 2282 | 2283 | /// <summary> 2284 | /// Robust component resolver that avoids Assembly.LoadFrom and supports assembly definitions. 2285 | /// Prioritizes runtime (Player) assemblies over Editor assemblies. 2286 | /// </summary> 2287 | internal static class ComponentResolver 2288 | { 2289 | private static readonly Dictionary<string, Type> CacheByFqn = new(StringComparer.Ordinal); 2290 | private static readonly Dictionary<string, Type> CacheByName = new(StringComparer.Ordinal); 2291 | 2292 | /// <summary> 2293 | /// Resolve a Component/MonoBehaviour type by short or fully-qualified name. 2294 | /// Prefers runtime (Player) script assemblies; falls back to Editor assemblies. 2295 | /// Never uses Assembly.LoadFrom. 2296 | /// </summary> 2297 | public static bool TryResolve(string nameOrFullName, out Type type, out string error) 2298 | { 2299 | error = string.Empty; 2300 | type = null!; 2301 | 2302 | // Handle null/empty input 2303 | if (string.IsNullOrWhiteSpace(nameOrFullName)) 2304 | { 2305 | error = "Component name cannot be null or empty"; 2306 | return false; 2307 | } 2308 | 2309 | // 1) Exact cache hits 2310 | if (CacheByFqn.TryGetValue(nameOrFullName, out type)) return true; 2311 | if (!nameOrFullName.Contains(".") && CacheByName.TryGetValue(nameOrFullName, out type)) return true; 2312 | type = Type.GetType(nameOrFullName, throwOnError: false); 2313 | if (IsValidComponent(type)) { Cache(type); return true; } 2314 | 2315 | // 2) Search loaded assemblies (prefer Player assemblies) 2316 | var candidates = FindCandidates(nameOrFullName); 2317 | if (candidates.Count == 1) { type = candidates[0]; Cache(type); return true; } 2318 | if (candidates.Count > 1) { error = Ambiguity(nameOrFullName, candidates); type = null!; return false; } 2319 | 2320 | #if UNITY_EDITOR 2321 | // 3) Last resort: Editor-only TypeCache (fast index) 2322 | var tc = TypeCache.GetTypesDerivedFrom<Component>() 2323 | .Where(t => NamesMatch(t, nameOrFullName)); 2324 | candidates = PreferPlayer(tc).ToList(); 2325 | if (candidates.Count == 1) { type = candidates[0]; Cache(type); return true; } 2326 | if (candidates.Count > 1) { error = Ambiguity(nameOrFullName, candidates); type = null!; return false; } 2327 | #endif 2328 | 2329 | error = $"Component type '{nameOrFullName}' not found in loaded runtime assemblies. " + 2330 | "Use a fully-qualified name (Namespace.TypeName) and ensure the script compiled."; 2331 | type = null!; 2332 | return false; 2333 | } 2334 | 2335 | private static bool NamesMatch(Type t, string q) => 2336 | t.Name.Equals(q, StringComparison.Ordinal) || 2337 | (t.FullName?.Equals(q, StringComparison.Ordinal) ?? false); 2338 | 2339 | private static bool IsValidComponent(Type t) => 2340 | t != null && typeof(Component).IsAssignableFrom(t); 2341 | 2342 | private static void Cache(Type t) 2343 | { 2344 | if (t.FullName != null) CacheByFqn[t.FullName] = t; 2345 | CacheByName[t.Name] = t; 2346 | } 2347 | 2348 | private static List<Type> FindCandidates(string query) 2349 | { 2350 | bool isShort = !query.Contains('.'); 2351 | var loaded = AppDomain.CurrentDomain.GetAssemblies(); 2352 | 2353 | #if UNITY_EDITOR 2354 | // Names of Player (runtime) script assemblies (asmdefs + Assembly-CSharp) 2355 | var playerAsmNames = new HashSet<string>( 2356 | UnityEditor.Compilation.CompilationPipeline.GetAssemblies(UnityEditor.Compilation.AssembliesType.Player).Select(a => a.name), 2357 | StringComparer.Ordinal); 2358 | 2359 | IEnumerable<System.Reflection.Assembly> playerAsms = loaded.Where(a => playerAsmNames.Contains(a.GetName().Name)); 2360 | IEnumerable<System.Reflection.Assembly> editorAsms = loaded.Except(playerAsms); 2361 | #else 2362 | IEnumerable<System.Reflection.Assembly> playerAsms = loaded; 2363 | IEnumerable<System.Reflection.Assembly> editorAsms = Array.Empty<System.Reflection.Assembly>(); 2364 | #endif 2365 | static IEnumerable<Type> SafeGetTypes(System.Reflection.Assembly a) 2366 | { 2367 | try { return a.GetTypes(); } 2368 | catch (ReflectionTypeLoadException rtle) { return rtle.Types.Where(t => t != null)!; } 2369 | } 2370 | 2371 | Func<Type, bool> match = isShort 2372 | ? (t => t.Name.Equals(query, StringComparison.Ordinal)) 2373 | : (t => t.FullName!.Equals(query, StringComparison.Ordinal)); 2374 | 2375 | var fromPlayer = playerAsms.SelectMany(SafeGetTypes) 2376 | .Where(IsValidComponent) 2377 | .Where(match); 2378 | var fromEditor = editorAsms.SelectMany(SafeGetTypes) 2379 | .Where(IsValidComponent) 2380 | .Where(match); 2381 | 2382 | var list = new List<Type>(fromPlayer); 2383 | if (list.Count == 0) list.AddRange(fromEditor); 2384 | return list; 2385 | } 2386 | 2387 | #if UNITY_EDITOR 2388 | private static IEnumerable<Type> PreferPlayer(IEnumerable<Type> seq) 2389 | { 2390 | var player = new HashSet<string>( 2391 | UnityEditor.Compilation.CompilationPipeline.GetAssemblies(UnityEditor.Compilation.AssembliesType.Player).Select(a => a.name), 2392 | StringComparer.Ordinal); 2393 | 2394 | return seq.OrderBy(t => player.Contains(t.Assembly.GetName().Name) ? 0 : 1); 2395 | } 2396 | #endif 2397 | 2398 | private static string Ambiguity(string query, IEnumerable<Type> cands) 2399 | { 2400 | var lines = cands.Select(t => $"{t.FullName} (assembly {t.Assembly.GetName().Name})"); 2401 | return $"Multiple component types matched '{query}':\n - " + string.Join("\n - ", lines) + 2402 | "\nProvide a fully qualified type name to disambiguate."; 2403 | } 2404 | 2405 | /// <summary> 2406 | /// Gets all accessible property and field names from a component type. 2407 | /// </summary> 2408 | public static List<string> GetAllComponentProperties(Type componentType) 2409 | { 2410 | if (componentType == null) return new List<string>(); 2411 | 2412 | var properties = componentType.GetProperties(BindingFlags.Public | BindingFlags.Instance) 2413 | .Where(p => p.CanRead && p.CanWrite) 2414 | .Select(p => p.Name); 2415 | 2416 | var fields = componentType.GetFields(BindingFlags.Public | BindingFlags.Instance) 2417 | .Where(f => !f.IsInitOnly && !f.IsLiteral) 2418 | .Select(f => f.Name); 2419 | 2420 | // Also include SerializeField private fields (common in Unity) 2421 | var serializeFields = componentType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance) 2422 | .Where(f => f.GetCustomAttribute<SerializeField>() != null) 2423 | .Select(f => f.Name); 2424 | 2425 | return properties.Concat(fields).Concat(serializeFields).Distinct().OrderBy(x => x).ToList(); 2426 | } 2427 | 2428 | /// <summary> 2429 | /// Uses AI to suggest the most likely property matches for a user's input. 2430 | /// </summary> 2431 | public static List<string> GetAIPropertySuggestions(string userInput, List<string> availableProperties) 2432 | { 2433 | if (string.IsNullOrWhiteSpace(userInput) || !availableProperties.Any()) 2434 | return new List<string>(); 2435 | 2436 | // Simple caching to avoid repeated AI calls for the same input 2437 | var cacheKey = $"{userInput.ToLowerInvariant()}:{string.Join(",", availableProperties)}"; 2438 | if (PropertySuggestionCache.TryGetValue(cacheKey, out var cached)) 2439 | return cached; 2440 | 2441 | try 2442 | { 2443 | var prompt = $"A Unity developer is trying to set a component property but used an incorrect name.\n\n" + 2444 | $"User requested: \"{userInput}\"\n" + 2445 | $"Available properties: [{string.Join(", ", availableProperties)}]\n\n" + 2446 | $"Find 1-3 most likely matches considering:\n" + 2447 | $"- Unity Inspector display names vs actual field names (e.g., \"Max Reach Distance\" → \"maxReachDistance\")\n" + 2448 | $"- camelCase vs PascalCase vs spaces\n" + 2449 | $"- Similar meaning/semantics\n" + 2450 | $"- Common Unity naming patterns\n\n" + 2451 | $"Return ONLY the matching property names, comma-separated, no quotes or explanation.\n" + 2452 | $"If confidence is low (<70%), return empty string.\n\n" + 2453 | $"Examples:\n" + 2454 | $"- \"Max Reach Distance\" → \"maxReachDistance\"\n" + 2455 | $"- \"Health Points\" → \"healthPoints, hp\"\n" + 2456 | $"- \"Move Speed\" → \"moveSpeed, movementSpeed\""; 2457 | 2458 | // For now, we'll use a simple rule-based approach that mimics AI behavior 2459 | // This can be replaced with actual AI calls later 2460 | var suggestions = GetRuleBasedSuggestions(userInput, availableProperties); 2461 | 2462 | PropertySuggestionCache[cacheKey] = suggestions; 2463 | return suggestions; 2464 | } 2465 | catch (Exception ex) 2466 | { 2467 | Debug.LogWarning($"[AI Property Matching] Error getting suggestions for '{userInput}': {ex.Message}"); 2468 | return new List<string>(); 2469 | } 2470 | } 2471 | 2472 | private static readonly Dictionary<string, List<string>> PropertySuggestionCache = new(); 2473 | 2474 | /// <summary> 2475 | /// Rule-based suggestions that mimic AI behavior for property matching. 2476 | /// This provides immediate value while we could add real AI integration later. 2477 | /// </summary> 2478 | private static List<string> GetRuleBasedSuggestions(string userInput, List<string> availableProperties) 2479 | { 2480 | var suggestions = new List<string>(); 2481 | var cleanedInput = userInput.ToLowerInvariant().Replace(" ", "").Replace("-", "").Replace("_", ""); 2482 | 2483 | foreach (var property in availableProperties) 2484 | { 2485 | var cleanedProperty = property.ToLowerInvariant().Replace(" ", "").Replace("-", "").Replace("_", ""); 2486 | 2487 | // Exact match after cleaning 2488 | if (cleanedProperty == cleanedInput) 2489 | { 2490 | suggestions.Add(property); 2491 | continue; 2492 | } 2493 | 2494 | // Check if property contains all words from input 2495 | var inputWords = userInput.ToLowerInvariant().Split(new[] { ' ', '-', '_' }, StringSplitOptions.RemoveEmptyEntries); 2496 | if (inputWords.All(word => cleanedProperty.Contains(word.ToLowerInvariant()))) 2497 | { 2498 | suggestions.Add(property); 2499 | continue; 2500 | } 2501 | 2502 | // Levenshtein distance for close matches 2503 | if (LevenshteinDistance(cleanedInput, cleanedProperty) <= Math.Max(2, cleanedInput.Length / 4)) 2504 | { 2505 | suggestions.Add(property); 2506 | } 2507 | } 2508 | 2509 | // Prioritize exact matches, then by similarity 2510 | return suggestions.OrderBy(s => LevenshteinDistance(cleanedInput, s.ToLowerInvariant().Replace(" ", ""))) 2511 | .Take(3) 2512 | .ToList(); 2513 | } 2514 | 2515 | /// <summary> 2516 | /// Calculates Levenshtein distance between two strings for similarity matching. 2517 | /// </summary> 2518 | private static int LevenshteinDistance(string s1, string s2) 2519 | { 2520 | if (string.IsNullOrEmpty(s1)) return s2?.Length ?? 0; 2521 | if (string.IsNullOrEmpty(s2)) return s1.Length; 2522 | 2523 | var matrix = new int[s1.Length + 1, s2.Length + 1]; 2524 | 2525 | for (int i = 0; i <= s1.Length; i++) matrix[i, 0] = i; 2526 | for (int j = 0; j <= s2.Length; j++) matrix[0, j] = j; 2527 | 2528 | for (int i = 1; i <= s1.Length; i++) 2529 | { 2530 | for (int j = 1; j <= s2.Length; j++) 2531 | { 2532 | int cost = (s2[j - 1] == s1[i - 1]) ? 0 : 1; 2533 | matrix[i, j] = Math.Min(Math.Min( 2534 | matrix[i - 1, j] + 1, // deletion 2535 | matrix[i, j - 1] + 1), // insertion 2536 | matrix[i - 1, j - 1] + cost); // substitution 2537 | } 2538 | } 2539 | 2540 | return matrix[s1.Length, s2.Length]; 2541 | } 2542 | 2543 | // Removed duplicate ParseVector3 - using the one at line 1114 2544 | 2545 | // Removed GetGameObjectData, GetComponentData, and related private helpers/caching/serializer setup. 2546 | // They are now in Helpers.GameObjectSerializer 2547 | } 2548 | } 2549 | ```