#
tokens: 37221/50000 1/263 files (page 18/18)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 18 of 18. Use http://codebase.md/justinpbarnett/unity-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .claude
│   ├── prompts
│   │   ├── nl-unity-suite-nl.md
│   │   └── nl-unity-suite-t.md
│   └── settings.json
├── .github
│   ├── scripts
│   │   └── mark_skipped.py
│   └── workflows
│       ├── bump-version.yml
│       ├── claude-nl-suite.yml
│       ├── github-repo-stats.yml
│       └── unity-tests.yml
├── .gitignore
├── deploy-dev.bat
├── docs
│   ├── CURSOR_HELP.md
│   ├── CUSTOM_TOOLS.md
│   ├── README-DEV-zh.md
│   ├── README-DEV.md
│   ├── screenshots
│   │   ├── v5_01_uninstall.png
│   │   ├── v5_02_install.png
│   │   ├── v5_03_open_mcp_window.png
│   │   ├── v5_04_rebuild_mcp_server.png
│   │   ├── v5_05_rebuild_success.png
│   │   ├── v6_2_create_python_tools_asset.png
│   │   ├── v6_2_python_tools_asset.png
│   │   ├── v6_new_ui_asset_store_version.png
│   │   ├── v6_new_ui_dark.png
│   │   └── v6_new_ui_light.png
│   ├── TELEMETRY.md
│   ├── v5_MIGRATION.md
│   └── v6_NEW_UI_CHANGES.md
├── LICENSE
├── logo.png
├── mcp_source.py
├── MCPForUnity
│   ├── Editor
│   │   ├── AssemblyInfo.cs
│   │   ├── AssemblyInfo.cs.meta
│   │   ├── Data
│   │   │   ├── DefaultServerConfig.cs
│   │   │   ├── DefaultServerConfig.cs.meta
│   │   │   ├── McpClients.cs
│   │   │   ├── McpClients.cs.meta
│   │   │   ├── PythonToolsAsset.cs
│   │   │   └── PythonToolsAsset.cs.meta
│   │   ├── Data.meta
│   │   ├── Dependencies
│   │   │   ├── DependencyManager.cs
│   │   │   ├── DependencyManager.cs.meta
│   │   │   ├── Models
│   │   │   │   ├── DependencyCheckResult.cs
│   │   │   │   ├── DependencyCheckResult.cs.meta
│   │   │   │   ├── DependencyStatus.cs
│   │   │   │   └── DependencyStatus.cs.meta
│   │   │   ├── Models.meta
│   │   │   ├── PlatformDetectors
│   │   │   │   ├── IPlatformDetector.cs
│   │   │   │   ├── IPlatformDetector.cs.meta
│   │   │   │   ├── LinuxPlatformDetector.cs
│   │   │   │   ├── LinuxPlatformDetector.cs.meta
│   │   │   │   ├── MacOSPlatformDetector.cs
│   │   │   │   ├── MacOSPlatformDetector.cs.meta
│   │   │   │   ├── PlatformDetectorBase.cs
│   │   │   │   ├── PlatformDetectorBase.cs.meta
│   │   │   │   ├── WindowsPlatformDetector.cs
│   │   │   │   └── WindowsPlatformDetector.cs.meta
│   │   │   └── PlatformDetectors.meta
│   │   ├── Dependencies.meta
│   │   ├── External
│   │   │   ├── Tommy.cs
│   │   │   └── Tommy.cs.meta
│   │   ├── External.meta
│   │   ├── Helpers
│   │   │   ├── AssetPathUtility.cs
│   │   │   ├── AssetPathUtility.cs.meta
│   │   │   ├── CodexConfigHelper.cs
│   │   │   ├── CodexConfigHelper.cs.meta
│   │   │   ├── ConfigJsonBuilder.cs
│   │   │   ├── ConfigJsonBuilder.cs.meta
│   │   │   ├── ExecPath.cs
│   │   │   ├── ExecPath.cs.meta
│   │   │   ├── GameObjectSerializer.cs
│   │   │   ├── GameObjectSerializer.cs.meta
│   │   │   ├── McpConfigFileHelper.cs
│   │   │   ├── McpConfigFileHelper.cs.meta
│   │   │   ├── McpConfigurationHelper.cs
│   │   │   ├── McpConfigurationHelper.cs.meta
│   │   │   ├── McpLog.cs
│   │   │   ├── McpLog.cs.meta
│   │   │   ├── McpPathResolver.cs
│   │   │   ├── McpPathResolver.cs.meta
│   │   │   ├── PackageDetector.cs
│   │   │   ├── PackageDetector.cs.meta
│   │   │   ├── PackageInstaller.cs
│   │   │   ├── PackageInstaller.cs.meta
│   │   │   ├── PortManager.cs
│   │   │   ├── PortManager.cs.meta
│   │   │   ├── PythonToolSyncProcessor.cs
│   │   │   ├── PythonToolSyncProcessor.cs.meta
│   │   │   ├── Response.cs
│   │   │   ├── Response.cs.meta
│   │   │   ├── ServerInstaller.cs
│   │   │   ├── ServerInstaller.cs.meta
│   │   │   ├── ServerPathResolver.cs
│   │   │   ├── ServerPathResolver.cs.meta
│   │   │   ├── TelemetryHelper.cs
│   │   │   ├── TelemetryHelper.cs.meta
│   │   │   ├── Vector3Helper.cs
│   │   │   └── Vector3Helper.cs.meta
│   │   ├── Helpers.meta
│   │   ├── Importers
│   │   │   ├── PythonFileImporter.cs
│   │   │   └── PythonFileImporter.cs.meta
│   │   ├── Importers.meta
│   │   ├── MCPForUnity.Editor.asmdef
│   │   ├── MCPForUnity.Editor.asmdef.meta
│   │   ├── MCPForUnityBridge.cs
│   │   ├── MCPForUnityBridge.cs.meta
│   │   ├── Models
│   │   │   ├── Command.cs
│   │   │   ├── Command.cs.meta
│   │   │   ├── McpClient.cs
│   │   │   ├── McpClient.cs.meta
│   │   │   ├── McpConfig.cs
│   │   │   ├── McpConfig.cs.meta
│   │   │   ├── MCPConfigServer.cs
│   │   │   ├── MCPConfigServer.cs.meta
│   │   │   ├── MCPConfigServers.cs
│   │   │   ├── MCPConfigServers.cs.meta
│   │   │   ├── McpStatus.cs
│   │   │   ├── McpStatus.cs.meta
│   │   │   ├── McpTypes.cs
│   │   │   ├── McpTypes.cs.meta
│   │   │   ├── ServerConfig.cs
│   │   │   └── ServerConfig.cs.meta
│   │   ├── Models.meta
│   │   ├── Resources
│   │   │   ├── McpForUnityResourceAttribute.cs
│   │   │   ├── McpForUnityResourceAttribute.cs.meta
│   │   │   ├── MenuItems
│   │   │   │   ├── GetMenuItems.cs
│   │   │   │   └── GetMenuItems.cs.meta
│   │   │   ├── MenuItems.meta
│   │   │   ├── Tests
│   │   │   │   ├── GetTests.cs
│   │   │   │   └── GetTests.cs.meta
│   │   │   └── Tests.meta
│   │   ├── Resources.meta
│   │   ├── Services
│   │   │   ├── BridgeControlService.cs
│   │   │   ├── BridgeControlService.cs.meta
│   │   │   ├── ClientConfigurationService.cs
│   │   │   ├── ClientConfigurationService.cs.meta
│   │   │   ├── IBridgeControlService.cs
│   │   │   ├── IBridgeControlService.cs.meta
│   │   │   ├── IClientConfigurationService.cs
│   │   │   ├── IClientConfigurationService.cs.meta
│   │   │   ├── IPackageUpdateService.cs
│   │   │   ├── IPackageUpdateService.cs.meta
│   │   │   ├── IPathResolverService.cs
│   │   │   ├── IPathResolverService.cs.meta
│   │   │   ├── IPythonToolRegistryService.cs
│   │   │   ├── IPythonToolRegistryService.cs.meta
│   │   │   ├── ITestRunnerService.cs
│   │   │   ├── ITestRunnerService.cs.meta
│   │   │   ├── IToolSyncService.cs
│   │   │   ├── IToolSyncService.cs.meta
│   │   │   ├── MCPServiceLocator.cs
│   │   │   ├── MCPServiceLocator.cs.meta
│   │   │   ├── PackageUpdateService.cs
│   │   │   ├── PackageUpdateService.cs.meta
│   │   │   ├── PathResolverService.cs
│   │   │   ├── PathResolverService.cs.meta
│   │   │   ├── PythonToolRegistryService.cs
│   │   │   ├── PythonToolRegistryService.cs.meta
│   │   │   ├── TestRunnerService.cs
│   │   │   ├── TestRunnerService.cs.meta
│   │   │   ├── ToolSyncService.cs
│   │   │   └── ToolSyncService.cs.meta
│   │   ├── Services.meta
│   │   ├── Setup
│   │   │   ├── SetupWizard.cs
│   │   │   ├── SetupWizard.cs.meta
│   │   │   ├── SetupWizardWindow.cs
│   │   │   └── SetupWizardWindow.cs.meta
│   │   ├── Setup.meta
│   │   ├── Tools
│   │   │   ├── CommandRegistry.cs
│   │   │   ├── CommandRegistry.cs.meta
│   │   │   ├── ExecuteMenuItem.cs
│   │   │   ├── ExecuteMenuItem.cs.meta
│   │   │   ├── ManageAsset.cs
│   │   │   ├── ManageAsset.cs.meta
│   │   │   ├── ManageEditor.cs
│   │   │   ├── ManageEditor.cs.meta
│   │   │   ├── ManageGameObject.cs
│   │   │   ├── ManageGameObject.cs.meta
│   │   │   ├── ManageScene.cs
│   │   │   ├── ManageScene.cs.meta
│   │   │   ├── ManageScript.cs
│   │   │   ├── ManageScript.cs.meta
│   │   │   ├── ManageShader.cs
│   │   │   ├── ManageShader.cs.meta
│   │   │   ├── McpForUnityToolAttribute.cs
│   │   │   ├── McpForUnityToolAttribute.cs.meta
│   │   │   ├── Prefabs
│   │   │   │   ├── ManagePrefabs.cs
│   │   │   │   └── ManagePrefabs.cs.meta
│   │   │   ├── Prefabs.meta
│   │   │   ├── ReadConsole.cs
│   │   │   ├── ReadConsole.cs.meta
│   │   │   ├── RunTests.cs
│   │   │   └── RunTests.cs.meta
│   │   ├── Tools.meta
│   │   ├── Windows
│   │   │   ├── ManualConfigEditorWindow.cs
│   │   │   ├── ManualConfigEditorWindow.cs.meta
│   │   │   ├── MCPForUnityEditorWindow.cs
│   │   │   ├── MCPForUnityEditorWindow.cs.meta
│   │   │   ├── MCPForUnityEditorWindowNew.cs
│   │   │   ├── MCPForUnityEditorWindowNew.cs.meta
│   │   │   ├── MCPForUnityEditorWindowNew.uss
│   │   │   ├── MCPForUnityEditorWindowNew.uss.meta
│   │   │   ├── MCPForUnityEditorWindowNew.uxml
│   │   │   ├── MCPForUnityEditorWindowNew.uxml.meta
│   │   │   ├── VSCodeManualSetupWindow.cs
│   │   │   └── VSCodeManualSetupWindow.cs.meta
│   │   └── Windows.meta
│   ├── Editor.meta
│   ├── package.json
│   ├── package.json.meta
│   ├── README.md
│   ├── README.md.meta
│   ├── Runtime
│   │   ├── MCPForUnity.Runtime.asmdef
│   │   ├── MCPForUnity.Runtime.asmdef.meta
│   │   ├── Serialization
│   │   │   ├── UnityTypeConverters.cs
│   │   │   └── UnityTypeConverters.cs.meta
│   │   └── Serialization.meta
│   ├── Runtime.meta
│   └── UnityMcpServer~
│       └── src
│           ├── __init__.py
│           ├── config.py
│           ├── Dockerfile
│           ├── models.py
│           ├── module_discovery.py
│           ├── port_discovery.py
│           ├── pyproject.toml
│           ├── pyrightconfig.json
│           ├── registry
│           │   ├── __init__.py
│           │   ├── resource_registry.py
│           │   └── tool_registry.py
│           ├── reload_sentinel.py
│           ├── resources
│           │   ├── __init__.py
│           │   ├── menu_items.py
│           │   └── tests.py
│           ├── server_version.txt
│           ├── server.py
│           ├── telemetry_decorator.py
│           ├── telemetry.py
│           ├── test_telemetry.py
│           ├── tools
│           │   ├── __init__.py
│           │   ├── execute_menu_item.py
│           │   ├── manage_asset.py
│           │   ├── manage_editor.py
│           │   ├── manage_gameobject.py
│           │   ├── manage_prefabs.py
│           │   ├── manage_scene.py
│           │   ├── manage_script.py
│           │   ├── manage_shader.py
│           │   ├── read_console.py
│           │   ├── resource_tools.py
│           │   ├── run_tests.py
│           │   └── script_apply_edits.py
│           ├── unity_connection.py
│           └── uv.lock
├── prune_tool_results.py
├── README-zh.md
├── README.md
├── restore-dev.bat
├── scripts
│   └── validate-nlt-coverage.sh
├── test_unity_socket_framing.py
├── TestProjects
│   └── UnityMCPTests
│       ├── .gitignore
│       ├── Assets
│       │   ├── Editor.meta
│       │   ├── Scenes
│       │   │   ├── SampleScene.unity
│       │   │   └── SampleScene.unity.meta
│       │   ├── Scenes.meta
│       │   ├── Scripts
│       │   │   ├── Hello.cs
│       │   │   ├── Hello.cs.meta
│       │   │   ├── LongUnityScriptClaudeTest.cs
│       │   │   ├── LongUnityScriptClaudeTest.cs.meta
│       │   │   ├── TestAsmdef
│       │   │   │   ├── CustomComponent.cs
│       │   │   │   ├── CustomComponent.cs.meta
│       │   │   │   ├── TestAsmdef.asmdef
│       │   │   │   └── TestAsmdef.asmdef.meta
│       │   │   └── TestAsmdef.meta
│       │   ├── Scripts.meta
│       │   ├── Tests
│       │   │   ├── EditMode
│       │   │   │   ├── Data
│       │   │   │   │   ├── PythonToolsAssetTests.cs
│       │   │   │   │   └── PythonToolsAssetTests.cs.meta
│       │   │   │   ├── Data.meta
│       │   │   │   ├── Helpers
│       │   │   │   │   ├── CodexConfigHelperTests.cs
│       │   │   │   │   ├── CodexConfigHelperTests.cs.meta
│       │   │   │   │   ├── WriteToConfigTests.cs
│       │   │   │   │   └── WriteToConfigTests.cs.meta
│       │   │   │   ├── Helpers.meta
│       │   │   │   ├── MCPForUnityTests.Editor.asmdef
│       │   │   │   ├── MCPForUnityTests.Editor.asmdef.meta
│       │   │   │   ├── Resources
│       │   │   │   │   ├── GetMenuItemsTests.cs
│       │   │   │   │   └── GetMenuItemsTests.cs.meta
│       │   │   │   ├── Resources.meta
│       │   │   │   ├── Services
│       │   │   │   │   ├── PackageUpdateServiceTests.cs
│       │   │   │   │   ├── PackageUpdateServiceTests.cs.meta
│       │   │   │   │   ├── PythonToolRegistryServiceTests.cs
│       │   │   │   │   ├── PythonToolRegistryServiceTests.cs.meta
│       │   │   │   │   ├── ToolSyncServiceTests.cs
│       │   │   │   │   └── ToolSyncServiceTests.cs.meta
│       │   │   │   ├── Services.meta
│       │   │   │   ├── Tools
│       │   │   │   │   ├── AIPropertyMatchingTests.cs
│       │   │   │   │   ├── AIPropertyMatchingTests.cs.meta
│       │   │   │   │   ├── CommandRegistryTests.cs
│       │   │   │   │   ├── CommandRegistryTests.cs.meta
│       │   │   │   │   ├── ComponentResolverTests.cs
│       │   │   │   │   ├── ComponentResolverTests.cs.meta
│       │   │   │   │   ├── ExecuteMenuItemTests.cs
│       │   │   │   │   ├── ExecuteMenuItemTests.cs.meta
│       │   │   │   │   ├── ManageGameObjectTests.cs
│       │   │   │   │   ├── ManageGameObjectTests.cs.meta
│       │   │   │   │   ├── ManagePrefabsTests.cs
│       │   │   │   │   ├── ManagePrefabsTests.cs.meta
│       │   │   │   │   ├── ManageScriptValidationTests.cs
│       │   │   │   │   └── ManageScriptValidationTests.cs.meta
│       │   │   │   ├── Tools.meta
│       │   │   │   ├── Windows
│       │   │   │   │   ├── ManualConfigJsonBuilderTests.cs
│       │   │   │   │   └── ManualConfigJsonBuilderTests.cs.meta
│       │   │   │   └── Windows.meta
│       │   │   └── EditMode.meta
│       │   └── Tests.meta
│       ├── Packages
│       │   └── manifest.json
│       └── ProjectSettings
│           ├── Packages
│           │   └── com.unity.testtools.codecoverage
│           │       └── Settings.json
│           └── ProjectVersion.txt
├── tests
│   ├── conftest.py
│   ├── test_edit_normalization_and_noop.py
│   ├── test_edit_strict_and_warnings.py
│   ├── test_find_in_file_minimal.py
│   ├── test_get_sha.py
│   ├── test_improved_anchor_matching.py
│   ├── test_logging_stdout.py
│   ├── test_manage_script_uri.py
│   ├── test_read_console_truncate.py
│   ├── test_read_resource_minimal.py
│   ├── test_resources_api.py
│   ├── test_script_editing.py
│   ├── test_script_tools.py
│   ├── test_telemetry_endpoint_validation.py
│   ├── test_telemetry_queue_worker.py
│   ├── test_telemetry_subaction.py
│   ├── test_transport_framing.py
│   └── test_validate_script_summary.py
├── tools
│   └── stress_mcp.py
└── UnityMcpBridge
    ├── Editor
    │   ├── AssemblyInfo.cs
    │   ├── AssemblyInfo.cs.meta
    │   ├── Data
    │   │   ├── DefaultServerConfig.cs
    │   │   ├── DefaultServerConfig.cs.meta
    │   │   ├── McpClients.cs
    │   │   └── McpClients.cs.meta
    │   ├── Data.meta
    │   ├── Dependencies
    │   │   ├── DependencyManager.cs
    │   │   ├── DependencyManager.cs.meta
    │   │   ├── Models
    │   │   │   ├── DependencyCheckResult.cs
    │   │   │   ├── DependencyCheckResult.cs.meta
    │   │   │   ├── DependencyStatus.cs
    │   │   │   └── DependencyStatus.cs.meta
    │   │   ├── Models.meta
    │   │   ├── PlatformDetectors
    │   │   │   ├── IPlatformDetector.cs
    │   │   │   ├── IPlatformDetector.cs.meta
    │   │   │   ├── LinuxPlatformDetector.cs
    │   │   │   ├── LinuxPlatformDetector.cs.meta
    │   │   │   ├── MacOSPlatformDetector.cs
    │   │   │   ├── MacOSPlatformDetector.cs.meta
    │   │   │   ├── PlatformDetectorBase.cs
    │   │   │   ├── PlatformDetectorBase.cs.meta
    │   │   │   ├── WindowsPlatformDetector.cs
    │   │   │   └── WindowsPlatformDetector.cs.meta
    │   │   └── PlatformDetectors.meta
    │   ├── Dependencies.meta
    │   ├── External
    │   │   ├── Tommy.cs
    │   │   └── Tommy.cs.meta
    │   ├── External.meta
    │   ├── Helpers
    │   │   ├── AssetPathUtility.cs
    │   │   ├── AssetPathUtility.cs.meta
    │   │   ├── CodexConfigHelper.cs
    │   │   ├── CodexConfigHelper.cs.meta
    │   │   ├── ConfigJsonBuilder.cs
    │   │   ├── ConfigJsonBuilder.cs.meta
    │   │   ├── ExecPath.cs
    │   │   ├── ExecPath.cs.meta
    │   │   ├── GameObjectSerializer.cs
    │   │   ├── GameObjectSerializer.cs.meta
    │   │   ├── McpConfigFileHelper.cs
    │   │   ├── McpConfigFileHelper.cs.meta
    │   │   ├── McpConfigurationHelper.cs
    │   │   ├── McpConfigurationHelper.cs.meta
    │   │   ├── McpLog.cs
    │   │   ├── McpLog.cs.meta
    │   │   ├── McpPathResolver.cs
    │   │   ├── McpPathResolver.cs.meta
    │   │   ├── PackageDetector.cs
    │   │   ├── PackageDetector.cs.meta
    │   │   ├── PackageInstaller.cs
    │   │   ├── PackageInstaller.cs.meta
    │   │   ├── PortManager.cs
    │   │   ├── PortManager.cs.meta
    │   │   ├── Response.cs
    │   │   ├── Response.cs.meta
    │   │   ├── ServerInstaller.cs
    │   │   ├── ServerInstaller.cs.meta
    │   │   ├── ServerPathResolver.cs
    │   │   ├── ServerPathResolver.cs.meta
    │   │   ├── TelemetryHelper.cs
    │   │   ├── TelemetryHelper.cs.meta
    │   │   ├── Vector3Helper.cs
    │   │   └── Vector3Helper.cs.meta
    │   ├── Helpers.meta
    │   ├── MCPForUnity.Editor.asmdef
    │   ├── MCPForUnity.Editor.asmdef.meta
    │   ├── MCPForUnityBridge.cs
    │   ├── MCPForUnityBridge.cs.meta
    │   ├── Models
    │   │   ├── Command.cs
    │   │   ├── Command.cs.meta
    │   │   ├── McpClient.cs
    │   │   ├── McpClient.cs.meta
    │   │   ├── McpConfig.cs
    │   │   ├── McpConfig.cs.meta
    │   │   ├── MCPConfigServer.cs
    │   │   ├── MCPConfigServer.cs.meta
    │   │   ├── MCPConfigServers.cs
    │   │   ├── MCPConfigServers.cs.meta
    │   │   ├── McpStatus.cs
    │   │   ├── McpStatus.cs.meta
    │   │   ├── McpTypes.cs
    │   │   ├── McpTypes.cs.meta
    │   │   ├── ServerConfig.cs
    │   │   └── ServerConfig.cs.meta
    │   ├── Models.meta
    │   ├── Setup
    │   │   ├── SetupWizard.cs
    │   │   ├── SetupWizard.cs.meta
    │   │   ├── SetupWizardWindow.cs
    │   │   └── SetupWizardWindow.cs.meta
    │   ├── Setup.meta
    │   ├── Tools
    │   │   ├── CommandRegistry.cs
    │   │   ├── CommandRegistry.cs.meta
    │   │   ├── ManageAsset.cs
    │   │   ├── ManageAsset.cs.meta
    │   │   ├── ManageEditor.cs
    │   │   ├── ManageEditor.cs.meta
    │   │   ├── ManageGameObject.cs
    │   │   ├── ManageGameObject.cs.meta
    │   │   ├── ManageScene.cs
    │   │   ├── ManageScene.cs.meta
    │   │   ├── ManageScript.cs
    │   │   ├── ManageScript.cs.meta
    │   │   ├── ManageShader.cs
    │   │   ├── ManageShader.cs.meta
    │   │   ├── McpForUnityToolAttribute.cs
    │   │   ├── McpForUnityToolAttribute.cs.meta
    │   │   ├── MenuItems
    │   │   │   ├── ManageMenuItem.cs
    │   │   │   ├── ManageMenuItem.cs.meta
    │   │   │   ├── MenuItemExecutor.cs
    │   │   │   ├── MenuItemExecutor.cs.meta
    │   │   │   ├── MenuItemsReader.cs
    │   │   │   └── MenuItemsReader.cs.meta
    │   │   ├── MenuItems.meta
    │   │   ├── Prefabs
    │   │   │   ├── ManagePrefabs.cs
    │   │   │   └── ManagePrefabs.cs.meta
    │   │   ├── Prefabs.meta
    │   │   ├── ReadConsole.cs
    │   │   └── ReadConsole.cs.meta
    │   ├── Tools.meta
    │   ├── Windows
    │   │   ├── ManualConfigEditorWindow.cs
    │   │   ├── ManualConfigEditorWindow.cs.meta
    │   │   ├── MCPForUnityEditorWindow.cs
    │   │   ├── MCPForUnityEditorWindow.cs.meta
    │   │   ├── VSCodeManualSetupWindow.cs
    │   │   └── VSCodeManualSetupWindow.cs.meta
    │   └── Windows.meta
    ├── Editor.meta
    ├── package.json
    ├── package.json.meta
    ├── README.md
    ├── README.md.meta
    ├── Runtime
    │   ├── MCPForUnity.Runtime.asmdef
    │   ├── MCPForUnity.Runtime.asmdef.meta
    │   ├── Serialization
    │   │   ├── UnityTypeConverters.cs
    │   │   └── UnityTypeConverters.cs.meta
    │   └── Serialization.meta
    ├── Runtime.meta
    └── UnityMcpServer~
        └── src
            ├── __init__.py
            ├── config.py
            ├── Dockerfile
            ├── port_discovery.py
            ├── pyproject.toml
            ├── pyrightconfig.json
            ├── registry
            │   ├── __init__.py
            │   └── tool_registry.py
            ├── reload_sentinel.py
            ├── server_version.txt
            ├── server.py
            ├── telemetry_decorator.py
            ├── telemetry.py
            ├── test_telemetry.py
            ├── tools
            │   ├── __init__.py
            │   ├── manage_asset.py
            │   ├── manage_editor.py
            │   ├── manage_gameobject.py
            │   ├── manage_menu_item.py
            │   ├── manage_prefabs.py
            │   ├── manage_scene.py
            │   ├── manage_script.py
            │   ├── manage_shader.py
            │   ├── read_console.py
            │   ├── resource_tools.py
            │   └── script_apply_edits.py
            ├── unity_connection.py
            └── uv.lock
```

# Files

--------------------------------------------------------------------------------
/MCPForUnity/Editor/Tools/ManageScript.cs:
--------------------------------------------------------------------------------

```csharp
   1 | using System;
   2 | using System.IO;
   3 | using System.Linq;
   4 | using System.Collections.Generic;
   5 | using System.Text.RegularExpressions;
   6 | using Newtonsoft.Json.Linq;
   7 | using UnityEditor;
   8 | using UnityEngine;
   9 | using MCPForUnity.Editor.Helpers;
  10 | using System.Threading;
  11 | using System.Security.Cryptography;
  12 | 
  13 | #if USE_ROSLYN
  14 | using Microsoft.CodeAnalysis;
  15 | using Microsoft.CodeAnalysis.CSharp;
  16 | using Microsoft.CodeAnalysis.Formatting;
  17 | #endif
  18 | 
  19 | #if UNITY_EDITOR
  20 | using UnityEditor.Compilation;
  21 | #endif
  22 | 
  23 | 
  24 | namespace MCPForUnity.Editor.Tools
  25 | {
  26 |     /// <summary>
  27 |     /// Handles CRUD operations for C# scripts within the Unity project.
  28 |     /// 
  29 |     /// ROSLYN INSTALLATION GUIDE:
  30 |     /// To enable advanced syntax validation with Roslyn compiler services:
  31 |     /// 
  32 |     /// 1. Install Microsoft.CodeAnalysis.CSharp NuGet package:
  33 |     ///    - Open Package Manager in Unity
  34 |     ///    - Follow the instruction on https://github.com/GlitchEnzo/NuGetForUnity
  35 |     ///    
  36 |     /// 2. Open NuGet Package Manager and Install Microsoft.CodeAnalysis.CSharp:
  37 |     ///    
  38 |     /// 3. Alternative: Manual DLL installation:
  39 |     ///    - Download Microsoft.CodeAnalysis.CSharp.dll and dependencies
  40 |     ///    - Place in Assets/Plugins/ folder
  41 |     ///    - Ensure .NET compatibility settings are correct
  42 |     ///    
  43 |     /// 4. Define USE_ROSLYN symbol:
  44 |     ///    - Go to Player Settings > Scripting Define Symbols
  45 |     ///    - Add "USE_ROSLYN" to enable Roslyn-based validation
  46 |     ///    
  47 |     /// 5. Restart Unity after installation
  48 |     /// 
  49 |     /// Note: Without Roslyn, the system falls back to basic structural validation.
  50 |     /// Roslyn provides full C# compiler diagnostics with line numbers and detailed error messages.
  51 |     /// </summary>
  52 |     [McpForUnityTool("manage_script")]
  53 |     public static class ManageScript
  54 |     {
  55 |         /// <summary>
  56 |         /// Resolves a directory under Assets/, preventing traversal and escaping.
  57 |         /// Returns fullPathDir on disk and canonical 'Assets/...' relative path.
  58 |         /// </summary>
  59 |         private static bool TryResolveUnderAssets(string relDir, out string fullPathDir, out string relPathSafe)
  60 |         {
  61 |             string assets = Application.dataPath.Replace('\\', '/');
  62 | 
  63 |             // Normalize caller path: allow both "Scripts/..." and "Assets/Scripts/..."
  64 |             string rel = (relDir ?? "Scripts").Replace('\\', '/').Trim();
  65 |             if (string.IsNullOrEmpty(rel)) rel = "Scripts";
  66 |             if (rel.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) rel = rel.Substring(7);
  67 |             rel = rel.TrimStart('/');
  68 | 
  69 |             string targetDir = Path.Combine(assets, rel).Replace('\\', '/');
  70 |             string full = Path.GetFullPath(targetDir).Replace('\\', '/');
  71 | 
  72 |             bool underAssets = full.StartsWith(assets + "/", StringComparison.OrdinalIgnoreCase)
  73 |                                || string.Equals(full, assets, StringComparison.OrdinalIgnoreCase);
  74 |             if (!underAssets)
  75 |             {
  76 |                 fullPathDir = null;
  77 |                 relPathSafe = null;
  78 |                 return false;
  79 |             }
  80 | 
  81 |             // Best-effort symlink guard: if the directory OR ANY ANCESTOR (up to Assets/) is a reparse point/symlink, reject
  82 |             try
  83 |             {
  84 |                 var di = new DirectoryInfo(full);
  85 |                 while (di != null)
  86 |                 {
  87 |                     if (di.Exists && (di.Attributes & FileAttributes.ReparsePoint) != 0)
  88 |                     {
  89 |                         fullPathDir = null;
  90 |                         relPathSafe = null;
  91 |                         return false;
  92 |                     }
  93 |                     var atAssets = string.Equals(
  94 |                         di.FullName.Replace('\\', '/'),
  95 |                         assets,
  96 |                         StringComparison.OrdinalIgnoreCase
  97 |                     );
  98 |                     if (atAssets) break;
  99 |                     di = di.Parent;
 100 |                 }
 101 |             }
 102 |             catch { /* best effort; proceed */ }
 103 | 
 104 |             fullPathDir = full;
 105 |             string tail = full.Length > assets.Length ? full.Substring(assets.Length).TrimStart('/') : string.Empty;
 106 |             relPathSafe = ("Assets/" + tail).TrimEnd('/');
 107 |             return true;
 108 |         }
 109 |         /// <summary>
 110 |         /// Main handler for script management actions.
 111 |         /// </summary>
 112 |         public static object HandleCommand(JObject @params)
 113 |         {
 114 |             // Handle null parameters
 115 |             if (@params == null)
 116 |             {
 117 |                 return Response.Error("invalid_params", "Parameters cannot be null.");
 118 |             }
 119 | 
 120 |             // Extract parameters
 121 |             string action = @params["action"]?.ToString()?.ToLower();
 122 |             string name = @params["name"]?.ToString();
 123 |             string path = @params["path"]?.ToString(); // Relative to Assets/
 124 |             string contents = null;
 125 | 
 126 |             // Check if we have base64 encoded contents
 127 |             bool contentsEncoded = @params["contentsEncoded"]?.ToObject<bool>() ?? false;
 128 |             if (contentsEncoded && @params["encodedContents"] != null)
 129 |             {
 130 |                 try
 131 |                 {
 132 |                     contents = DecodeBase64(@params["encodedContents"].ToString());
 133 |                 }
 134 |                 catch (Exception e)
 135 |                 {
 136 |                     return Response.Error($"Failed to decode script contents: {e.Message}");
 137 |                 }
 138 |             }
 139 |             else
 140 |             {
 141 |                 contents = @params["contents"]?.ToString();
 142 |             }
 143 | 
 144 |             string scriptType = @params["scriptType"]?.ToString(); // For templates/validation
 145 |             string namespaceName = @params["namespace"]?.ToString(); // For organizing code
 146 | 
 147 |             // Validate required parameters
 148 |             if (string.IsNullOrEmpty(action))
 149 |             {
 150 |                 return Response.Error("Action parameter is required.");
 151 |             }
 152 |             if (string.IsNullOrEmpty(name))
 153 |             {
 154 |                 return Response.Error("Name parameter is required.");
 155 |             }
 156 |             // Basic name validation (alphanumeric, underscores, cannot start with number)
 157 |             if (!Regex.IsMatch(name, @"^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.CultureInvariant, TimeSpan.FromSeconds(2)))
 158 |             {
 159 |                 return Response.Error(
 160 |                     $"Invalid script name: '{name}'. Use only letters, numbers, underscores, and don't start with a number."
 161 |                 );
 162 |             }
 163 | 
 164 |             // Resolve and harden target directory under Assets/
 165 |             if (!TryResolveUnderAssets(path, out string fullPathDir, out string relPathSafeDir))
 166 |             {
 167 |                 return Response.Error($"Invalid path. Target directory must be within 'Assets/'. Provided: '{(path ?? "(null)")}'");
 168 |             }
 169 | 
 170 |             // Construct file paths
 171 |             string scriptFileName = $"{name}.cs";
 172 |             string fullPath = Path.Combine(fullPathDir, scriptFileName);
 173 |             string relativePath = Path.Combine(relPathSafeDir, scriptFileName).Replace('\\', '/');
 174 | 
 175 |             // Ensure the target directory exists for create/update
 176 |             if (action == "create" || action == "update")
 177 |             {
 178 |                 try
 179 |                 {
 180 |                     Directory.CreateDirectory(fullPathDir);
 181 |                 }
 182 |                 catch (Exception e)
 183 |                 {
 184 |                     return Response.Error(
 185 |                         $"Could not create directory '{fullPathDir}': {e.Message}"
 186 |                     );
 187 |                 }
 188 |             }
 189 | 
 190 |             // Route to specific action handlers
 191 |             switch (action)
 192 |             {
 193 |                 case "create":
 194 |                     return CreateScript(
 195 |                         fullPath,
 196 |                         relativePath,
 197 |                         name,
 198 |                         contents,
 199 |                         scriptType,
 200 |                         namespaceName
 201 |                     );
 202 |                 case "read":
 203 |                     McpLog.Warn("manage_script.read is deprecated; prefer resources/read. Serving read for backward compatibility.");
 204 |                     return ReadScript(fullPath, relativePath);
 205 |                 case "update":
 206 |                     McpLog.Warn("manage_script.update is deprecated; prefer apply_text_edits. Serving update for backward compatibility.");
 207 |                     return UpdateScript(fullPath, relativePath, name, contents);
 208 |                 case "delete":
 209 |                     return DeleteScript(fullPath, relativePath);
 210 |                 case "apply_text_edits":
 211 |                     {
 212 |                         var textEdits = @params["edits"] as JArray;
 213 |                         string precondition = @params["precondition_sha256"]?.ToString();
 214 |                         // Respect optional options
 215 |                         string refreshOpt = @params["options"]?["refresh"]?.ToString()?.ToLowerInvariant();
 216 |                         string validateOpt = @params["options"]?["validate"]?.ToString()?.ToLowerInvariant();
 217 |                         return ApplyTextEdits(fullPath, relativePath, name, textEdits, precondition, refreshOpt, validateOpt);
 218 |                     }
 219 |                 case "validate":
 220 |                     {
 221 |                         string level = @params["level"]?.ToString()?.ToLowerInvariant() ?? "standard";
 222 |                         var chosen = level switch
 223 |                         {
 224 |                             "basic" => ValidationLevel.Basic,
 225 |                             "standard" => ValidationLevel.Standard,
 226 |                             "strict" => ValidationLevel.Strict,
 227 |                             "comprehensive" => ValidationLevel.Comprehensive,
 228 |                             _ => ValidationLevel.Standard
 229 |                         };
 230 |                         string fileText;
 231 |                         try { fileText = File.ReadAllText(fullPath); }
 232 |                         catch (Exception ex) { return Response.Error($"Failed to read script: {ex.Message}"); }
 233 | 
 234 |                         bool ok = ValidateScriptSyntax(fileText, chosen, out string[] diagsRaw);
 235 |                         var diags = (diagsRaw ?? Array.Empty<string>()).Select(s =>
 236 |                         {
 237 |                             var m = Regex.Match(
 238 |                                 s,
 239 |                                 @"^(ERROR|WARNING|INFO): (.*?)(?: \(Line (\d+)\))?$",
 240 |                                 RegexOptions.CultureInvariant | RegexOptions.Multiline,
 241 |                                 TimeSpan.FromMilliseconds(250)
 242 |                             );
 243 |                             string severity = m.Success ? m.Groups[1].Value.ToLowerInvariant() : "info";
 244 |                             string message = m.Success ? m.Groups[2].Value : s;
 245 |                             int lineNum = m.Success && int.TryParse(m.Groups[3].Value, out var l) ? l : 0;
 246 |                             return new { line = lineNum, col = 0, severity, message };
 247 |                         }).ToArray();
 248 | 
 249 |                         var result = new { diagnostics = diags };
 250 |                         return ok ? Response.Success("Validation completed.", result)
 251 |                                    : Response.Error("Validation failed.", result);
 252 |                     }
 253 |                 case "edit":
 254 |                     Debug.LogWarning("manage_script.edit is deprecated; prefer apply_text_edits. Serving structured edit for backward compatibility.");
 255 |                     var structEdits = @params["edits"] as JArray;
 256 |                     var options = @params["options"] as JObject;
 257 |                     return EditScript(fullPath, relativePath, name, structEdits, options);
 258 |                 case "get_sha":
 259 |                     {
 260 |                         try
 261 |                         {
 262 |                             if (!File.Exists(fullPath))
 263 |                                 return Response.Error($"Script not found at '{relativePath}'.");
 264 | 
 265 |                             string text = File.ReadAllText(fullPath);
 266 |                             string sha = ComputeSha256(text);
 267 |                             var fi = new FileInfo(fullPath);
 268 |                             long lengthBytes;
 269 |                             try { lengthBytes = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetByteCount(text); }
 270 |                             catch { lengthBytes = fi.Exists ? fi.Length : 0; }
 271 |                             var data = new
 272 |                             {
 273 |                                 uri = $"unity://path/{relativePath}",
 274 |                                 path = relativePath,
 275 |                                 sha256 = sha,
 276 |                                 lengthBytes,
 277 |                                 lastModifiedUtc = fi.Exists ? fi.LastWriteTimeUtc.ToString("o") : string.Empty
 278 |                             };
 279 |                             return Response.Success($"SHA computed for '{relativePath}'.", data);
 280 |                         }
 281 |                         catch (Exception ex)
 282 |                         {
 283 |                             return Response.Error($"Failed to compute SHA: {ex.Message}");
 284 |                         }
 285 |                     }
 286 |                 default:
 287 |                     return Response.Error(
 288 |                         $"Unknown action: '{action}'. Valid actions are: create, delete, apply_text_edits, validate, read (deprecated), update (deprecated), edit (deprecated)."
 289 |                     );
 290 |             }
 291 |         }
 292 | 
 293 |         /// <summary>
 294 |         /// Decode base64 string to normal text
 295 |         /// </summary>
 296 |         private static string DecodeBase64(string encoded)
 297 |         {
 298 |             byte[] data = Convert.FromBase64String(encoded);
 299 |             return System.Text.Encoding.UTF8.GetString(data);
 300 |         }
 301 | 
 302 |         /// <summary>
 303 |         /// Encode text to base64 string
 304 |         /// </summary>
 305 |         private static string EncodeBase64(string text)
 306 |         {
 307 |             byte[] data = System.Text.Encoding.UTF8.GetBytes(text);
 308 |             return Convert.ToBase64String(data);
 309 |         }
 310 | 
 311 |         private static object CreateScript(
 312 |             string fullPath,
 313 |             string relativePath,
 314 |             string name,
 315 |             string contents,
 316 |             string scriptType,
 317 |             string namespaceName
 318 |         )
 319 |         {
 320 |             // Check if script already exists
 321 |             if (File.Exists(fullPath))
 322 |             {
 323 |                 return Response.Error(
 324 |                     $"Script already exists at '{relativePath}'. Use 'update' action to modify."
 325 |                 );
 326 |             }
 327 | 
 328 |             // Generate default content if none provided
 329 |             if (string.IsNullOrEmpty(contents))
 330 |             {
 331 |                 contents = GenerateDefaultScriptContent(name, scriptType, namespaceName);
 332 |             }
 333 | 
 334 |             // Validate syntax with detailed error reporting using GUI setting
 335 |             ValidationLevel validationLevel = GetValidationLevelFromGUI();
 336 |             bool isValid = ValidateScriptSyntax(contents, validationLevel, out string[] validationErrors);
 337 |             if (!isValid)
 338 |             {
 339 |                 return Response.Error("validation_failed", new { status = "validation_failed", diagnostics = validationErrors ?? Array.Empty<string>() });
 340 |             }
 341 |             else if (validationErrors != null && validationErrors.Length > 0)
 342 |             {
 343 |                 // Log warnings but don't block creation
 344 |                 Debug.LogWarning($"Script validation warnings for {name}:\n" + string.Join("\n", validationErrors));
 345 |             }
 346 | 
 347 |             try
 348 |             {
 349 |                 // Atomic create without BOM; schedule refresh after reply
 350 |                 var enc = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
 351 |                 var tmp = fullPath + ".tmp";
 352 |                 File.WriteAllText(tmp, contents, enc);
 353 |                 try
 354 |                 {
 355 |                     File.Move(tmp, fullPath);
 356 |                 }
 357 |                 catch (IOException)
 358 |                 {
 359 |                     File.Copy(tmp, fullPath, overwrite: true);
 360 |                     try { File.Delete(tmp); } catch { }
 361 |                 }
 362 | 
 363 |                 var uri = $"unity://path/{relativePath}";
 364 |                 var ok = Response.Success(
 365 |                     $"Script '{name}.cs' created successfully at '{relativePath}'.",
 366 |                     new { uri, scheduledRefresh = false }
 367 |                 );
 368 | 
 369 |                 ManageScriptRefreshHelpers.ImportAndRequestCompile(relativePath);
 370 | 
 371 |                 return ok;
 372 |             }
 373 |             catch (Exception e)
 374 |             {
 375 |                 return Response.Error($"Failed to create script '{relativePath}': {e.Message}");
 376 |             }
 377 |         }
 378 | 
 379 |         private static object ReadScript(string fullPath, string relativePath)
 380 |         {
 381 |             if (!File.Exists(fullPath))
 382 |             {
 383 |                 return Response.Error($"Script not found at '{relativePath}'.");
 384 |             }
 385 | 
 386 |             try
 387 |             {
 388 |                 string contents = File.ReadAllText(fullPath);
 389 | 
 390 |                 // Return both normal and encoded contents for larger files
 391 |                 bool isLarge = contents.Length > 10000; // If content is large, include encoded version
 392 |                 var uri = $"unity://path/{relativePath}";
 393 |                 var responseData = new
 394 |                 {
 395 |                     uri,
 396 |                     path = relativePath,
 397 |                     contents = contents,
 398 |                     // For large files, also include base64-encoded version
 399 |                     encodedContents = isLarge ? EncodeBase64(contents) : null,
 400 |                     contentsEncoded = isLarge,
 401 |                 };
 402 | 
 403 |                 return Response.Success(
 404 |                     $"Script '{Path.GetFileName(relativePath)}' read successfully.",
 405 |                     responseData
 406 |                 );
 407 |             }
 408 |             catch (Exception e)
 409 |             {
 410 |                 return Response.Error($"Failed to read script '{relativePath}': {e.Message}");
 411 |             }
 412 |         }
 413 | 
 414 |         private static object UpdateScript(
 415 |             string fullPath,
 416 |             string relativePath,
 417 |             string name,
 418 |             string contents
 419 |         )
 420 |         {
 421 |             if (!File.Exists(fullPath))
 422 |             {
 423 |                 return Response.Error(
 424 |                     $"Script not found at '{relativePath}'. Use 'create' action to add a new script."
 425 |                 );
 426 |             }
 427 |             if (string.IsNullOrEmpty(contents))
 428 |             {
 429 |                 return Response.Error("Content is required for the 'update' action.");
 430 |             }
 431 | 
 432 |             // Validate syntax with detailed error reporting using GUI setting
 433 |             ValidationLevel validationLevel = GetValidationLevelFromGUI();
 434 |             bool isValid = ValidateScriptSyntax(contents, validationLevel, out string[] validationErrors);
 435 |             if (!isValid)
 436 |             {
 437 |                 return Response.Error("validation_failed", new { status = "validation_failed", diagnostics = validationErrors ?? Array.Empty<string>() });
 438 |             }
 439 |             else if (validationErrors != null && validationErrors.Length > 0)
 440 |             {
 441 |                 // Log warnings but don't block update
 442 |                 Debug.LogWarning($"Script validation warnings for {name}:\n" + string.Join("\n", validationErrors));
 443 |             }
 444 | 
 445 |             try
 446 |             {
 447 |                 // Safe write with atomic replace when available, without BOM
 448 |                 var encoding = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
 449 |                 string tempPath = fullPath + ".tmp";
 450 |                 File.WriteAllText(tempPath, contents, encoding);
 451 | 
 452 |                 string backupPath = fullPath + ".bak";
 453 |                 try
 454 |                 {
 455 |                     File.Replace(tempPath, fullPath, backupPath);
 456 |                     try { if (File.Exists(backupPath)) File.Delete(backupPath); } catch { }
 457 |                 }
 458 |                 catch (PlatformNotSupportedException)
 459 |                 {
 460 |                     File.Copy(tempPath, fullPath, true);
 461 |                     try { File.Delete(tempPath); } catch { }
 462 |                     try { if (File.Exists(backupPath)) File.Delete(backupPath); } catch { }
 463 |                 }
 464 |                 catch (IOException)
 465 |                 {
 466 |                     File.Copy(tempPath, fullPath, true);
 467 |                     try { File.Delete(tempPath); } catch { }
 468 |                     try { if (File.Exists(backupPath)) File.Delete(backupPath); } catch { }
 469 |                 }
 470 | 
 471 |                 // Prepare success response BEFORE any operation that can trigger a domain reload
 472 |                 var uri = $"unity://path/{relativePath}";
 473 |                 var ok = Response.Success(
 474 |                     $"Script '{name}.cs' updated successfully at '{relativePath}'.",
 475 |                     new { uri, path = relativePath, scheduledRefresh = true }
 476 |                 );
 477 | 
 478 |                 // Schedule a debounced import/compile on next editor tick to avoid stalling the reply
 479 |                 ManageScriptRefreshHelpers.ScheduleScriptRefresh(relativePath);
 480 | 
 481 |                 return ok;
 482 |             }
 483 |             catch (Exception e)
 484 |             {
 485 |                 return Response.Error($"Failed to update script '{relativePath}': {e.Message}");
 486 |             }
 487 |         }
 488 | 
 489 |         /// <summary>
 490 |         /// Apply simple text edits specified by line/column ranges. Applies transactionally and validates result.
 491 |         /// </summary>
 492 |         private const int MaxEditPayloadBytes = 64 * 1024;
 493 | 
 494 |         private static object ApplyTextEdits(
 495 |             string fullPath,
 496 |             string relativePath,
 497 |             string name,
 498 |             JArray edits,
 499 |             string preconditionSha256,
 500 |             string refreshModeFromCaller = null,
 501 |             string validateMode = null)
 502 |         {
 503 |             if (!File.Exists(fullPath))
 504 |                 return Response.Error($"Script not found at '{relativePath}'.");
 505 |             // Refuse edits if the target or any ancestor is a symlink
 506 |             try
 507 |             {
 508 |                 var di = new DirectoryInfo(Path.GetDirectoryName(fullPath) ?? "");
 509 |                 while (di != null && !string.Equals(di.FullName.Replace('\\', '/'), Application.dataPath.Replace('\\', '/'), StringComparison.OrdinalIgnoreCase))
 510 |                 {
 511 |                     if (di.Exists && (di.Attributes & FileAttributes.ReparsePoint) != 0)
 512 |                         return Response.Error("Refusing to edit a symlinked script path.");
 513 |                     di = di.Parent;
 514 |                 }
 515 |             }
 516 |             catch
 517 |             {
 518 |                 // If checking attributes fails, proceed without the symlink guard
 519 |             }
 520 |             if (edits == null || edits.Count == 0)
 521 |                 return Response.Error("No edits provided.");
 522 | 
 523 |             string original;
 524 |             try { original = File.ReadAllText(fullPath); }
 525 |             catch (Exception ex) { return Response.Error($"Failed to read script: {ex.Message}"); }
 526 | 
 527 |             // Require precondition to avoid drift on large files
 528 |             string currentSha = ComputeSha256(original);
 529 |             if (string.IsNullOrEmpty(preconditionSha256))
 530 |                 return Response.Error("precondition_required", new { status = "precondition_required", current_sha256 = currentSha });
 531 |             if (!preconditionSha256.Equals(currentSha, StringComparison.OrdinalIgnoreCase))
 532 |                 return Response.Error("stale_file", new { status = "stale_file", expected_sha256 = preconditionSha256, current_sha256 = currentSha });
 533 | 
 534 |             // Convert edits to absolute index ranges
 535 |             var spans = new List<(int start, int end, string text)>();
 536 |             long totalBytes = 0;
 537 |             foreach (var e in edits)
 538 |             {
 539 |                 try
 540 |                 {
 541 |                     int sl = Math.Max(1, e.Value<int>("startLine"));
 542 |                     int sc = Math.Max(1, e.Value<int>("startCol"));
 543 |                     int el = Math.Max(1, e.Value<int>("endLine"));
 544 |                     int ec = Math.Max(1, e.Value<int>("endCol"));
 545 |                     string newText = e.Value<string>("newText") ?? string.Empty;
 546 | 
 547 |                     if (!TryIndexFromLineCol(original, sl, sc, out int sidx))
 548 |                         return Response.Error($"apply_text_edits: start out of range (line {sl}, col {sc})");
 549 |                     if (!TryIndexFromLineCol(original, el, ec, out int eidx))
 550 |                         return Response.Error($"apply_text_edits: end out of range (line {el}, col {ec})");
 551 |                     if (eidx < sidx) (sidx, eidx) = (eidx, sidx);
 552 | 
 553 |                     spans.Add((sidx, eidx, newText));
 554 |                     checked
 555 |                     {
 556 |                         totalBytes += System.Text.Encoding.UTF8.GetByteCount(newText);
 557 |                     }
 558 |                 }
 559 |                 catch (Exception ex)
 560 |                 {
 561 |                     return Response.Error($"Invalid edit payload: {ex.Message}");
 562 |                 }
 563 |             }
 564 | 
 565 |             // Header guard: refuse edits that touch before the first 'using ' directive (after optional BOM) to prevent file corruption
 566 |             int headerBoundary = (original.Length > 0 && original[0] == '\uFEFF') ? 1 : 0; // skip BOM once if present
 567 |             // Find first top-level using (supports alias, static, and dotted namespaces)
 568 |             var mUsing = System.Text.RegularExpressions.Regex.Match(
 569 |                 original,
 570 |                 @"(?m)^\s*using\s+(?:static\s+)?(?:[A-Za-z_]\w*\s*=\s*)?[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*\s*;",
 571 |                 System.Text.RegularExpressions.RegexOptions.CultureInvariant,
 572 |                 TimeSpan.FromSeconds(2)
 573 |             );
 574 |             if (mUsing.Success)
 575 |             {
 576 |                 headerBoundary = Math.Min(Math.Max(headerBoundary, mUsing.Index), original.Length);
 577 |             }
 578 |             foreach (var sp in spans)
 579 |             {
 580 |                 if (sp.start < headerBoundary)
 581 |                 {
 582 |                     return Response.Error("using_guard", new { status = "using_guard", hint = "Refusing to edit before the first 'using'. Use anchor_insert near a method or a structured edit." });
 583 |                 }
 584 |             }
 585 | 
 586 |             // Attempt auto-upgrade: if a single edit targets a method header/body, re-route as structured replace_method
 587 |             if (spans.Count == 1)
 588 |             {
 589 |                 var sp = spans[0];
 590 |                 // Heuristic: around the start of the edit, try to match a method header in original
 591 |                 int searchStart = Math.Max(0, sp.start - 200);
 592 |                 int searchEnd = Math.Min(original.Length, sp.start + 200);
 593 |                 string slice = original.Substring(searchStart, searchEnd - searchStart);
 594 |                 var rx = new System.Text.RegularExpressions.Regex(@"(?m)^[\t ]*(?:\[[^\]]+\][\t ]*)*[\t ]*(?:public|private|protected|internal|static|virtual|override|sealed|async|extern|unsafe|new|partial)[\s\S]*?\b([A-Za-z_][A-Za-z0-9_]*)\s*\(");
 595 |                 var mh = rx.Match(slice);
 596 |                 if (mh.Success)
 597 |                 {
 598 |                     string methodName = mh.Groups[1].Value;
 599 |                     // Find class span containing the edit
 600 |                     if (TryComputeClassSpan(original, name, null, out var clsStart, out var clsLen, out _))
 601 |                     {
 602 |                         if (TryComputeMethodSpan(original, clsStart, clsLen, methodName, null, null, null, out var mStart, out var mLen, out _))
 603 |                         {
 604 |                             // If the edit overlaps the method span significantly, treat as replace_method
 605 |                             if (sp.start <= mStart + 2 && sp.end >= mStart + 1)
 606 |                             {
 607 |                                 var structEdits = new JArray();
 608 | 
 609 |                                 // Apply the edit to get a candidate string, then recompute method span on the edited text
 610 |                                 string candidate = original.Remove(sp.start, sp.end - sp.start).Insert(sp.start, sp.text ?? string.Empty);
 611 |                                 string replacementText;
 612 |                                 if (TryComputeClassSpan(candidate, name, null, out var cls2Start, out var cls2Len, out _)
 613 |                                     && TryComputeMethodSpan(candidate, cls2Start, cls2Len, methodName, null, null, null, out var m2Start, out var m2Len, out _))
 614 |                                 {
 615 |                                     replacementText = candidate.Substring(m2Start, m2Len);
 616 |                                 }
 617 |                                 else
 618 |                                 {
 619 |                                     // Fallback: adjust method start by the net delta if the edit was before the method
 620 |                                     int delta = (sp.text?.Length ?? 0) - (sp.end - sp.start);
 621 |                                     int adjustedStart = mStart + (sp.start <= mStart ? delta : 0);
 622 |                                     adjustedStart = Math.Max(0, Math.Min(adjustedStart, candidate.Length));
 623 | 
 624 |                                     // If the edit was within the original method span, adjust the length by the delta within-method
 625 |                                     int withinMethodDelta = 0;
 626 |                                     if (sp.start >= mStart && sp.start <= mStart + mLen)
 627 |                                     {
 628 |                                         withinMethodDelta = delta;
 629 |                                     }
 630 |                                     int adjustedLen = mLen + withinMethodDelta;
 631 |                                     adjustedLen = Math.Max(0, Math.Min(candidate.Length - adjustedStart, adjustedLen));
 632 |                                     replacementText = candidate.Substring(adjustedStart, adjustedLen);
 633 |                                 }
 634 | 
 635 |                                 var op = new JObject
 636 |                                 {
 637 |                                     ["mode"] = "replace_method",
 638 |                                     ["className"] = name,
 639 |                                     ["methodName"] = methodName,
 640 |                                     ["replacement"] = replacementText
 641 |                                 };
 642 |                                 structEdits.Add(op);
 643 |                                 // Reuse structured path
 644 |                                 return EditScript(fullPath, relativePath, name, structEdits, new JObject { ["refresh"] = "immediate", ["validate"] = "standard" });
 645 |                             }
 646 |                         }
 647 |                     }
 648 |                 }
 649 |             }
 650 | 
 651 |             if (totalBytes > MaxEditPayloadBytes)
 652 |             {
 653 |                 return Response.Error("too_large", new { status = "too_large", limitBytes = MaxEditPayloadBytes, hint = "split into smaller edits" });
 654 |             }
 655 | 
 656 |             // Ensure non-overlap and apply from back to front
 657 |             spans = spans.OrderByDescending(t => t.start).ToList();
 658 |             for (int i = 1; i < spans.Count; i++)
 659 |             {
 660 |                 if (spans[i].end > spans[i - 1].start)
 661 |                 {
 662 |                     var conflict = new[] { new { startA = spans[i].start, endA = spans[i].end, startB = spans[i - 1].start, endB = spans[i - 1].end } };
 663 |                     return Response.Error("overlap", new { status = "overlap", conflicts = conflict, hint = "Sort ranges descending by start and compute from the same snapshot." });
 664 |                 }
 665 |             }
 666 | 
 667 |             string working = original;
 668 |             bool relaxed = string.Equals(validateMode, "relaxed", StringComparison.OrdinalIgnoreCase);
 669 |             bool syntaxOnly = string.Equals(validateMode, "syntax", StringComparison.OrdinalIgnoreCase);
 670 |             foreach (var sp in spans)
 671 |             {
 672 |                 string next = working.Remove(sp.start, sp.end - sp.start).Insert(sp.start, sp.text ?? string.Empty);
 673 |                 if (relaxed)
 674 |                 {
 675 |                     // Scoped balance check: validate just around the changed region to avoid false positives  
 676 |                     int originalLength = sp.end - sp.start;
 677 |                     int newLength = sp.text?.Length ?? 0;
 678 |                     int endPos = sp.start + newLength;
 679 |                     if (!CheckScopedBalance(next, Math.Max(0, sp.start - 500), Math.Min(next.Length, endPos + 500)))
 680 |                     {
 681 |                         return Response.Error("unbalanced_braces", new { status = "unbalanced_braces", line = 0, expected = "{}()[] (scoped)", hint = "Use standard validation or shrink the edit range." });
 682 |                     }
 683 |                 }
 684 |                 working = next;
 685 |             }
 686 | 
 687 |             // No-op guard: if resulting text is identical, avoid writes and return explicit no-op
 688 |             if (string.Equals(working, original, StringComparison.Ordinal))
 689 |             {
 690 |                 string noChangeSha = ComputeSha256(original);
 691 |                 return Response.Success(
 692 |                     $"No-op: contents unchanged for '{relativePath}'.",
 693 |                     new
 694 |                     {
 695 |                         uri = $"unity://path/{relativePath}",
 696 |                         path = relativePath,
 697 |                         editsApplied = 0,
 698 |                         no_op = true,
 699 |                         sha256 = noChangeSha,
 700 |                         evidence = new { reason = "identical_content" }
 701 |                     }
 702 |                 );
 703 |             }
 704 | 
 705 |             // Always check final structural balance regardless of relaxed mode
 706 |             if (!CheckBalancedDelimiters(working, out int line, out char expected))
 707 |             {
 708 |                 int startLine = Math.Max(1, line - 5);
 709 |                 int endLine = line + 5;
 710 |                 string hint = $"unbalanced_braces at line {line}. Call resources/read for lines {startLine}-{endLine} and resend a smaller apply_text_edits that restores balance.";
 711 |                 return Response.Error(hint, new { status = "unbalanced_braces", line, expected = expected.ToString(), evidenceWindow = new { startLine, endLine } });
 712 |             }
 713 | 
 714 | #if USE_ROSLYN
 715 |             if (!syntaxOnly)
 716 |             {
 717 |                 var tree = CSharpSyntaxTree.ParseText(working);
 718 |                 var diagnostics = tree.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).Take(3)
 719 |                     .Select(d => new {
 720 |                         line = d.Location.GetLineSpan().StartLinePosition.Line + 1,
 721 |                         col = d.Location.GetLineSpan().StartLinePosition.Character + 1,
 722 |                         code = d.Id,
 723 |                         message = d.GetMessage()
 724 |                     }).ToArray();
 725 |                 if (diagnostics.Length > 0)
 726 |                 {
 727 |                     int firstLine = diagnostics[0].line;
 728 |                     int startLineRos = Math.Max(1, firstLine - 5);
 729 |                     int endLineRos = firstLine + 5;
 730 |                     return Response.Error("syntax_error", new { status = "syntax_error", diagnostics, evidenceWindow = new { startLine = startLineRos, endLine = endLineRos } });
 731 |                 }
 732 | 
 733 |                 // Optional formatting
 734 |                 try
 735 |                 {
 736 |                     var root = tree.GetRoot();
 737 |                     var workspace = new AdhocWorkspace();
 738 |                     root = Microsoft.CodeAnalysis.Formatting.Formatter.Format(root, workspace);
 739 |                     working = root.ToFullString();
 740 |                 }
 741 |                 catch { }
 742 |             }
 743 | #endif
 744 | 
 745 |             string newSha = ComputeSha256(working);
 746 | 
 747 |             // Atomic write and schedule refresh
 748 |             try
 749 |             {
 750 |                 var enc = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
 751 |                 var tmp = fullPath + ".tmp";
 752 |                 File.WriteAllText(tmp, working, enc);
 753 |                 string backup = fullPath + ".bak";
 754 |                 try
 755 |                 {
 756 |                     File.Replace(tmp, fullPath, backup);
 757 |                     try { if (File.Exists(backup)) File.Delete(backup); } catch { /* ignore */ }
 758 |                 }
 759 |                 catch (PlatformNotSupportedException)
 760 |                 {
 761 |                     File.Copy(tmp, fullPath, true);
 762 |                     try { File.Delete(tmp); } catch { }
 763 |                     try { if (File.Exists(backup)) File.Delete(backup); } catch { }
 764 |                 }
 765 |                 catch (IOException)
 766 |                 {
 767 |                     File.Copy(tmp, fullPath, true);
 768 |                     try { File.Delete(tmp); } catch { }
 769 |                     try { if (File.Exists(backup)) File.Delete(backup); } catch { }
 770 |                 }
 771 | 
 772 |                 // Respect refresh mode: immediate vs debounced
 773 |                 bool immediate = string.Equals(refreshModeFromCaller, "immediate", StringComparison.OrdinalIgnoreCase) ||
 774 |                                   string.Equals(refreshModeFromCaller, "sync", StringComparison.OrdinalIgnoreCase);
 775 |                 if (immediate)
 776 |                 {
 777 |                     McpLog.Info($"[ManageScript] ApplyTextEdits: immediate refresh for '{relativePath}'");
 778 |                     AssetDatabase.ImportAsset(
 779 |                         relativePath,
 780 |                         ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate
 781 |                     );
 782 | #if UNITY_EDITOR
 783 |                     UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation();
 784 | #endif
 785 |                 }
 786 |                 else
 787 |                 {
 788 |                     McpLog.Info($"[ManageScript] ApplyTextEdits: debounced refresh scheduled for '{relativePath}'");
 789 |                     ManageScriptRefreshHelpers.ScheduleScriptRefresh(relativePath);
 790 |                 }
 791 | 
 792 |                 return Response.Success(
 793 |                     $"Applied {spans.Count} text edit(s) to '{relativePath}'.",
 794 |                     new
 795 |                     {
 796 |                         uri = $"unity://path/{relativePath}",
 797 |                         path = relativePath,
 798 |                         editsApplied = spans.Count,
 799 |                         sha256 = newSha,
 800 |                         scheduledRefresh = !immediate
 801 |                     }
 802 |                 );
 803 |             }
 804 |             catch (Exception ex)
 805 |             {
 806 |                 return Response.Error($"Failed to write edits: {ex.Message}");
 807 |             }
 808 |         }
 809 | 
 810 |         private static bool TryIndexFromLineCol(string text, int line1, int col1, out int index)
 811 |         {
 812 |             // 1-based line/col to absolute index (0-based), col positions are counted in code points
 813 |             int line = 1, col = 1;
 814 |             for (int i = 0; i <= text.Length; i++)
 815 |             {
 816 |                 if (line == line1 && col == col1)
 817 |                 {
 818 |                     index = i;
 819 |                     return true;
 820 |                 }
 821 |                 if (i == text.Length) break;
 822 |                 char c = text[i];
 823 |                 if (c == '\r')
 824 |                 {
 825 |                     // Treat CRLF as a single newline; skip the LF if present
 826 |                     if (i + 1 < text.Length && text[i + 1] == '\n')
 827 |                         i++;
 828 |                     line++;
 829 |                     col = 1;
 830 |                 }
 831 |                 else if (c == '\n')
 832 |                 {
 833 |                     line++;
 834 |                     col = 1;
 835 |                 }
 836 |                 else
 837 |                 {
 838 |                     col++;
 839 |                 }
 840 |             }
 841 |             index = -1;
 842 |             return false;
 843 |         }
 844 | 
 845 |         private static string ComputeSha256(string contents)
 846 |         {
 847 |             using (var sha = SHA256.Create())
 848 |             {
 849 |                 var bytes = System.Text.Encoding.UTF8.GetBytes(contents);
 850 |                 var hash = sha.ComputeHash(bytes);
 851 |                 return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
 852 |             }
 853 |         }
 854 | 
 855 |         private static bool CheckBalancedDelimiters(string text, out int line, out char expected)
 856 |         {
 857 |             var braceStack = new Stack<int>();
 858 |             var parenStack = new Stack<int>();
 859 |             var bracketStack = new Stack<int>();
 860 |             bool inString = false, inChar = false, inSingle = false, inMulti = false, escape = false;
 861 |             line = 1; expected = '\0';
 862 | 
 863 |             for (int i = 0; i < text.Length; i++)
 864 |             {
 865 |                 char c = text[i];
 866 |                 char next = i + 1 < text.Length ? text[i + 1] : '\0';
 867 | 
 868 |                 if (c == '\n') { line++; if (inSingle) inSingle = false; }
 869 | 
 870 |                 if (escape) { escape = false; continue; }
 871 | 
 872 |                 if (inString)
 873 |                 {
 874 |                     if (c == '\\') { escape = true; }
 875 |                     else if (c == '"') inString = false;
 876 |                     continue;
 877 |                 }
 878 |                 if (inChar)
 879 |                 {
 880 |                     if (c == '\\') { escape = true; }
 881 |                     else if (c == '\'') inChar = false;
 882 |                     continue;
 883 |                 }
 884 |                 if (inSingle) continue;
 885 |                 if (inMulti)
 886 |                 {
 887 |                     if (c == '*' && next == '/') { inMulti = false; i++; }
 888 |                     continue;
 889 |                 }
 890 | 
 891 |                 if (c == '"') { inString = true; continue; }
 892 |                 if (c == '\'') { inChar = true; continue; }
 893 |                 if (c == '/' && next == '/') { inSingle = true; i++; continue; }
 894 |                 if (c == '/' && next == '*') { inMulti = true; i++; continue; }
 895 | 
 896 |                 switch (c)
 897 |                 {
 898 |                     case '{': braceStack.Push(line); break;
 899 |                     case '}':
 900 |                         if (braceStack.Count == 0) { expected = '{'; return false; }
 901 |                         braceStack.Pop();
 902 |                         break;
 903 |                     case '(': parenStack.Push(line); break;
 904 |                     case ')':
 905 |                         if (parenStack.Count == 0) { expected = '('; return false; }
 906 |                         parenStack.Pop();
 907 |                         break;
 908 |                     case '[': bracketStack.Push(line); break;
 909 |                     case ']':
 910 |                         if (bracketStack.Count == 0) { expected = '['; return false; }
 911 |                         bracketStack.Pop();
 912 |                         break;
 913 |                 }
 914 |             }
 915 | 
 916 |             if (braceStack.Count > 0) { line = braceStack.Peek(); expected = '}'; return false; }
 917 |             if (parenStack.Count > 0) { line = parenStack.Peek(); expected = ')'; return false; }
 918 |             if (bracketStack.Count > 0) { line = bracketStack.Peek(); expected = ']'; return false; }
 919 | 
 920 |             return true;
 921 |         }
 922 | 
 923 |         // Lightweight scoped balance: checks delimiters within a substring, ignoring outer context
 924 |         private static bool CheckScopedBalance(string text, int start, int end)
 925 |         {
 926 |             start = Math.Max(0, Math.Min(text.Length, start));
 927 |             end = Math.Max(start, Math.Min(text.Length, end));
 928 |             int brace = 0, paren = 0, bracket = 0;
 929 |             bool inStr = false, inChr = false, esc = false;
 930 |             for (int i = start; i < end; i++)
 931 |             {
 932 |                 char c = text[i];
 933 |                 char n = (i + 1 < end) ? text[i + 1] : '\0';
 934 |                 if (inStr)
 935 |                 {
 936 |                     if (!esc && c == '"') inStr = false; esc = (!esc && c == '\\'); continue;
 937 |                 }
 938 |                 if (inChr)
 939 |                 {
 940 |                     if (!esc && c == '\'') inChr = false; esc = (!esc && c == '\\'); continue;
 941 |                 }
 942 |                 if (c == '"') { inStr = true; esc = false; continue; }
 943 |                 if (c == '\'') { inChr = true; esc = false; continue; }
 944 |                 if (c == '/' && n == '/') { while (i < end && text[i] != '\n') i++; continue; }
 945 |                 if (c == '/' && n == '*') { i += 2; while (i + 1 < end && !(text[i] == '*' && text[i + 1] == '/')) i++; i++; continue; }
 946 |                 if (c == '{') brace++;
 947 |                 else if (c == '}') brace--;
 948 |                 else if (c == '(') paren++;
 949 |                 else if (c == ')') paren--;
 950 |                 else if (c == '[') bracket++; else if (c == ']') bracket--;
 951 |                 // Allow temporary negative balance - will check tolerance at end
 952 |             }
 953 |             return brace >= -3 && paren >= -3 && bracket >= -3; // tolerate more context from outside region
 954 |         }
 955 | 
 956 |         private static object DeleteScript(string fullPath, string relativePath)
 957 |         {
 958 |             if (!File.Exists(fullPath))
 959 |             {
 960 |                 return Response.Error($"Script not found at '{relativePath}'. Cannot delete.");
 961 |             }
 962 | 
 963 |             try
 964 |             {
 965 |                 // Use AssetDatabase.MoveAssetToTrash for safer deletion (allows undo)
 966 |                 bool deleted = AssetDatabase.MoveAssetToTrash(relativePath);
 967 |                 if (deleted)
 968 |                 {
 969 |                     AssetDatabase.Refresh();
 970 |                     return Response.Success(
 971 |                         $"Script '{Path.GetFileName(relativePath)}' moved to trash successfully.",
 972 |                         new { deleted = true }
 973 |                     );
 974 |                 }
 975 |                 else
 976 |                 {
 977 |                     // Fallback or error if MoveAssetToTrash fails
 978 |                     return Response.Error(
 979 |                         $"Failed to move script '{relativePath}' to trash. It might be locked or in use."
 980 |                     );
 981 |                 }
 982 |             }
 983 |             catch (Exception e)
 984 |             {
 985 |                 return Response.Error($"Error deleting script '{relativePath}': {e.Message}");
 986 |             }
 987 |         }
 988 | 
 989 |         /// <summary>
 990 |         /// Structured edits (AST-backed where available) on existing scripts.
 991 |         /// Supports class-level replace/delete with Roslyn span computation if USE_ROSLYN is defined,
 992 |         /// otherwise falls back to a conservative balanced-brace scan.
 993 |         /// </summary>
 994 |         private static object EditScript(
 995 |             string fullPath,
 996 |             string relativePath,
 997 |             string name,
 998 |             JArray edits,
 999 |             JObject options)
1000 |         {
1001 |             if (!File.Exists(fullPath))
1002 |                 return Response.Error($"Script not found at '{relativePath}'.");
1003 |             // Refuse edits if the target is a symlink
1004 |             try
1005 |             {
1006 |                 var attrs = File.GetAttributes(fullPath);
1007 |                 if ((attrs & FileAttributes.ReparsePoint) != 0)
1008 |                     return Response.Error("Refusing to edit a symlinked script path.");
1009 |             }
1010 |             catch
1011 |             {
1012 |                 // ignore failures checking attributes and proceed
1013 |             }
1014 |             if (edits == null || edits.Count == 0)
1015 |                 return Response.Error("No edits provided.");
1016 | 
1017 |             string original;
1018 |             try { original = File.ReadAllText(fullPath); }
1019 |             catch (Exception ex) { return Response.Error($"Failed to read script: {ex.Message}"); }
1020 | 
1021 |             string working = original;
1022 | 
1023 |             try
1024 |             {
1025 |                 var replacements = new List<(int start, int length, string text)>();
1026 |                 int appliedCount = 0;
1027 | 
1028 |                 // Apply mode: atomic (default) computes all spans against original and applies together.
1029 |                 // Sequential applies each edit immediately to the current working text (useful for dependent edits).
1030 |                 string applyMode = options?["applyMode"]?.ToString()?.ToLowerInvariant();
1031 |                 bool applySequentially = applyMode == "sequential";
1032 | 
1033 |                 foreach (var e in edits)
1034 |                 {
1035 |                     var op = (JObject)e;
1036 |                     var mode = (op.Value<string>("mode") ?? op.Value<string>("op") ?? string.Empty).ToLowerInvariant();
1037 | 
1038 |                     switch (mode)
1039 |                     {
1040 |                         case "replace_class":
1041 |                             {
1042 |                                 string className = op.Value<string>("className");
1043 |                                 string ns = op.Value<string>("namespace");
1044 |                                 string replacement = ExtractReplacement(op);
1045 | 
1046 |                                 if (string.IsNullOrWhiteSpace(className))
1047 |                                     return Response.Error("replace_class requires 'className'.");
1048 |                                 if (replacement == null)
1049 |                                     return Response.Error("replace_class requires 'replacement' (inline or base64).");
1050 | 
1051 |                                 if (!TryComputeClassSpan(working, className, ns, out var spanStart, out var spanLength, out var why))
1052 |                                     return Response.Error($"replace_class failed: {why}");
1053 | 
1054 |                                 if (!ValidateClassSnippet(replacement, className, out var vErr))
1055 |                                     return Response.Error($"Replacement snippet invalid: {vErr}");
1056 | 
1057 |                                 if (applySequentially)
1058 |                                 {
1059 |                                     working = working.Remove(spanStart, spanLength).Insert(spanStart, NormalizeNewlines(replacement));
1060 |                                     appliedCount++;
1061 |                                 }
1062 |                                 else
1063 |                                 {
1064 |                                     replacements.Add((spanStart, spanLength, NormalizeNewlines(replacement)));
1065 |                                 }
1066 |                                 break;
1067 |                             }
1068 | 
1069 |                         case "delete_class":
1070 |                             {
1071 |                                 string className = op.Value<string>("className");
1072 |                                 string ns = op.Value<string>("namespace");
1073 |                                 if (string.IsNullOrWhiteSpace(className))
1074 |                                     return Response.Error("delete_class requires 'className'.");
1075 | 
1076 |                                 if (!TryComputeClassSpan(working, className, ns, out var s, out var l, out var why))
1077 |                                     return Response.Error($"delete_class failed: {why}");
1078 | 
1079 |                                 if (applySequentially)
1080 |                                 {
1081 |                                     working = working.Remove(s, l);
1082 |                                     appliedCount++;
1083 |                                 }
1084 |                                 else
1085 |                                 {
1086 |                                     replacements.Add((s, l, string.Empty));
1087 |                                 }
1088 |                                 break;
1089 |                             }
1090 | 
1091 |                         case "replace_method":
1092 |                             {
1093 |                                 string className = op.Value<string>("className");
1094 |                                 string ns = op.Value<string>("namespace");
1095 |                                 string methodName = op.Value<string>("methodName");
1096 |                                 string replacement = ExtractReplacement(op);
1097 |                                 string returnType = op.Value<string>("returnType");
1098 |                                 string parametersSignature = op.Value<string>("parametersSignature");
1099 |                                 string attributesContains = op.Value<string>("attributesContains");
1100 | 
1101 |                                 if (string.IsNullOrWhiteSpace(className)) return Response.Error("replace_method requires 'className'.");
1102 |                                 if (string.IsNullOrWhiteSpace(methodName)) return Response.Error("replace_method requires 'methodName'.");
1103 |                                 if (replacement == null) return Response.Error("replace_method requires 'replacement' (inline or base64).");
1104 | 
1105 |                                 if (!TryComputeClassSpan(working, className, ns, out var clsStart, out var clsLen, out var whyClass))
1106 |                                     return Response.Error($"replace_method failed to locate class: {whyClass}");
1107 | 
1108 |                                 if (!TryComputeMethodSpan(working, clsStart, clsLen, methodName, returnType, parametersSignature, attributesContains, out var mStart, out var mLen, out var whyMethod))
1109 |                                 {
1110 |                                     bool hasDependentInsert = edits.Any(j => j is JObject jo &&
1111 |                                         string.Equals(jo.Value<string>("className"), className, StringComparison.Ordinal) &&
1112 |                                         string.Equals(jo.Value<string>("methodName"), methodName, StringComparison.Ordinal) &&
1113 |                                         ((jo.Value<string>("mode") ?? jo.Value<string>("op") ?? string.Empty).ToLowerInvariant() == "insert_method"));
1114 |                                     string hint = hasDependentInsert && !applySequentially ? " Hint: This batch inserts this method. Use options.applyMode='sequential' or split into separate calls." : string.Empty;
1115 |                                     return Response.Error($"replace_method failed: {whyMethod}.{hint}");
1116 |                                 }
1117 | 
1118 |                                 if (applySequentially)
1119 |                                 {
1120 |                                     working = working.Remove(mStart, mLen).Insert(mStart, NormalizeNewlines(replacement));
1121 |                                     appliedCount++;
1122 |                                 }
1123 |                                 else
1124 |                                 {
1125 |                                     replacements.Add((mStart, mLen, NormalizeNewlines(replacement)));
1126 |                                 }
1127 |                                 break;
1128 |                             }
1129 | 
1130 |                         case "delete_method":
1131 |                             {
1132 |                                 string className = op.Value<string>("className");
1133 |                                 string ns = op.Value<string>("namespace");
1134 |                                 string methodName = op.Value<string>("methodName");
1135 |                                 string returnType = op.Value<string>("returnType");
1136 |                                 string parametersSignature = op.Value<string>("parametersSignature");
1137 |                                 string attributesContains = op.Value<string>("attributesContains");
1138 | 
1139 |                                 if (string.IsNullOrWhiteSpace(className)) return Response.Error("delete_method requires 'className'.");
1140 |                                 if (string.IsNullOrWhiteSpace(methodName)) return Response.Error("delete_method requires 'methodName'.");
1141 | 
1142 |                                 if (!TryComputeClassSpan(working, className, ns, out var clsStart, out var clsLen, out var whyClass))
1143 |                                     return Response.Error($"delete_method failed to locate class: {whyClass}");
1144 | 
1145 |                                 if (!TryComputeMethodSpan(working, clsStart, clsLen, methodName, returnType, parametersSignature, attributesContains, out var mStart, out var mLen, out var whyMethod))
1146 |                                 {
1147 |                                     bool hasDependentInsert = edits.Any(j => j is JObject jo &&
1148 |                                         string.Equals(jo.Value<string>("className"), className, StringComparison.Ordinal) &&
1149 |                                         string.Equals(jo.Value<string>("methodName"), methodName, StringComparison.Ordinal) &&
1150 |                                         ((jo.Value<string>("mode") ?? jo.Value<string>("op") ?? string.Empty).ToLowerInvariant() == "insert_method"));
1151 |                                     string hint = hasDependentInsert && !applySequentially ? " Hint: This batch inserts this method. Use options.applyMode='sequential' or split into separate calls." : string.Empty;
1152 |                                     return Response.Error($"delete_method failed: {whyMethod}.{hint}");
1153 |                                 }
1154 | 
1155 |                                 if (applySequentially)
1156 |                                 {
1157 |                                     working = working.Remove(mStart, mLen);
1158 |                                     appliedCount++;
1159 |                                 }
1160 |                                 else
1161 |                                 {
1162 |                                     replacements.Add((mStart, mLen, string.Empty));
1163 |                                 }
1164 |                                 break;
1165 |                             }
1166 | 
1167 |                         case "insert_method":
1168 |                             {
1169 |                                 string className = op.Value<string>("className");
1170 |                                 string ns = op.Value<string>("namespace");
1171 |                                 string position = (op.Value<string>("position") ?? "end").ToLowerInvariant();
1172 |                                 string afterMethodName = op.Value<string>("afterMethodName");
1173 |                                 string afterReturnType = op.Value<string>("afterReturnType");
1174 |                                 string afterParameters = op.Value<string>("afterParametersSignature");
1175 |                                 string afterAttributesContains = op.Value<string>("afterAttributesContains");
1176 |                                 string snippet = ExtractReplacement(op);
1177 |                                 // Harden: refuse empty replacement for inserts
1178 |                                 if (snippet == null || snippet.Trim().Length == 0)
1179 |                                     return Response.Error("insert_method requires a non-empty 'replacement' text.");
1180 | 
1181 |                                 if (string.IsNullOrWhiteSpace(className)) return Response.Error("insert_method requires 'className'.");
1182 |                                 if (snippet == null) return Response.Error("insert_method requires 'replacement' (inline or base64) containing a full method declaration.");
1183 | 
1184 |                                 if (!TryComputeClassSpan(working, className, ns, out var clsStart, out var clsLen, out var whyClass))
1185 |                                     return Response.Error($"insert_method failed to locate class: {whyClass}");
1186 | 
1187 |                                 if (position == "after")
1188 |                                 {
1189 |                                     if (string.IsNullOrEmpty(afterMethodName)) return Response.Error("insert_method with position='after' requires 'afterMethodName'.");
1190 |                                     if (!TryComputeMethodSpan(working, clsStart, clsLen, afterMethodName, afterReturnType, afterParameters, afterAttributesContains, out var aStart, out var aLen, out var whyAfter))
1191 |                                         return Response.Error($"insert_method(after) failed to locate anchor method: {whyAfter}");
1192 |                                     int insAt = aStart + aLen;
1193 |                                     string text = NormalizeNewlines("\n\n" + snippet.TrimEnd() + "\n");
1194 |                                     if (applySequentially)
1195 |                                     {
1196 |                                         working = working.Insert(insAt, text);
1197 |                                         appliedCount++;
1198 |                                     }
1199 |                                     else
1200 |                                     {
1201 |                                         replacements.Add((insAt, 0, text));
1202 |                                     }
1203 |                                 }
1204 |                                 else if (!TryFindClassInsertionPoint(working, clsStart, clsLen, position, out var insAt, out var whyIns))
1205 |                                     return Response.Error($"insert_method failed: {whyIns}");
1206 |                                 else
1207 |                                 {
1208 |                                     string text = NormalizeNewlines("\n\n" + snippet.TrimEnd() + "\n");
1209 |                                     if (applySequentially)
1210 |                                     {
1211 |                                         working = working.Insert(insAt, text);
1212 |                                         appliedCount++;
1213 |                                     }
1214 |                                     else
1215 |                                     {
1216 |                                         replacements.Add((insAt, 0, text));
1217 |                                     }
1218 |                                 }
1219 |                                 break;
1220 |                             }
1221 | 
1222 |                         case "anchor_insert":
1223 |                             {
1224 |                                 string anchor = op.Value<string>("anchor");
1225 |                                 string position = (op.Value<string>("position") ?? "before").ToLowerInvariant();
1226 |                                 string text = op.Value<string>("text") ?? ExtractReplacement(op);
1227 |                                 if (string.IsNullOrWhiteSpace(anchor)) return Response.Error("anchor_insert requires 'anchor' (regex).");
1228 |                                 if (string.IsNullOrEmpty(text)) return Response.Error("anchor_insert requires non-empty 'text'.");
1229 | 
1230 |                                 try
1231 |                                 {
1232 |                                     var rx = new Regex(anchor, RegexOptions.Multiline, TimeSpan.FromSeconds(2));
1233 |                                     var m = rx.Match(working);
1234 |                                     if (!m.Success) return Response.Error($"anchor_insert: anchor not found: {anchor}");
1235 |                                     int insAt = position == "after" ? m.Index + m.Length : m.Index;
1236 |                                     string norm = NormalizeNewlines(text);
1237 |                                     if (!norm.EndsWith("\n"))
1238 |                                     {
1239 |                                         norm += "\n";
1240 |                                     }
1241 | 
1242 |                                     // Duplicate guard: if identical snippet already exists within this class, skip insert
1243 |                                     if (TryComputeClassSpan(working, name, null, out var clsStartDG, out var clsLenDG, out _))
1244 |                                     {
1245 |                                         string classSlice = working.Substring(clsStartDG, Math.Min(clsLenDG, working.Length - clsStartDG));
1246 |                                         if (classSlice.IndexOf(norm, StringComparison.Ordinal) >= 0)
1247 |                                         {
1248 |                                             // Do not insert duplicate; treat as no-op
1249 |                                             break;
1250 |                                         }
1251 |                                     }
1252 |                                     if (applySequentially)
1253 |                                     {
1254 |                                         working = working.Insert(insAt, norm);
1255 |                                         appliedCount++;
1256 |                                     }
1257 |                                     else
1258 |                                     {
1259 |                                         replacements.Add((insAt, 0, norm));
1260 |                                     }
1261 |                                 }
1262 |                                 catch (Exception ex)
1263 |                                 {
1264 |                                     return Response.Error($"anchor_insert failed: {ex.Message}");
1265 |                                 }
1266 |                                 break;
1267 |                             }
1268 | 
1269 |                         case "anchor_delete":
1270 |                             {
1271 |                                 string anchor = op.Value<string>("anchor");
1272 |                                 if (string.IsNullOrWhiteSpace(anchor)) return Response.Error("anchor_delete requires 'anchor' (regex).");
1273 |                                 try
1274 |                                 {
1275 |                                     var rx = new Regex(anchor, RegexOptions.Multiline, TimeSpan.FromSeconds(2));
1276 |                                     var m = rx.Match(working);
1277 |                                     if (!m.Success) return Response.Error($"anchor_delete: anchor not found: {anchor}");
1278 |                                     int delAt = m.Index;
1279 |                                     int delLen = m.Length;
1280 |                                     if (applySequentially)
1281 |                                     {
1282 |                                         working = working.Remove(delAt, delLen);
1283 |                                         appliedCount++;
1284 |                                     }
1285 |                                     else
1286 |                                     {
1287 |                                         replacements.Add((delAt, delLen, string.Empty));
1288 |                                     }
1289 |                                 }
1290 |                                 catch (Exception ex)
1291 |                                 {
1292 |                                     return Response.Error($"anchor_delete failed: {ex.Message}");
1293 |                                 }
1294 |                                 break;
1295 |                             }
1296 | 
1297 |                         case "anchor_replace":
1298 |                             {
1299 |                                 string anchor = op.Value<string>("anchor");
1300 |                                 string replacement = op.Value<string>("text") ?? op.Value<string>("replacement") ?? ExtractReplacement(op) ?? string.Empty;
1301 |                                 if (string.IsNullOrWhiteSpace(anchor)) return Response.Error("anchor_replace requires 'anchor' (regex).");
1302 |                                 try
1303 |                                 {
1304 |                                     var rx = new Regex(anchor, RegexOptions.Multiline, TimeSpan.FromSeconds(2));
1305 |                                     var m = rx.Match(working);
1306 |                                     if (!m.Success) return Response.Error($"anchor_replace: anchor not found: {anchor}");
1307 |                                     int at = m.Index;
1308 |                                     int len = m.Length;
1309 |                                     string norm = NormalizeNewlines(replacement);
1310 |                                     if (applySequentially)
1311 |                                     {
1312 |                                         working = working.Remove(at, len).Insert(at, norm);
1313 |                                         appliedCount++;
1314 |                                     }
1315 |                                     else
1316 |                                     {
1317 |                                         replacements.Add((at, len, norm));
1318 |                                     }
1319 |                                 }
1320 |                                 catch (Exception ex)
1321 |                                 {
1322 |                                     return Response.Error($"anchor_replace failed: {ex.Message}");
1323 |                                 }
1324 |                                 break;
1325 |                             }
1326 | 
1327 |                         default:
1328 |                             return Response.Error($"Unknown edit mode: '{mode}'. Allowed: replace_class, delete_class, replace_method, delete_method, insert_method, anchor_insert, anchor_delete, anchor_replace.");
1329 |                     }
1330 |                 }
1331 | 
1332 |                 if (!applySequentially)
1333 |                 {
1334 |                     if (HasOverlaps(replacements))
1335 |                     {
1336 |                         var ordered = replacements.OrderByDescending(r => r.start).ToList();
1337 |                         for (int i = 1; i < ordered.Count; i++)
1338 |                         {
1339 |                             if (ordered[i].start + ordered[i].length > ordered[i - 1].start)
1340 |                             {
1341 |                                 var conflict = new[] { new { startA = ordered[i].start, endA = ordered[i].start + ordered[i].length, startB = ordered[i - 1].start, endB = ordered[i - 1].start + ordered[i - 1].length } };
1342 |                                 return Response.Error("overlap", new { status = "overlap", conflicts = conflict, hint = "Sort ranges descending by start and compute from the same snapshot." });
1343 |                             }
1344 |                         }
1345 |                         return Response.Error("overlap", new { status = "overlap" });
1346 |                     }
1347 | 
1348 |                     foreach (var r in replacements.OrderByDescending(r => r.start))
1349 |                         working = working.Remove(r.start, r.length).Insert(r.start, r.text);
1350 |                     appliedCount = replacements.Count;
1351 |                 }
1352 | 
1353 |                 // Guard against structural imbalance before validation
1354 |                 if (!CheckBalancedDelimiters(working, out int lineBal, out char expectedBal))
1355 |                     return Response.Error("unbalanced_braces", new { status = "unbalanced_braces", line = lineBal, expected = expectedBal.ToString() });
1356 | 
1357 |                 // No-op guard for structured edits: if text unchanged, return explicit no-op
1358 |                 if (string.Equals(working, original, StringComparison.Ordinal))
1359 |                 {
1360 |                     var sameSha = ComputeSha256(original);
1361 |                     return Response.Success(
1362 |                         $"No-op: contents unchanged for '{relativePath}'.",
1363 |                         new
1364 |                         {
1365 |                             path = relativePath,
1366 |                             uri = $"unity://path/{relativePath}",
1367 |                             editsApplied = 0,
1368 |                             no_op = true,
1369 |                             sha256 = sameSha,
1370 |                             evidence = new { reason = "identical_content" }
1371 |                         }
1372 |                     );
1373 |                 }
1374 | 
1375 |                 // Validate result using override from options if provided; otherwise GUI strictness
1376 |                 var level = GetValidationLevelFromGUI();
1377 |                 try
1378 |                 {
1379 |                     var validateOpt = options?["validate"]?.ToString()?.ToLowerInvariant();
1380 |                     if (!string.IsNullOrEmpty(validateOpt))
1381 |                     {
1382 |                         level = validateOpt switch
1383 |                         {
1384 |                             "basic" => ValidationLevel.Basic,
1385 |                             "standard" => ValidationLevel.Standard,
1386 |                             "comprehensive" => ValidationLevel.Comprehensive,
1387 |                             "strict" => ValidationLevel.Strict,
1388 |                             _ => level
1389 |                         };
1390 |                     }
1391 |                 }
1392 |                 catch { /* ignore option parsing issues */ }
1393 |                 if (!ValidateScriptSyntax(working, level, out var errors))
1394 |                     return Response.Error("validation_failed", new { status = "validation_failed", diagnostics = errors ?? Array.Empty<string>() });
1395 |                 else if (errors != null && errors.Length > 0)
1396 |                     Debug.LogWarning($"Script validation warnings for {name}:\n" + string.Join("\n", errors));
1397 | 
1398 |                 // Atomic write with backup; schedule refresh
1399 |                 // Decide refresh behavior
1400 |                 string refreshMode = options?["refresh"]?.ToString()?.ToLowerInvariant();
1401 |                 bool immediate = refreshMode == "immediate" || refreshMode == "sync";
1402 | 
1403 |                 // Persist changes atomically (no BOM), then compute/return new file SHA
1404 |                 var enc = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
1405 |                 var tmp = fullPath + ".tmp";
1406 |                 File.WriteAllText(tmp, working, enc);
1407 |                 var backup = fullPath + ".bak";
1408 |                 try
1409 |                 {
1410 |                     File.Replace(tmp, fullPath, backup);
1411 |                     try { if (File.Exists(backup)) File.Delete(backup); } catch { }
1412 |                 }
1413 |                 catch (PlatformNotSupportedException)
1414 |                 {
1415 |                     File.Copy(tmp, fullPath, true);
1416 |                     try { File.Delete(tmp); } catch { }
1417 |                     try { if (File.Exists(backup)) File.Delete(backup); } catch { }
1418 |                 }
1419 |                 catch (IOException)
1420 |                 {
1421 |                     File.Copy(tmp, fullPath, true);
1422 |                     try { File.Delete(tmp); } catch { }
1423 |                     try { if (File.Exists(backup)) File.Delete(backup); } catch { }
1424 |                 }
1425 | 
1426 |                 var newSha = ComputeSha256(working);
1427 |                 var ok = Response.Success(
1428 |                     $"Applied {appliedCount} structured edit(s) to '{relativePath}'.",
1429 |                     new
1430 |                     {
1431 |                         path = relativePath,
1432 |                         uri = $"unity://path/{relativePath}",
1433 |                         editsApplied = appliedCount,
1434 |                         scheduledRefresh = !immediate,
1435 |                         sha256 = newSha
1436 |                     }
1437 |                 );
1438 | 
1439 |                 if (immediate)
1440 |                 {
1441 |                     McpLog.Info($"[ManageScript] EditScript: immediate refresh for '{relativePath}'", always: false);
1442 |                     ManageScriptRefreshHelpers.ImportAndRequestCompile(relativePath);
1443 |                 }
1444 |                 else
1445 |                 {
1446 |                     ManageScriptRefreshHelpers.ScheduleScriptRefresh(relativePath);
1447 |                 }
1448 |                 return ok;
1449 |             }
1450 |             catch (Exception ex)
1451 |             {
1452 |                 return Response.Error($"Edit failed: {ex.Message}");
1453 |             }
1454 |         }
1455 | 
1456 |         private static bool HasOverlaps(IEnumerable<(int start, int length, string text)> list)
1457 |         {
1458 |             var arr = list.OrderBy(x => x.start).ToArray();
1459 |             for (int i = 1; i < arr.Length; i++)
1460 |             {
1461 |                 if (arr[i - 1].start + arr[i - 1].length > arr[i].start)
1462 |                     return true;
1463 |             }
1464 |             return false;
1465 |         }
1466 | 
1467 |         private static string ExtractReplacement(JObject op)
1468 |         {
1469 |             var inline = op.Value<string>("replacement");
1470 |             if (!string.IsNullOrEmpty(inline)) return inline;
1471 | 
1472 |             var b64 = op.Value<string>("replacementBase64");
1473 |             if (!string.IsNullOrEmpty(b64))
1474 |             {
1475 |                 try { return System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(b64)); }
1476 |                 catch { return null; }
1477 |             }
1478 |             return null;
1479 |         }
1480 | 
1481 |         private static string NormalizeNewlines(string t)
1482 |         {
1483 |             if (string.IsNullOrEmpty(t)) return t;
1484 |             return t.Replace("\r\n", "\n").Replace("\r", "\n");
1485 |         }
1486 | 
1487 |         private static bool ValidateClassSnippet(string snippet, string expectedName, out string err)
1488 |         {
1489 | #if USE_ROSLYN
1490 |             try
1491 |             {
1492 |                 var tree = CSharpSyntaxTree.ParseText(snippet);
1493 |                 var root = tree.GetRoot();
1494 |                 var classes = root.DescendantNodes().OfType<Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax>().ToList();
1495 |                 if (classes.Count != 1) { err = "snippet must contain exactly one class declaration"; return false; }
1496 |                 // Optional: enforce expected name
1497 |                 // if (classes[0].Identifier.ValueText != expectedName) { err = $"snippet declares '{classes[0].Identifier.ValueText}', expected '{expectedName}'"; return false; }
1498 |                 err = null; return true;
1499 |             }
1500 |             catch (Exception ex) { err = ex.Message; return false; }
1501 | #else
1502 |             if (string.IsNullOrWhiteSpace(snippet) || !snippet.Contains("class ")) { err = "no 'class' keyword found in snippet"; return false; }
1503 |             err = null; return true;
1504 | #endif
1505 |         }
1506 | 
1507 |         private static bool TryComputeClassSpan(string source, string className, string ns, out int start, out int length, out string why)
1508 |         {
1509 | #if USE_ROSLYN
1510 |             try
1511 |             {
1512 |                 var tree = CSharpSyntaxTree.ParseText(source);
1513 |                 var root = tree.GetRoot();
1514 |                 var classes = root.DescendantNodes()
1515 |                     .OfType<Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax>()
1516 |                     .Where(c => c.Identifier.ValueText == className);
1517 | 
1518 |                 if (!string.IsNullOrEmpty(ns))
1519 |                 {
1520 |                     classes = classes.Where(c =>
1521 |                         (c.FirstAncestorOrSelf<Microsoft.CodeAnalysis.CSharp.Syntax.NamespaceDeclarationSyntax>()?.Name?.ToString() ?? "") == ns
1522 |                         || (c.FirstAncestorOrSelf<Microsoft.CodeAnalysis.CSharp.Syntax.FileScopedNamespaceDeclarationSyntax>()?.Name?.ToString() ?? "") == ns);
1523 |                 }
1524 | 
1525 |                 var list = classes.ToList();
1526 |                 if (list.Count == 0) { start = length = 0; why = $"class '{className}' not found" + (ns != null ? $" in namespace '{ns}'" : ""); return false; }
1527 |                 if (list.Count > 1) { start = length = 0; why = $"class '{className}' matched {list.Count} declarations (partial/nested?). Disambiguate."; return false; }
1528 | 
1529 |                 var cls = list[0];
1530 |                 var span = cls.FullSpan; // includes attributes & leading trivia
1531 |                 start = span.Start; length = span.Length; why = null; return true;
1532 |             }
1533 |             catch
1534 |             {
1535 |                 // fall back below
1536 |             }
1537 | #endif
1538 |             return TryComputeClassSpanBalanced(source, className, ns, out start, out length, out why);
1539 |         }
1540 | 
1541 |         private static bool TryComputeClassSpanBalanced(string source, string className, string ns, out int start, out int length, out string why)
1542 |         {
1543 |             start = length = 0; why = null;
1544 |             var idx = IndexOfClassToken(source, className);
1545 |             if (idx < 0) { why = $"class '{className}' not found (balanced scan)"; return false; }
1546 | 
1547 |             if (!string.IsNullOrEmpty(ns) && !AppearsWithinNamespaceHeader(source, idx, ns))
1548 |             { why = $"class '{className}' not under namespace '{ns}' (balanced scan)"; return false; }
1549 | 
1550 |             // Include modifiers/attributes on the same line: back up to the start of line
1551 |             int lineStart = idx;
1552 |             while (lineStart > 0 && source[lineStart - 1] != '\n' && source[lineStart - 1] != '\r') lineStart--;
1553 | 
1554 |             int i = idx;
1555 |             while (i < source.Length && source[i] != '{') i++;
1556 |             if (i >= source.Length) { why = "no opening brace after class header"; return false; }
1557 | 
1558 |             int depth = 0; bool inStr = false, inChar = false, inSL = false, inML = false, esc = false;
1559 |             int startSpan = lineStart;
1560 |             for (; i < source.Length; i++)
1561 |             {
1562 |                 char c = source[i];
1563 |                 char n = i + 1 < source.Length ? source[i + 1] : '\0';
1564 | 
1565 |                 if (inSL) { if (c == '\n') inSL = false; continue; }
1566 |                 if (inML) { if (c == '*' && n == '/') { inML = false; i++; } continue; }
1567 |                 if (inStr) { if (!esc && c == '"') inStr = false; esc = (!esc && c == '\\'); continue; }
1568 |                 if (inChar) { if (!esc && c == '\'') inChar = false; esc = (!esc && c == '\\'); continue; }
1569 | 
1570 |                 if (c == '/' && n == '/') { inSL = true; i++; continue; }
1571 |                 if (c == '/' && n == '*') { inML = true; i++; continue; }
1572 |                 if (c == '"') { inStr = true; continue; }
1573 |                 if (c == '\'') { inChar = true; continue; }
1574 | 
1575 |                 if (c == '{') { depth++; }
1576 |                 else if (c == '}')
1577 |                 {
1578 |                     depth--;
1579 |                     if (depth == 0) { start = startSpan; length = (i - startSpan) + 1; return true; }
1580 |                     if (depth < 0) { why = "brace underflow"; return false; }
1581 |                 }
1582 |             }
1583 |             why = "unterminated class block"; return false;
1584 |         }
1585 | 
1586 |         private static bool TryComputeMethodSpan(
1587 |             string source,
1588 |             int classStart,
1589 |             int classLength,
1590 |             string methodName,
1591 |             string returnType,
1592 |             string parametersSignature,
1593 |             string attributesContains,
1594 |             out int start,
1595 |             out int length,
1596 |             out string why)
1597 |         {
1598 |             start = length = 0; why = null;
1599 |             int searchStart = classStart;
1600 |             int searchEnd = Math.Min(source.Length, classStart + classLength);
1601 | 
1602 |             // 1) Find the method header using a stricter regex (allows optional attributes above)
1603 |             string rtPattern = string.IsNullOrEmpty(returnType) ? @"[^\s]+" : Regex.Escape(returnType).Replace("\\ ", "\\s+");
1604 |             string namePattern = Regex.Escape(methodName);
1605 |             // If a parametersSignature is provided, it may include surrounding parentheses. Strip them so
1606 |             // we can safely embed the signature inside our own parenthesis group without duplicating.
1607 |             string paramsPattern;
1608 |             if (string.IsNullOrEmpty(parametersSignature))
1609 |             {
1610 |                 paramsPattern = @"[\s\S]*?"; // permissive when not specified
1611 |             }
1612 |             else
1613 |             {
1614 |                 string ps = parametersSignature.Trim();
1615 |                 if (ps.StartsWith("(") && ps.EndsWith(")") && ps.Length >= 2)
1616 |                 {
1617 |                     ps = ps.Substring(1, ps.Length - 2);
1618 |                 }
1619 |                 // Escape literal text of the signature
1620 |                 paramsPattern = Regex.Escape(ps);
1621 |             }
1622 |             string pattern =
1623 |                 @"(?m)^[\t ]*(?:\[[^\]]+\][\t ]*)*[\t ]*" +
1624 |                 @"(?:(?:public|private|protected|internal|static|virtual|override|sealed|async|extern|unsafe|new|partial|readonly|volatile|event|abstract|ref|in|out)\s+)*" +
1625 |                 rtPattern + @"[\t ]+" + namePattern + @"\s*(?:<[^>]+>)?\s*\(" + paramsPattern + @"\)";
1626 | 
1627 |             string slice = source.Substring(searchStart, searchEnd - searchStart);
1628 |             var headerMatch = Regex.Match(slice, pattern, RegexOptions.Multiline, TimeSpan.FromSeconds(2));
1629 |             if (!headerMatch.Success)
1630 |             {
1631 |                 why = $"method '{methodName}' header not found in class"; return false;
1632 |             }
1633 |             int headerIndex = searchStart + headerMatch.Index;
1634 | 
1635 |             // Optional attributes filter: look upward from headerIndex for contiguous attribute lines
1636 |             if (!string.IsNullOrEmpty(attributesContains))
1637 |             {
1638 |                 int attrScanStart = headerIndex;
1639 |                 while (attrScanStart > searchStart)
1640 |                 {
1641 |                     int prevNl = source.LastIndexOf('\n', attrScanStart - 1);
1642 |                     if (prevNl < 0 || prevNl < searchStart) break;
1643 |                     string prevLine = source.Substring(prevNl + 1, attrScanStart - (prevNl + 1));
1644 |                     if (prevLine.TrimStart().StartsWith("[")) { attrScanStart = prevNl; continue; }
1645 |                     break;
1646 |                 }
1647 |                 string attrBlock = source.Substring(attrScanStart, headerIndex - attrScanStart);
1648 |                 if (attrBlock.IndexOf(attributesContains, StringComparison.Ordinal) < 0)
1649 |                 {
1650 |                     why = $"method '{methodName}' found but attributes filter did not match"; return false;
1651 |                 }
1652 |             }
1653 | 
1654 |             // backtrack to the very start of header/attributes to include in span
1655 |             int lineStart = headerIndex;
1656 |             while (lineStart > searchStart && source[lineStart - 1] != '\n' && source[lineStart - 1] != '\r') lineStart--;
1657 |             // If previous lines are attributes, include them
1658 |             int attrStart = lineStart;
1659 |             int probe = lineStart - 1;
1660 |             while (probe > searchStart)
1661 |             {
1662 |                 int prevNl = source.LastIndexOf('\n', probe);
1663 |                 if (prevNl < 0 || prevNl < searchStart) break;
1664 |                 string prev = source.Substring(prevNl + 1, attrStart - (prevNl + 1));
1665 |                 if (prev.TrimStart().StartsWith("[")) { attrStart = prevNl + 1; probe = prevNl - 1; }
1666 |                 else break;
1667 |             }
1668 | 
1669 |             // 2) Walk from the end of signature to detect body style ('{' or '=> ...;') and compute end
1670 |             // Find the '(' that belongs to the method signature, not attributes
1671 |             int nameTokenIdx = IndexOfTokenWithin(source, methodName, headerIndex, searchEnd);
1672 |             if (nameTokenIdx < 0) { why = $"method '{methodName}' token not found after header"; return false; }
1673 |             int sigOpenParen = IndexOfTokenWithin(source, "(", nameTokenIdx, searchEnd);
1674 |             if (sigOpenParen < 0) { why = "method parameter list '(' not found"; return false; }
1675 | 
1676 |             int i = sigOpenParen;
1677 |             int parenDepth = 0; bool inStr = false, inChar = false, inSL = false, inML = false, esc = false;
1678 |             for (; i < searchEnd; i++)
1679 |             {
1680 |                 char c = source[i];
1681 |                 char n = i + 1 < searchEnd ? source[i + 1] : '\0';
1682 |                 if (inSL) { if (c == '\n') inSL = false; continue; }
1683 |                 if (inML) { if (c == '*' && n == '/') { inML = false; i++; } continue; }
1684 |                 if (inStr) { if (!esc && c == '"') inStr = false; esc = (!esc && c == '\\'); continue; }
1685 |                 if (inChar) { if (!esc && c == '\'') inChar = false; esc = (!esc && c == '\\'); continue; }
1686 | 
1687 |                 if (c == '/' && n == '/') { inSL = true; i++; continue; }
1688 |                 if (c == '/' && n == '*') { inML = true; i++; continue; }
1689 |                 if (c == '"') { inStr = true; continue; }
1690 |                 if (c == '\'') { inChar = true; continue; }
1691 | 
1692 |                 if (c == '(') parenDepth++;
1693 |                 if (c == ')') { parenDepth--; if (parenDepth == 0) { i++; break; } }
1694 |             }
1695 | 
1696 |             // After params: detect expression-bodied or block-bodied
1697 |             // Skip whitespace/comments
1698 |             for (; i < searchEnd; i++)
1699 |             {
1700 |                 char c = source[i];
1701 |                 char n = i + 1 < searchEnd ? source[i + 1] : '\0';
1702 |                 if (char.IsWhiteSpace(c)) continue;
1703 |                 if (c == '/' && n == '/') { while (i < searchEnd && source[i] != '\n') i++; continue; }
1704 |                 if (c == '/' && n == '*') { i += 2; while (i + 1 < searchEnd && !(source[i] == '*' && source[i + 1] == '/')) i++; i++; continue; }
1705 |                 break;
1706 |             }
1707 | 
1708 |             // Tolerate generic constraints between params and body: multiple 'where T : ...'
1709 |             for (; ; )
1710 |             {
1711 |                 // Skip whitespace/comments before checking for 'where'
1712 |                 for (; i < searchEnd; i++)
1713 |                 {
1714 |                     char c = source[i];
1715 |                     char n = i + 1 < searchEnd ? source[i + 1] : '\0';
1716 |                     if (char.IsWhiteSpace(c)) continue;
1717 |                     if (c == '/' && n == '/') { while (i < searchEnd && source[i] != '\n') i++; continue; }
1718 |                     if (c == '/' && n == '*') { i += 2; while (i + 1 < searchEnd && !(source[i] == '*' && source[i + 1] == '/')) i++; i++; continue; }
1719 |                     break;
1720 |                 }
1721 | 
1722 |                 // Check word-boundary 'where'
1723 |                 bool hasWhere = false;
1724 |                 if (i + 5 <= searchEnd)
1725 |                 {
1726 |                     hasWhere = source[i] == 'w' && source[i + 1] == 'h' && source[i + 2] == 'e' && source[i + 3] == 'r' && source[i + 4] == 'e';
1727 |                     if (hasWhere)
1728 |                     {
1729 |                         // Left boundary
1730 |                         if (i - 1 >= 0)
1731 |                         {
1732 |                             char lb = source[i - 1];
1733 |                             if (char.IsLetterOrDigit(lb) || lb == '_') hasWhere = false;
1734 |                         }
1735 |                         // Right boundary
1736 |                         if (hasWhere && i + 5 < searchEnd)
1737 |                         {
1738 |                             char rb = source[i + 5];
1739 |                             if (char.IsLetterOrDigit(rb) || rb == '_') hasWhere = false;
1740 |                         }
1741 |                     }
1742 |                 }
1743 |                 if (!hasWhere) break;
1744 | 
1745 |                 // Advance past the entire where-constraint clause until we hit '{' or '=>' or ';'
1746 |                 i += 5; // past 'where'
1747 |                 while (i < searchEnd)
1748 |                 {
1749 |                     char c = source[i];
1750 |                     char n = i + 1 < searchEnd ? source[i + 1] : '\0';
1751 |                     if (c == '{' || c == ';' || (c == '=' && n == '>')) break;
1752 |                     // Skip comments inline
1753 |                     if (c == '/' && n == '/') { while (i < searchEnd && source[i] != '\n') i++; continue; }
1754 |                     if (c == '/' && n == '*') { i += 2; while (i + 1 < searchEnd && !(source[i] == '*' && source[i + 1] == '/')) i++; i++; continue; }
1755 |                     i++;
1756 |                 }
1757 |             }
1758 | 
1759 |             // Re-check for expression-bodied after constraints
1760 |             if (i < searchEnd - 1 && source[i] == '=' && source[i + 1] == '>')
1761 |             {
1762 |                 // expression-bodied method: seek to terminating semicolon
1763 |                 int j = i;
1764 |                 bool done = false;
1765 |                 while (j < searchEnd)
1766 |                 {
1767 |                     char c = source[j];
1768 |                     if (c == ';') { done = true; break; }
1769 |                     j++;
1770 |                 }
1771 |                 if (!done) { why = "unterminated expression-bodied method"; return false; }
1772 |                 start = attrStart; length = (j - attrStart) + 1; return true;
1773 |             }
1774 | 
1775 |             if (i >= searchEnd || source[i] != '{') { why = "no opening brace after method signature"; return false; }
1776 | 
1777 |             int depth = 0; inStr = false; inChar = false; inSL = false; inML = false; esc = false;
1778 |             int startSpan = attrStart;
1779 |             for (; i < searchEnd; i++)
1780 |             {
1781 |                 char c = source[i];
1782 |                 char n = i + 1 < searchEnd ? source[i + 1] : '\0';
1783 |                 if (inSL) { if (c == '\n') inSL = false; continue; }
1784 |                 if (inML) { if (c == '*' && n == '/') { inML = false; i++; } continue; }
1785 |                 if (inStr) { if (!esc && c == '"') inStr = false; esc = (!esc && c == '\\'); continue; }
1786 |                 if (inChar) { if (!esc && c == '\'') inChar = false; esc = (!esc && c == '\\'); continue; }
1787 | 
1788 |                 if (c == '/' && n == '/') { inSL = true; i++; continue; }
1789 |                 if (c == '/' && n == '*') { inML = true; i++; continue; }
1790 |                 if (c == '"') { inStr = true; continue; }
1791 |                 if (c == '\'') { inChar = true; continue; }
1792 | 
1793 |                 if (c == '{') depth++;
1794 |                 else if (c == '}')
1795 |                 {
1796 |                     depth--;
1797 |                     if (depth == 0) { start = startSpan; length = (i - startSpan) + 1; return true; }
1798 |                     if (depth < 0) { why = "brace underflow in method"; return false; }
1799 |                 }
1800 |             }
1801 |             why = "unterminated method block"; return false;
1802 |         }
1803 | 
1804 |         private static int IndexOfTokenWithin(string s, string token, int start, int end)
1805 |         {
1806 |             int idx = s.IndexOf(token, start, StringComparison.Ordinal);
1807 |             return (idx >= 0 && idx < end) ? idx : -1;
1808 |         }
1809 | 
1810 |         private static bool TryFindClassInsertionPoint(string source, int classStart, int classLength, string position, out int insertAt, out string why)
1811 |         {
1812 |             insertAt = 0; why = null;
1813 |             int searchStart = classStart;
1814 |             int searchEnd = Math.Min(source.Length, classStart + classLength);
1815 | 
1816 |             if (position == "start")
1817 |             {
1818 |                 // find first '{' after class header, insert just after with a newline
1819 |                 int i = IndexOfTokenWithin(source, "{", searchStart, searchEnd);
1820 |                 if (i < 0) { why = "could not find class opening brace"; return false; }
1821 |                 insertAt = i + 1; return true;
1822 |             }
1823 |             else // end
1824 |             {
1825 |                 // walk to matching closing brace of class and insert just before it
1826 |                 int i = IndexOfTokenWithin(source, "{", searchStart, searchEnd);
1827 |                 if (i < 0) { why = "could not find class opening brace"; return false; }
1828 |                 int depth = 0; bool inStr = false, inChar = false, inSL = false, inML = false, esc = false;
1829 |                 for (; i < searchEnd; i++)
1830 |                 {
1831 |                     char c = source[i];
1832 |                     char n = i + 1 < searchEnd ? source[i + 1] : '\0';
1833 |                     if (inSL) { if (c == '\n') inSL = false; continue; }
1834 |                     if (inML) { if (c == '*' && n == '/') { inML = false; i++; } continue; }
1835 |                     if (inStr) { if (!esc && c == '"') inStr = false; esc = (!esc && c == '\\'); continue; }
1836 |                     if (inChar) { if (!esc && c == '\'') inChar = false; esc = (!esc && c == '\\'); continue; }
1837 | 
1838 |                     if (c == '/' && n == '/') { inSL = true; i++; continue; }
1839 |                     if (c == '/' && n == '*') { inML = true; i++; continue; }
1840 |                     if (c == '"') { inStr = true; continue; }
1841 |                     if (c == '\'') { inChar = true; continue; }
1842 | 
1843 |                     if (c == '{') depth++;
1844 |                     else if (c == '}')
1845 |                     {
1846 |                         depth--;
1847 |                         if (depth == 0) { insertAt = i; return true; }
1848 |                         if (depth < 0) { why = "brace underflow while scanning class"; return false; }
1849 |                     }
1850 |                 }
1851 |                 why = "could not find class closing brace"; return false;
1852 |             }
1853 |         }
1854 | 
1855 |         private static int IndexOfClassToken(string s, string className)
1856 |         {
1857 |             // simple token search; could be tightened with Regex for word boundaries
1858 |             var pattern = "class " + className;
1859 |             return s.IndexOf(pattern, StringComparison.Ordinal);
1860 |         }
1861 | 
1862 |         private static bool AppearsWithinNamespaceHeader(string s, int pos, string ns)
1863 |         {
1864 |             int from = Math.Max(0, pos - 2000);
1865 |             var slice = s.Substring(from, pos - from);
1866 |             return slice.Contains("namespace " + ns);
1867 |         }
1868 | 
1869 |         /// <summary>
1870 |         /// Generates basic C# script content based on name and type.
1871 |         /// </summary>
1872 |         private static string GenerateDefaultScriptContent(
1873 |             string name,
1874 |             string scriptType,
1875 |             string namespaceName
1876 |         )
1877 |         {
1878 |             string usingStatements = "using UnityEngine;\nusing System.Collections;\n";
1879 |             string classDeclaration;
1880 |             string body =
1881 |                 "\n    // Use this for initialization\n    void Start() {\n\n    }\n\n    // Update is called once per frame\n    void Update() {\n\n    }\n";
1882 | 
1883 |             string baseClass = "";
1884 |             if (!string.IsNullOrEmpty(scriptType))
1885 |             {
1886 |                 if (scriptType.Equals("MonoBehaviour", StringComparison.OrdinalIgnoreCase))
1887 |                     baseClass = " : MonoBehaviour";
1888 |                 else if (scriptType.Equals("ScriptableObject", StringComparison.OrdinalIgnoreCase))
1889 |                 {
1890 |                     baseClass = " : ScriptableObject";
1891 |                     body = ""; // ScriptableObjects don't usually need Start/Update
1892 |                 }
1893 |                 else if (
1894 |                     scriptType.Equals("Editor", StringComparison.OrdinalIgnoreCase)
1895 |                     || scriptType.Equals("EditorWindow", StringComparison.OrdinalIgnoreCase)
1896 |                 )
1897 |                 {
1898 |                     usingStatements += "using UnityEditor;\n";
1899 |                     if (scriptType.Equals("Editor", StringComparison.OrdinalIgnoreCase))
1900 |                         baseClass = " : Editor";
1901 |                     else
1902 |                         baseClass = " : EditorWindow";
1903 |                     body = ""; // Editor scripts have different structures
1904 |                 }
1905 |                 // Add more types as needed
1906 |             }
1907 | 
1908 |             classDeclaration = $"public class {name}{baseClass}";
1909 | 
1910 |             string fullContent = $"{usingStatements}\n";
1911 |             bool useNamespace = !string.IsNullOrEmpty(namespaceName);
1912 | 
1913 |             if (useNamespace)
1914 |             {
1915 |                 fullContent += $"namespace {namespaceName}\n{{\n";
1916 |                 // Indent class and body if using namespace
1917 |                 classDeclaration = "    " + classDeclaration;
1918 |                 body = string.Join("\n", body.Split('\n').Select(line => "    " + line));
1919 |             }
1920 | 
1921 |             fullContent += $"{classDeclaration}\n{{\n{body}\n}}";
1922 | 
1923 |             if (useNamespace)
1924 |             {
1925 |                 fullContent += "\n}"; // Close namespace
1926 |             }
1927 | 
1928 |             return fullContent.Trim() + "\n"; // Ensure a trailing newline
1929 |         }
1930 | 
1931 |         /// <summary>
1932 |         /// Gets the validation level from the GUI settings
1933 |         /// </summary>
1934 |         private static ValidationLevel GetValidationLevelFromGUI()
1935 |         {
1936 |             string savedLevel = EditorPrefs.GetString("MCPForUnity_ScriptValidationLevel", "standard");
1937 |             return savedLevel.ToLower() switch
1938 |             {
1939 |                 "basic" => ValidationLevel.Basic,
1940 |                 "standard" => ValidationLevel.Standard,
1941 |                 "comprehensive" => ValidationLevel.Comprehensive,
1942 |                 "strict" => ValidationLevel.Strict,
1943 |                 _ => ValidationLevel.Standard // Default fallback
1944 |             };
1945 |         }
1946 | 
1947 |         /// <summary>
1948 |         /// Validates C# script syntax using multiple validation layers.
1949 |         /// </summary>
1950 |         private static bool ValidateScriptSyntax(string contents)
1951 |         {
1952 |             return ValidateScriptSyntax(contents, ValidationLevel.Standard, out _);
1953 |         }
1954 | 
1955 |         /// <summary>
1956 |         /// Advanced syntax validation with detailed diagnostics and configurable strictness.
1957 |         /// </summary>
1958 |         private static bool ValidateScriptSyntax(string contents, ValidationLevel level, out string[] errors)
1959 |         {
1960 |             var errorList = new System.Collections.Generic.List<string>();
1961 |             errors = null;
1962 | 
1963 |             if (string.IsNullOrEmpty(contents))
1964 |             {
1965 |                 return true; // Empty content is valid
1966 |             }
1967 | 
1968 |             // Basic structural validation
1969 |             if (!ValidateBasicStructure(contents, errorList))
1970 |             {
1971 |                 errors = errorList.ToArray();
1972 |                 return false;
1973 |             }
1974 | 
1975 | #if USE_ROSLYN
1976 |             // Advanced Roslyn-based validation: only run for Standard+; fail on Roslyn errors
1977 |             if (level >= ValidationLevel.Standard)
1978 |             {
1979 |                 if (!ValidateScriptSyntaxRoslyn(contents, level, errorList))
1980 |                 {
1981 |                     errors = errorList.ToArray();
1982 |                     return false;
1983 |                 }
1984 |             }
1985 | #endif
1986 | 
1987 |             // Unity-specific validation
1988 |             if (level >= ValidationLevel.Standard)
1989 |             {
1990 |                 ValidateScriptSyntaxUnity(contents, errorList);
1991 |             }
1992 | 
1993 |             // Semantic analysis for common issues
1994 |             if (level >= ValidationLevel.Comprehensive)
1995 |             {
1996 |                 ValidateSemanticRules(contents, errorList);
1997 |             }
1998 | 
1999 | #if USE_ROSLYN
2000 |             // Full semantic compilation validation for Strict level
2001 |             if (level == ValidationLevel.Strict)
2002 |             {
2003 |                 if (!ValidateScriptSemantics(contents, errorList))
2004 |                 {
2005 |                     errors = errorList.ToArray();
2006 |                     return false; // Strict level fails on any semantic errors
2007 |                 }
2008 |             }
2009 | #endif
2010 | 
2011 |             errors = errorList.ToArray();
2012 |             return errorList.Count == 0 || (level != ValidationLevel.Strict && !errorList.Any(e => e.StartsWith("ERROR:")));
2013 |         }
2014 | 
2015 |         /// <summary>
2016 |         /// Validation strictness levels
2017 |         /// </summary>
2018 |         private enum ValidationLevel
2019 |         {
2020 |             Basic,        // Only syntax errors
2021 |             Standard,     // Syntax + Unity best practices
2022 |             Comprehensive, // All checks + semantic analysis
2023 |             Strict        // Treat all issues as errors
2024 |         }
2025 | 
2026 |         /// <summary>
2027 |         /// Validates basic code structure (braces, quotes, comments)
2028 |         /// </summary>
2029 |         private static bool ValidateBasicStructure(string contents, System.Collections.Generic.List<string> errors)
2030 |         {
2031 |             bool isValid = true;
2032 |             int braceBalance = 0;
2033 |             int parenBalance = 0;
2034 |             int bracketBalance = 0;
2035 |             bool inStringLiteral = false;
2036 |             bool inCharLiteral = false;
2037 |             bool inSingleLineComment = false;
2038 |             bool inMultiLineComment = false;
2039 |             bool escaped = false;
2040 | 
2041 |             for (int i = 0; i < contents.Length; i++)
2042 |             {
2043 |                 char c = contents[i];
2044 |                 char next = i + 1 < contents.Length ? contents[i + 1] : '\0';
2045 | 
2046 |                 // Handle escape sequences
2047 |                 if (escaped)
2048 |                 {
2049 |                     escaped = false;
2050 |                     continue;
2051 |                 }
2052 | 
2053 |                 if (c == '\\' && (inStringLiteral || inCharLiteral))
2054 |                 {
2055 |                     escaped = true;
2056 |                     continue;
2057 |                 }
2058 | 
2059 |                 // Handle comments
2060 |                 if (!inStringLiteral && !inCharLiteral)
2061 |                 {
2062 |                     if (c == '/' && next == '/' && !inMultiLineComment)
2063 |                     {
2064 |                         inSingleLineComment = true;
2065 |                         continue;
2066 |                     }
2067 |                     if (c == '/' && next == '*' && !inSingleLineComment)
2068 |                     {
2069 |                         inMultiLineComment = true;
2070 |                         i++; // Skip next character
2071 |                         continue;
2072 |                     }
2073 |                     if (c == '*' && next == '/' && inMultiLineComment)
2074 |                     {
2075 |                         inMultiLineComment = false;
2076 |                         i++; // Skip next character
2077 |                         continue;
2078 |                     }
2079 |                 }
2080 | 
2081 |                 if (c == '\n')
2082 |                 {
2083 |                     inSingleLineComment = false;
2084 |                     continue;
2085 |                 }
2086 | 
2087 |                 if (inSingleLineComment || inMultiLineComment)
2088 |                     continue;
2089 | 
2090 |                 // Handle string and character literals
2091 |                 if (c == '"' && !inCharLiteral)
2092 |                 {
2093 |                     inStringLiteral = !inStringLiteral;
2094 |                     continue;
2095 |                 }
2096 |                 if (c == '\'' && !inStringLiteral)
2097 |                 {
2098 |                     inCharLiteral = !inCharLiteral;
2099 |                     continue;
2100 |                 }
2101 | 
2102 |                 if (inStringLiteral || inCharLiteral)
2103 |                     continue;
2104 | 
2105 |                 // Count brackets and braces
2106 |                 switch (c)
2107 |                 {
2108 |                     case '{': braceBalance++; break;
2109 |                     case '}': braceBalance--; break;
2110 |                     case '(': parenBalance++; break;
2111 |                     case ')': parenBalance--; break;
2112 |                     case '[': bracketBalance++; break;
2113 |                     case ']': bracketBalance--; break;
2114 |                 }
2115 | 
2116 |                 // Check for negative balances (closing without opening)
2117 |                 if (braceBalance < 0)
2118 |                 {
2119 |                     errors.Add("ERROR: Unmatched closing brace '}'");
2120 |                     isValid = false;
2121 |                 }
2122 |                 if (parenBalance < 0)
2123 |                 {
2124 |                     errors.Add("ERROR: Unmatched closing parenthesis ')'");
2125 |                     isValid = false;
2126 |                 }
2127 |                 if (bracketBalance < 0)
2128 |                 {
2129 |                     errors.Add("ERROR: Unmatched closing bracket ']'");
2130 |                     isValid = false;
2131 |                 }
2132 |             }
2133 | 
2134 |             // Check final balances
2135 |             if (braceBalance != 0)
2136 |             {
2137 |                 errors.Add($"ERROR: Unbalanced braces (difference: {braceBalance})");
2138 |                 isValid = false;
2139 |             }
2140 |             if (parenBalance != 0)
2141 |             {
2142 |                 errors.Add($"ERROR: Unbalanced parentheses (difference: {parenBalance})");
2143 |                 isValid = false;
2144 |             }
2145 |             if (bracketBalance != 0)
2146 |             {
2147 |                 errors.Add($"ERROR: Unbalanced brackets (difference: {bracketBalance})");
2148 |                 isValid = false;
2149 |             }
2150 |             if (inStringLiteral)
2151 |             {
2152 |                 errors.Add("ERROR: Unterminated string literal");
2153 |                 isValid = false;
2154 |             }
2155 |             if (inCharLiteral)
2156 |             {
2157 |                 errors.Add("ERROR: Unterminated character literal");
2158 |                 isValid = false;
2159 |             }
2160 |             if (inMultiLineComment)
2161 |             {
2162 |                 errors.Add("WARNING: Unterminated multi-line comment");
2163 |             }
2164 | 
2165 |             return isValid;
2166 |         }
2167 | 
2168 | #if USE_ROSLYN
2169 |         /// <summary>
2170 |         /// Cached compilation references for performance
2171 |         /// </summary>
2172 |         private static System.Collections.Generic.List<MetadataReference> _cachedReferences = null;
2173 |         private static DateTime _cacheTime = DateTime.MinValue;
2174 |         private static readonly TimeSpan CacheExpiry = TimeSpan.FromMinutes(5);
2175 | 
2176 |         /// <summary>
2177 |         /// Validates syntax using Roslyn compiler services
2178 |         /// </summary>
2179 |         private static bool ValidateScriptSyntaxRoslyn(string contents, ValidationLevel level, System.Collections.Generic.List<string> errors)
2180 |         {
2181 |             try
2182 |             {
2183 |                 var syntaxTree = CSharpSyntaxTree.ParseText(contents);
2184 |                 var diagnostics = syntaxTree.GetDiagnostics();
2185 |                 
2186 |                 bool hasErrors = false;
2187 |                 foreach (var diagnostic in diagnostics)
2188 |                 {
2189 |                     string severity = diagnostic.Severity.ToString().ToUpper();
2190 |                     string message = $"{severity}: {diagnostic.GetMessage()}";
2191 |                     
2192 |                     if (diagnostic.Severity == DiagnosticSeverity.Error)
2193 |                     {
2194 |                         hasErrors = true;
2195 |                     }
2196 |                     
2197 |                     // Include warnings in comprehensive mode
2198 |                     if (level >= ValidationLevel.Standard || diagnostic.Severity == DiagnosticSeverity.Error) //Also use Standard for now
2199 |                     {
2200 |                         var location = diagnostic.Location.GetLineSpan();
2201 |                         if (location.IsValid)
2202 |                         {
2203 |                             message += $" (Line {location.StartLinePosition.Line + 1})";
2204 |                         }
2205 |                         errors.Add(message);
2206 |                     }
2207 |                 }
2208 |                 
2209 |                 return !hasErrors;
2210 |             }
2211 |             catch (Exception ex)
2212 |             {
2213 |                 errors.Add($"ERROR: Roslyn validation failed: {ex.Message}");
2214 |                 return false;
2215 |             }
2216 |         }
2217 | 
2218 |         /// <summary>
2219 |         /// Validates script semantics using full compilation context to catch namespace, type, and method resolution errors
2220 |         /// </summary>
2221 |         private static bool ValidateScriptSemantics(string contents, System.Collections.Generic.List<string> errors)
2222 |         {
2223 |             try
2224 |             {
2225 |                 // Get compilation references with caching
2226 |                 var references = GetCompilationReferences();
2227 |                 if (references == null || references.Count == 0)
2228 |                 {
2229 |                     errors.Add("WARNING: Could not load compilation references for semantic validation");
2230 |                     return true; // Don't fail if we can't get references
2231 |                 }
2232 | 
2233 |                 // Create syntax tree
2234 |                 var syntaxTree = CSharpSyntaxTree.ParseText(contents);
2235 | 
2236 |                 // Create compilation with full context
2237 |                 var compilation = CSharpCompilation.Create(
2238 |                     "TempValidation",
2239 |                     new[] { syntaxTree },
2240 |                     references,
2241 |                     new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
2242 |                 );
2243 | 
2244 |                 // Get semantic diagnostics - this catches all the issues you mentioned!
2245 |                 var diagnostics = compilation.GetDiagnostics();
2246 |                 
2247 |                 bool hasErrors = false;
2248 |                 foreach (var diagnostic in diagnostics)
2249 |                 {
2250 |                     if (diagnostic.Severity == DiagnosticSeverity.Error)
2251 |                     {
2252 |                         hasErrors = true;
2253 |                         var location = diagnostic.Location.GetLineSpan();
2254 |                         string locationInfo = location.IsValid ? 
2255 |                             $" (Line {location.StartLinePosition.Line + 1}, Column {location.StartLinePosition.Character + 1})" : "";
2256 |                         
2257 |                         // Include diagnostic ID for better error identification
2258 |                         string diagnosticId = !string.IsNullOrEmpty(diagnostic.Id) ? $" [{diagnostic.Id}]" : "";
2259 |                         errors.Add($"ERROR: {diagnostic.GetMessage()}{diagnosticId}{locationInfo}");
2260 |                     }
2261 |                     else if (diagnostic.Severity == DiagnosticSeverity.Warning)
2262 |                     {
2263 |                         var location = diagnostic.Location.GetLineSpan();
2264 |                         string locationInfo = location.IsValid ? 
2265 |                             $" (Line {location.StartLinePosition.Line + 1}, Column {location.StartLinePosition.Character + 1})" : "";
2266 |                         
2267 |                         string diagnosticId = !string.IsNullOrEmpty(diagnostic.Id) ? $" [{diagnostic.Id}]" : "";
2268 |                         errors.Add($"WARNING: {diagnostic.GetMessage()}{diagnosticId}{locationInfo}");
2269 |                     }
2270 |                 }
2271 |                 
2272 |                 return !hasErrors;
2273 |             }
2274 |             catch (Exception ex)
2275 |             {
2276 |                 errors.Add($"ERROR: Semantic validation failed: {ex.Message}");
2277 |                 return false;
2278 |             }
2279 |         }
2280 | 
2281 |         /// <summary>
2282 |         /// Gets compilation references with caching for performance
2283 |         /// </summary>
2284 |         private static System.Collections.Generic.List<MetadataReference> GetCompilationReferences()
2285 |         {
2286 |             // Check cache validity
2287 |             if (_cachedReferences != null && DateTime.Now - _cacheTime < CacheExpiry)
2288 |             {
2289 |                 return _cachedReferences;
2290 |             }
2291 | 
2292 |             try
2293 |             {
2294 |                 var references = new System.Collections.Generic.List<MetadataReference>();
2295 | 
2296 |                 // Core .NET assemblies
2297 |                 references.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); // mscorlib/System.Private.CoreLib
2298 |                 references.Add(MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location)); // System.Linq
2299 |                 references.Add(MetadataReference.CreateFromFile(typeof(System.Collections.Generic.List<>).Assembly.Location)); // System.Collections
2300 | 
2301 |                 // Unity assemblies
2302 |                 try
2303 |                 {
2304 |                     references.Add(MetadataReference.CreateFromFile(typeof(UnityEngine.Debug).Assembly.Location)); // UnityEngine
2305 |                 }
2306 |                 catch (Exception ex)
2307 |                 {
2308 |                     Debug.LogWarning($"Could not load UnityEngine assembly: {ex.Message}");
2309 |                 }
2310 | 
2311 | #if UNITY_EDITOR
2312 |                 try
2313 |                 {
2314 |                     references.Add(MetadataReference.CreateFromFile(typeof(UnityEditor.Editor).Assembly.Location)); // UnityEditor
2315 |                 }
2316 |                 catch (Exception ex)
2317 |                 {
2318 |                     Debug.LogWarning($"Could not load UnityEditor assembly: {ex.Message}");
2319 |                 }
2320 | 
2321 |                 // Get Unity project assemblies
2322 |                 try
2323 |                 {
2324 |                     var assemblies = CompilationPipeline.GetAssemblies();
2325 |                     foreach (var assembly in assemblies)
2326 |                     {
2327 |                         if (File.Exists(assembly.outputPath))
2328 |                         {
2329 |                             references.Add(MetadataReference.CreateFromFile(assembly.outputPath));
2330 |                         }
2331 |                     }
2332 |                 }
2333 |                 catch (Exception ex)
2334 |                 {
2335 |                     Debug.LogWarning($"Could not load Unity project assemblies: {ex.Message}");
2336 |                 }
2337 | #endif
2338 | 
2339 |                 // Cache the results
2340 |                 _cachedReferences = references;
2341 |                 _cacheTime = DateTime.Now;
2342 | 
2343 |                 return references;
2344 |             }
2345 |             catch (Exception ex)
2346 |             {
2347 |                 Debug.LogError($"Failed to get compilation references: {ex.Message}");
2348 |                 return new System.Collections.Generic.List<MetadataReference>();
2349 |             }
2350 |         }
2351 | #else
2352 |         private static bool ValidateScriptSyntaxRoslyn(string contents, ValidationLevel level, System.Collections.Generic.List<string> errors)
2353 |         {
2354 |             // Fallback when Roslyn is not available
2355 |             return true;
2356 |         }
2357 | #endif
2358 | 
2359 |         /// <summary>
2360 |         /// Validates Unity-specific coding rules and best practices
2361 |         /// //TODO: Naive Unity Checks and not really yield any results, need to be improved
2362 |         /// </summary>
2363 |         private static void ValidateScriptSyntaxUnity(string contents, System.Collections.Generic.List<string> errors)
2364 |         {
2365 |             // Check for common Unity anti-patterns
2366 |             if (contents.Contains("FindObjectOfType") && contents.Contains("Update()"))
2367 |             {
2368 |                 errors.Add("WARNING: FindObjectOfType in Update() can cause performance issues");
2369 |             }
2370 | 
2371 |             if (contents.Contains("GameObject.Find") && contents.Contains("Update()"))
2372 |             {
2373 |                 errors.Add("WARNING: GameObject.Find in Update() can cause performance issues");
2374 |             }
2375 | 
2376 |             // Check for proper MonoBehaviour usage
2377 |             if (contents.Contains(": MonoBehaviour") && !contents.Contains("using UnityEngine"))
2378 |             {
2379 |                 errors.Add("WARNING: MonoBehaviour requires 'using UnityEngine;'");
2380 |             }
2381 | 
2382 |             // Check for SerializeField usage
2383 |             if (contents.Contains("[SerializeField]") && !contents.Contains("using UnityEngine"))
2384 |             {
2385 |                 errors.Add("WARNING: SerializeField requires 'using UnityEngine;'");
2386 |             }
2387 | 
2388 |             // Check for proper coroutine usage
2389 |             if (contents.Contains("StartCoroutine") && !contents.Contains("IEnumerator"))
2390 |             {
2391 |                 errors.Add("WARNING: StartCoroutine typically requires IEnumerator methods");
2392 |             }
2393 | 
2394 |             // Check for Update without FixedUpdate for physics
2395 |             if (contents.Contains("Rigidbody") && contents.Contains("Update()") && !contents.Contains("FixedUpdate()"))
2396 |             {
2397 |                 errors.Add("WARNING: Consider using FixedUpdate() for Rigidbody operations");
2398 |             }
2399 | 
2400 |             // Check for missing null checks on Unity objects
2401 |             if (contents.Contains("GetComponent<") && !contents.Contains("!= null"))
2402 |             {
2403 |                 errors.Add("WARNING: Consider null checking GetComponent results");
2404 |             }
2405 | 
2406 |             // Check for proper event function signatures
2407 |             if (contents.Contains("void Start(") && !contents.Contains("void Start()"))
2408 |             {
2409 |                 errors.Add("WARNING: Start() should not have parameters");
2410 |             }
2411 | 
2412 |             if (contents.Contains("void Update(") && !contents.Contains("void Update()"))
2413 |             {
2414 |                 errors.Add("WARNING: Update() should not have parameters");
2415 |             }
2416 | 
2417 |             // Check for inefficient string operations
2418 |             if (contents.Contains("Update()") && contents.Contains("\"") && contents.Contains("+"))
2419 |             {
2420 |                 errors.Add("WARNING: String concatenation in Update() can cause garbage collection issues");
2421 |             }
2422 |         }
2423 | 
2424 |         /// <summary>
2425 |         /// Validates semantic rules and common coding issues
2426 |         /// </summary>
2427 |         private static void ValidateSemanticRules(string contents, System.Collections.Generic.List<string> errors)
2428 |         {
2429 |             // Check for potential memory leaks
2430 |             if (contents.Contains("new ") && contents.Contains("Update()"))
2431 |             {
2432 |                 errors.Add("WARNING: Creating objects in Update() may cause memory issues");
2433 |             }
2434 | 
2435 |             // Check for magic numbers
2436 |             var magicNumberPattern = new Regex(@"\b\d+\.?\d*f?\b(?!\s*[;})\]])", RegexOptions.CultureInvariant, TimeSpan.FromSeconds(2));
2437 |             var matches = magicNumberPattern.Matches(contents);
2438 |             if (matches.Count > 5)
2439 |             {
2440 |                 errors.Add("WARNING: Consider using named constants instead of magic numbers");
2441 |             }
2442 | 
2443 |             // Check for long methods (simple line count check)
2444 |             var methodPattern = new Regex(@"(public|private|protected|internal)?\s*(static)?\s*\w+\s+\w+\s*\([^)]*\)\s*{", RegexOptions.CultureInvariant, TimeSpan.FromSeconds(2));
2445 |             var methodMatches = methodPattern.Matches(contents);
2446 |             foreach (Match match in methodMatches)
2447 |             {
2448 |                 int startIndex = match.Index;
2449 |                 int braceCount = 0;
2450 |                 int lineCount = 0;
2451 |                 bool inMethod = false;
2452 | 
2453 |                 for (int i = startIndex; i < contents.Length; i++)
2454 |                 {
2455 |                     if (contents[i] == '{')
2456 |                     {
2457 |                         braceCount++;
2458 |                         inMethod = true;
2459 |                     }
2460 |                     else if (contents[i] == '}')
2461 |                     {
2462 |                         braceCount--;
2463 |                         if (braceCount == 0 && inMethod)
2464 |                             break;
2465 |                     }
2466 |                     else if (contents[i] == '\n' && inMethod)
2467 |                     {
2468 |                         lineCount++;
2469 |                     }
2470 |                 }
2471 | 
2472 |                 if (lineCount > 50)
2473 |                 {
2474 |                     errors.Add("WARNING: Method is very long, consider breaking it into smaller methods");
2475 |                     break; // Only report once
2476 |                 }
2477 |             }
2478 | 
2479 |             // Check for proper exception handling
2480 |             if (contents.Contains("catch") && contents.Contains("catch()"))
2481 |             {
2482 |                 errors.Add("WARNING: Empty catch blocks should be avoided");
2483 |             }
2484 | 
2485 |             // Check for proper async/await usage
2486 |             if (contents.Contains("async ") && !contents.Contains("await"))
2487 |             {
2488 |                 errors.Add("WARNING: Async method should contain await or return Task");
2489 |             }
2490 | 
2491 |             // Check for hardcoded tags and layers
2492 |             if (contents.Contains("\"Player\"") || contents.Contains("\"Enemy\""))
2493 |             {
2494 |                 errors.Add("WARNING: Consider using constants for tags instead of hardcoded strings");
2495 |             }
2496 |         }
2497 | 
2498 |         //TODO: A easier way for users to update incorrect scripts (now duplicated with the updateScript method and need to also update server side, put aside for now)
2499 |         /// <summary>
2500 |         /// Public method to validate script syntax with configurable validation level
2501 |         /// Returns detailed validation results including errors and warnings
2502 |         /// </summary>
2503 |         // public static object ValidateScript(JObject @params)
2504 |         // {
2505 |         //     string contents = @params["contents"]?.ToString();
2506 |         //     string validationLevel = @params["validationLevel"]?.ToString() ?? "standard";
2507 | 
2508 |         //     if (string.IsNullOrEmpty(contents))
2509 |         //     {
2510 |         //         return Response.Error("Contents parameter is required for validation.");
2511 |         //     }
2512 | 
2513 |         //     // Parse validation level
2514 |         //     ValidationLevel level = ValidationLevel.Standard;
2515 |         //     switch (validationLevel.ToLower())
2516 |         //     {
2517 |         //         case "basic": level = ValidationLevel.Basic; break;
2518 |         //         case "standard": level = ValidationLevel.Standard; break;
2519 |         //         case "comprehensive": level = ValidationLevel.Comprehensive; break;
2520 |         //         case "strict": level = ValidationLevel.Strict; break;
2521 |         //         default:
2522 |         //             return Response.Error($"Invalid validation level: '{validationLevel}'. Valid levels are: basic, standard, comprehensive, strict.");
2523 |         //     }
2524 | 
2525 |         //     // Perform validation
2526 |         //     bool isValid = ValidateScriptSyntax(contents, level, out string[] validationErrors);
2527 | 
2528 |         //     var errors = validationErrors?.Where(e => e.StartsWith("ERROR:")).ToArray() ?? new string[0];
2529 |         //     var warnings = validationErrors?.Where(e => e.StartsWith("WARNING:")).ToArray() ?? new string[0];
2530 | 
2531 |         //     var result = new
2532 |         //     {
2533 |         //         isValid = isValid,
2534 |         //         validationLevel = validationLevel,
2535 |         //         errorCount = errors.Length,
2536 |         //         warningCount = warnings.Length,
2537 |         //         errors = errors,
2538 |         //         warnings = warnings,
2539 |         //         summary = isValid 
2540 |         //             ? (warnings.Length > 0 ? $"Validation passed with {warnings.Length} warnings" : "Validation passed with no issues")
2541 |         //             : $"Validation failed with {errors.Length} errors and {warnings.Length} warnings"
2542 |         //     };
2543 | 
2544 |         //     if (isValid)
2545 |         //     {
2546 |         //         return Response.Success("Script validation completed successfully.", result);
2547 |         //     }
2548 |         //     else
2549 |         //     {
2550 |         //         return Response.Error("Script validation failed.", result);
2551 |         //     }
2552 |         // }
2553 |     }
2554 | 
2555 |     // Debounced refresh/compile scheduler to coalesce bursts of edits
2556 |     static class RefreshDebounce
2557 |     {
2558 |         private static int _pending;
2559 |         private static readonly object _lock = new object();
2560 |         private static readonly HashSet<string> _paths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
2561 | 
2562 |         // The timestamp of the most recent schedule request.
2563 |         private static DateTime _lastRequest;
2564 | 
2565 |         // Guard to ensure we only have a single ticking callback running.
2566 |         private static bool _scheduled;
2567 | 
2568 |         public static void Schedule(string relPath, TimeSpan window)
2569 |         {
2570 |             // Record that work is pending and track the path in a threadsafe way.
2571 |             Interlocked.Exchange(ref _pending, 1);
2572 |             lock (_lock)
2573 |             {
2574 |                 _paths.Add(relPath);
2575 |                 _lastRequest = DateTime.UtcNow;
2576 | 
2577 |                 // If a debounce timer is already scheduled it will pick up the new request.
2578 |                 if (_scheduled)
2579 |                     return;
2580 | 
2581 |                 _scheduled = true;
2582 |             }
2583 | 
2584 |             // Kick off a ticking callback that waits until the window has elapsed
2585 |             // from the last request before performing the refresh.
2586 |             EditorApplication.delayCall += () => Tick(window);
2587 |             // Nudge the editor loop so ticks run even if the window is unfocused
2588 |             EditorApplication.QueuePlayerLoopUpdate();
2589 |         }
2590 | 
2591 |         private static void Tick(TimeSpan window)
2592 |         {
2593 |             bool ready;
2594 |             lock (_lock)
2595 |             {
2596 |                 // Only proceed once the debounce window has fully elapsed.
2597 |                 ready = (DateTime.UtcNow - _lastRequest) >= window;
2598 |                 if (ready)
2599 |                 {
2600 |                     _scheduled = false;
2601 |                 }
2602 |             }
2603 | 
2604 |             if (!ready)
2605 |             {
2606 |                 // Window has not yet elapsed; check again on the next editor tick.
2607 |                 EditorApplication.delayCall += () => Tick(window);
2608 |                 return;
2609 |             }
2610 | 
2611 |             if (Interlocked.Exchange(ref _pending, 0) == 1)
2612 |             {
2613 |                 string[] toImport;
2614 |                 lock (_lock) { toImport = _paths.ToArray(); _paths.Clear(); }
2615 |                 foreach (var p in toImport)
2616 |                 {
2617 |                     var sp = ManageScriptRefreshHelpers.SanitizeAssetsPath(p);
2618 |                     AssetDatabase.ImportAsset(sp, ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
2619 |                 }
2620 | #if UNITY_EDITOR
2621 |                 UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation();
2622 | #endif
2623 |                 // Fallback if needed:
2624 |                 // AssetDatabase.Refresh();
2625 |             }
2626 |         }
2627 |     }
2628 | 
2629 |     static class ManageScriptRefreshHelpers
2630 |     {
2631 |         public static string SanitizeAssetsPath(string p)
2632 |         {
2633 |             if (string.IsNullOrEmpty(p)) return p;
2634 |             p = p.Replace('\\', '/').Trim();
2635 |             if (p.StartsWith("unity://path/", StringComparison.OrdinalIgnoreCase))
2636 |                 p = p.Substring("unity://path/".Length);
2637 |             while (p.StartsWith("Assets/Assets/", StringComparison.OrdinalIgnoreCase))
2638 |                 p = p.Substring("Assets/".Length);
2639 |             if (!p.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
2640 |                 p = "Assets/" + p.TrimStart('/');
2641 |             return p;
2642 |         }
2643 | 
2644 |         public static void ScheduleScriptRefresh(string relPath)
2645 |         {
2646 |             var sp = SanitizeAssetsPath(relPath);
2647 |             RefreshDebounce.Schedule(sp, TimeSpan.FromMilliseconds(200));
2648 |         }
2649 | 
2650 |         public static void ImportAndRequestCompile(string relPath, bool synchronous = true)
2651 |         {
2652 |             var sp = SanitizeAssetsPath(relPath);
2653 |             var opts = ImportAssetOptions.ForceUpdate;
2654 |             if (synchronous) opts |= ImportAssetOptions.ForceSynchronousImport;
2655 |             AssetDatabase.ImportAsset(sp, opts);
2656 | #if UNITY_EDITOR
2657 |             UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation();
2658 | #endif
2659 |         }
2660 |     }
2661 | }
2662 | 
```
Page 18/18FirstPrevNextLast