#
tokens: 34095/50000 1/263 files (page 16/18)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 16 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

--------------------------------------------------------------------------------
/UnityMcpBridge/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 | 
```
Page 16/18FirstPrevNextLast